诗号:六道同坠,魔劫万千,引渡如来。

/img/bdx/yiyeshu-001.jpg

stb-vue-next 完全拷贝于 vue-next ,主要目的用于学习。

声明 :vue-next runtime-core 中的 render 函数部分,

本篇所使用的的测试例子是普 通的数字数组,实际中每个节点都是 VNode 结构,也就是说是唯一的,所以不存在不同节 点共享同一个内存空间问题,这也就是为何用例中没有重复数字的原因。(_就算是静态的可 复用节点也会执行 cloneVNode 克隆出一个全新的对象_)

更新日志&Todos

  1. [2021-01-26 14:15:15] 创建

/img/tmp/20210126143153.png

2229 - 423 = 1806 🤦‍♀️ 一个函数就将近两千行,😲!!

捷径👣:

  1. 如果只想了解如何 diff ? 更新?

脑图

runtime-core/src/render.ts 脑图:

/img/vue3/runtime-core/vue-runtime-core-render-ts.svg

render 函数创建函数 baseCreateRender(options, createHydrationFns?)

/img/vue3/runtime-core/vue-runtime-core-render-baseCreateRender.svg

init

feat(init): render function · gcclll/stb-vue-next@fb9738c · GitHub

两个 create render 函数:

  1. createRenderer(options)

  2. createHydrationRenderer(options)

    这个还不清楚是干什么的,通过代码观察貌似跟 SSR 有关,先搁置先不管。

两个函数最终都是调用的 baseCreateRenderer(options, createHydrationFns)

并且就是这个函数将近两千行~~~~

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options);
}

export function createHydrationRenderer(
  options: RendererOptions<Node, Element>
) {
  return baseCreateRenderer(options, createHydrationFunctions);
}

// implementation
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
) {
  // TODO
}

function list

feat(init): renderer -> baseCreateRenderer TODOs · gcclll/stb-vue-next@b7f55a8 · GitHub

baseCreateRenderer(options, createHydrationFns) 之所以这么长,是因为这里面包含 了三十几个函数的定义,下面将一个个按照流程逐一实现。

stepwhat?stepwhat?
options解构patchfunction
processText文本处理processCommentNode注释节点
mountStaticNode加载静态节点patchStaticNode-
moveStaticNode-removeStaticNode删除静态节点
processElement-mountElement-
setScopeId-mountChildren-
patchElement-patchBlockChildren-
patchProps-processFragment-
processComponent-mountComponent-
updateComponent-setupRenderEffect-
updateComponentPreRender-patchChildren-
patchUnkeyedChildren-patchKeyedChildren-
moveunmount
remove-removeFragment-
unmountComponent-unmountChildren-
getNextHostNode-render-
internalsobject, 上述函数别名createHydrationFns-

最后函数返回 { render, hydrate, createApp }

render(vnode, container)

feat(init): baseCreateRender-> render · gcclll/stb-vue-next@9f5b40b · GitHub

feat(init): baseCreateRender-> render with unmount · gcclll/stb-vue-next@d4e10d4 · GitHub

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const render: RootRenderFunction = (vnode, container) => {
  // render(h('div'), root)
  if (vnode == null) {
    if (container._vnode) {
      unmount(container._vnode, null, null, true);
    }
  } else {
    patch(container._vnode || null, vnode, container);
  }
  // 执行所有 post 异步任务
  flushPostFlushCbs();
  container._vnode = vnode;
};
  1. vnode 为空,且 conatainer 上有注册过 _vnode,组要进行卸载

    如: render(ref.value ? h('div') : null)

    ref.value = true 时候进入 else -> patch

    ref.value = false 时候进入 if -> unmount

  2. 否则执行 patch(),干什么了?

  3. flushPostFlushCbs() 此时组件应该 mounted 了,手动刷掉所有 post cbs 。

  4. 保存 _vnode,方便下次进入是检测

接下来,需要继续实现 unmount()patch()

patch(…args)

feat(init): baseCreateRender-> patch -> processElement · gcclll/stb-vue-next@eb48eb9 · GitHub

参数:

参数名描述
n1VNode, 老节点
n2VNode, 新节点
container容器
anchor?
parentComponent父级组件
parentSuspenseSuspense ?
isSVG?
optimized是否优化过?
  1. 检测节点类型是不是一样,如果不一样直接卸载老的

    因为类型都不一样了,可能整个🌲都发生了变化,直接卸载老的重新 patch 新的(n2)。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
      if (
        __DEV__ &&
        n2.shapeFlag & ShapeFlags.COMPONENT &&
        hmrDirtyComponents.has(n2.type as ConcreteComponent)
      ) {
        // HMR only: if the component has been hot-updated, force a reload.
        // 组件被热更新,强制重新加载
        return false;
      }
      return n1.type === n2.type && n1.key === n2.key;
    }
    
    • 组件发生了热更新(HMR启用情况下),强制重新加载组件

    • 同时判断 type 和 key,有可能 type 一样(比如: ul>li 同类型元素的删除移动操作)

  2. switch -> n2.type 根据类型不同走不同分支进行处理

    只支持的类型: Text|Comment|Static 节点类型

    组件类型(default 分支): ELEMENT/TELEPORT/COMPONENT/SUSPENSE

patch->processElement(…args)

feat(init): baseCreateRender-> patch -> processElement imp · gcclll/stb-vue-next@761db2b · GitHub

args 同 patch 的 args

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const processElement = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVB: boolean,
  isSVG: boolean,
  optimized: boolean
) => {
  isSVG = isSVG || (n2.type as string) === "svg";
  if (n1 == null) {
    // no old
    mountElement(
      n2,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      optimized
    );
  } else {
    patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized);
  }
};
  1. 没有 n1 老节点,直接 mount 新的 n2 节点

  2. 否则,进行 patch 操作

接下来按照 patchElement -> mountElement 顺序实现。

mountElement(…args)

进行到这里我们可以进行初步的判断, patch 和 mount 的区别,

patch: 非首次加载组件的时候,用 new 和 old vnode 节点进行比较然后对发生变更的 节点进行替换或更新操作。

mount: 属于首次加载组件的时候,属于重新创建节点的操作,不存在比较什么的一些操 作。

比如: render 里面的根据 vnode 来判断是 Unmount 还是 patch,以及 processElement 中根据 old vnode 来检测是不是有旧的节点(非首次)来判定是直接 Mount 组件还是 patch 比较更新组件。

default ELEMENT

feat(add): patch element · gcclll/stb-vue-next@81af385 · GitHub

render 函数实现, vnode 为空会进入卸载 unmount 流程,否则执行的是 patch ,这个应 该就是通过 vnode 节点结构执行 diff 和 dom 操作的入口了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const render: RootRenderFunction = (vnode, container) => {
    console.log('render.......xxx')
    // render(h('div'), root)
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      patch(container._vnode || null, vnode, container)
    }
    // 执行所有 post 异步任务
    flushPostFlushCbs()
    container._vnode = vnode
  }

注意上面的 flushPostFlushCbs() 是在 patch 之后执行的,也就是说 post cbs 会在组 件 mount/unmount 完成之后的下一个 tick 去执行的回调。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    optimized = false
  ) => {
    console.log('patching...')
    // 不同类型节点,直接卸载老的🌲
    if (n1 && !isSameVNodeType(n1, n2)) {
      // TODO
    }

    // TODO patch bail, 进行全比较(full diff)

    // 新节点处理
    const { type, ref, shapeFlag } = n2
    switch (type) {
      default:
        // ELEMENT/COMPONENT/TELEPORT/SUSPENSE
        // 默认只支持这四种组件
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        }
        break
    }

    if (ref != null && parentComponent) {
      // TODO set ref
    }
  }

patch 函数里面通过 switch 分支根据 ShapeFlags 的类型类调用对应的 processXxx 函数进行处理 old/new vnode 节点,而这里的 ShapeFlags 值的依据来自哪里?是在哪 里赋值的,由由什么作用? 。

这里以普通的 ELEMENT 标签作为切入点来实现一个完整的过程,这里需要用到 processElement 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const processElement = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    isSVG = isSVG || (n2.type as string) === 'svg'
    if (n1 == null) {
      // no old
      mountElement(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    } else {
      // 该阶段还不会到这里
      patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
    }
  }

这里就是个很简单 if…else 判断是不是有旧的节点,没有是 mount 有则是 patch 操作, 所以需要完成 mountElement

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const mountElement = (
    vnode: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    console.log('mount element...')
    let el: RendererElement
    let vnodeHook: VNodeHook | undefined | null
    const { type, shapeFlag, patchFlag, props } = vnode

    if (
      !__DEV__ &&
      vnode.el &&
      hostCloneNode !== undefined &&
      patchFlag === PatchFlags.HOISTED
    ) {
      // TODO
    } else {
      el = vnode.el = hostCreateElement(
        vnode.type as string,
        isSVG,
        props && props.is
      )
    }

    // hostInsert
    hostInsert(el, container, anchor)
  }

mountElement 里面两个核心的函数 hostCreateElementhostInsert 分别来自 baseCreateRender(option) 的 option 参数。

这里就需要深入了解 runtime-test 这个包,它是作用为了能测试 runtime-core 编写的 一个测试报,这里包含了一些列的 DOM 操作函数,这些函数也会在封装 render 的时候 传递给 baseCreateRender(option) ,所以上面的 hostElement 和 hostInsert 就是来 自 runtime-test这里链接可以跳转查看该包里面具体包含哪些函数,又是做什么的, 这里就不展开细讲,主要看下相关的两个函数实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function createElement(tag: string): TestElement {
  const node: TestElement = {
    id: nodeId++,
    type: NodeTypes.ELEMENT,
    tag,
    children: [],
    props: {},
    parentNode: null,
    eventListeners: null
  }
  // ... log
  // avoid test nodes from being observed
  markRaw(node)
  return node
}

function insert(child: TestNode, parent: TestElement, ref?: TestNode | null) {
  let refIndex
  if (ref) {
    refIndex = parent.children.indexOf(ref)
    if (refIndex === -1) {
      console.error('ref: ', ref)
      console.error('parent: ', parent)
      throw new Error('ref is not a child of parent')
    }
  }
  //...log
  // remove the node first, but don't log it as a REMOVE op
  remove(child, false)
  // re-calculate the ref index because the child's removal may have affected it
  refIndex = ref ? parent.children.indexOf(ref) : -1
  if (refIndex === -1) {
    parent.children.push(child)
    child.parentNode = parent
  } else {
    parent.children.splice(refIndex, 0, child)
    child.parentNode = parent
  }
}

所以说,截至目前还并没有涉及到实际的 DOM 操作,还只是在 vnode 结构上进行插入删除 操作。

这里开始应该可以测试了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const { log, runtime_test } = require(process.env.BLOG_DIR_VUE + "/lib.js");
log("xx");
runtime_test().then(
  ({ h, render, nodeOps, serializeInner: inner }) => {
    let root = nodeOps.createElement("div");
    log('>>> root ast, 这里 children 里面应该还没有节点')
    log.f(root, ['type', 'children'])
    log.f(h("div"), ["type"]);
    log('>>> begin render...')
    render(h("div"), root);
    log('>>> after seririlize inner')
    log(inner(root), ['type', 'children']);
  },
  (e) => console.log(e.message)
);
xx
undefinedfalse
>>> root ast, 这里 children 里面应该还没有节点
{ type: 'element', children: [] }
{ type: 'div' }
>>> begin render...
render.......xxx
patching...
mount element...
mountElment else...
el = vnode.el = hostCreateElement =  {
  id: 1,
  type: 'element',
  tag: 'div',
  children: [],
  props: {},
  parentNode: null,
  eventListeners: null
}
<ref *1> {
  id: 0,
  type: 'element',
  tag: 'div',
  children: [
    {
      id: 1,
      type: 'element',
      tag: 'div',
      children: [],
      props: {},
      parentNode: [Circular *1],
      eventListeners: null
    }
  ],
  props: {},
  parentNode: null,
  eventListeners: null
}
>>> after seririlize inner
<div></div>

注意看上面的结果,最后 h('div') 生成的节点别 insert 进了 root.children 中, 然后注意 insert 最后的实现插入替换部分: 当没有找到时 refIndex = -1,直接执行 尾部插入操作 push(...), 如果找到了就执行 splice(refIndex, 1, child)

所以这里直接执行的是直接尾部插入操作。

最后输出的 <div></div> 是由于调用了 serializeInner(root) 结果,也是相当于 DOM 操作了(serializeInner -> seririlize>children -> serializeElement -> 最后根据 tag, props, children 递归解析生成对应的 DOM 元素结构)。

serializeElement 实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function serializeElement(
  node: TestElement,
  indent: number,
  depth: number
): string {
  const props = Object.keys(node.props)
    .map(key => {
      const value = node.props[key]
      return isOn(key) || value == null
        ? ``
        : value === ``
          ? key
          : `${key}=${JSON.stringify(value)}`
    })
    .filter(Boolean)
    .join(' ')
  const padding = indent ? ` `.repeat(indent).repeat(depth) : ``
  return (
    `${padding}<${node.tag}${props ? ` ${props}` : ``}>` +
    `${serializeInner(node, indent, depth)}` +
    `${padding}</${node.tag}>`
  )
}

所以到此应该是完成了最普通的 ELEMENT 类型元素从

ast -> compiler-dom >> compiler-core >> compiler-sfc vnode -> runtime-core >> runtime-test(测试用) render -> runtime-core >> baseCreateRender >> render >> mount/unmount/patch -> 生成 DOM 元素结构较为完整的代码。

with props

feat(add): baseRenderer->element with props · gcclll/stb-vue-next@46fc2a0 · GitHub

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const { log, f, runtime_test } = require(process.env.BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  ({ h, render, nodeOps, serializeInner: inner }) => {
    const root = nodeOps.createElement("div");
    render(h("div", { id: "foo", class: "bar" }), root);
    log(inner(root));
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedfalse
render.......
patching...
mount element...
mountElment else...
<div id="foo" class="bar"></div>

最后输出结果: <div id="foo" class="bar"></div>

还记得 runtime-core > h function 一节我们详细描述了 h 函数的用法,这里简单回顾下

h 第二个参数描述
普通对象当做 props 处理
数组类型当做 children 处理
是个 VNode 类型对象带有 __v_isVNode = true 属性, [vnode] 当做 children 处理

所以上面的 { id: 'foo', class: 'bar' } 被当做属性传递给 createVNode(type, props, children ...) 函数

新增代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// mountElement 增加 props 处理逻辑
const mountElement = (
    vnode: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    console.log('mount element...')
    // TODO
    let el: RendererElement
    let vnodeHook: VNodeHook | undefined | null
    const { type, shapeFlag, patchFlag, props } = vnode

    if (
      !__DEV__ &&
      vnode.el &&
      hostCloneNode !== undefined &&
      patchFlag === PatchFlags.HOISTED
    ) {
      // ...
    } else {


      // 新增 start
      if (props) {
        for (const key in props) {
          // vue 保留属性 ref/key/onVnodeXxx 生命周期
          if (!isReservedProp(key)) {
            hostPatchProp(
              el,
              key,
              null,
              props[key],
              isSVG,
              vnode.children as VNode[],
              parentComponent,
              parentSuspense,
              unmountChildren
            )
          }
        }

        if ((vnodeHook = props.onVnodeBeforeMount)) {
          // 执行 before mount hook
          invokeVNodeHook(vnodeHook, parentComponent, vnode)
        }
      } // end 新增
    }


    // ...

  }

render -> patch -> case ELEMENT -> processElement -> mountElement

在 mountElement 中增加 props 处理逻辑,针对每个 prop 检测是不是保留名字

key/ref/onVnodeXxx 等生命周期名,非保留名字才需要处理,调用 hostPatchProp() 处 理,后面加上 BeforeMount 生命周期钩子函数调用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// runtime-test/src/patchProp.ts
export function patchProp(
  el: TestElement,
  key: string,
  prevValue: any,
  nextValue: any
) {
  logNodeOp({
    type: NodeOpTypes.PATCH,
    targetNode: el,
    propKey: key,
    propPrevValue: prevValue,
    propNextValue: nextValue
  })
  el.props[key] = nextValue
  if (isOn(key)) {
    const event = key.slice(2).toLowerCase()
    ;(el.eventListeners || (el.eventListeners = {}))[event] = nextValue
  }
}

普通属性直接更新到 el.props 中,如果是 onXxx 类型的事件,取出 xxx 作为 el.eventListeners 的 key 将事件名和其处理句柄保存起来。

这里的 el 实际上是个 ast 结构类型的对象,保存这每个节点的所有信息。

with text children

纯文本单节点 child

将纯文本做为 child ,将会被 h 函数转成 [child] 传递给 createVNode(type, props, children, ...) 做为它的children 参数处理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const { log, f, runtime_test } = require(process.env.BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  ({ h, render, nodeOps, serializeInner: inner }) => {
    const _root = tag => nodeOps.createElement(tag || "div")
    log('>>> 纯文本作为 children')
    const r1 = _root()
    render(h('div', 'pure test as children'), r1);
    log(inner(r1));
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedfalse
>>> 纯文本作为 children
render.......
patching...
mount element...
mountElment else...
<div>pure test as children</div>

上面示例是将纯文本作为 children 去渲染进 root 节点,涉及代码修改(mountElement()):

feat(add): pure text as children to render · gcclll/stb-vue-next@43b868e · GitHub

/img/tmp/diff-mountElement.png

数组类型(多个) children:

feat(add): render->array children · gcclll/stb-vue-next@e6a5e61 · GitHub

当 h(type, propsOrChildren) 第二个参数为数组时会被当做 children 给 createVNode

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const { log, f, runtime_test } = require(process.env.BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  ({ h, render, nodeOps, serializeInner: inner }) => {
    const root = nodeOps.createElement("div");
    render(h("div", ["foo", " ", "bar"]), root);
    log(inner(root));
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedfalse
render.......
patching...
{ type: 'div', shapeFlag: 17 }
xxxx
case default...
process element...
mount element...
mountElment else...
patching...
{ type: Symbol(Text), shapeFlag: 8 }
process text...
patching...
{ type: Symbol(Text), shapeFlag: 8 }
process text...
patching...
{ type: Symbol(Text), shapeFlag: 8 }
process text...
<div>foo bar</div>

从上面的输出可得出 render(h('div'), ['foo', ' ', 'bar']), root) 大概执行流程:

  1. root->div

    • render, 根据 vnode 为空检测决定是 unmount 还是 patch

    • patch, 根据 new vnode 的 type(四种类 型 Text|Comment|Fragment|Static|default ) 决 定调用什么 processXxx 进行处理

    • case default 由于这里是根节点,且是 'div' 普通类型元素,进入 processElement

    • processElement, 根据 old vnode 判断是 mount 还是 patch 操作

    • 无 old vnode, 没有旧的vnode表示是新节点,需要执行 mount 操作

    • mountElement, 需要检测 vnode.el 来判断是不是静态提升的节点,如果是静态节点 属于可复用的节点,需要 cloneVNode 出来使用,否则创建新的

    • else: hostCreateElement 创建新的元素,然后通过 shapeFlag 判断 children 是什么类型进入不同分支进行处理,这里是数组(ShapeFlags.ARRAY_CHILDREN) 所 以会调用 mountChildren(vnode.children, el, ...) 开始 mount children.

    • mountChldren , 会对 children 进行遍历,如果 child.el 存在说明是可复用节点 (静态提升的),则将 child clone 出来使用,否则进行 normailize 处理(其实也就 是根据 child 数据类型不同执行 createVNode 返回新的 vnode 给 child),最后将 child 传入 patch 回到第 二步进行递归 mount children

  2. root->div->'foo'

    1 最后进入递归之后,会进入到 patch 检测到 type 是 Text 类型,去调用 processText() 处理 'foo' 完成之后,再回溯递归处理下一个元素 ' ' 直到结束。

  3. root->div->' ' 同 2

  4. root->idv->'bar' 同 2

涉及修改内容(renderer.ts -> baseCreateRender):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// patch()
// 增加 Text 类型分支处理 children: ['foo', ' ', 'bar']
switch (type) {
  case Text:
    processText(n1, n2, container, anchor);
    break;
}

// 新增 processText(n1, n2, container, anchor)
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
  console.log("process text...");
  if (n1 == null /* old */) {
    // 新节点,插入处理
    hostInsert(
      (n2.el = hostCreateText(n2.children as string)),
      container,
      anchor
    );
  } else {
    // has old vnode, need to diff
  }
};

// hostInert -> 将 child insert 到 container.children 中去
// hostCreateText -> 创建 TEXT 类型的节点结构
// runtime-test/src/nodeOps.ts -> createText
const node: TestText = {
  id: nodeId++,
  type: NodeTypes.TEXT,
  text,
  parentNode: null,
};

// processElement -> mountElement 增加 ARRAY_CHILDREN
// 分支处理, mountChildren
/* else */ if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  mountChildren(
    vnode.children as VNodeArrayChildren,
    el,
    null,
    parentComponent,
    parentSuspense,
    isSVG && type !== "foreignObject",
    optimized || !!vnode.dynamicChildren
  );
}

// mountChildren 遍历 vnode.children
// 递归调用 patch() 处理每个 child
// cloneIfMounted 是需要优化(静态提升的节点),可复用的节点
// 将其 clone 出一份新的 vnode 出来使用
// normailizeVNode 是根据 child 的数据类型不同执行 createVNode 返回
// 新的 vnode 或 child 本身(vnode.el 存在的情况,被复用了)
const mountChildren: MountChildrenFn = (
  children,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized,
  start = 0
) => {
  for (let i = start; i < children.length; i++) {
    const child = (children[i] = optimized
      ? // 这里是检测 child.el 是不是存在,如果存在则是可服用的 vnode
        // 即需要提升的静态节点,则需要进行 cloneVNode 之后返回
        // 新的 vnode 对象
        cloneIfMounted(children[i] as VNode)
      : // 根据 child 的类型进行拆分处理
        // 1. boolean, 创建一个空的 Comment
        // 2. array, 使用 Fragment 将 child 包起来
        // 3. object, 如果是对象,child.el 存在与否进行 clone
        // 4. 其他情况,字符串或数字,当做 Text 类型处理
        normalizeVNode(children[i]));
    // 然后进入 patch 递归处理 children
    patch(
      null,
      child,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      optimized
    );
  }
};

// cloneIfMounted 是检测 vnode.el 是不是存在,如果存在说明有复用的情况
// 针对 template-compiled render fns 做的优化
export function cloneIfMounted(child: VNode): VNode {
  // child.el 如果存在的话,child 属于静态节点会被静态提升
  // 所以需要 clone 一份出来,否则直接返回 child
  return child.el === null ? child : cloneVNode(child);
}

children+props 混合测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const { log, f, runtime_test } = require(process.env.BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  ({ h, render, nodeOps, serializeInner: inner }) => {
    const root = nodeOps.createElement("div");
    render(h("div", { id: "foo", class: 'baz' }, ["bar", ' ', h('div')]), root);
    log(inner(root));
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedfalse
render.......
patching...
{ type: 'div', shapeFlag: 17 }
xxxx
case default...
process element...
mount element...
mountElment else...
patching...
{ type: Symbol(Text), shapeFlag: 8 }
process text...
patching...
{ type: Symbol(Text), shapeFlag: 8 }
process text...
patching...
{ type: 'div', shapeFlag: 1 }
xxxx
case default...
process element...
mount element...
mountElment else...
<div id="foo" class="baz">bar <div></div></div>

小结

执行流程:

render() -> vnode !== null -> patch() -> switch case ->

default: processElement() -> 由于是首次加载 old vnode 为 null ->

所以执行 mountElement() 新创建元素进行 mount 操作。

mountElement() 里面区分是否是可复用组件(HOISTED, 静态提升的组件),通过检测 vnode.el 是否有值,因为如果曾经被使用过必定会进入 mountElement -> else 对 vnode.el 进行赋值操作。

如果是可复用的组件,直接 clone 一份新的 vnode 出来使用,否则进入 else 分支 createElement 创建新的节点 el = vnode.el = hostCreateElement(...)

mountElement 中优先对 children 进行 mount,然后处理 props ,因为有些时候 props 需要依赖 children 是不是加载完成了,比如: <option value> 元素,需要根据 value 最终的值选择使用哪个 child(前提是这个 child 必须已经加载完成了) 。

children 的处理,有两个类型分支处理(TEXT_CHILDRENARRAY_CHILDREN),为什么 只有两个呢?

这是因为在 createVNode() 函数中会调用 normalizeChildren()children 进行 检测,分几种情况处理:

children 类型type(ShapeFlags)描述
ArrayARRAY_CHILDREN-
ObjectSLOTS_CHILDREN区分是 ELEMENT/TELEPORT 或其他类型
FunctionSLOTS_CHILDREN函数直接当做插槽处理
StringNumberTEXT_CHILDREN当做文本处理

上面有个插槽类型,还记得 compiler-core 里面对插槽的编译结果吗?

compiler-core 阶段对 slot标签和 v-slot 的解析源码分析 ->

大致解析结果就是组件内的所有元素按照一定的规则解析成插槽,最后生成的 render 函数 大概是:

1
2
3
4
5
6
return (_openBlock(), _createBlock('Comp', null /* props */, {
  // 默认插槽
  defualt: _withCtx(() => [ /* ...slot children... */ ]),
  [named]: _withCtx(() => [/* 动态具名插槽 */]),
  name: _withCtx(() => [/* 具名插槽 */]),
}))

所以当 children 是个对象的时候在 createVNode() -> normalizeChildren() 中会被 当做插槽来处理。

patchElement

render() -> patch() -> processElement() ->

当检测到 old vnode 存在的时候会进入到这个函数 patchElement() 进行更新操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const patchElement = (
    n1: VNode,
    n2: VNode,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    // 旧的 el 替换掉新的 el ?
    // const el = (n2.el = n1.el!)
    let { patchFlag, dynamicChildren } = n2
    // #1426 take the old vnode's patch flag into account since user may clone a
    // compiler-generated vnode, which de-opts to FULL_PROPS
    patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS
    // const oldProps = n1.props || EMPTY_OBJ
    // const newProps = n2.props || EMPTY_OBJ

    // TODO before update hooks

    // TODO dirs, 指令处理

    // TODO HRM updating

    if (patchFlag > 0) {
      console.log(`patch flag > 0 ? ${patchFlag}`)
    } else if (!optimized && dynamicChildren == null) {
      console.log({ optimized, patchFlag })
    }

    // const areaChildrenSVG = isSVG && n2.type !== 'foreignObject'

    if (dynamicChildren) {
      console.log('dynamic children...')
    } else if (!optimized) {
      console.log('optimized null, 非可复用节点')
    }

    // TODO vnode hook or dirs 处理
  }

先做个测试,看下代码执行流程(patchElement() 里面加了点打印):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const { log, f, shuffle, toSpan: _toSpan, runtime_test } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  ({ h, render, nodeOps, serializeInner: inner }) => {
    let elm
    let root = nodeOps.createElement("div");
    const toSpan = (v) => _toSpan(v, h);
    const renderChildren = (arr) => {
      // 给 root>div 中插入 children
      // <div><span>1</span>...</div>
      render(h("div", arr.map(toSpan)), root);
      return root.children[0];
    };
    // root 上挂一个 '<div id="1">hello</div>'
    render(h("div", { id: 1 }, "hello"), root);

    // 增加一个 <span>1</span>
    elm = renderChildren([1])

    log(`elm.children.length = ${elm.children.length}`)
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedfalse
render()...
patch()...
processElement()...
mountElement()...
mountElment else...
render()...
patch()...
processElement()...
patchElement()...
{ optimized: false, patchFlag: 0 }
optimized null, 非可复用节点
patchChildren()...
patchChildren, 非 text children
patchChildren, 非 text children, 非 array children...
elm.children.length = 1

从上面的结果可知我们该阶段需要实现的部分代码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// patchElement()
// 这里是 props 处理
/* else */ if (!optimized && dynamicChildren == null) {
  console.log({ optimized, patchFlag });
  // TODO patchProps
}

// 和
/* else */ if (!optimized) {
  console.log("optimized null, 非可复用节点");
}

// patchChildren()
/* else */ {
  console.log("patchChildren, old 非 text children");

  /* else */ {
    console.log(
      "patchChildren, old 非 text children, new 非 array children..."
    );
    // prev children was text or null
    // new children is array or null
    // 老的 children 是 text,新的又是数组情况
    if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
      // 先清空?
      hostSetElementText(container, "");
    }
    // 然后直接重新加载新的 array children -> c2
    // old children 是 array
    if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      mountChildren(
        c2 as VNodeArrayChildren,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      );
    }
  }
}

重新测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const { log, f, shuffle, toSpan: _toSpan, runtime_test } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  ({ h, render, nodeOps, serializeInner: inner }) => {
    let elm
    let root = nodeOps.createElement("div");
    const toSpan = (v) => _toSpan(v, h);
    const renderChildren = (arr) => {
      // 给 root>div 中插入 children
      // <div><span>1</span>...</div>
      render(h("div", arr.map(toSpan)), root);
      return root.children[0];
    };
    // root 上挂一个 '<div id="1">hello</div>'
    render(h("div", { id: 1 }, "hello"), root);

    // 增加一个 <span>1</span>
    elm = renderChildren([1])

    log(`elm.children.length = ${elm.children.length}`)
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedfalse
render()...
patch()...
processElement()...
mountElement()...
mountElment else...
render()...
patch()...
processElement()...
patchElement()...
{ optimized: false, patchFlag: 0 }
optimized null, 非可复用节点
patchChildren()...
patchChildren, old 非 text children
patchChildren, old 非 text children, new 非 array children...
patch()...
processElement()...
mountElement()...
mountElment else...
elm.children.length = 1

因此到这里将会进入 patchChildren(n1, n2, …) 去解析 "hello" 这个文本孩子节点。

feat(add): patchElement->patchChildren · gcclll/stb-vue-next@26d2bfd

patchChildren(n1,n2,…)

/img/vue3/runtime-core/vue-runtime-core-renderer-patchChildren.jpg

简化代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const patchChildren: PatchChildrenFn = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized = false) {
  if (patchFlag > 0) {
    if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
      // 针对有 key 属性被 fragment 包裹起来的元素(例如: v-for)
      // ... patchKeyedChildren(...)
      return
    } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
      // ... patchUnkeyedChildren(...)
      return
    }
  }

  // children 有三种可能,text, array, 或没有孩子节点
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // text children
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // 如果是数组,直接 unmount 掉
      // unmountChildren(c1, ...)
    }
    if (c2 !== c1) {
      // hostSetElementText(container, c2) 直接替换文本
    }
  } else {
    // 非文本节点处理
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN)  {
      // 之前的 children 是 数组类型
      if (shapeFlag & shapeFlag.ARRAY_CHILDREN) {
        // 新的也是数组,直接进行 full diff
        // patchKeyedChildren(...)
      } else {
        // 到这里表示没有新的孩子节点,等价于删除操作,直接卸载老的就行
        // unmountChildren(c1, ...)
      }
    } else {
      // 这种情况,old 是 text | null
      // 新的是数组或 null
      if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN){
        // 先清空老的文本节点
        // hostSetElementText(container, '')
      }

      // 如果新的是数组,直接 mount,因为之前的如果是文本在上面已经清空了
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // mountChildren(c2, container, ...)
      }
    }
  }
}

所以总结下来有几种情况的组合:

  1. 首先是 patchFlag > 0 情况,需要局部 diff update(比如: v-for),这里需要区分是 否有 key 属性

    1. keyed: patchKeyedChildren(c1, c2, …)

    2. unkeyed: patchUnkeyedChildren(c1, c2, …)

  2. 到这里 patchFlag <= 0 ,需要进行 full diff 的情况

    这种情况下只有三种可能的 children: text|array|null

    这三种情况结合 old + new 有多重组合需要考虑。

    1. new text + old array: 直接卸载 old array, 将 parent 内容设置成 new text

    2. new array + old array: 当做 keyed children 调用 patchKeyedChildren(c1, c2, …) 处理

    3. new null + old array: 直接卸载 old array(unmountChildren(c1, …))

    4. new array + old null: 直接 mount new array(mountChildren(c2, …))

这里涉及到几个相关函数:

patchKeyedChildren(c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized)

patchUnkeyedChildren(c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized)

unmountChildren(children, parentComponent, parentSuspense, doRemove, optimized, start)

mountChildren(children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start)

根据上面的分析,会逐一实现各种情况。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
digraph G{
    rankdir=LR;
	  node[shape=box, style=filled, color=".7.3 1.0"];//一个node的属性
	  size = "6, 4";//图片大小
    patch->processElement
    processElement->n1[label="节点已存在?"]
    n1[shape=diamond];
    n1->mountElement[label="no"]
    n1->patchElement[label="yes"]
    mountElement->patch[style=dotted, color = red]
}

/img/tmp/jIpIofqUV448p8A.png

new text + old array

feat(add): new text + old array · gcclll/stb-vue-next@7019c9d

patchChildren: 先 unmountChildren(c1) -> hostSetElementText(container, c2)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// children 有三种可能: text, array, 或没有 children
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  console.log("patchChildren, new text...");
  // text children fast path
  if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    unmountChildren(c1 as VNode[], parentComponent, parentSuspense);
  }

  if (c2 !== c1) {
    hostSetElementText(container, c2 as string);
  }
}

unmountChildren(…) -> 遍历 children 调用 unmount(children[i], ..)

unmount(vnode, …) 中递归调用 unmountChildren(children, …)

但是这部分逻辑自始至终 doRemove 都是 false,所以不会执行 doRemove: remove(vnode), 因为如上面的代码,在 c1 !== c2 的时候执行了 hostSetElementText(container, c2)这 里面首先会直接清空 container.children 然后重新赋值,因此 remove(vnode) 没有执 行也会实现直接替换操作,这里属于 full diff。

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const { log, f, shuffle, runtime_test } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  ({ h, render, nodeOps, serializeInner: inner }) => {
    let elm
    let root = nodeOps.createElement("div");

    // root 上挂一个
    // '<div id="1"><span>1</span><span>2</span></div>'
    render(h("div", { id: 1 }, [ // #1
      h('span', '1'),
      h('span', '2')
    ]), root);

    // 增加一个 <span>1</span>
    log(['1. div children length = ', root.children[0].children.length])

    render(h('div', { id: 1 }, 'hello'), root) // #2
    log(['2. div children length = ', root.children[0].children.length])
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedfalse
render()...
patch()...
processElement()...
mountElement()...
mountElment else...
patch()...
processElement()...
mountElement()...
mountElment else...
patch()...
processElement()...
mountElement()...
mountElment else...
1. div children length =  2
render()...
patch()...
processElement()...
patchElement()...
{ optimized: false, patchFlag: 0 }
optimized null, 非可复用节点
patchChildren()...
patchChildren, new text...
2. div children length =  1

如上结果,最开始有三个递归:

patch() -> processElement() -> mountElement() -> patch()

1
2
3
4
5
6
7
8
digraph G{
        rankdir=LR;
	size = "6, 4";//图片大小
	node[shape = box, style = filled, color = ".7.3 1.0"];//一个node的属性
    patch->processElement;
    processElement->mountElement;
    mountElement->patch[style = dotted, color = red]
}

/img/tmp/J0XD2pb6FUK5Rs8.png

#1 渲染过程中,分别处理 div -> span 1 -> span 2

#2 渲染过程中,属于 full diff 操作,检测到 old array, new text,所以直接清空 了 div.children~,然后复制 ~div.children = [text node]

new null + old array

feat(add): patchChildren -> patch new null, old array · gcclll/stb-vue-next@3f45ac5

新增代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    console.log("patchChildren, new array, old array...");
    // TODO patchKeyedChildren
  } else {
    // new null, old array 直接卸载 old
    unmountChildren(
      c1 as VNode[],
      parentComponent,
      parentSuspense,
      true /* doRemove */
    );
  }
}

如果 new null 直接卸载 old array 就好了,注意第四个参数传的是 doRemove:true 这 样 unmount() 里面就会去调用 remove()

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const { log, f, shuffle, runtime_test } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  ({ h, render, nodeOps, serializeInner: inner }) => {
    let elm
    let root = nodeOps.createElement("div");

    // root 上挂一个
    // '<div id="1"><span>1</span><span>2</span></div>'
    render(h("div", { id: 1 }, [ // #1
      h('span', '1'),
      h('span', '2')
    ]), root);

    // 增加一个 <span>1</span>
    log(['1. div children length = ', root.children[0].children.length])

    render(h('div', { id: 1 }, null), root) // #2
    
    log(['2. div children length = ', root.children[0].children.length])
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedfalse
render()...
patch()...
processElement()...
mountElement()...
mountElment else...
patch()...
processElement()...
mountElement()...
mountElment else...
patch()...
processElement()...
mountElement()...
mountElment else...
1. div children length =  2
render()...
patch()...
processElement()...
patchElement()...
{ optimized: false, patchFlag: 0 }
optimized null, 非可复用节点
patchChildren()...
patchChildren, new not text...
patchChildren, new null, old array...
2. div children length =  0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
digraph G{
    rankdir=LR;
	size = "6, 4";//图片大小
	node[shape = box, style = filled, color = ".7.3 1.0"];//一个node的属性
    render2,render1[shape=circle,fillcolor=red]
    render1->patch[label="two span"];
    patch->processElement;
    processElement->mountElement[label="节点不存在"];
    processElement->patchElement[label="节点存在",color="blue"];
    mountElement->patch[style=dotted, color=red]
    render2->patch[label="null",color="blue"];
    patchElement->patchChildren[color="blue"];
    patchChildren->unmountChildren[label="new null",color="blue"]
    
}

/img/tmp/I666vpKZVdkMluo.svg

new array + old null/text

这种情况,如果是 old text,会先执行

1
2
3
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
  hostSetElementText(container, "");
}

将 conteiner.children 清空。

然后执行 mountChidren(c2) 插入新的 array node 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// prev children was text OR null
// new children is array OR null
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
  hostSetElementText(container, "");
}
// mount new if array
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  mountChildren(
    c2 as VNodeArrayChildren,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  );
}

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const { log, f, shuffle, runtime_test } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  ({ h, render, nodeOps, serializeInner: inner }) => {
    let elm
    let root = nodeOps.createElement("div");

    // root 上挂一个
    // '<div id="1"><span>1</span><span>2</span></div>'
    render(h("div", { id: 1 }, null), root);

    // 增加一个 <span>1</span>
    log(['1. div children length = ', root.children[0].children.length])

    render(h('div', { id: 1 }, [ // #1
      h('span', '1'),
      h('span', '2')
    ]), root) // #2

    log(['2. div children length = ', root.children[0].children.length])
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedfalse
render()...
patch()...
processElement()...
mountElement()...
mountElment else...
1. div children length =  0
render()...
patch()...
processElement()...
patchElement()...
{ optimized: false, patchFlag: 0 }
optimized null, 非可复用节点
patchChildren()...
patchChildren, new not text...
patchChildren, old text | null...
patchChildren, new array...
patch()...
processElement()...
mountElement()...
mountElment else...
patch()...
processElement()...
mountElement()...
mountElment else...
2. div children length =  2

new array + old array

patchKeyedChildren

/img/vue3/runtime-core/vue-runtime-core-diff.svg

feat(add): patchKeyedChildren · gcclll/stb-vue-next@4a6a1f2

代码中列出了几种可能的情况:

  1. old, new nodes 开头相同,从左到右方向以不同位置为起点开始比较

  2. old, new nodes 结尾相同,从右到左方向以不同位置为起点开始比较

  3. old ⊂ new,old 为 new 的真子集,这种情况视为新增节点,需要对新增的节点进行 mount 操作

  4. old ⊃ new , new 为 old 的真子集,这种情况视为删除节点,需要对多余的节点进行 unmount 操作

  5. old,new 没有特别明显的规律可遵循的,处理起来会比较麻烦

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 24. 可能所有都是 keyed 也可能部分
const patchKeyedChildren = (
  c1: VNode[],
  c2: VNodeArrayChildren,
  container: RendererElement,
  parentAnchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  optimized: boolean
) => {
  let i = 0;
  const l2 = c2.length;
  let e1 = c1.length - 1; // 上一个结束索引
  let e2 = l2 - 1; // 下一个结束索引

  // 1. sync from start
  // (a b) c
  // (a b) d e
  while (i <= e1 && i <= e2) {
    // TODO
  }

  // 2. sync from end
  // a (b c)
  // d e (b c)
  while (i <= e1 && i <= e2) {
    // TODO
  }

  // 3. common sequence + mount
  // (a b)
  // (a b) c
  // i = 2, e1 = 1, e2 = 2
  // (a b)
  // c (a b)
  // i = 0, e1 = -1, e2 = 0
  if (i > e1) {
    // TODO
  }

  // 4. common sequence + unmount
  // (a b) c
  // (a b)
  // i = 2, e1 = 2, e2 = 1
  // a (b c)
  // (b c)
  // i = 0, e1 = 0, e2 = -1
  else if (i > e2) {
    // TODO
  }

  // 5. unknown sequence, 未知序列
  // [i ... e1 + 1]: a b [c d e] f g
  // [i ... e2 + 1]: a b [e d c h] f g
  // i = 2, e1 = 4, e2 = 5
  else {
    // TODO
  }
};

下面来一个个实现,揭开 diff -> patch 的神秘面纱!!!

在进行之前先看下一个函数 isSameVNodeType(n1,n2)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  if (
    __DEV__ &&
    n2.shapeFlag & ShapeFlags.COMPONENT &&
    hmrDirtyComponents.has(n2.type as ConcreteComponent)
  ) {
    // HMR only: if the component has been hot-updated, force a reload.
    // 组件被热更新,强制重新加载
    return false
  }
  return n1.type === n2.type && n1.key === n2.key
}

这个函数用来检测两个节点是不是类似节点(需同时满足 type 和 key 相同)。

有点复杂,整的头疼🤕🤕。。。休息会😴😴!!!

[2021-02-24 18:16:56] 通过画图终于把这块逻辑搞得有点清楚了!!!

剧情有点复杂,还是根据官方的测试用例来逐步熟悉各种情况的 diff -> patch 吧。

声明:

  1. 所有 children [1,2,3] 都将自身值作为节点的属性 key 值

  2. 下面的所有用例都基于节点有 key 属性为前提

fix: patchKeydChildren if · gcclll/stb-vue-next@b7edc1b

大致移动规则流程图:

/img/vue3/vue-router/vue-runtime-core-diff-move-rules.svg

append([1] -> [1,2,3])

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const { log, f, shuffle, runtime_test, renderChildren } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  ({ h, render, nodeOps, serializeInner: inner }) => {
    let elm;
    let root = nodeOps.createElement("div");
    // <div>hello</div>
    render(h("div", { id: 1 }, "hello"), root);
    const rc = (arr) => renderChildren(render, root, h, arr);
    const logRoot = () => log("root: " + inner(root));

    logRoot();
    elm = rc([1]);
    log(">>> render [1] DONE.");
    logRoot();

    elm = rc([1, 2, 3]);
    log(">>> render [1,2,3] DONE.");
    logRoot();
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedfalse
root: <div id=1>hello</div>
>>> render [1] DONE.
root: <div id=1><span>1</span></div>
patchKeyedChildren...
while 1, sync from start...
patch keyed 新增 ...
>>> render [1,2,3] DONE.
root: <div id=1><span>1</span><span>2</span><span>3</span></div>

如上结果,当执行 patchChildren 的时候,由于 old array , new array 所以会执行 patchKeyedChildren 对两个 array 进行对比更新。

while 1: 从左到右对同类型的 VNode 进行 patch ,所以这里 1 节点会在这里被 patch 掉 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
while (i <= e1 && i <= e2) {
  console.log("while 1, sync from start...");
  const n1 = c1[i];
  const n2 = (c2[i] = optimized // 静态节点
    ? cloneIfMounted(c2[i] as VNode)
    : normalizeVNode(c2[i]));

  // type & key 相同
  if (isSameVNodeType(n1, n2)) {
    patch(
      n1,
      n2,
      container,
      null,
      parentComponent,
      parentSuspense,
      isSVG,
      optimized
    );
  } else {
    break;
  }
  i++;
}

然后: i=1,e1=0,e2=2 满足 if(i>e1) 新增节点条件,对 [2,3] 进入新增节点逻辑 代码(if 分支)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
if (i > e1) {
  console.log("patch keyed 新增 ...");
  if (i <= e2) {
    const nextPos = e2 + 1;
    const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor;
    while (i <= e2) {
      patch(
        null,
        (c2[i] = optimized
          ? cloneIfMounted(c2[i] as VNode)
          : normalizeVNode(c2[i])),
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG
      );
      i++;
    }
  }
}

针对 [2,3] 分别执行:

patch(null, c2[i], container, anchor,...)

注意这里 anchor[3] 这个节点,但是由于在 container.children 是不存在的, 所以对于 [2] 会执行 append 操作(具体请查看 runtime-test/src/nodeOpts.ts:insert 函数实现)

直到全部 append 到 container.children 结束。

old: [1], new: [1,2,3]

这种情况还是比较简单的,直接 append 2,3 就行了。

prepend([4,5]->[1,2,3,4,5])

实例分析: n1=[4,5], n2=[1,2,3,4,5] 经过 while1 什么都没做,经过 while2 同化掉 尾部 [4,5],i=0,e1=-1,e2=2= 满足 if(i>e1)&&if(i<e2) 属于新增节点操作,插入时 的参考节点为 c2[e2+1] ,之前分析过如果需要经过前两个 while 处理的节点都会在 patch 的过程中直接替换掉,比如这里的 [4,5] 会在 while2 中被替换掉(体现在 container.children 中),新的 [4,5] 替换掉老的 [4,5] ,所以这里发生插入时的 anchor 实际是对应 container.children 中的 [4] 位置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const { log, f, shuffle, runtime_test, renderChildren } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  ({ h, render, nodeOps, serializeInner: inner }) => {
    let elm;
    let root = nodeOps.createElement("div");
    // <div>hello</div>
    render(h("div", { id: 1 }, "hello"), root);
    const rc = (arr) => renderChildren(render, root, h, arr);
    const logRoot = () => log("root: " + inner(root));

    logRoot();
    elm = rc([4, 5]);
    log(">>> render [4,5] DONE.");
    logRoot();

    elm = rc([1, 2, 3, 4, 5]);
    log(">>> render [1,2,3,4,5] DONE.");
    logRoot();
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedfalse
root: <div id=1>hello</div>
>>> render [4,5] DONE.
root: <div id=1><span>4</span><span>5</span></div>
patchKeyedChildren...
while 1, sync from start...
while 2, sync from end...
while 2, sync from end...
patch keyed 新增 ...
>>> render [1,2,3,4,5] DONE.
root: <div id=1><span>1</span><span>2</span><span>3</span><span>4</span><span>5</span></div>

上面两次 while 2 分别对应的是 5->4 同化过程。

同类用例,不做多余分析了,直接看结果吧!

  1. [1,2,4,5][1,2,3,4,5] 经过 while1(替换12) 和 while2(替换45) 之后 i=2,e1=1,e2=2 满足~if(i>e1)&&if(i<=e2)~ 插入操作,参考节点: anchor=c2[e2+1]=4 所以执行 patch时候会在 4(因为 anchor 有值) 之前插入 3 。

insert begin&end([2,3,4]->[1,2,3,4,5])

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const { log, f, shuffle, runtime_test, renderChildren } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  ({ h, render, nodeOps, serializeInner: inner }) => {
    let elm;
    let root = nodeOps.createElement("div");
    // <div>hello</div>
    render(h("div", { id: 1 }, "hello"), root);
    const rc = (arr) => renderChildren(render, root, h, arr);
    const logRoot = () => log("root: " + inner(root));

    logRoot();
    elm = rc([2,3,4]);
    log(">>> render [2,3,4] DONE.");
    logRoot();

    elm = rc([1, 2, 3, 4, 5]);
    log(">>> render [1,2,3,4,5] DONE.");
    logRoot();
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedfalse
root: <div id=1>hello</div>
>>> render [4,5] DONE.
root: <div id=1><span>2</span><span>3</span><span>4</span></div>
patchKeyedChildren...
while 1, sync from start...
while 2, sync from end...
>>> render [1,2,3,4,5] DONE.
root: <div id=1><span>1</span><span>2</span><span>3</span><span>4</span><span>5</span></div>

[2,3,4][1,2,3,4,5] 经过 while1 和 while2 什么都没做, i=0,e1=2,e2=4 既 不满足 if(i>e1) 也不满足 elseif(i>e2) 所以会进入 else 的无规则比较阶段。根 据之前脑图分析结果可知, else 执行的步骤大致是:

  1. 遍历 old children 替换 [2,3,4]

    用 old [2,3,4] 的每个元素的 key 去 new [1,2,3,4,5] 里面去找对应的 key(type,key相等的节点)去替换老的,那么这里将会找到 [2,3,4] ,此时经过一个 for old children 循环执行替换,这里重点在于 newIndexToOldIndexMap 结果会更 新为 [0,1,2,3,0] 这里的 123 分别对应 [2,3,4] 在 old children 中的索引 + 1 的结果。

  2. 遍历 new children 检测 newIndexToOldIndexMap

    这一步的循环是针对 new children 而言,作用是找出 newIndexToOldIndexMap 中不 为 0 的元素(也就是还未被使用的元素),来执行插入操作。循环顺序从右到左执行, 则有(i-递减索引,index-newchild 的索引值,el-newchild,val-使用状态):

    i=4,index=4,el=c2[index],val=0,anchor=null:

    未使用,没有参考节点,属于纯 append 操作。

    i=3,2,1,val!==0 已经被使用了,跳过

    i=0,index=0,el=c2[index],val=0,anchor=c2[index+1]=1:

    未使用,插入节点为 [1] 参考节点为 [2] 属于插入操作,在 [1] 之前插入。

    最后得到结果: children=[1,2,3,4,5] 完成。

其实这里还是比较容易理解的,因为还没用到“有序递增序列”算法,因为 for old children 中的 key 是有序的且是 new children 的子集,所以遍历过程中 newIndex 为 0,1,2 后面的总是比前面的大,因此 maxNewIndexSoFar=2 直到结束🔚。

类似用例:

  1. [][1,2,3,4,5] 这种情况等于是 newIndexToOldIndexMap=[0,0,0,0,0] 所有 new child 元素执行的都是尾部 append 操作。

  2. [1,2,3,4,5]render(h('div'), root) 等于是 children=[] 直接删除 old children 操作

  3. [1,2,3,4,5][3,4,5] 经过 while2 替换掉 [3,4,5] 剩下 old [1,2] 因在 new children 中找不到对应的元素,则会被删除。

  4. [1,2,3,4,5][1,2,3] 经过 while1 替换掉 [1,2,3] 剩下的 old [4,5] 因 找不到对应的 new child 被删除。

  5. [1,2,3,4,5][1,2,4,5] 经过 while1 替换掉 [1,2] 经过 while2 替换掉 [4,5] 剩下 [3] 因找不到 new child 而被删除。

move([1,2,3,4]->[2,3,1,4])

这种情况会触发“最长递增序列”规则,进行替换,因为发生 diff-update 的原则是:更新 之后的顺序要和 new children 顺序一致,即原来是 1234 更新之后要保持 2314 顺序。

更新过程:

  1. 经过 while1 什么都不发生,因为 [1]->[2] 非同类节点

  2. 经过 while2 替换掉 [4],i=0,e1=2,e2=2

  3. if(i>e1) 不满足新增条件

  4. elif(i>e2) 不满足删除条件

  5. 进入 else 无规则比较更新

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const { log, f, shuffle, runtime_test, renderChildren } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  ({ h, render, nodeOps, serializeInner: inner }) => {
    let elm;
    let root = nodeOps.createElement("div");
    // <div>hello</div>
    render(h("div", { id: 1 }, "hello"), root);
    const rc = (arr) => renderChildren(render, root, h, arr);
    const logRoot = () => log("root: " + inner(root));

    logRoot();
    elm = rc([1,2,3,4]);
    log(">>> render [1,2,3,4] DONE.");
    logRoot();

    elm = rc([2,3,1,4]);
    log(">>> render [2,3,1,4] DONE.");
    logRoot();
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedfalse
root: <div id=1>hello</div>
>>> render [1,2,3,4] DONE.
root: <div id=1><span>1</span><span>2</span><span>3</span><span>4</span></div>
patchKeyedChildren...
while 1, sync from start...
while 2, sync from end...
while 2, sync from end...
{ arr: [ 2, 3, 1 ] }
{ result: [ 2, 1 ] }
最长增长序列: 0,1
move 交换...
>>> render [2,3,1,4] DONE.
root: <div id=1><span>2</span><span>3</span><span>1</span><span>4</span></div>

在上面执行过程中,进入 else 分支,执行:

  1. for old children

    执行完之后(i=0,e1=2,e2=2), newIndexToOldIndexMap=[2,3,1] 分别对应 [1,2,3][2,3,1] 中的索引值 + 1,因为存在 newIndex < maxNewIndexSoFar 所以 moved=true 在随后的流程中用来触发“最长增长序列”操作。

  2. for new children

    在执行这个循环之前,我们需要用到 newIndexToOldIndexMap=[2,3,1] 并且从中找 到最长增长序列([2,3]),然而 getSequence(arr) 返回的是它们的索引值,所以是 [0,1] 所以最后 increasingNewIndexSequence=[0,1]

    然后在 for toBePatched new children 里面,因为检测到 moved=true 则会进入到 移动交换操作,这里执行 move() 也有个条件: j<0 || i !== increasingNewIndexSequence[j] 这两个条件 有分别代表两种情况(假设: val=increasingNewIndexSequence[j]):

    • j<0 代表 increasingNewIndexSequence 增长序列没有内容,这说明什么?说明 newIndexToOldIndexMap 是个完全递减数组,如: [3,2,1] 这种情况每个元素都 需要进行移动,最后变成 [1,2,3] , 1移到3位置,2不变,3移动1的位置。

    • i!==val

      比如这里(newIndexToOldIndexMap=[2,3,1],old=[1,2,3,4],new=[2,3,1,4])

      i=2,val=1,nextchild=[1],anchor=[4] 意味着要在 [4] 前面插入 [1] 记住这 里执行的依旧是插入操作,只是在插入之前会将原来的 [1] 从 container.children 中删除,所以看似是交换实际只是变相插入而已。

      i=1,val=1,nextchild=[3],anchor=[1] 这里 i===val 所以执行 j--

      i=0,val=0,nextchild=[2],anchor=[3] 这里 i===val 所以执行 j--

      到此 for 循环已经退出了,上面两个 j-- 说明触及的是增长序列里面的元素即不 需要移动的元素,所以最后 children 由 [1,2,3,4] 变成 [2,3,1,4], 只是 1 进 行了移动。

只需要移动一次就可完成。。。。。。。我是分界点~~~~~~~~~

同案例分析:

  1. [1, 2, 3, 4][1, 4, 2, 3]

    在经过 while1 之后开始进入 else 分支, newIndexToOldIndexMap=[4,2,3] 最后得 到增长序列: [2,3]~对应的索引 ~[1,2] 即需要执行插入的逻辑是:

    i=2,j=1,val=2,next=[3],anchor=null : i===val, j–

    i=1,j=0,val=1,next=[2],anchor=[3] : i===val, j–

    i=0,j=-1,val=undefined,next=[4],anchor=[2] : 4 要插入到 2 前面, children=[1,4,2,3]

    i=-1 结束。

    所以只需要执行一次移动就可以了,在递增序列内的元素是不需要动的。

  2. [1,2,3][2,3,1]

    由于前后都不一样,所以 while1,while2 都没处理,并且进入 else 乱序情况处理。

    newIndexToOldIndexMap=[2,4,1] 增长序列= [2,4] ,索引: [0,1]

    i=2,j=1,val=1,next=[1],anchor=null1 append 到最后,变成 children=[2,3,1]

    i=1,j=1,val=1,next=[3],anchor=[1] : i===val, j–

    i=0,j=0,val=0,next=[2],anchor=[3] : i ===val, j–

    i=-1 结束.

    只需要将 [1] 移到最后就完成了交换。

  3. [1,2,3,4][4,2,3,1]

    newIndexToOldIndexMap=[4,2,3,1] 增长序列: [2,3] ,索引: [1,2]

    i=3,j=1,val=2,next=[1],anchor=null : [1] append 到最后, children=[2,3,4,1]

    i=2,j=1,val=2,next=[3],anchor=[1] : i===val, j–

    i=1,j=0,val=1,next=[2],anchor=[3] : i===val, j–

    i=0,j=-1,val=undefined,next=[4],anchor=[2]: j<0, 执行移动, children=[4,2,3,1], 将 4 移动到 2 前面。

    i=-1 结束。

    这里只需要执行两次移动操作, 1<->4 交换。

move&replace([1,2,3,4,5]->[4,1,2,3,6])

这里需要完成的动作有: 用 6 替换 5,将 4移到 1 前面

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const { log, f, shuffle, runtime_test, renderChildren } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  ({ h, render, nodeOps, serializeInner: inner }) => {
    let elm;
    let root = nodeOps.createElement("div");
    // <div>hello</div>
    render(h("div", { id: 1 }, "hello"), root);
    const rc = (arr) => renderChildren(render, root, h, arr);
    const logRoot = () => log("root: " + inner(root));

    logRoot();
    elm = rc([1,2,3,4,5]);
    log(">>> render [1,2,3,4,5] DONE.");
    logRoot();

    elm = rc([4,1,2,3,6]);
    log(">>> render [4,1,2,3,6] DONE.");
    logRoot();
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedfalse
root: <div id=1>hello</div>
>>> render [1,2,3,4,5] DONE.
root: <div id=1><span>1</span><span>2</span><span>3</span><span>4</span><span>5</span></div>
patchKeyedChildren...
while 1, sync from start...
while 2, sync from end...
{ arr: [ 4, 1, 2, 3, 0 ] }
{ result: [ 1, 2, 3 ] }
{ toBePatched: 5 }
最长增长序列: 1,2,3
{ val: 3, i: 3, j: 2, toBePatched: 5 }
{ val: 2, i: 2, j: 1, toBePatched: 5 }
{ val: 1, i: 1, j: 0, toBePatched: 5 }
{ val: undefined, i: 0, j: -1, toBePatched: 5 }
moving...
move 交换...
>>> render [4,1,2,3,6] DONE.
root: <div id=1><span>4</span><span>1</span><span>2</span><span>3</span><span>6</span></div>

分析

经过 while1, while2 实际什么都没做,因为前后并没有共通节点,也因此会进入 else 进 行无序序列处理。

old=[1,2,3,4,5]

new=[4,1,2,3,6]

newIndexToOldIndexMap=[0,0,0,0,0] 经过 for old 之后

newIndexToOldIndexMap=[4,1,2,3,0] 最后 5 由于没找到对应 key 的 new node或者无 key 的 new node而 被执行删除(unmount())操作。

找最长增长序列: [1,2,3] 得到 newIndexToOldIndexMap 中对应的索引 increasingNewIndexSequence=[1,2,3],j=2

开始从右到左遍历 new children(val=increasingNewIndexSequence[j]):

i=4,map=0,newchild=6: 由于 index map 中的值为 0,说明并没有对应的 old child 属 于需要新增的节点,执行 mount new 节点操作 children=[1,2,3,4,6] (注意 : 5 在 上面已经被 unmount 掉了)

i=3,j=2,val=3,next=3,anchor=6 : i===val, j–

i=2,j=1,val=2,next=2,anchor=3 : i===val, j–

i=1,j=0,val=1,next=1,anchor=2 : i===val, j–

i=0,j=-1,val=undefined,next=4,anchor=1 : j<0, 执行 move() ,将 4 移动到 1 前面,变成: children=[4,1,2,3,6]

i=-1 结束。

这里实际上有三个动作:

  1. 5 在 new children 中没找到 keyed child,也没有 non-keyed child 所以被 unmount 删除了;

  2. 6newIndexToOldIndexMap 中对应的值为 0,说明并没有 old child 与之对应, 属于新节点,执行 mount new 操作;

  3. 4 节点满足 move 条件,将其移动到 1 前面。

同案例分析

  1. [1,4,5][4,6]

    for old 时 1 和 5 被 unmount 掉

    newIndexToOldIndexMap=[2,0] 因为不存在 newIndex > maxNewIndexSoFar 导致 moved=false 随之 increasingNewIndexSequence=[]

    for new 时,由于增长序列为空,所以只会进入 newIndexToOldIndexMap 检测是否为 0 的 if 分支执行 mount new 操作,即新增 6 这个节点:

    i=1,newIndexToOldIndexMap[i]===0 进入 if mount new 6 node.

  2. [2,4,5][4,5,3] 删除 2,新增 3

  3. [1,2,3,4,5,6,7,8][8,7,6,5,4,3,2,1]

    这应该是最糟糕的情况了

    newIndexToOldIndexMap=[8,7,6,5,4,3,2,1],moved=true

    增长序列为 [7] ,所以 increasingNewIndexSequence=[1],j=0

    i=7,j=0,val=7,next=1,anchor=undefined, i===val, j–, 即节点 [1] 不需要动

    i=6,j=-1,next=2,anchor=1 ->2移到1前面-> children=[3,4,5,6,7,8,2,1]

    i=5,j=-1,next=3,anchor=2 ->3移到2前面-> children=[4,5,6,7,8,3,2,1]

    i=4,j=-1,next=4,anchor=3 ->4移到3前面-> children=[5,6,7,8,4,3,2,1]

    i=3,j=-1,next=5,anchor=4 ->5移到4前面-> children=[6,7,8,5,4,3,2,1]

    i=2,j=-1,next=6,anchor=5 ->6移到5前面-> children=[7,8,6,5,4,3,2,1]

    i=1,j=-1,next=7,anchor=6 ->7移到6前面-> children=[8,7,6,5,4,3,2,1]

    i=0,j=-1,next=8,anchor=7 这里也会执行一次插入吗(如下面测试结果)?

    i=-1 结束,共执行了七次移动。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    const { log, f, shuffle, runtime_test, renderChildren } = require(process.env
    .BLOG_DIR_VUE + "/lib.js");
    import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
    ({ h, render, nodeOps, serializeInner: inner }) => {
    let elm;
    let root = nodeOps.createElement("div");
    // <div>hello</div>
    render(h("div", { id: 1 }, "hello"), root);
    const rc = (arr) => renderChildren(render, root, h, arr);
    const logRoot = () => log("root: " + inner(root));
    
    logRoot();
    elm = rc([1,2,3,4,5,6,7,8]);
    log(">>> render [1,2,3,4,5,6,7,8] DONE.");
    logRoot();
    
    elm = rc([1,2,3,4,5,6,7,8].reverse());
    log(">>> render [8,7,6,5,4,3,2,1] DONE.");
    logRoot();
    },
    (err) => {
    console.log(err.message);
    }
    );
    
    undefinedfalse
    root: <div id=1>hello</div>
    >>> render [1,2,3,4,5,6,7,8] DONE.
    root: <div id=1><span>1</span><span>2</span><span>3</span><span>4</span><span>5</span><span>6</span><span>7</span><span>8</span></div>
    patchKeyedChildren...
    while 1, sync from start...
    while 2, sync from end...
    { arr: [
        8, 7, 6, 5,
        4, 3, 2, 1
    ] }
    { result: [ 7 ] }
    { toBePatched: 8 }
    最长增长序列: 7
    { val: 7, i: 7, j: 0, next: '1', anchor: null, toBePatched: 8 }
    { val: undefined, i: 6, j: -1, next: '2', anchor: '1', toBePatched: 8 }
    move 交换...
    { val: undefined, i: 5, j: -1, next: '3', anchor: '2', toBePatched: 8 }
    move 交换...
    { val: undefined, i: 4, j: -1, next: '4', anchor: '3', toBePatched: 8 }
    move 交换...
    { val: undefined, i: 3, j: -1, next: '5', anchor: '4', toBePatched: 8 }
    move 交换...
    { val: undefined, i: 2, j: -1, next: '6', anchor: '5', toBePatched: 8 }
    move 交换...
    { val: undefined, i: 1, j: -1, next: '7', anchor: '6', toBePatched: 8 }
    move 交换...
    { val: undefined, i: 0, j: -1, next: '8', anchor: '7', toBePatched: 8 }
    move 交换...
    >>> render [8,7,6,5,4,3,2,1] DONE.
    root: <div id=1><span>8</span><span>7</span><span>6</span><span>5</span><span>4</span><span>3</span><span>2</span><span>1</span></div>
    

patchUnkeyedChildren

feat(add): unkeyd children patch · gcclll/stb-vue-next@2d06710

对于明确 unkeyed 的 children 处理和 keyed 处理区别在于,会将前面的 children 先进 行 patch, 因为在 patchKeyedChildren 一节已经详细分析过,如果没有 old child 是 unkeyed 会从 new children 中依序找到第一个符合条件的 unkeyed child 去替换。

所以这里分三步走:

  1. 找到最小长度,针对此长度内的 child 进行 patch,因为对于 unkeyed old child 只 需要找到对应的 unkeyed new child 替换就行

  2. 新增的情况 new child len > old child len

  3. 减少的情况 new child len < old child len

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
const patchUnkeyedChildren = (
  c1: VNode[],
  c2: VNodeArrayChildren,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  optimized: boolean
) => {
  c1 = c1 || EMPTY_ARR;
  c2 = c2 || EMPTY_ARR;
  const oldLength = c1.length;
  const newLength = c2.length;
  const commonLength = Math.min(oldLength, newLength);
  let i;
  for (i = 0; i < commonLength; i++) {
    const nextChild = (c2[i] = optimized
      ? cloneIfMounted(c2[i] as VNode)
      : normalizeVNode(c2[i]));
    patch(
      c1[i],
      nextChild,
      container,
      null,
      parentComponent,
      parentSuspense,
      isSVG,
      optimized
    );
  }

  if (oldLength > newLength) {
    // remove old
    unmountChildren(
      c1,
      parentComponent,
      parentSuspense,
      true,
      false,
      commonLength
    );
  } else {
    // mount new
    mountChildren(
      c2,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      optimized,
      commonLength
    );
  }
};

patch->processComponent(如何patch组件的?)

测试