昨天我们分析了一小部分patch
过程,主要是在分析unmount
做了什么
坏蛋Dan:vue runtime源码分析学习——day6:patch打补丁part1:注销旧节点
今天我们继续往下分析
这里开始针对不同type
做不同处理
如果是第一次patch
,那么n1
必然是null
,此时直接执行hostInsert
。
我们来看下这个hostCreateText
实际上它就是document.createTextNode
[1]这个api
而hostInsert
方法实际上就是Node.insertBefore
[2]这个api
那么这里的逻辑就很简单了,由于是第一次patch
,即没有dom
可以使用,所以调用hostCreateText
创建一个textNode
的dom
,然后赋值给n2.el
,然后通过parent
将它插入到anchor
的前面。
这个anchor
就是前面我们获取的nextHostNode
,也就是n1
所在位置的下一个节点。
而如果不是第一次patch
,这个时候直接复用n1
的dom
,因为它俩都是文本节点,没必要重新创建一个,浪费性能。如果文本节点的内容不同,则将新节点的文案赋值给这个dom
,hostSetText
方法实际上就是赋值处理。
这里你可能有些疑惑,这里不用担心两个dom
的类型不一样吗?前面只是判断n2
的type
而已。实际上昨天分析的内容就是在处理这个问题,当俩type
不同的时候是直接注销n1
的。
此时n1
会因为没有引用被垃圾回收
这个方法是用来处理注释节点的。
注释节点也是有必要保留的,因为比如v-if
条件不满足的时候就会变成一个注释节点占位,占位的目的就是为了保证dom
的位置的正确。
扯远了,回到代码中。
hostCreateComment
就是document.createComment
[3]hostInsert
前面说过了,这里就不多说了。
然后是如果不是第一次patch
, 新节点直接复用旧节点的dom
,因为注释节点并不支持动态。
静态的节点只需要渲染一次,当然,如果是开发阶段热更新,那另说。
那么什么情况下会是staticNode
呢?
通过createStaticVNode
创建的
我们先来看下mountStaticNode
直接就是调用hostInsetStaticContent
方法
这个方法执行有两种场景,一种是innerHTML
的方式,另一种还是insertBefore
。
我们编译阶段的时候有分析过stringifyStatic
,如果静态节点达到了某个阈值,那么将这块节点stringify
,然后runtime
的时候直接innerHTML
插入。具体分析可以去看我之前的文章。
这里有个点很有趣,这里是直接通过一个节点早就创建好的template
的dom
,然后将我们的staticString
直接innerHTML
插入到它里面,然后再从它里面拿。这样就用创建documentFragment
[4]了。
然后我们再来看下patchStaticNode
方法
因为热更新的原因,所以并不能只渲染一次。
如果新旧的staticNode
不同内容不同,直接就将旧的移除,然后插入新的。这里可以直接判断的原因是children
就是一串字符串,所以可以直接比较。
如果新旧的dom
字符串都一样,那么直接复用n1
的。
removeStaticNode
: 这个就不看代码了,就是在移除旧的所有n1
的内容,包括它的anchor
。
如果是开发模式热更新,就需要将patchFlag
、optimized
、dynamicChildren
都置为false
,强制所有的节点都渲染。
fagmentSlotScopeIds
是用于css :slotted selector
的,简单的说就是支持子组件对slot
中的dom
样式做修改,这在2.x
是无法实现的,因为slot
的内容被视作是父组件里的一部分。如果是第一次patch
,则将两个空文本节点插入到这个fragment
的前后,作为锚点anchor
。注意这俩anchor
和我们之前通过nextSideHost
获取的anchor
是不同的。
mountChildren
方法我们之前说过了,渲染这个fragment
。
我们来看下patchBlockChildren
这里简单的说就是在diff
两个dynamicChildren
,也就是diff
一块区域block
,选择parent
然后递归patch
处理。
旧节点的dom
可能没有,因为它可能是一个被Suspense
包裹的error
状态的async component
,这种场景是不会渲染的。
在旧节点确实被渲染的情况下,如果旧的节点类型是fragment
| 和新的节点类型不一致 | 是自定义组件或者是Teleport
,这个时候的parent
则是旧节点的parentNode
。
这三种场景是一定会使用到parentNode
的。
如果不是,那么直接用传入的父节点的container
,为什么呢?因为这些剩下的类型不一定会用到这个parentNode
,不过为了避免拿到的是null
,还是得绑定上一个。(这一段先mark
,暂时未看懂这么做的缘由)。
回到processFragment
里面,那么什么情况下会进入到这个patchBlockChildren
里面呢?
首先这个fragment
得是stable
的,然后新旧的节点都需要有dynamicChildren
也就是被跟踪的子孙节点。
我们前面说的v-for
是基本没有可能的,除非你这个for
的目标是一个静态的,比如数字10
或者一个对象。
而如果不是,还有两种场景
第一种,没有绑定key
的
第二种,有key
的
但是v-for
里面的item
还是有必要判断下的,因为它们自己可能就是一个dynamicChildren
,比如用<template>
包裹的div
,它自身就是一个fragment
,这个时候就很有必要进去到里面去diff
了。
又比如同<template>
里的子项超出两个,这个时候会单独给它搞个fragment
。我们前面分析编译阶段的时候有说过这种情况,3.x
虽然默认会保留template
标签,但是如果你这个template
是搭配v-if
或者v-for
等,那么这个时候你的这个template
就会被移除,如果这个template
里面超出两个项,那么就会有问题,所以这个时候就需要一个fragment
包裹起来。
我们再来看下traverseStaticChildren
方法
这个方法看名字就知道是用来遍历静态节点的,那么为什么要遍历静态节点呢?
热更新就不用多说了,开发阶段基本都得full patch
。
然后如果是新的节点带有一个key
或者是组件的根节点,那么它有可能会被移动。所以为了确保移动的位置是正确的,新节点需要继承旧节点的el
,也就是对应的dom
。
这里需要注意的是注释节点也是需要继承的,因为它们还有别的作用,并不能丢失,在这个过程中如果丢失很有可能导致后面节点移动失败。
回到processFragment
方法中,最后是处理v-for
非stable
的场景,当然,并非只有v-for
这一种情况,还有可能是手动的fragment
。
前面有说过v-for
如果不是stable
的,那么就有两种情况,一种是绑定了key
的,另一种自然就是没有key
的。
由于我们的代码是经过编译之后才会有这俩场景,所以这个时候我们的v-for
的item
都是block
,所以不用做跟踪处理。
这个时候会调用patchChildren
方法,我们来看下这个方法。
带有key
的执行patchKeyedChildren
(也有可能只有部分item
有key
),如果没有则执行patchUnkeyedChildren
。
patchKeyedChildren
:放下面分析,这里的diff
应该是大家最熟悉的patch
阶段。patchUnkeyedChildren
:这个方法就不用分析了,由于新旧的节点组里的元素都没有key
,所以可以直接一把梭,多的插,少的删。如果不是上面两种场景,那么就是普通的场景,旧的节点可能是文本,数组或者啥也没有。
Array
,那么这个时候直接把旧的数组整组移除了。这个unmountChildren
就不看代码了,相信大家也都知道是在干什么的。然后暴力判断俩是否相同,直接就覆盖,因为只要是不同那就一定需要文本覆盖。hostSetElementText
就不多说了。patchKeyedChildren
来算,如果新的不是数组,那就说明新的是空节点,直接将旧的allin
删除。这个方法很简单,如果是第一次patch
,直接调用mountElement
将新的 vnode
渲染即可。
而如果不是,则调用patchElement
分析两个的不同。
mountElement
这个方法我们放到下面去分析,看名字就知道是将vnode
转换成真实dom
然后渲染到浏览器上patchElement
也是放到下面去分析。简单的说就是对比新旧俩节点内容的不同,然后patch
。这里面基本是在做prop
的处理,因为到这里两个节点的类型、key等都确定是相等的了。不相等的老早就被拉出去patch
了。代码很好理解。如果是keep-alive
包裹下的组件,交给keep-alive
处理。
如果不是,判断此时新的节点是否为空,为空执行mountComponent
,不为空则调用updateComponent
。
mountComponent
:由于篇幅问题,这里就放到后面文章中去分析。updateComponent
:同上。跳过,后面单独分析
同上
就是下面的diff
算法,这里就不多说了
hostCreateElement
:这个方法我们就不看代码了,就是document.createElement
。如果是文本节点,将文案赋值给这个dom
。
如果是这个节点数据类型是数组,遍历递归调用patch
它们。
这里有一点需要注意,数组这个是先mount
子节点,这么做是防止父节点中有对子节点的依赖拿不到的问题,比如props
。
如果存在指令@vue:xx
,则触发,这个之前有说过了,这里就不多说了。不过这里是触发created
。
然后是处理props
:
如果这个props
不是value
或者保留字段就调用hostPathProp方法
我们来看下这个patchProp
方法
这个方法看名字就知道是在给vnode
的属性转换成真实dom
的属性。
patchClass
:这个代码很好懂,不过这里还有一个和transition
相关的,并且也是最核心的,如果我们的dom
是包裹在transition
里的,那么dom
在patch
的时候就会加上这些样式。
patchStyle
:来看下代码这个方法也很好懂,如果新的值不是一个字符串,那么就肯定是一个对象,这个时候迭代这个对象,一个一个调用setStyle
处理。
而如果之前的style
还在,并且也是一个对象,迭代这个旧对象,如果这个旧对象的key
对应的新对象中的数值是null
,那么就直接将这个样式给去掉。
如果不能再新的值,那就直接remove
整个style
即可。
而如果这个新的值是一个字符串,那么这个时候将原来style
的cssText
覆盖为最新的。
如果元素存在_vod
这个属性,那么表示这个元素有一个v-show
指令。此时的display
属性不能由style
控制,它得由我们的handle
来控制,所以需要做覆盖处理,将原来的值覆盖最新的style
里的display
来保持原来的效果。
我们再来看下setStyle
方法
这个方法也很好懂,如果传进来的是一个null
,直接将值赋值为空字符串。
而如果是一个数组,那么这个时候将这个值递归调用setStyle
挨个处理。
而如果名字开始带有--
,说明这个是一个css
变量,得通过setProperty
](CSSStyleDeclaration.setProperty() - Web APIs | MDN (mozilla.org))来加上,因为这个是自定义的样式名。
而一般的指令都会带上自动的前缀,相当于postcss
的autoprefixer
做的事情。
如果你这个样式还带有!important
,那么也会给你加上。
然后我们回到patchProp
方法中。
isOn
方法我们就不看代码了,就是判断你这个是不是v-on
绑定的指令的。
如果有,那么需要过滤v-model
的指令,那个不再需要处理了,因为编译阶段我们已经把它变成了prop + emit
的效果,其它调用patchEvent
方法绑定。
我们来看下patchEvent
方法做了什么。
这里有个点我忘了说了,我们在编译阶段时会把v-bind
的指令都给加上on
开头,比如v-bind:click
会变成onClick
,所以这个方法中实际上是在处理事件绑定,那么为什么要过滤v-model
呢?因为编译阶段我们把v-model
拆解为prop + emit
的方式,而emit
出来的名字就是onUpdate:
,所以这个时候就需要过滤处理。
而这些事件都放到了el._vei
里面,这个是一个对象。后面就叫这个_vei
里的项为触发器了。
如果这个触发器之前就有了,那么将新的事件传覆盖旧的即可。
而如果之前不存在,那么久加到这个触发器集合上。并且给这个元素加上监听上这个事件。
而如果之前存在事件,但是现在不存在了,那么这个时候就去掉这个事件监听。
我们再来看下这个createInvoker
方法做了什么。实际上看名字就知道是创建一个触发器。
这个方法里面涉及到了事件循环机制和异步问题。
这里的注释我们来看下:
内部单击事件触发patch
,事件处理程序在修补程序期间附加到外部元素,并再次触发。发生这种情况是因为浏览器在事件传播之间触发微任务刻度。 vue3
的template
模式不再有这个问题,但理论上对于手写渲染函数来说(也就是通过h
函数来写的)仍然是可能的。解决方案:在附加处理程序时保存时间戳,并将时间戳附加到 Vue 首次处理的任何事件(以避免不一致的事件时间戳实现或从 iframe 触发的事件,例如 #2513) 仅当传递给它的事件被触发时,处理程序才会触发。
简单地说就是可能存在该事件在patch
阶段二次触发的问题,所以这里给加上时间戳用来避免这个问题。
其它没啥好说的了。
然后我们再来看下这个patchStopImmediatePropagation
方法做了什么。
这里就是重写stopImmediatePropagation
方法, 遇到冒泡的给它绕过,然后触发事件。
然后回到我们的patchProp
方法中。我们继续往下看。
如果是是以.
开头或者^
开头的key
,或者是需要通过setAttribute
才能插入的prop
,那么就调用patchDOMProp
方法。
用.
开头的属性我们在编译阶段就遇到过,v-bind
的.prop
语法糖写法。
而^
开头我暂时不清楚是什么场景,有些眼熟。。。有知道的大佬可以评论里说一下,不胜感激!
我们再来看下shouldSetAsProp
方法
方法很简单,就是知道这个key
是否是需要通过setAttribute
才能插入的。
不过在开始分析之前,我们有一个之前一直疏忽掉的点要补充,那就是property
和attribute
的区别。
从翻译的角度来说,它俩确实都叫"属性"。但是实际上它们是有区别的。
从MDN
中对它俩的解释就可以看出来:
attribute
:An attribute extends an HTML or XML element, changing its behavior or providing metadata.An attribute always has the form name="value"
(the attribute's identifier followed by its associated value).property
:A JavaScript property is a member of an object that associates a key with a value. A JavaScript object is a data structure that stores a collection of properties.简单地说:一个是针对HTML Element
的,而另一个则是针对于Object
,在我们这个环境中指的是DOM
即文档对象模型,当然,也是个对象。
svg
的场景我们就不分析了。
如果是spellcheck
、draggable
或者translate
,则返回false
。这三个属性都是global attribute
并且都是枚举类型。由于它们接收的值都是布尔值,而我们的值是字符串,这就会导致我们给他一个'false'
会被强制转换为布尔值true
。所以这里的视它仨作attribute
。
和它们类似的contentEditable
就完全没问题,因为它是一个dom
(我个人猜测背后的实现原理类似video
标签,有可能是shadow DOM
)。
form
属性也得视作attribute
,因为在form
元素上是只读的。list
在input
标签中也得是attribute
。textarea
同上。attribute
。最后放回对应的key
,这个key
是property
。
然后我们回到patchProp
方法中,来看下patchDOMProp
方法。
如果是innerHTML
或者textContent
,那么这个时候批量移除之前的节点,然后将属性插入到el
中。这个应该是仅针对于svg
的。
而如果属性名是value
且标签名不为PROGRESS
且标签名不包含-
符号,一般是自定义元素之类的。先保留原来的值,如果新的值和旧的不相同或者这个标签是OPTION
,那么就将新的值覆盖原来的值。
如果新的值为null
,直接removeAttribute
处理。
而如果属性名不是value
。
先是判断值是否为空,如果为空,判断就的值是否存在,并且判断它的类型。如果类型为boolean
,那么就将这个新的空值从字符串类型转换为布尔值。而如果是字符串类型并且新的值是null
,那么就新的值转换为空字符。然后标记为needRemove
。如果是数字,直接赋值为0
,同时标记为needRemove
。
兼容性代码老规矩就不分析了。
最后来到新值覆盖旧值阶段,当然,这个阶段是有可能失败的,因为有的可能是readonly
等。如果存在需要remove
的属性就直接调用element.removeAttribute
。
然后我们回到patchProp
方法中。
剩下的场景中还有一种需要特殊处理的,那就是<input type="checkbox">
何种类型的。它有俩特殊属性:true-value
和:false-value
。需要将_trueValue/_falseValue
赋值为新的值。
最后就是常规的属性处理了,调用patchAttr
方法,将它们插入到element
里的属性中去。
最后再来简单的看下这个patchAttr
方法做了什么。
注意这里是在处理attribute
,而不是property
, property
前面已经处理过了。
兼容性的跳过
这个specialBooleanAttr
自然就是值是boolean
类型的。
然后就是set
入值,如果是需要插布尔类型值的直接传空字符,这样就会被判定为false
。
那么这个patchProp
方法就说完了,这个方法就和它的名字一样,在将vnode
的属性插入到真实dom
里。
然后回到我们的mountElement
方法中。
上面分析patchProp
是针对key
不是value
的场景,而key
是value
需要特别处理。
在 DOM 元素上设置value
的特殊情况:
#2353 建议添加另一个渲染器选项来配置它,但属性影响是如此有限,值得对其进行特殊大小写。这里是为了降低复杂性。(特殊大小写也不应该影响非 DOM 渲染器)
简单地说,就是value
这个字段在set
的时候需要注意顺序,它会受别的属性值影响。
所以这里干脆把它留到最后了。
接着触发vnode
自身的生命周期方法:onVNodeBeforeMount
。
然后给这个dom
插入scopeId
。
之后触发dom
自己的beforeMount
。
接着触发@vue:beforeMount
注册的回调。
我们来看下这个setScopeId
做了什么。
ok
,没啥好看的。
然后开始插节点到container
里。
执行完毕之后开始触发@vue:mounted
和节点自身的mounted
。
这里需要注意,如果是组件创建的过程中的生命周期函数,触发顺序是先触发child
的生命周期回调,而后才触发父组件中通过@vue:xxx
监听的生命周期。
而注销的时候则相反,是先触发父组件中监听的,然后才触发子组件自己的。
这个方法里面的一些方法前面都有说到过,所以这里就不多说了。
toggleRecurse
:代码就不看了,就是禁止递归触发update
回调。和mountElement
不同的是,这里触发的是beforeUpdate
和onUpdated
这两个生命周期方法。
这里又有一段hmr
相关的代码,如果是开发阶段热更新,需要进行full diff
,也就是不能skip
等操作。
如果存在需要跟踪的子孙节点,这里就调用patchBlockChildren
处理它们,这个方法前面说过了,这里就不多说了。
如果该节点的patchFlag > 0
,说明是可以跟踪的属性。
如果需要full diff
,那么就调用patchProps
做diff
匹配。这种一般是带有动态key
的场景。
另外FULL_PROPS
和CLASS
、STYLE
以及PROPS
互斥。
而如果不需要,就处理几个可能会引起变得的即可。
PatchFlags.CLASS
表示class
中有动态的。
同样PatchFlags.STYLE
表示style
中有动态的。
hostPatchProp
就是patchProp
方法,这里就不多说了。
PatchFlags.PROPS
表示元素的class
和style
没有动态,但是有动态的props
。
有些绕了,实际上搞个例子就很简单了,比如这样::[foo]="bar"
。这种就是动态的,得在runtime
的时候才能确定下来。
记住尽量不要用动态的,这样会有性能问题。
回到我们的代码中,对于PROPS
和FULL_PROPS
来说,基本上就是full diff
了,其中PROPS
会对比新旧的值,不相同或者属性名是value
时才会去patchProp
。
PatchFlags.TEXT
表示这个节点带有动态的文本。这个时候由于n1
和n2
的type
必然相等,所以这里直接就可以判断两个children
也就是字符串是否相同就可以判断是否需要更新。而如果patchFlag < 0
也就是HOISTED
或者BAIL
。
如果此时optimized
为false
,基本上就是热更新的情况下这个字段才是false
。前面好像没说过optimized
这个字段的作用,实际上这个字段是用来控制是否需要skip
等操作的。
如果optimized
为false
,并且dynamicChildren
为null
,这个时候需要full diff
。
最后再触发updated
以及@vue:onUpdated="xxx"
。
这里的diff
算法相信大家都比较熟悉了,这个是vue
面试八股文基本绕不过的点。
我们今天来跟着源码分析下到底是怎么做的。
在开始分析之前,我们需要搞一个例子,这样可视化方便分析。
现在我们有一组旧的节点集合[a, b, c, d]
;
新的具体情况具体分析
这里有点需要注意,while
的条件是按最短的那个来的,也就是短桶规则。
此时我们模拟新的节点集合是[a, b, d, e]
这个时候则是:
先是从前往后,如果类型和key
有一点不同就直接跳过,如果相同就递归调用patch
。
和上面的逻辑一样,不过这次是从后往前。
模拟例子:[a, b, d]
你可能会有些担心这里和前面从前往后同步的节点patch
重复了。
其实完全没必要担心,观察下我们的这个i
,它并没有被重置,也就是说它是整个patch
过程共用的一个索引,而我们的这个第二步从后往前同步的操作则是为了第一步执行完了还有遗漏的节点
我们再来看个例子
模拟的例子还是[a, b, d]
可以看到每部分patch
的内容是不同的,这里是并集,也就是ABD
都被patch
了。
另外还有一点就是patch
阶段第一部分:判断两个节点是否引用的同一块内存空间,如果是则直接return
,所以完全没必要担心。
这里遗漏的也可能是新节点,因为我们是根据短桶效应来实现的。所以哪个短,哪个先跑完。剩下的就被遗漏了。
mount
(common sequence + mount
)在开始分析之前,我们需要先确认下我们的三个标志位:
假设我们这里的例子是 旧的:[a, b, c, d]
,而新的: [a, b, d]
,那么最短的就是新的那组。e1
表示旧的数组length - 1
,初始值是3
; e2
表示新的数组length - 1
,初始值是2
;而i
的初始值为0
。
i
:从前往后第一次匹配到不相同节点的索引值,最大值是e2 + 1
即新的数组后面几个元素被删掉了的场景。在这里在上面的这个假设中的值是2
,也就是就数组c
和新数组d
匹配不上,break
之后的值。e1
:在我们的这个假设里,走完第二步之后值是2
。它的最小值是i - 1
。e2
:在我们的这个假设里,走完第二步之后值是1
。它的最小值是i - 1
。e1
和e2
都为最小值的场景既是没有发生顺序上的变化。
不过需要注意,可能存在-1
的场景,比如旧:[a, b, c, d]
,新:[e, a, b, c, d]
,此时i
为0
,而在执行第二步的时候由于后面两个是一样的,导致多执行了一次让e1
变成了-1
。
为什么要分析这三个标志位之间的关系呢?因为我们需要清晰知道这三个字段的逻辑,这样分析起来才能通顺。
看完上面的几个图,大家应该发现了我们前面两步并没有将它们彻底处理完毕,会有节点被遗漏。比如旧的[a, b, c, d]
,新的[a, b, c, d, e]
或者[e, a, b, c, d]
,这个时候e
会被遗漏。
来看下代码
e1
小于i
的会是什么场景呢?
只有头或者尾部新增了节点的场景,也就是这个例子: 旧的[a, b, c, d]
,新的[a, b, c, d, e]
或者[e, a, b, c, d]
所以此时的旧节点数组全都匹配完毕了,剩下的都是新增的节点放在头或者尾。这个时候只需要patch
i
到e2
之间的内容即可。
总结下:我们第三步就是把头尾任意一方新增了节点的场景给处理了。
注意,这里只处理任意一方加上,而不是头尾都有,第三步还无法处理这个问题。
unmount
(common sequence + unmount
)这个就好理解了,第三步是旧节点被匹配完了,现在变成新节点被匹配完了,还剩下头或者尾任意一处地方还剩下一些旧节点需要被移除。
第四步也无法处理头尾两头都有移除的场景
注意,这里可以看出我们的每一步都不是必要的,有可能不符合条件被跳过。
在进入下一步分析之前,我们来稍微总结下我们现在可以处理哪些场景:
[a, b, c, d]
,新的[a, d]
。[a, b, c, d]
,新的[a, b, e, f, c, d]
。[a, b, c, d]
,新的[e, f, a, b, c, d]
[a, b, c, d]
,新的[a, b, c, d, e, f]
。[a, b, c, d]
,新的[c, d]
。[a, b, c, d]
,新的[a, b]
。现在还不能处理:
[a, b, c, d]
,新的[e, a, b, c, d, f]
。[a, b, c, d]
,新的[b, c]
。[a, b, c, d]
,新的[b, c, d, e]
。[a, b, c, d]
,新的[e, b, c, f]
。[a, b, c, d, e]
,新的[a, c, e]。
[a, b, c, d]
,新的[a, e, b, f, c, g, d]
。[a, b, c, d, e]
,新的[a, c, f, d, e]
总结下,如果新旧任意一方在前面两步中被匹配完了,也就是e1/e2 < i
的情况,那么这个时候我们就可以处理。反之则不行。
unknown sequence
)代码稍微有些长。
我们一块一块来分析
5.1 为新的节点创建key:index
的map
这里直接给e2
一组数组,创建一个key: index
的map
,也就是keyToNewIndexMap
。
这里还有一个我们非常常见的warn
,那就是key
值重复的时候。
这个keyToNewIndexMap
表示的是新数组中存在旧数据的节点(不包括前面四步patch
处理过的)的key
和它所在的位置index
。
5.2 遍历旧节点组,尝试patch匹配到的节点以及移除匹配不到的节点
patched
:这个字段是用来记录当前到底搞到了多少个节点了,如果超出新数组里剩余的数量,那旧数组中的遍历剩下的节点就可以直接删除了。toBePatched
:它表示剩余的需要被patch
的节点个数,由于索引是从0
开始,所以这里给它加1。moved
:这个字段表示存在有节点被移动了位置。maxNewIndexSoFar
:这个应该是用来跟踪是否有节点被moved
了。newIndexToOldIndexMap
:用来表示最长的稳定子序列,也就是删除/新增的连续的子序列最长的长度。它的作用相当于Map
,这里需要注意oldIndex
都是+ 1
的,如果其中一个元素的oldIndex
为0
,那么就表示这个节点是新增的,没有与之对应的旧节点。先给newIndexToOldIndexMap
填充toBePatched
个0
,表示这些节点都是需要被patch
的。
然后循环旧节点数组中还没有识别过的节点,i
从s1
开始,也就是第五步之前的i
的值,而结束的位置是e1
。
如果patched
大于toBePatched
,那么就将剩下循环中的旧节点移除,因为此时新的数组早已经处理完毕了。
接着如果旧节点的key
不是null
,那么这里就拿来和keyToNewIndexMap
里的新节点的key
做匹配,然后赋值给newIndex
,这里就是在通过key
匹配新旧节点。
这个keyToNewIndexMap
前面说过它的类型是Map
表示新的数组中依旧保留的旧节点(不包括前面四步patch
处理过的)的key
以及它的位置index
。
如果匹配到了,那这个节点就可以保留,并且它最新的位置也就是index
也已经知道了,就不用费劲脑子去知道它在新的数组里的状态是移动还是其它场景了。
而如果key
不存在,那么表示这个旧节点之前就没有设置key
(这是一种不好的习惯)这个时候只能尽力去匹配了,如果newIndexToOldIndexMap
里还有元素是0
,那就表示这个节点并没有被匹配过,可能是这个没有设置key
的旧节点的对应节点。
当然,也有可能不是,所以这里还需要进一步判断,调用isSameVNodeType
来判断下这俩节点是否是同一个type
,这里已经尽力了,可能还是有问题,所以我们开发的时候得带上key
就是这个原因。
而如果旧节点的key
不为null
,但是却在新的里面也匹配不到,那么这个节点就可以被抛弃了,直接调用unmount
方法将它移除。
如果newIndex
不为undefined
,那么就表示这个旧的节点有对应的新的节点,且这个newIndex
就是这个旧节点在新节点里的位置。此时将newIndexToOldIndexMap
里的newIndex
索引对应的元素标记为i + 1
,表示这个旧节点已经心有所属了。
如果newIndex
大于等于maxNewIndexSoFar
,那么就将newIndex
的值赋值给maxNewIndexSoFar
,表示这个子序列是连续的,而如果不是,那么将moved
标志位置为true
,表示这个序列断开了。
然后再调用patch
方法将这俩新旧节点进行打补丁处理。
5.3 move 和 mount, 仅当节点们被moved
时创建最长的稳定子序列
getSequence
:这个方法我们就不看了,简单的说就是一个计算最长递增子序列的算法,简单的举个例子:0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15
的最长递增子序列是0, 2, 6, 9, 11, 15.
,当然并不一定只有一个,这个是其中一个。具体可以看:https://en.wikipedia.org/wiki/Longest_increasing_subsequence这一步就是在处理新的节点,由于前面5.2中我们是按顺序自增匹配的,因为里面可能存在没有key
的旧节点找到新节点的场景,所以从后往前找命中还没有patch
的新增节点的概率更大一些。
如果找到newIndexToOldIndexMap
里面为0
的元素,那就代表这个元素对应的下标值在新数组中对应的节点是新增的还没有被patch
。
这个时候将它patch
掉,那么到这里,我们的diff
就完全匹配完了。
这里还有一个场景,那就是moved
为true
,且这个节点不是新增节点的时候。
如果此时没有子序列或者当前遍历的这个节点不在这个子序列里的时候,调用move
方法。
我们这个图里的例子最长递增子序列只有一个元素,所以不为0
的时候就满足这个场景。
如果moved
为true
,那么就表示这里有节点是被移动了位置的。
而获取最长稳定递增子序列是为了避免没必要的move
,因为,递增的顺序保证了新增节点里它们的顺序还是一致的。
我们来看下move
方法做了什么。
前面如果moved
为true
,那么就表示这里有节点是被移动了位置的。
而获取最长稳定递增子序列是为了避免没必要的move
,因为,递增的顺序保证了新增节点里它们的顺序还是一致的。
vnode
:这里的vnode
是新数组里的。MoveType
:一个枚举类型,有三个变体,在这里传入的是REORDER
的变体,因为它们是被移动的旧节点。move
,将组件的子节点传入。suspense
,调用它自己的move
方法,不过经过前面的分析,这里大家应该也都知道了,suspense
的move
实际上就是当前这个move
方法,不过做了一些自己的操作再调用的mvoe
。这里还是不分析,后面我们分析suspense
的时候再说。Teleport
的同上Fragment
,先将他插入到container
之前。然后递归调用move
插入它的子节点。递归完了,再插入anchor
,这个anchor
一般是一个空文本节点,用来表示当前的fragment
结束的位置。static
的节点,那么这个时候调用moveStaticNode
的方法,这个方法就不多说了,也是跟着锚点插。Transition
这里也不分析了,到时也是单独分析。所以这个方法实际上就是在将vnode
对应的dom
插入到页面上去。
这里大家应该都注意到了vue
是边diff
边render
的,有些框架则是先diff
完之后再render
。两者各有各的优点,前者代码量肯定少了很多,而后者比较好维护。
另外还有一点, diff
是在patch
函数执行那一刻的时候就开始了,而不是上面提到的那个diff
算法,实际上上面说的diff
算法只是一小块diff
逻辑。
这块内容很杂并且由于是runtime
代码,所以我这里也没有处处去debug
调试看数据过程和调用栈。这里肯定有些地方分析的不到位甚至是错误的,如果大佬你发现了,务必指出,不胜感激。
如果觉得对你有帮助,也请点个赞,谢谢~
编辑于 2023-03-08 15:55・IP 属地广东