Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Изначальная VNode регистрируется внутри Vue и остаётся при замене через v-render на новую #1327

Open
sinelnikov-web opened this issue Jul 10, 2024 · 1 comment

Comments

@sinelnikov-web
Copy link
Contributor

sinelnikov-web commented Jul 10, 2024

TL;DR

< template v-render сгенерирует DOM узел template, который мы заменим, но в момент создания template сразу же попадет в dynamicChildren родителя, ситуация похожа как с patchFlag #1322, что мы заменяем что-то уже после создания, но Vue внутри у себя начал следить за узлом, который мы заменяем.

Подробное описание

Рассмотрим проблему на примере шаблона:

< template &
  :key = el.uuid |
  v-render = renderChunk(vdom.getRenderFn(el.component + '/'))({params})
.

На выходе получаем следующий скомпилированный узел:

_withDirectives.call(
  _ctx,
  
  // Создаём ноду template
  (_openBlock.call(_ctx,), _createElementBlock.call(_ctx,"template", {key: el.uuid})), 

  // Директива с её параметрами
  [
    [
      (_directive_render = _directive_render || _resolveDirective.call(_ctx,"render")),
      _ctx.renderChunk(_ctx.vdom.getRenderFn(el.component + '/'))({params} })
    ]
  ]
)

Из документации по миграции на Vue3:

template tags with no special directives (v-if/else-if/else, v-for, or v-slot) are now treated as plain elements and will
result in a native template element instead of rendering its inner content.

Предварительно я также модифицировал v-render, добавив возможность добавлять ключ для заменяемой VNode.

ComponentEngine.directive('render', {
  beforeCreate(params: DirectiveParams, vnode: VNode): CanUndef<VNode> {
    // ...
    
    if (canReplaceOriginalVNode) {
      newVNode.key = vnode.key ?? newVNode.key;
      return SSR ? renderSSRFragment(newVNode) : newVNode;
    }

  // ...
  }
})

Видим в скомпилированном коде, что для создания template вызывается _createElementBlock

function createElementBlock(type, props, children, patchFlag, dynamicProps, shapeFlag) {
  return setupBlock(createBaseVNode(type, props, children, patchFlag, dynamicProps, shapeFlag, true /* isBlock */));
}

А далее вызывается setupBlock

function setupBlock(vnode) {
  // save current block children on the block vnode
  vnode.dynamicChildren = isBlockTreeEnabled > 0 ? currentBlock || EMPTY_ARR : null;
  // close block
  closeBlock();
  // a block is always going to be patched, so track it as a child of its
  // parent block
  if (isBlockTreeEnabled > 0 && currentBlock) {
	  currentBlock.push(vnode);
  }
  return vnode;
}

В setupBlock попадает и создаваемый выше template, и VNode, которую вернёт v-render вместо template. Они обе попадают в условие if (isBlockTreeEnabled > 0 && currentBlock) т.к. у обеих есть родитель и их нужно будет сравнивать в процессе обновлений.

Отличие в том, что template попадает туда уже имея атрибут key, а VNode которая будет когда-то возвращена из v-render, попадает туда ещё не имея атрибута key. Это значит, что VNode, попадает туда до вызова v-render.
Таким образом, у нас в dynamicChildren родительского узла, попадают 2 элемента: template и узел создаваемый при вызове _ctx.vdom.getRenderFn(el.component + '/')({params}).

Проблема в том, что т.к. template мы подменили на новый узел, то у VNode template атрибуты anchor и el всегда будут null, потому что в настоящем доме этого узла не будет. А атрибут key и у template, и у новой VNode одинаковый. Это значит, что если измениться key, то под сравнение попадут оба узла, и они оба попадут под замену т.к. ключ будет отличаться. Далее vue будет пытаться найти опорный узел в реальном доме, для замены нашего template на новый, но не найдёт и получит ошибку Cannot read properties of null (reading 'nextSibling')

const getNextHostNode = vnode => {
  if (vnode.shapeFlag & 6 /* ShapeFlags.COMPONENT */) {
    return getNextHostNode(vnode.component.subTree);
  }
  if (vnode.shapeFlag & 128 /* ShapeFlags.SUSPENSE */) {
    return vnode.suspense.next();
  }
  return hostNextSibling((vnode.anchor || vnode.el));
};

Если резюмировать, то узел, который мы заменяем с помощью v-render, попадает в dynamicChildren родителя, что и является багом

@shining-mind
Copy link
Contributor

shining-mind commented Jul 10, 2024

Если v-render используется совместно с template, то как правило template должен заменяться на узел, который вернула рендер функция, однако если же render функция вернет массив, то сейчас будет создан DOM узел template, где будут эти дочерние узлы, что в общем-то лишено смысла.

Возможно стоит v-render на template обрабатывать как не директиву, а конструкцию шаблона и сразу генерировать такой код, вместо узла с директивой:

// это массив children
[
..._ctx.renderChunk(_ctx.vdom.getRenderFn(el.component + '/'))({params} })
]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants