例子代码
本篇将要解说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.tsconst 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.tsconst 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}