共计 11777 个字符,预计需要花费 30 分钟才能阅读完成。
例子代码
本篇将要解说 dom diff,那么咱们联合上面的例子来进行解说,这个例子是在上一篇文章的根底上,加了一个数据变更,也就是 list 的值产生了扭转。html 中减少了一个按钮 change,通过点击 change 按钮来调用 change 函数,来扭转 list 的值。例子位于源代码 /packages/vue/examples/classic/
目录下,上面是例子的代码:
const app = Vue.createApp({data() { | |
return {list: ['a', 'b', 'c', 'd'] | |
} | |
}, | |
methods: {change() {this.list = ['a', 'd', 'e', 'b'] | |
} | |
} | |
}); | |
app.mount('#demo') |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta name="viewport" | |
content="initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=no,target-densitydpi=medium-dpi,viewport-fit=cover" | |
/> | |
<title>Vue3.js hello example</title> | |
<script src="../../dist/vue.global.js"></script> | |
</head> | |
<body> | |
<div id="demo"> | |
<ul> | |
<li v-for="item in list" :key="item"> | |
{{item}} | |
</li> | |
</ul> | |
<button @click="change">change</button> | |
</div> | |
<script src="./hello.js"></script> | |
</body> | |
</html> |
源码解读
对于 Vue3 中数据产生变更,最终影响到页面发生变化的过程,咱们本篇文章只对 componentEffect 以及当前的代码进行解说,对于数据变更后,是如何执行到 componentEffect 函数,以及为何会执行 componentEffect,前面的文章再进行解说。
componentEffect
来看下 componentEffect 更新局部的代码:
// @file packages/runtime-core/src/renderer.ts | |
function componentEffect() {if (!instance.isMounted) {// first render} else {let {next, bu, u, parent, vnode} = instance | |
let originNext = next | |
let vnodeHook: VNodeHook | null | undefined | |
if (next) {updateComponentPreRender(instance, next, optimized) | |
} else {next = vnode} | |
next.el = vnode.el | |
// beforeUpdate hook | |
if (bu) {invokeArrayFns(bu) | |
} | |
// onVnodeBeforeUpdate | |
if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {invokeVNodeHook(vnodeHook, parent, next, vnode) | |
} | |
const nextTree = renderComponentRoot(instance) | |
const prevTree = instance.subTree | |
instance.subTree = nextTree | |
if (instance.refs !== EMPTY_OBJ) {instance.refs = {} | |
} | |
patch( | |
prevTree, | |
nextTree, | |
hostParentNode(prevTree.el!)!, | |
getNextHostNode(prevTree), | |
instance, | |
parentSuspense, | |
isSVG | |
) | |
next.el = nextTree.el | |
if (originNext === null) {updateHOCHostEl(instance, nextTree.el) | |
} | |
// updated hook | |
if (u) {queuePostRenderEffect(u, parentSuspense) | |
} | |
// onVnodeUpdated | |
if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {queuePostRenderEffect(() => {invokeVNodeHook(vnodeHook!, parent, next!, vnode) | |
}, parentSuspense) | |
} | |
} | |
} |
当数据发生变化的时候,最终会走到下面的 else 的逻辑局部。Vue3 源码视频解说:进入学习
- 默认状况下 next 是 null,父组件调用 processComponent 触发以后调用的时候会是 VNode,此时 next 为 null;
- 调用以后实例 beforeUpdate 钩子函数;调用要更新的 Vnode(next)的父组件的 beforeUpdate 钩子函数;
- 获取以后实例的 vNode => prevTree;获取要更新的 vNode=> nextTree;而后调用 patch;
调用 patch 函数的过程,也就是依据 VNode 的 type,走不同的干流的过程;点击 change 按钮:n1 的值:n2 的值:依据这个值,能够通晓,会走到 processFragment 函数;
processFragment
调用 processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
函数,参数的值:
- 此时 n1 和 n2 如上图;
- container 为 #demo;
- anchor 为 null;
- parentComponent 为 instance 实例;
- parentSuspense 为 null;
- isSVG 为 false;
- optimized 为 false;
来看下 processFragment 函数的源码:
// @file packages/runtime-core/src/renderer.ts | |
const processFragment = (n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean) => {const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))! | |
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))! | |
let {patchFlag, dynamicChildren} = n2 | |
if (patchFlag > 0) {optimized = true} | |
if (n1 == null) {// first render 的逻辑} else { | |
if (patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT && dynamicChildren) { | |
patchBlockChildren( | |
n1.dynamicChildren!, | |
dynamicChildren, | |
container, | |
parentComponent, | |
parentSuspense, | |
isSVG | |
) | |
if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {traverseStaticChildren(n1, n2) | |
} else if ( | |
n2.key != null || | |
(parentComponent && n2 === parentComponent.subTree) | |
) {traverseStaticChildren(n1, n2, true /* shallow */) | |
} | |
} else { | |
patchChildren( | |
n1, | |
n2, | |
container, | |
fragmentEndAnchor, | |
parentComponent, | |
parentSuspense, | |
isSVG, | |
optimized | |
) | |
} | |
} | |
} |
刨除掉 first render 的代码后,能够看到上面还是分为了两个分支;依据 n1 和 n2 可知,咱们将会走 if 分支,执行 patchBlockChildren。
patchBlockChildren
调用 patchBlockChildren(n1.dynamicChildren, n2.dynamicChildren, container, parentComponent, parentSuspense, isSVG)
函数,此时参数如下:
- oldChildren:n1.dynamicChildren,也就是 Symbol(Fragment) =>ul 和 button 两个元素组成的数组;
- newChildren:n2.dynamicChildren,也就是 Symbol(Fragment) =>ul 和 button 两个元素组成的数组;
- fallbackContainer:container,也就是 #demo;
- parentComponent:instance 实例;
- parentSuspense:null;
- isSVG:false。
来看下 patchBlockChildren 的源码:
// @file packages/runtime-core/src/renderer.ts | |
const patchBlockChildren: PatchBlockChildrenFn = (oldChildren, newChildren, fallbackContainer, parentComponent, parentSuspense, isSVG) => {for (let i = 0; i < newChildren.length; i++) {const oldVNode = oldChildren[i] | |
const newVNode = newChildren[i] | |
const container = | |
oldVNode.type === Fragment || | |
!isSameVNodeType(oldVNode, newVNode) || | |
oldVNode.shapeFlag & ShapeFlags.COMPONENT || | |
oldVNode.shapeFlag & ShapeFlags.TELEPORT | |
? hostParentNode(oldVNode.el!)! | |
: fallbackContainer | |
patch( | |
oldVNode, | |
newVNode, | |
container, | |
null, | |
parentComponent, | |
parentSuspense, | |
isSVG, | |
true | |
) | |
} | |
} |
能够看到 patchBlockChildren 是 for 循环调用 patch 函数,下面看到 newChildren 是一个长度为 2 的数组。循环遍历调用patch(oldVNode, newVNode, container, null, parentComponent, parentSuspense, isSVG, true)
;
- 第一次循环:
-
- oldVNode: 老的 ul 数组生成的 VNode 对象;
-
- newVNode:新的 ul 数组生成的 VNode 对象;
-
- container:ul 元素;
-
- anchor:下面传递的是 null;
-
- parentComponent: instance 实例;
-
- parentSuspense: null;
-
- isSVG: false;
-
- optimized: true;
- 第二次循环:
-
- oldVNode: 老的 change 按钮形成的 VNode 对象;
-
- newVNode:新的 change 按钮形成的 VNode 对象;
-
- container:此时的 container 为 #demo;
-
- anchor:下面传递的是 null;
-
- parentComponent: instance 实例;
-
- parentSuspense: null;
-
- isSVG: false;
-
- optimized: true;
processElement
咱们先说第二次循环,第二次比较简单;下面说到调用 patch 函数,通过下面理解到第二次循环 newVNode 的 type 是 button;则会走到processElement
,参数全副是透传过来的:
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) {// first render} else {patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized) | |
} | |
} |
如上代码,会间接调用 patchElement,此时参数为:
- n1: 老的 change 按钮形成的 VNode 对象;
- n2:新的 change 按钮形成的 VNode 对象;
- parentComponent: instance 实例;
- parentSuspense: null;
- isSVG: false;
- optimized: true;
patchChildren
当初再来说第一次循环,执行 patch 的时候,newVNode 的 type 为 Symbol(Fragment) => ul,此时还是会走到 processFragment
函数,不过此时的 dynamicChildren 为空,会持续运行到 patchChildren
函数。
patchChildren
此时运行到 patchChildren 函数,咱们来看下运行到此时的参数:
- n1:老的 ul 数组生成的 VNode 对象;
- n2:新的 ul 数组生成的 VNode 对象;
- container:ul 元素;
- anchor:ul 结尾生成的对象;
- parentComponent:instance 实例;
- parentSuspense:null
- isSVG:false;
- optimized:true;
上面看下 patchChildren 的源码:
const patchChildren: PatchChildrenFn = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized = false) => { | |
const c1 = n1 && n1.children | |
const prevShapeFlag = n1 ? n1.shapeFlag : 0 | |
const c2 = n2.children | |
const {patchFlag, shapeFlag} = n2 | |
if (patchFlag > 0) {if (patchFlag & PatchFlags.KEYED_FRAGMENT) { | |
patchKeyedChildren(c1 as VNode[], | |
c2 as VNodeArrayChildren, | |
container, | |
anchor, | |
parentComponent, | |
parentSuspense, | |
isSVG, | |
optimized | |
) | |
return | |
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) { | |
// patchUnkeyedChildren | |
return | |
} | |
} | |
// other ...... | |
} |
此时 patchFlag 的值为 128,同时咱们的 list 渲染是有 key 的,so 会运行 patchKeyedChildren
函数,c1 为四个 li 组成的数组(a,b,c,d);c2 为新的 li 组成的数组(a,d,e,b); 其余值透传到 patchKeyedChildren。
patchKeyedChildren
上面对 patchKeyedChildren 函数的参数曾经进行了阐明,在这里咱们再回顾下:
- c1:四个 li 组成的数组(a,b,c,d);
- c2:新的 li 组成的数组(a,d,e,b);
- container:ul 元素;
- parentAnchor:ul 结尾生成的对象;
- parentComponent:instance 实例;
- parentSuspense:null
- isSVG:false;
- optimized:true;
接下来看下 patchKeyedChildren 函数的源码:
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 | |
while (i <= e1 && i <= e2) {const n1 = c1[i] | |
const n2 = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) | |
if (isSameVNodeType(n1, n2)) {patch(n1,n2,container,null,parentComponent,parentSuspense,isSVG,optimized) | |
} else {break} | |
i++ | |
} | |
while (i <= e1 && i <= e2) {const n1 = c1[e1] | |
const n2 = (c2[e2] = optimized ? cloneIfMounted(c2[e2] as VNode) : normalizeVNode(c2[e2])) | |
if (isSameVNodeType(n1, n2)) {patch(n1,n2,container,null,parentComponent,parentSuspense,isSVG,optimized) | |
} else {break} | |
e1-- | |
e2-- | |
} | |
if (i > e1) {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++ | |
} | |
} | |
} | |
else if (i > e2) {while (i <= e1) {unmount(c1[i], parentComponent, parentSuspense, true) | |
i++ | |
} | |
} | |
else { | |
const s1 = i | |
const s2 = i | |
for (i = s2; i <= e2; i++) {const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) | |
if (nextChild.key != null) {keyToNewIndexMap.set(nextChild.key, i) | |
} | |
} | |
let j | |
let patched = 0 | |
const toBePatched = e2 - s2 + 1 | |
let moved = false | |
let maxNewIndexSoFar = 0 | |
const newIndexToOldIndexMap = new Array(toBePatched) | |
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0 | |
for (i = s1; i <= e1; i++) {const prevChild = c1[i] | |
if (patched >= toBePatched) {unmount(prevChild, parentComponent, parentSuspense, true) | |
continue | |
} | |
let newIndex | |
if (prevChild.key != null) {newIndex = keyToNewIndexMap.get(prevChild.key) | |
} else {for (j = s2; j <= e2; j++) { | |
if (newIndexToOldIndexMap[j - s2] === 0 && | |
isSameVNodeType(prevChild, c2[j] as VNode) | |
) { | |
newIndex = j | |
break | |
} | |
} | |
} | |
if (newIndex === undefined) {unmount(prevChild, parentComponent, parentSuspense, true) | |
} else {newIndexToOldIndexMap[newIndex - s2] = i + 1 | |
if (newIndex >= maxNewIndexSoFar) {maxNewIndexSoFar = newIndex} else {moved = true} | |
patch( | |
prevChild, | |
c2[newIndex] as VNode, | |
container, | |
null, | |
parentComponent, | |
parentSuspense, | |
isSVG, | |
optimized | |
) | |
patched++ | |
} | |
} | |
const increasingNewIndexSequence = moved | |
? getSequence(newIndexToOldIndexMap) | |
: EMPTY_ARR | |
j = increasingNewIndexSequence.length - 1 | |
for (i = toBePatched - 1; i >= 0; i--) { | |
const nextIndex = s2 + i | |
const nextChild = c2[nextIndex] as VNode | |
const anchor = | |
nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor | |
if (newIndexToOldIndexMap[i] === 0) { | |
patch( | |
null, | |
nextChild, | |
container, | |
anchor, | |
parentComponent, | |
parentSuspense, | |
isSVG | |
) | |
} else if (moved) {if (j < 0 || i !== increasingNewIndexSequence[j]) {move(nextChild, container, anchor, MoveType.REORDER) | |
} else {j--} | |
} | |
} | |
} | |
} |
下面代码蕴含的有两个 while 循环和两对 if-else;
- i=0,循环开始下标;e1、e2 为 c1 和 c2 的长度;l2 为新的 children 的长度;
- 第一个 while 循环,从头开始对列表进行遍历:
-
- 当 nodeType 一样的时候,调用 patch;
-
- 当 nodeType 不一样的时候,跳出循环;
- 第二个 while 循环,当第一个 while 循环对 c1 和 c2 都没有遍历完的时候,从尾部开始对其进行遍历:
-
- 当 nodeType 一样的时候,调用 patch;
-
- 当 nodeType 不一样的时候,跳出循环;
- 第一个 if,i>e1 证实 c1 曾经遍历完,i<=e2 证实 c2 还没遍历完,对残余的 c2 持续遍历,调用 patch;
- 第二个 else-if,i>e2 证实 c2 曾经遍历完,i<=e1 证实 c1 还没遍历完,对残余的 c1 持续遍历,因为 c1 为老的列表,则调用 unmount 把无用的列表内容卸载掉:
- 第二个 else:c1 和 c2 至多有一个没有遍历完,走到最初一个 else 的逻辑:
-
for (i = s2; i <= e2; i++)
for 循环遍历残余 c2,收集每个 c2 的元素的 key,形成 map => keyToNewIndexMap;
-
for (i = 0; i < toBePatched; i++)
for 循环遍历残余 c2 局部长度来生成映射,并赋值为 0;
-
for (i = s1; i <= e1; i++)
for 循环遍历残余 c1,应用 key 进行间接获取 (for 循环残余 c2 进行获取)newIndex, 此处证实还是要绑定好 key,唯一性很重要;newIndex 有值阐明 c2 中存在以后老的元素在 c1 中,老的 preChild,在 c2 中还须要,则调用patch
;如果 newIndex 为 undefined,则阐明老的 preChild 在 c2 中不须要了,调用 unmount,把以后 preChild 卸载掉;
-
- 遍历完残余 c1 后,再倒着遍历残余 c2:
for (i = toBePatched - 1; i >= 0; i--)
;如果(newIndexToOldIndexMap[i] === 0
则证实以后 nextChild 为新的节点,调用 patch;否则判断之前是否产生了挪动 moved,通过逻辑判断,调用 move;
- 遍历完残余 c1 后,再倒着遍历残余 c2:
patchKeyedChildren 例子
依据咱们下面的例子,由 old: [‘a’, ‘b’, ‘c’, ‘d’]变更为 new: [‘a’, ‘d’, ‘e’, ‘b’]的过程如下:
- 首先进入第一个 while 循环,此时 i 为 0,l2 为 4,e1 为 3,e2 为 3;
-
- 第一次循环,old- a 与 new- a 是雷同的,调用 patch,不发生变化;
-
- 第二次循环,old- b 与 new- b 是不雷同的,break;
-
- 跳出循环,从头开始的循环完结;
- 进入第二个 while 循环,此时 i 为 1,l2 为 4,e1 为 3,e2 为 3;
-
- 第一次循环,old- d 与 new- b 是不雷同的,break;
-
- 跳出循环,从尾部开始的循环完结;
- 进入第一个 if 判断为 false,进入第二个 else-if 判断为 false,进入 else;
- for 循环收集每个 c2 的元素的 key,keyToNewIndexMap = [‘d’ => 1, ‘e’ => 2, ‘b’ => 3];
- 建设长度为残余 c2 长度的数组newIndexToOldIndexMap = [0, 0 ,0];
- 此时进入
for (i = s1; i <= e1; i++)
for 循环遍历残余 c1 阶段,此时 i 为 1,s1 为 1,s2 为 1: -
- 第一次循环:遍历的元素为 old-b, 发现在 new 中存在,通过 keyToNewIndexMap 取得在 new 中的 index 为 3;调用 patch;
-
- 第二次循环:遍历的元素为 old-c, 在 new 中不存在,调用 unmount 卸载以后 old-c,扭转后c1 为[‘a’, ‘b’, ‘d’]
-
- 第三次循环:遍历的元素为 old-d,在 new 中存在,通过 keyToNewIndexMap 取得在 new 中的 index 为 1;调用 patch;
-
- 跳出循环,遍历 c1 残余阶段完结;
- 此时进入
for (i = toBePatched - 1; i >= 0; i--)
倒着遍历残余 c2 阶段,此时 i 为 2,j 为 0,s1 为 1,s2 为 1,newIndexToOldIndexMap 为[4, 0, 2]: -
- 第一次循环,判断以后 nextChild(new-b)存不存在,通过 newIndexToOldIndexMap 发现 nextChild 存在,并且在 old 外面的索引值为 2,j–, 此时 j 为 -1;i–,i 为 1;
-
- 第二次循环,判断以后 nextChild(new-e)存不存在, 通过 newIndexToOldIndexMap 发现 nextChild 的索引值为 0,示意不存在,则调用 patch;i–,i 为 0;扭转后c1 为[‘a’, ‘e’, ‘b’, ‘d’];
-
- 第三次循环,判断以后 nextChild(new-d)存不存在, 通过 newIndexToOldIndexMap 发现 nextChild 的索引值为 4,示意存在,则调用 move;i–,i 为 -1;扭转后c1 为[‘a’,, ‘d’ ‘e’, ‘b’];
-
- 此时 i 为 -1,跳出循环,循环完结
- 遍历完结,后果变更为了 new: [‘a’, ‘d’, ‘e’, ‘b’]
isSameVNodeType
大家能够看下上面 isSameVNodeType 的代码,大家在写代码的时候,为了可能进步页面性能,dom diff 的速度,如果没有产生变更的元素,key 肯定要放弃一样,不要 v-for="(item, index) in list" :key="index"
这样来写,因为当只有数组外部元素产生了地位挪动而元素未产生扭转时,index 的值是变更的,这样在 dom diff 的时候就会使程序产生误会。key 的唯一性很重要
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {return n1.type === n2.type && n1.key === n2.key}