大厂技术 高级前端 Node进阶
点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群
前言
终于迎来了DOM diff
流程的重头戏:diff
算法,前面的流程只能算是附加项,重要的是各种节点是如何进行对比,然后进行更新。下面就对每一种节点的对比流程进行分析。
![f2fd31da458770528b77eb00af3e186e.png](https://i-blog.csdnimg.cn/blog_migrate/395f66dbff2d550a22a40baa310a5672.png)
在vue3.2 初始化的时候做了什么?[1]文章的的末尾,提到了传入effect
的回调函数和响应式数据之前产生一个依赖关系,等同于产生了一个watcher
。当数据发生变化的时候,会以参数二的方法执行参数一,具体细节和调度器有关,以后再说,最终会进入componentUpdateFn
函数中,我们就直接进入到更新阶段的componentUpdateFn
。
patch
之前的处理
![c3db8bf3bf3e2bd4c578734115d5b022.png](https://i-blog.csdnimg.cn/blog_migrate/4142a5d6b5b7b6cb1bc2991f3505f5be.png)
在开始执行patch
函数之前,会先执行一些生命周期钩子函数,有beforeUpdate
和VNode
的hook:beforeUpdate
。
![281f91ebdc3686a87c44b3aea8825b43.png](https://i-blog.csdnimg.cn/blog_migrate/6b0c6bd9c129e95f32c6a4a665a8b5f3.png)
![bf2dd68139d1add596350c475a69d143.png](https://i-blog.csdnimg.cn/blog_migrate/77cd3552a8c7f36eb8cc7855f0053d39.png)
最主要的一点,如果是父组件数据变化而导致的子组件更新,会多执行一个东西,里面会进行更新props
和slots
以及换成新的VNode
,做完这些之后可能会导致更新,需要在patch
之前把它们执行。(PS:更新props
和slots
流程可以看看我前面的文章《Vue3.2 vDOM diff 流程之一:插槽的初始化和更新》[2]和《Vue3.2 vDOM diff流程分析之一:props和attrs的初始化和更新》[3])
![e97634377c831ec3f63b39d974413a31.png](https://i-blog.csdnimg.cn/blog_migrate/009af1963da9513a7adacf116a9d8efc.png)
做完这一些,就可以产生新的VNode
,将新旧VNode
传入patch
开始进行对比,Suspense
和Teleport
的diff
已经在前面的文章中说明,这里就不在提及。
对比元素类型节点
![820dc2119a15259d81460ff2cd8bde24.png](https://i-blog.csdnimg.cn/blog_migrate/3c003a289c7fd7a556f3c470e345a0de.png)
![bd9a773b0337b96a5e5b8a0e7986dc17.png](https://i-blog.csdnimg.cn/blog_migrate/2d3c7f578d796815031ac7cd26aef8ba.png)
对比元素进入processElement
,这次是进入更新流程,执行patchElement
。n1是旧VNode
,n2是新VNode
![d25b1902ef3ecfe788cb54f74793baf1.png](https://i-blog.csdnimg.cn/blog_migrate/2581d7e6f0d0065fae09c3de6ce3742a.png)
函数开头,需要重新通过旧节点的patchFlag
重新确认新节点patchFlag
,因为用户可以克隆由complie
产生的VNode
,或许可能添加一些新的props
,比如cloneVNode(vnode, {class: 'cloneVNode'})
,它将选择FULL_PROPS
。
![c3a6bcaed7327471c1b161e239252562.png](https://i-blog.csdnimg.cn/blog_migrate/74c19f5c389ceec01777bac783d945f4.png)
紧接着执行新的VNode
自定义指令的beforeUpdate
生命周期函数,如果在dev
模式下且HMR正在更新,则放弃优化且把dynamicChildren
清空,使用全量diff
。这会影响后面diff
,但是prod模式下一般都是优化模式,使用areChildrenSVG
是判断新VNode
是不是SVG。
![d327842418297e0f1558f4b1577a3352.png](https://i-blog.csdnimg.cn/blog_migrate/3a960a2c420ce59e68bfe57f858087c0.png)
这里分为优化模式和非优化模式,这里进入优化模式的条件是dynamicChildren
不为空,非优化模式是optimized
为true
,但是这两个是互斥的,一个存在另一个肯定不存在。
优化模式下进行diff
![04e1825ea21800b2c34197c68fb1aa0d.png](https://i-blog.csdnimg.cn/blog_migrate/56c69c70ea3cfd4bc822970c0f4bfc4f.png)
进入到这个函数,他会遍历新VNode
中dynamicChildren
,并从旧的VNode
的dynamicChildren
取出按索引顺序一致的节点进行对比。
在这之前,先要找到parent node
,也就这一大坨的三元运算符,不要慌张,逐个逐个条件分析,oldVNode.el
是为了在异步组件的情况下确保元素节点的真实DOM
要存在。
在oldVNode.el
存在的情况下,并且符合以下三个条件中的其中一个:1. oldVNode
的节点类型是Fragment
、2.oldVNode
和newVNode
不是同一种元素(key
值不一样也算)、3.oldVNode
是组件,就组件而言,它可以包含任何东西。container
就是oldVNode.el
的parent
。不然在其他的情况下,实际上没有父容器,因此传递一个block
元素,避免parentNode
,就是传递fallbackContainer
(是n2的真实DOM
),
确认好container
就和oldVNode
与newVNode
再次传递给patch
,接下来就要根据newVNode
的节点类型从而确定走哪个分支进行diff
。
![c2275182bfd996a3bfa6c2297e79c74b.png](https://i-blog.csdnimg.cn/blog_migrate/79de5e18b87710e879703c8c28fc1b7e.png)
diff
流程结束之后还需要做一件事,在dev模式下,如果parentComponent
存在并且parentComponent
启用 HMR,需要递归寻找或者是定位旧的el 以便在更新节点进行引用 防止更新阶段会抛出el is null
。优化模式分析完毕。
非优化模式下进行全量diff
![142305f1a80c84d475f9a7fd2d8a6fd8.png](https://i-blog.csdnimg.cn/blog_migrate/dfe7d9fadc7b6eb23ce241c7d8d366a0.png)
非优化模式下交给patchChildren
处理,在diff
之前先要拿到一些东西:n1、n2的Children
和n2的shapeFlag
。接下来的流程分为很多种情况,一一分析。
快速diff
首先根据n2的patchFlag
判断能不能快速更新,也就是“靶向更新”,进入之后又分为两种情况,是否键控(是否绑定了key
),键控可以是完全键控也可以是混合键控(一部分带key
,一部分不带key
),分别交给patchKeyedChildren
和patchUnKeyedChildren
处理。
不带有key
的对比
![e5c3fd78442ab587f303218eb47230fa.png](https://i-blog.csdnimg.cn/blog_migrate/f77bf071074d7759e59a5c964503fcbc.png)
由于带有key
的对比有点复杂,我放的后面说,这里先看没有带key
。没有带key
的对比简单粗暴,因为不确保n1和n2都有children
列表,没有就默认给一个空数组。需要注意这里获取长度,从新旧children
列表两个列表长度中取出长度的最小的作为基准,接下来的对比最多只会对比到这个位置。具体用图解释。
![33f2bde7e52be7c833cd8431dc35405f.png](https://i-blog.csdnimg.cn/blog_migrate/6d89a505a1191bc60e0a759815ee13d5.png)
如图所示,旧children
列表长度是5,新children
列表长度是3,取小的也就是3,代表在循环一对一对比中只会对比前三个,剩下会交给下面的流程。
![acf3f3a7af3bfc8254b1ec6fff229ddc.png](https://i-blog.csdnimg.cn/blog_migrate/76bd37fd25f737bbee4010af4d071e22.png)
剩下流程分为两种情况,在循环对比后,如果是新children
列表比旧children
列表长度长说明有新节点,就会去挂载新节点,反之说明有不需要的旧节点,就会去卸载。流程结束。
带key
的对比
回到patchChildren
中,我们看带有key
是如何对比,将会结合图一步步分析。
![eaac16ad1276d60095bcb92db6d2029a.png](https://i-blog.csdnimg.cn/blog_migrate/39d2c687d928681bcb4a0b9a2e910d63.png)
这里先拿到一些东西,l2
是新children
列表的长度,e1
是旧children
列表中最后一位的索引,e2
是新children
列表最后一位的索引。i
这里有特殊意义,代表对比的开始索引。带有key
的对比主要有五个流程,
假如有如下新旧children
列表,可以准确看出只有2移动了位置,下面就看经过五个流程是如何进行对比的。
![c24ba22cfaa46a5bac04bb6d6e7e9216.png](https://i-blog.csdnimg.cn/blog_migrate/e7be5449c74fa043e771a1d1d8e7268c.png)
1.流程一:对比开始位置
![c69c877591ae88a30770744a1e78e911.png](https://i-blog.csdnimg.cn/blog_migrate/e30c1df1b3e7bbabb8008b74e074212a.png)
在这一阶段会遍历新旧children
列表,只有新旧节点是用一种元素才会交给patch
函数对比,每过一对新旧子节点,i
就会加一,如果有一方遍历到最后一个就会结束或者是遍历到两个是不同元素。例子中,前面没有相同的节点,所以不会有任何操作
![08e6c134f8f691875fed71f63a0e4289.png](https://i-blog.csdnimg.cn/blog_migrate/911d734e2e54d6bd92c96f9f36714afb.png)
2.流程二:对比末尾位置
![2ab12fce1869a372e620f848fa7ee54b.png](https://i-blog.csdnimg.cn/blog_migrate/6f366550be1b05449952143e5c909649.png)
在这一阶段一样会遍历新旧children
列表,和阶段一一样,新旧节点是同一种元素才会交给patch
函数对比,不同的是从末尾开始对比子节点,每过一对子节点,新旧最大位置索引同时会减一。例子中,从末尾的3、4、5是相同元素可以排除。
![e8a47aff37ddff98d850e11fc5f16f78.png](https://i-blog.csdnimg.cn/blog_migrate/49aaa5ce5aa05bda8e747b005c0542d6.png)
走完前面的两个,说明新旧children
列表中首尾的相同节点已经被处理了,就剩下中间的部分,接下来的三个流程是挂载列表中的新节点和卸载不需要的旧节点以及无序对比。
但这三个流程中只会执行其中一个或者都不执行,总共有三种情况:1. 只需要安装新节点、 2. 只需要卸载旧节点、 3. 无序。这和前面的讲到的全量diff
和像,这就要看i
了,如果i
大于e1
并且小于或者等于e2
说明有新节点,执行流程三,如果i
是大于e2
说明有不需要的旧节点,执行流程四。都不符合执行流程五
3.流程三:挂载新节点(此流程不一定执行)
![c9add542713621c9de6f115f6aed7a57.png](https://i-blog.csdnimg.cn/blog_migrate/48b18c88f7c80574afeea1db8b6d6b0e.png)
nextPos
是用来确定新增节点的位置,一般到了这一阶段e2
是没有处理的新节点列表的最大索引,要加一是因为vue
新增节点的方式了,vue
新增元素是通过insert
,实现原理是insertBefore
,所以这里会拿到将要插入元素的位置的后一个。具体看下面的示意图。(ps:红色框内是被处理过的)
![2443730382769808eeff03611f38c076.png](https://i-blog.csdnimg.cn/blog_migrate/d5af4a6653dd0cb37747a709b0d0eca5.png)
在这个案例中,6是新增的节点,因为经过了流程一和二的处理,i
变成了5,e2
变成5,e2
正好是节点6的索引,如果我们需要把它插入列表中,我们需要知道他的后一个节点是谁,以便做为瞄点,这就要加一后去新children
列表中找。
![120e63a4b07a2fa5e39384c5bd1cd2de.png](https://i-blog.csdnimg.cn/blog_migrate/084770bdf92ea97a5ebb6b5d48fd5a78.png)
但是还有第二种情况,如果新增的节点是新children
列表中的最后一个,那么加一就会超出其长度,那么就会把parentAnchor
作为瞄点,parentAnchor
是当前列表的父容器中的最后一个节点,一般都是空字符串,(注意:这里是节点,不是元素节点)。例子中不符合,不会执行该流程
4.流程四:卸载不需要的旧节点(此流程不一定执行)
![4d610246de1e3bbf80c813c44e0c124e.png](https://i-blog.csdnimg.cn/blog_migrate/126543d0bc7f1ae51c63465427ccb08e.png)
卸载旧节点的操作就比较简单了,每卸载一个i
就加一,通过unmount
方法进行卸载,实现原理是通过找到要卸载的节点的父节点,调用removeChildren
进行卸载。前提是i
大于e2
但小于等于e1
。例子中不符合,不会执行该流程
5.流程五:无序对比(此流程不一定执行)
如果到了流程五,说明children
列表中有一部分是无序的,前面的流程无法处理,需要进行无序对比。这流程五分为三部分。
这第一部分是为了产生
index
和新children
列表中的key
的映射图,它会拿i
作为新旧children
列表的开始索引,当找到newChildren
,准确来说是找到newChild
身上的key
,就会连同i
一起保存进keyToNewIndexMap
中。
![1fed78141623dbf95e40a3f55516a055.png](https://i-blog.csdnimg.cn/blog_migrate/623088aa22a3e1bcfd795b9424a13d2e.png)
这第二部分是循环旧节点列表 以匹配需要更新的节点和删除不需要的节点,先提前创建一个数组(newIndexToOldIndexMap
),长度是还需要进行对比(toBePatched
)的数量,作为新旧索引对应的存放(默认全部都是0)
开始循环旧children
列表,当patched
大于toBePatched
时就都是卸载节点,但是一开始patched
是0并不会大于,继续往下走,开始找newIndex
,先从在前面保存的key:index
的映射图中找,没找到就尝试在旧children
列表中定位同一种类型没有key
的节点的索引。还是没有就只能undefined
。
最后,如果newIndex
是undefined
,说明旧节点没有对应的新节点直接卸载,不然,会修改newIndexToOldIndexMap
中对应索引位置,如果newIndex
小于新节点最大位置(maxNewIndexSoFar
),说明这个节点移动了,不然maxNewIndexSoFar
就赋值成newIndex
。过了这么多,终于可以传递给patch
进行对比,patched
也会加一。
这最后一部分,主要是为了移动节点和新增节点,如果有需要移动节点它会先根据新旧节点索引的映射产生一个最长递增子序列。而从最后开始循环也便于我们可以使用最后一个修补的节点作为瞄点,找出新节点中的最长递增子序列,移动不在这个范围内的节点,如果映射的
oldIndex
是0说明是新增节点,需要进行挂载。在例子中,就会移动1。
![6d49242a4e64e3b49b264876830f80f8.png](https://i-blog.csdnimg.cn/blog_migrate/49cbb1943eebdd1f3311a0bf9d70b558.png)
这流程五是最复杂的,其中不仅包含了挂载和卸载,还包含了移动节点,提高了对节点利用,到此patchKeyedChildren
流程结束。
其他情况
回到patchChildren
中,继续看patchFlag
不存在如何进行对比,这要根据新旧节点的情况进行更新
![c4ac04cbc7b574e5766a9c68497e1b15.png](https://i-blog.csdnimg.cn/blog_migrate/40a391f84841564b2908d8060bbaed34.png)
看起来复杂其实很简单,先说如果新节点是TEXT_CHILDREN
,如果旧节点是ARRAY_CHILDREN
,会先卸载所有旧节点,再挂载新节点,旧节点也是TEXT_CHILDREN
需要和新节点对比确认不同后再更新。
如果两个都是ARRAY_CHILDREN
,需要走patchKeyedChldren
,但也有可能只是卸掉旧的并没有新节点,卸载所有旧节点。
当旧节点是TEXT_CHILDREN
新节点是ARRAY_CHILDREN
时,会先将其变为空字符串,再进行挂载新节点。
后面对比props
的部分,在我之前的文章Vue3.2 vDOM diff流程分析之一:props和attrs的初始化和更新[4]中讲过,感兴趣可以去看看,到这里对比元素的流程结束。
对比组件类型节点
![2a0985e3854dd3980cccf7162ca96920.png](https://i-blog.csdnimg.cn/blog_migrate/57775c5a172a5e8126ac162adab35265.png)
在patch
函数中,对比组件分支执行的是processComponent
,最终会执行updateComponent
,组件更新新的会继承旧的实例。
更新前他会执行shouldUpdateComponent
判断是否需要更新。但是属实是情况太多,这里就不一一列举了,具体可以到源码中查看`shouldUpdateComponent`[5]函数。
![164b25ebd0742c2369ceaefe54605416.png](https://i-blog.csdnimg.cn/blog_migrate/d7f7b08b462cddce8e11bfaa67ecd8de.png)
进入需要更新的流程,他是会优先处理Suspense
(存在asyncDep
且asyncResolved
不存在),不是Suspense
就正常更新,把新VNode
(instance.next
)赋值成n2,如果当前组件已经在更新队列中,请将它移除,避免重复更新同一组件,然后就可以调用实例上的更新器进行更新了。
![b13003b6b58214b12462e6549d76da15.png](https://i-blog.csdnimg.cn/blog_migrate/934d01725a6a979a2ec020296d740e96.png)
注意这里的instance.next
,如果这个存在,在调用componentUpdateFn
中会调用updateComponentPreRender
函数,这是因为组件数据变化导致其子组件更新,所以需要去更新实例中的VNode
以及props
和slots
,顺带把更新props
导致的更新执行了。如果只是单纯的数据变化,没有影响到子组件,那next
就会是原本实例上的VNode
。
后面的就是正常调用生命周期函数和钩子函数,产生新的VNode
和旧的VNode
一起交给patch
进行对比,后面的就要看组件里面是啥东西然后走哪个流程。
对比文本类型、注释类型、静态节点类型节点
文本类型
![f703ab0666ca3b7bf6195cad61b21ca4.png](https://i-blog.csdnimg.cn/blog_migrate/869c513991250d48051c32045177a0e7.png)
文本类型节点的更新在processText
中,会先进行对比,不同才会更新文本
注释类型
![304990d62df57bf305ed4bd358a4f743.png](https://i-blog.csdnimg.cn/blog_migrate/dd21d8876a79826ca483af3d861b0f46.png)
注释节点的更新在processCommentNode
中,但是因为不支持动态更新注释,所以是直接拿以前的。
静态节点类型
![7ef7fe7b52df15ad3125fa8f99b19190.png](https://i-blog.csdnimg.cn/blog_migrate/c5aea696192e9ffb221da1d3ccbf4d87.png)
静态节点的更新执行的是patchStaticNode
,因为vue会把静态节点进行序列化成字符串所以可以直接进行字符串对比,相同只会赋值以前的el
和anchor
,不同会先循环移除旧的,连带着anchor
一起移除,再挂载新的静态节点。
总结
本篇文章分析了vue中diff
算法的处理,清楚vue中diff算法的处理流程,知道每一个节点对比如何进行,如何书写模板可以进行最优的对比、复用节点,从而提高性能,在列表对比中,优化模式只会对比dynmaicChildren
中的节点,也就是动态节点,非优化模式下,虽然说是全量diff
但是可以复用节点也不会损耗太多性能。
好了,到了文章的最后,还是希望各位哥哥姐姐能指导指导。有说错或者遗漏的欢迎在评论区讲解,谢谢。
关于本文
作者:咸鱼是如何练成的
https://juejin.cn/post/7072321805792313357
Node 社群
我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。
如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:
1. 点个「在看」,让更多人也能看到这篇文章2. 订阅官方博客 www.inode.club 让我们一起成长
点赞和在看就是最大的支持❤️