Vue3 源码头脑风暴之 7 ☞ runtime-core(2) - render
文章目录
stb-vue-next 完全拷贝于 vue-next ,主要目的用于学习。
声明 :vue-next runtime-core 中的 render 函数部分,
本篇所使用的的测试例子是普 通的数字数组,实际中每个节点都是 VNode 结构,也就是说是唯一的,所以不存在不同节 点共享同一个内存空间问题,这也就是为何用例中没有重复数字的原因。(_就算是静态的可 复用节点也会执行 cloneVNode 克隆出一个全新的对象_)更新日志&Todos :
[2021-01-26 14:15:15] 创建
2229 - 423 = 1806 🤦♀️ 一个函数就将近两千行,😲!!
捷径👣:
脑图
runtime-core/src/render.ts 脑图:
render 函数创建函数 baseCreateRender(options, createHydrationFns?)
init
feat(init): render function · gcclll/stb-vue-next@fb9738c · GitHub
两个 create render 函数:
createRenderer(options)
createHydrationRenderer(options)
这个还不清楚是干什么的,通过代码观察貌似跟 SSR 有关,先搁置先不管。
两个函数最终都是调用的 baseCreateRenderer(options, createHydrationFns)
并且就是这个函数将近两千行~~~~
|
|
function list
feat(init): renderer -> baseCreateRenderer TODOs · gcclll/stb-vue-next@b7f55a8 · GitHub
baseCreateRenderer(options, createHydrationFns)
之所以这么长,是因为这里面包含
了三十几个函数的定义,下面将一个个按照流程逐一实现。
step | what? | step | what? |
---|---|---|---|
options | 解构 | patch | function |
processText | 文本处理 | processCommentNode | 注释节点 |
mountStaticNode | 加载静态节点 | patchStaticNode | - |
moveStaticNode | - | removeStaticNode | 删除静态节点 |
processElement | - | mountElement | - |
setScopeId | - | mountChildren | - |
patchElement | - | patchBlockChildren | - |
patchProps | - | processFragment | - |
processComponent | - | mountComponent | - |
updateComponent | - | setupRenderEffect | - |
updateComponentPreRender | - | patchChildren | - |
patchUnkeyedChildren | - | patchKeyedChildren | - |
move | unmount | ||
remove | - | removeFragment | - |
unmountComponent | - | unmountChildren | - |
getNextHostNode | - | render | - |
internals | object, 上述函数别名 | 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
|
|
vnode 为空,且 conatainer 上有注册过 _vnode,组要进行卸载
如:
render(ref.value ? h('div') : null)
ref.value = true 时候进入 else -> patch
ref.value = false 时候进入 if -> unmount
否则执行 patch(),干什么了?
flushPostFlushCbs()
此时组件应该 mounted 了,手动刷掉所有 post cbs 。保存 _vnode,方便下次进入是检测
接下来,需要继续实现 unmount()
和 patch()
patch(…args)
feat(init): baseCreateRender-> patch -> processElement · gcclll/stb-vue-next@eb48eb9 · GitHub
参数:
参数名 | 描述 |
---|---|
n1 | VNode, 老节点 |
n2 | VNode, 新节点 |
container | 容器 |
anchor | ? |
parentComponent | 父级组件 |
parentSuspense | Suspense ? |
isSVG | ? |
optimized | 是否优化过? |
检测节点类型是不是一样,如果不一样直接卸载老的
因为类型都不一样了,可能整个🌲都发生了变化,直接卸载老的重新 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
同类型元素的删除移动操作)
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 。
|
|
没有 n1 老节点,直接 mount 新的 n2 节点
否则,进行 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 操作的入口了。
|
|
注意上面的 flushPostFlushCbs()
是在 patch 之后执行的,也就是说 post cbs 会在组
件 mount/unmount 完成之后的下一个 tick 去执行的回调。
|
|
patch 函数里面通过 switch 分支根据 ShapeFlags
的类型类调用对应的 processXxx
函数进行处理 old/new vnode 节点,而这里的 ShapeFlags
值的依据来自哪里?是在哪
里赋值的,由由什么作用? 。
这里以普通的 ELEMENT 标签作为切入点来实现一个完整的过程,这里需要用到 processElement 。
|
|
这里就是个很简单 if…else 判断是不是有旧的节点,没有是 mount 有则是 patch 操作, 所以需要完成 mountElement
|
|
mountElement 里面两个核心的函数 hostCreateElement
和 hostInsert
分别来自
baseCreateRender(option)
的 option 参数。
这里就需要深入了解 runtime-test
这个包,它是作用为了能测试 runtime-core 编写的
一个测试报,这里包含了一些列的 DOM 操作函数,这些函数也会在封装 render
的时候
传递给 baseCreateRender(option)
,所以上面的 hostElement 和 hostInsert 就是来
自 runtime-test
,这里链接可以跳转查看该包里面具体包含哪些函数,又是做什么的,
这里就不展开细讲,主要看下相关的两个函数实现。
|
|
所以说,截至目前还并没有涉及到实际的 DOM 操作,还只是在 vnode 结构上进行插入删除 操作。
这里开始应该可以测试了:
|
|
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 实现:
|
|
所以到此应该是完成了最普通的 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
|
|
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 ...)
函数
新增代码:
|
|
render -> patch -> case ELEMENT -> processElement -> mountElement
在 mountElement 中增加 props 处理逻辑,针对每个 prop 检测是不是保留名字
key/ref/onVnodeXxx
等生命周期名,非保留名字才需要处理,调用 hostPatchProp() 处
理,后面加上 BeforeMount
生命周期钩子函数调用。
|
|
普通属性直接更新到 el.props
中,如果是 onXxx
类型的事件,取出 xxx
作为
el.eventListeners
的 key 将事件名和其处理句柄保存起来。
这里的 el
实际上是个 ast 结构类型的对象,保存这每个节点的所有信息。
with text children
纯文本单节点 child
将纯文本做为 child ,将会被 h
函数转成 [child]
传递给 createVNode(type,
props, children, ...)
做为它的children 参数处理。
|
|
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
数组类型(多个) children:
feat(add): render->array children · gcclll/stb-vue-next@e6a5e61 · GitHub
当 h(type, propsOrChildren) 第二个参数为数组时会被当做 children 给 createVNode
。
|
|
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)
大概执行流程:
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
root->div->'foo'
在 1 最后进入递归之后,会进入到 patch 检测到 type 是 Text 类型,去调用
processText()
处理'foo'
完成之后,再回溯递归处理下一个元素' '
直到结束。root->div->' ' 同 2
root->idv->'bar' 同 2
涉及修改内容(renderer.ts -> baseCreateRender
):
|
|
children+props 混合测试
|
|
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_CHILDREN
和 ARRAY_CHILDREN
),为什么
只有两个呢?
这是因为在 createVNode()
函数中会调用 normalizeChildren()
对 children
进行
检测,分几种情况处理:
children 类型 | type(ShapeFlags ) | 描述 |
---|---|---|
Array | ARRAY_CHILDREN | - |
Object | SLOTS_CHILDREN | 区分是 ELEMENT/TELEPORT 或其他类型 |
Function | SLOTS_CHILDREN | 函数直接当做插槽处理 |
String 或 Number | TEXT_CHILDREN | 当做文本处理 |
上面有个插槽类型,还记得 compiler-core 里面对插槽的编译结果吗?
compiler-core 阶段对 slot标签和 v-slot 的解析源码分析 ->
大致解析结果就是组件内的所有元素按照一定的规则解析成插槽,最后生成的 render 函数 大概是:
|
|
所以当 children 是个对象的时候在 createVNode()
-> normalizeChildren()
中会被
当做插槽来处理。
patchElement
render()
-> patch()
-> processElement()
->
当检测到 old vnode 存在的时候会进入到这个函数 patchElement()
进行更新操作。
|
|
先做个测试,看下代码执行流程(patchElement()
里面加了点打印):
|
|
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
从上面的结果可知我们该阶段需要实现的部分代码为:
|
|
重新测试:
|
|
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,…)
简化代码:
|
|
所以总结下来有几种情况的组合:
首先是 patchFlag > 0 情况,需要局部 diff update(比如: v-for),这里需要区分是 否有 key 属性
unkeyed: patchUnkeyedChildren(c1, c2, …)
到这里 patchFlag <= 0 ,需要进行 full diff 的情况
这种情况下只有三种可能的 children:
text|array|null
这三种情况结合 old + new 有多重组合需要考虑。
new text + old array: 直接卸载 old array, 将 parent 内容设置成 new text
new array + old array: 当做 keyed children 调用 patchKeyedChildren(c1, c2, …) 处理
new null + old array: 直接卸载 old array(unmountChildren(c1, …))
new array + old null: 直接 mount new array(mountChildren(c2, …))
这里涉及到几个相关函数:
unmountChildren(children, parentComponent, parentSuspense, doRemove, optimized, start)
mountChildren(children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start)
根据上面的分析,会逐一实现各种情况。
|
|
new text + old array
feat(add): new text + old array · gcclll/stb-vue-next@7019c9d
patchChildren: 先 unmountChildren(c1) -> hostSetElementText(container, c2)
|
|
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。
测试:
|
|
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 渲染过程中,分别处理 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
新增代码:
|
|
如果 new null 直接卸载 old array 就好了,注意第四个参数传的是 doRemove:true
这
样 unmount()
里面就会去调用 remove()
测试:
|
|
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
|
|
new array + old null/text
这种情况,如果是 old text,会先执行
|
|
将 conteiner.children 清空。
然后执行 mountChidren(c2)
插入新的 array node 。
|
|
测试:
|
|
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
feat(add): patchKeyedChildren · gcclll/stb-vue-next@4a6a1f2
代码中列出了几种可能的情况:
old, new nodes 开头相同,从左到右方向以不同位置为起点开始比较
old, new nodes 结尾相同,从右到左方向以不同位置为起点开始比较
old ⊂ new,old 为 new 的真子集,这种情况视为新增节点,需要对新增的节点进行 mount 操作
old ⊃ new , new 为 old 的真子集,这种情况视为删除节点,需要对多余的节点进行 unmount 操作
old,new 没有特别明显的规律可遵循的,处理起来会比较麻烦
|
|
下面来一个个实现,揭开 diff -> patch 的神秘面纱!!!
在进行之前先看下一个函数 isSameVNodeType(n1,n2)
:
|
|
这个函数用来检测两个节点是不是类似节点(需同时满足 type 和 key 相同)。
有点复杂,整的头疼🤕🤕。。。休息会😴😴!!!
[2021-02-24 18:16:56] 通过画图终于把这块逻辑搞得有点清楚了!!!
剧情有点复杂,还是根据官方的测试用例来逐步熟悉各种情况的 diff -> patch 吧。
声明:
所有
children [1,2,3]
都将自身值作为节点的属性 key 值下面的所有用例都基于节点有
key
属性为前提
fix: patchKeydChildren if · gcclll/stb-vue-next@b7edc1b
大致移动规则流程图:
append([1] -> [1,2,3])
|
|
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
掉 。
|
|
然后: i=1,e1=0,e2=2
满足 if(i>e1)
新增节点条件,对 [2,3]
进入新增节点逻辑
代码(if
分支)
|
|
针对 [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]
位置。
|
|
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,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])
|
|
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
执行的步骤大致是:
遍历 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 的结果。遍历 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,2,3,4,5]
这种情况等于是newIndexToOldIndexMap=[0,0,0,0,0]
所有 new child 元素执行的都是尾部 append 操作。
[1,2,3,4,5]
和render(h('div'), root)
等于是children=[]
直接删除 old children 操作
[1,2,3,4,5]
和[3,4,5]
经过 while2 替换掉[3,4,5]
剩下 old[1,2]
因在 new children 中找不到对应的元素,则会被删除。
[1,2,3,4,5]
和[1,2,3]
经过 while1 替换掉[1,2,3]
剩下的 old[4,5]
因 找不到对应的 new child 被删除。
[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 顺序。
更新过程:
经过 while1 什么都不发生,因为
[1]->[2]
非同类节点经过 while2 替换掉
[4],i=0,e1=2,e2=2
if(i>e1)
不满足新增条件elif(i>e2)
不满足删除条件进入 else 无规则比较更新
测试:
|
|
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 分支,执行:
for old children
执行完之后(
i=0,e1=2,e2=2
),newIndexToOldIndexMap=[2,3,1]
分别对应[1,2,3]
在[2,3,1]
中的索引值 + 1,因为存在newIndex < maxNewIndexSoFar
所以moved=true
在随后的流程中用来触发“最长增长序列”操作。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, 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
结束。所以只需要执行一次移动就可以了,在递增序列内的元素是不需要动的。
[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=null
:1
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]
移到最后就完成了交换。[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 前面 。
测试:
|
|
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
结束。
这里实际上有三个动作:
5 在 new children 中没找到 keyed child,也没有 non-keyed child 所以被 unmount 删除了;
6 在
newIndexToOldIndexMap
中对应的值为 0,说明并没有 old child 与之对应, 属于新节点,执行 mount new 操作;4 节点满足 move 条件,将其移动到 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,4,5]
和[4,5,3]
删除 2,新增 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 去替换。
所以这里分三步走:
找到最小长度,针对此长度内的 child 进行 patch,因为对于 unkeyed old child 只 需要找到对应的 unkeyed new child 替换就行
新增的情况 new child len > old child len
减少的情况 new child len < old child len
|
|