Vue3源码解读之patch
创始人
2024-01-29 21:57:00
0

例子代码

本篇将要讲解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')


Vue3.js hello example


  • {{item}}

源码解读

关于Vue3中数据发生变更,最终影响到页面发生变化的过程,我们本篇文章只对componentEffect以及以后的代码进行讲解,对于数据变更后,是如何执行到componentEffect函数,以及为何会执行componentEffect,后面的文章再进行讲解。

componentEffect

来看下componentEffect更新部分的代码:

  // @file packages/runtime-core/src/renderer.tsfunction componentEffect() {if (!instance.isMounted) {// first render} else {let {next, bu, u, parent, vnode} = instancelet originNext = nextlet vnodeHook: VNodeHook | null | undefinedif (next) {updateComponentPreRender(instance, next, optimized)} else {next = vnode}next.el = vnode.el// beforeUpdate hookif (bu) {invokeArrayFns(bu)}// onVnodeBeforeUpdateif ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {invokeVNodeHook(vnodeHook, parent, next, vnode)}const nextTree = renderComponentRoot(instance)const prevTree = instance.subTreeinstance.subTree = nextTreeif (instance.refs !== EMPTY_OBJ) {instance.refs = {}}patch(prevTree,nextTree,hostParentNode(prevTree.el!)!,getNextHostNode(prevTree),instance,parentSuspense,isSVG)next.el = nextTree.elif (originNext === null) {updateHOCHostEl(instance, nextTree.el)}// updated hookif (u) {queuePostRenderEffect(u, parentSuspense)}// onVnodeUpdatedif ((vnodeHook = next.props && next.props.onVnodeUpdated)) {queuePostRenderEffect(() => {invokeVNodeHook(vnodeHook!, parent, next!, vnode)}, parentSuspense)}}}

参考Vue3源码视频讲解:进入学习

当数据发生变化的时候,最终会走到上面的else的逻辑部分。

  • 默认情况下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} = n2if (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!)!: fallbackContainerpatch(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.childrenconst prevShapeFlag = n1 ? n1.shapeFlag : 0const c2 = n2.childrenconst {patchFlag, shapeFlag} = n2if (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) {// patchUnkeyedChildrenreturn}}// 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 = 0const l2 = c2.lengthlet 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 + 1const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchorwhile (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 = iconst 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 jlet patched = 0const toBePatched = e2 - s2 + 1let moved = falselet maxNewIndexSoFar = 0const newIndexToOldIndexMap = new Array(toBePatched)for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0for (i = s1; i <= e1; i++) {const prevChild = c1[i]if (patched >= toBePatched) {unmount(prevChild, parentComponent, parentSuspense, true)continue}let newIndexif (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 = jbreak}}}if (newIndex === undefined) {unmount(prevChild, parentComponent, parentSuspense, true)} else {newIndexToOldIndexMap[newIndex - s2] = i + 1if (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_ARRj = increasingNewIndexSequence.length - 1for (i = toBePatched - 1; i >= 0; i--) {const nextIndex = s2 + iconst nextChild = c2[nextIndex] as VNodeconst anchor =nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchorif (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;

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
}

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
一帆风顺二龙腾飞三阳开泰祝福语... 本篇文章极速百科给大家谈谈一帆风顺二龙腾飞三阳开泰祝福语,以及一帆风顺二龙腾飞三阳开泰祝福语结婚对应...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...