Vue3源码解析,打造自己的Vue3框架【无密分xiang】

226 阅读16分钟

downlaod:Vue3源码解析,打造自己的Vue3框架

Vue3源码解析,打造自己的Vue3框架 - Vue3 响应式API-Computed和Watch

什么是响应式
响应性是一种允许我们以声明式的方式去适应变化的编程范例。
当一个值依附另一个值进行计算时, 如果前一个值发生了变化, 依附的那个也会同时进行变更.
典型案例参见 excel 函数. 当单元格A3=SUM(A1:A2)时, 此时更改A1或A2任意一个发生改变时, A3会立即作出响应更新.
响应式是Vue3的核心API。
官方也把此响应式API进行单独打包,即便不使用Vue3的情况下也可以独立使用:@vue/reactivity
响应式API-Computed和Watch
在响应式核心库中一共有三大模块,ref、reactive响应式变量、computed计算属性变量、watch状态监听函数
computed 计算属性
方式一
接受一个 getter 函数,并为从 getter 返回的值返回一个不变的响应式 ref 对象。
const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误
复制代码
方式二
它也可以使用具有 get 和 set 函数的对象来创建可写的 ref 对象。
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})

plusOne.value = 1
console.log(count.value) // 0
复制代码
watch 状态监听
watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在侦听源发生更改时被调用。
侦听一个单一源
侦听器数据源可以是具有返回值的 getter 函数,也可以是 ref:
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)

// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... /
})
复制代码
侦听多个源
侦听器还可以使用数组同时侦听多个源:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/
... */
})
复制代码
深度监听对象源
尝试检查深度嵌套对象或数组中的 property 变化时,仍然需要 deep 选项设置为 true。
const state = reactive({ 
id: 1,
attributes: { 
name: ''
}
})

watch(
() => state,
(state, prevState) => {
console.log(
'deep',
state.attributes.name,
prevState.attributes.name
)
},
{ deep: true }
)

state.attributes.name = 'Alex' // 日志: "deep" "Alex" "Alex"
复制代码
或者,也可以使用深拷贝函数实现深度监听
import _ from 'lodash'

const state = reactive({
id: 1,
attributes: {
name: '',
}
})

watch(
() => _.cloneDeep(state),
(state, prevState) => {
console.log(
state.attributes.name,
prevState.attributes.name
)
}
)

state.attributes.name = 'Alex' // 日志: "Alex" ""
复制代码
watchEffect
为了根据响应式状态自动应用和重新应用副作用,我们可以使用 watchEffect 方法。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
const count = ref(0)

watchEffect(() => console.log(count.value))
// -> logs 0

setTimeout(() => {
count.value++
// -> logs 1
}, 100)
复制代码
此方法用法虽并非难事, 但在实际应用如稍处理不当代码可读性堪忧, 如果久置代码可能会令人茫然.
watchEffect->stop() 停止监听
显式调用返回值以停止监听:
const stop = watchEffect(() => {
/* ... */
})

stop()
复制代码
感悟与愿想
目前Vue3的响应式框架可算是把函数式玩的出神入化.
API使用不难, 但您实际编写项目时真能完全有效拆解并直观的读懂代码以及执行逻辑的运行轨迹么?
假设你写完的你认为拆分很棒的逻辑代码, 别人能直接上手改么?
函数式版响应式框架给我带来了一些苦恼:
第一个苦恼, 每次声明出来的东西都要加value. 很难受! 真的很难受! 非常难受!😣
第二个苦恼, 每次都要在callback()中进行. 令人抓狂!😫
如果声明响应式变量能有语法糖的解决方案, 避免每次都要const, 并把它移植入ECMAScript规范中, 那JS将会更上层楼.
以下是我的ref和computed语法糖想法:
ref count = 1;
computed plusOne = () => count + 1

console.log(plusOne) // 2
plusOne++ // 错误
复制代码
处理getter、setter并存的计算属性情况.
ref count = 1;
computed plusOne = {
get: () => count + 1,
set: val => {
count = val - 1
}
}

plusOne = 1
console.log(count) // 0

Vue3源码解析,打造自己的Vue3框架 - Vue3中runtime-dom的实现-详细步骤

Vue为了解耦,将逻辑分成了两个模块
运行时 (不依赖平台的browser),主要靠的是虚拟dom,它是可以跨平台的

针对不同平台的运行时,vue就是针对浏览器平台的

runtime-dom提供一个渲染器,这个渲染器可以渲染虚拟dom节点到指定的容器中。

官方提供
Vue3中runtime dom的实现 详细步骤 第1张

根据用户传入的内容进行渲染

我们实现这个runtime-dom,创建runtime-dom文件夹
Vue3中runtime dom的实现 详细步骤 第2张

创建src文件夹,下面创建index.ts,以及nodeOps.ts提供一些对节点的操作
插入节点
Vue3中runtime dom的实现 详细步骤 第3张

anchor是插入参照物,可以为null,当anchor参数为null的时候,inserBefore等同于appendChild

上述是对节点的一些操作,下面我们来写对属性的一系列操作,创建patchProp.ts文件以及ptachProp需要调用的一些方法单独抽离的文件
Vue3中runtime dom的实现 详细步骤 第12张

编写patchProp方法
Vue3中runtime dom的实现 详细步骤 第13张

整合nodeOps和patcProp
Vue3中runtime dom的实现 详细步骤 第14张

我们现在来详细写patchProp
首先判断key的类型,分别交由不同的函数处理

区分是class、style、事件和普通属性

Vue3中runtime dom的实现 详细步骤 第15张

处理class
Vue3中runtime dom的实现 详细步骤 第16张

新改了值直接覆盖class

新值是null,说明是删除操作,直接删除

处理style
Vue3中runtime dom的实现 详细步骤 第17张

循环最新值,把所有新值全部覆盖即可

去除之前有的,新的没有的属性

处理事件
Vue3中runtime dom的实现 详细步骤 第18张

如果没绑定过事件
我们可以在元素上绑定一个_vei来记录元素绑定过的事件

判断这个事件名是否存在,如果不存在,拿到新事件名字,on后面的值

然后判断当前的新的事件是否有值,如果有值,我们用createInvoker处理事件值后,缓存到invokers中

然后绑上事件

如果新设置的事件值为空,那么就解绑事件,然后清空缓存区里这个事件

createInvoker这种写法有利于我们换绑事件,如果不这样写,那么我们每次更改事件的时候都需要先解绑,再绑新事件。

如果绑定过事件并且也设置了新值
更换缓存区中该事件invoker的value值,更改为新值就可以了

createInvoker里为每一个事件创造了一个空函数,这个空函数的返回值调用这个空函数的value,而我们设置的事件就存在这个空函数的value属性上,这种操作有利于我们换绑事件

处理属性
Vue3中runtime dom的实现 详细步骤 第19张

如果新值有值,我们就直接设置到元素上

如果没有,我们就删掉

runtime-dom还提供一个api,render
Vue3中runtime dom的实现 详细步骤 第20张

render内部帮我们调用createRenderer后调用它的render进行渲染

createRenderer方法和h方法其实都不是在runtime-dom里实现,他俩是在runtime-core里实现的,因为渲染器他们本身是和平台无关的,你给我的是dom渲染还是canvas等,他们是不关心的
我们现在创建runtime-core模块
Vue3中runtime dom的实现 详细步骤 第21张

我们下一章再实现h方法和createVnode方法
总结
runtime-dom本质上没做什么,它只是封装了一些平台对应的代码,它封装好以后,内部去调用创建渲染器去进行一个渲染,那具体怎么渲染是内部去做的,内部runtime-core去根据你传进来的一些属性操作、事件操作和dom操作等等方法,把传进来元素生成的vnode虚拟节点,然后放进对应的容器中就可以了。所以runtime-dom跟平台有关,而runtime-core跟平台无关。

 vue2 和 vue3 核心diff算法

为什要使用diff算法
diff算法的使用要先从vue框架的设计说起,从范式角度来看,框架可以设计成命令式或声明式,权衡了性能和可维护性vue选择声明式的设计方案。
命令式和声明式
命令式:代码本身描述的是“做事的过程”,在代码开发的时候,我们需要维护实现目标的整个过程,包括要手动完成DOM元素的创建、更新、删除等工作。如:Jquery就是典型的命令式框架.声明式:更加关注结果,代码展示的就是我们要的结果,看上去更加直观,至于做事儿的过程,并不需要我们关心.
从上面可以看出声明式的可维护性高于命令式,心智负担也小于命令式,但性能比命令式要差。
命令式代码的更新性能消耗 = 直接修改的性能消耗,声明式 = 直接修改 + 找出差异的性能消耗。
那么我们只要能把找出差异的性能消耗最小化,就可以让声明式的消耗无限接近命令式。这个时候我们就要使用虚拟dom和diff算法了
什么是虚拟DOM和diff算法
虚拟DOM就是用来表示真实dom的对象,vue通过模版编译生成虚拟DOM树,然后在通过渲染器渲染成真实DOM,当数据更新时,产生新的虚拟dom树,如果直接用新的虚拟DOM树生成真实DOM并不是最优的方法。为了进一步降低找出差异的性能的性能消耗,就要使用diff算法。Diff算法是一种对比算法。对比两者是旧虚拟DOM和新虚拟DOM,对比出是哪个虚拟节点更改了,找出这个虚拟节点,并只更新这个虚拟节点所对应的真实节点,实现精准地更新真实DOM。
注:下图示中的a,b,c为节点的key值,所有的移动插入删除操作都在真实dom,下文所讲是diff算法中的核心部分
vue2 双端diff算法的实现
vue2采用了双端diff算法。核心方法是updateChildren,通过新前与旧前、新后与旧后、新后与旧前、新前与旧后、暴力比对5种查找。
新前:newChildren中所有未处理的第一个节点
新后:newChildren中所有未处理的最后一个节点
旧前:oldChildren中所有未处理的第一个节点
旧后:oldChildren中所有未处理的最后一个节点
在具体介绍前我们还需要了解isSameVnode这个用来对比两个节点是否相同的方法
// 判断两个vnode的标签和key是否相同 如果相同 就可以认为是同一节点就地复用
function isSameVnode(oldVnode, newVnode) {
return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;
}
复制代码
新前与旧前

新前与旧前对比,如果相同那么新,老的开始下标往后移动一格,上图中a的新老节点相同,位置移动b位置,此时新节点为f,两节点不同,进入新后与旧后比对
新后与旧后

新后与旧后对比,如果相同那么新,老的结束下标往前移动一格,上图中g的新老节点相同,位置移动f位置,此时新节点为b,两节点不同,这时发现新后与旧后,新前与旧前都不满足,进入新后与旧后比对
新后与旧前

新后与旧前对比,如果相同那么,把老的开始节点移动到老的结束节点后面,然后老的开始下标往后移动一格,新的结束下标往前移动一格。这时发现新的位置以上3种都不能满足,进入新前与旧后比对
新前与旧后

新前与旧后对比,如果相同那么,把老的结束节点移动到老的开始节点前面,然后新的开始下标往后移一格,老的结束下标往前移动一格。
暴力比对(乱序)

如果节点比对的时候上面4种方法都不适用时,此时我们只能用最暴力的方法,首先我们需要循环oldChildren生成一个key和index的映射表{'a': 0, 'b': 1},然后我们用新的开始节点的key,去映射表中查找,如果找到就把该节点移动到最前面,且原来的位置用undefined占位,避免数组塌陷 防止老节点移动走了之后破坏了初始的映射表位置,如果没有找到就直接把新节点插入
新节点有剩余

有时候我们会添加新的数据,这时后上面循环结束后,newChildren还有剩余的节点还没有处理,我们需要循环这些节点,逐个插入。如上图所示。当oldStartIndex > oldEndIndex时,新的子节点还有c, d两个节点多余,需循环插入这2个节点。
老节点有剩余

另一种情况就是当我们删除元素,这时当对比循环结束后,oldChildren还有剩余的节点还没有处理,我们需循环这些节点,逐个删除。如上图所示。当newStartIndex > newEndIndex时,老的子节点还有c,d两个节点多余,循环删除这2个节点
全部核心代码
// diff算法核心 采用双指针的方式 对比新老vnode的儿子节点
function updateChildren(el, oldChildren, newChildren) {
let oldStartIndex = 0; // 老儿子的开始下标
let oldStartVnode = oldChildren[0]; // 老儿子的第一个节点
let oldEndIndex = oldChildren.length - 1; // 老儿子的结束下标
let oldEndVnode = oldChildren[oldEndIndex] // 老儿子的最后一个节点
let newStartIndex = 0; // 新儿子的开始下标
let newStartVnode = newChildren[0]; // 新儿子的第一个节点
let newEndIndex = newChildren.length - 1; // 新儿子的结束下标
let newEndVnode = newChildren[newEndIndex] // 新儿子的最后一个节点

// 根据key来创建老的儿子的index映射表,如{'a': 0, 'b': 1}代表key为'a'的节点在第一个位置,'b'在第二个位置
const makeIndexBykey = (children) => {
return children.reduce((memo, cur, index) => {
memo[cur.key] = index
return memo
}, {})
}
const keysMap = makeIndexBykey(oldChildren)

// 只有当新、老儿子的开始下标都小于等于结束下标时才循环,一方不满足就结束循环
while(oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
// 因为暴力对比过程把移动的vnode置为 undefined 如果不存在节点直接跳过
if (!oldStartVnode) { 
// 开始位置 向后 +1
oldStartVnode = oldChildren[++oldStartIndex]
} else if (!oldEndVnode) {
// 结束位置 向前 -1
oldEndVnode = oldChildren[--oldEndIndex]
}

if (isSameVnode(oldStartVnode, newStartVnode)) { 
// 新前和后前相同
// 递归比较儿子以及他们的子节点
patch(oldStartVnode, newStartVnode)
// 新,老开始下标 +1, 对应的节点变为 +1 后的节点
oldStartVnode = oldChildren[++oldStartIndex]
newStartVnode = newChildren[++newStartIndex]
} else if (isSameVnode(oldEndVnode, newEndVnode)) {
// 新后和旧后相同
// 递归比较儿子以及他们的子节点
patch(oldEndVnode, newEndVnode)
// 新,老结束下标 -1, 对应的节点变为 -1 后的节点
oldEndVnode = oldChildren[--oldEndIndex]
newEndVnode = newChildren[--newEndIndex]
} else if (isSameVnode(oldStartVnode, newEndVnode)) { 
// 新后和旧前相同
// 递归比较儿子以及他们的子节点
patch(oldStartVnode, newEndVnode)
// 开始节点的真实dom,移动到结束节点的下一个前点的前面
el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling)
// 老的开始下标 +1, 对应的节点变为 +1 后的节点
oldStartVnode = oldChildren[++oldStartIndex]
// 新的结束下标 -1, 对应的节点变为 -1 后的节点
newEndVnode = newChildren[--newEndIndex]
} else if (isSameVnode(oldEndVnode, newStartVnode)) { 
// 新前和旧后相同
// 递归比较儿子以及他们的子节点
patch(oldEndVnode, newStartVnode)
// 结束结束的真实dom,移动到开始节点的前面
el.insertBefore(oldEndVnode.el, oldStartVnode.el)
// 老的结束下标 -1, 对应的节点变为 -1 后的节点
oldEndVnode = oldChildren[--oldEndIndex]
// 新的开始下标 +1, 对应的节点变为 +1 后的节点
newStartVnode = newChildren[++newStartIndex]
} else {
// 上述四种情况都不满足 那么需要暴力比对
// 用新的开始节点的key,去老的子节点生成的映射表中查找
const moveIndex = keysMap[newStartVnode.key]
if (!moveIndex) { 
// 如果没有找到直接把新节点的真实dom,插入到旧的开始节点的真实dom前面
el.insertBefore(createElm(newStartVnode), oldStartVnode.el)
} else {
// 如果找到,取出该节点
const moveNode = oldChildren[moveIndex] 
// 原来的位置用undefined占位 避免数组塌陷 防止老节点移动走了之后破坏了初始的映射表位置
oldChildren[moveIndex] = undefined
// 把取出的节点的真实dom插入到开始节点的真实dom前面
el.insertBefore(moveNode.el, oldStartVnode.el)
patch(newStartVnode, moveNode) //比较
}
// 新的开始下标 +1, 对应的节点变为 +1 后的节点
newStartVnode = newChildren[++newStartIndex]
}
}
// 如果老节点循环完毕了 但是新节点还有,如用户追加了一个,需要把剩余的节点插入
if (newStartIndex <= newEndIndex ) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
// 这是一个优化写法 insertBefore的第一个参数是null等同于appendChild作用
// 看一下 结束指针的下一个元素是否存在
let anchor = newChildren[newEndIndex + 1] == null ? null : newChildren[newEndIndex + 1].el
el.insertBefore(createElm(newChildren[i]), anchor)
}
}
// 如果新节点循环完毕了 但是老节点还有,如用户删除一个,需要把剩余的节点删除
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
// 该节点不是占位节点,才做删除操作
if (oldChildren[i] != null) {
el.removeChild(oldChildren[i].el)
}
}
}

}

复制代码
vuu2 双端diff算法流程图(放大查看)

vue3 快速diff算法的实现
注:下边所讲diff算法是vuejs设计与开发书的版本,与源码版有些差别,但核心部分是一样的,可去mini-vue查看源码版
vue3 使用了快速diff算法,核心方法是patchKeyedChildren,首先是借鉴了纯文本diff算法中的预处理思路,处理新旧两个组子节点中相同的前置节点和后置节点。处理完后,如果剩余节点无法简单的通过挂载新节点或者卸载已经不存在的节点来完成更新,则需要根据节点的索引关系,构建出一个最长递增子序列。最长递增子序列所指向的节点即为不需要移动的节点。
相同前置节点处理

前置节点的处理是定义了一个j变量,分别指向新,老两个组子节点,比较指向的新,老节点是否相同,如果相同指针 +1,直到两个节点不同时结束前置节点的处理
相同后置节点处理

后置节点的处理是定义了索引oldEnd指向旧的一组子节点的最后一个节点和索引newEnd指向新的一组子节点的最后一个节点。然后比较两个指向的新旧节点,如果相同指向 -1,直到两个节点不同时结束后置节点的处理
剩余节点的处理
当我们处理完相同的前置节点和后置节点后,如果还有剩余节点,就要对剩余节点进行处理,剩余节点分为3中情况,分别是只有新的一组的子节点有剩余,只有老的一组的子节点有剩余,新老两组的子节点都有剩余;
只有新的一组的子节点有剩余

当条件满足j > oldEnd 且 j <= newEnd时,表示只有新的一组的子节点还有未处理的节点,我们需要循环 j -> newEnd中的节点进行插入
只有老的一组的子节点有剩余

当条件满足 j > newEnd 且 j <= oldEnd时,表示只有老的一组的子节点还有未处理的节点,我们需要循环 j -> oldEnd中的节点进行删除
新老两组的子节点都有剩余
该状态下主要核心为3个部分:
构建source数组用于存放新的一组子节点每个节点在老的一组中存在的原来位置(索引) 首先是定义一个长度为剩余新的一组子节点的长度的数组source,初始值都为-1,还定义了一个变量patched用于记录,然后遍历新的一组子节点,构建key与index的映射表,最后遍历老的一组节点,去映射表中寻找,k = keyIndex[oldVnode.key];,如果找到就把对应的索引存入到source对应的位置中,没有找到说明该节点多余,直接删除。

判断是否需要移动节点,首页我们定义两个变量,moved用于记录是否需要移动的阀值,pos用与记录最后一个节点在新的里面的索引,最后用上述去映射寻找到值k 与 pos寻找,如果 k < pos,说明不是生序需要移动, 否则 pos = k

利用最长递增子序列来优化移动逻辑,如果moved = true, 首先通过最长递增子序列获取到生序列表存放的是索引,然后从后遍历新的一组节点,节点的索引与生序列表对比,如果对比上了说明不需要移动,否则需要移动。

全部核心代码
// 获取新的子节点
const newChildren = n2.children;
// 获取老的子节点
const oldChildren = n1.children;
// 更新相同的前置节点
// 新,老开始节点的下标
let j = 0;
// 获取老的一组子节点的开始节点
let oldVnode = oldChildren[j];
// 获取新的一组子节点的开始节点
let newVnode = newChildren[j];
// 如果新,老的开始节点相同
while(oldVnode.key === newChildren.key) {
// 递归处理子节点
patch(oldVnode, newVnode, container);
// 下标往后移动一格
j++;
// 获取 +1 后的新,老节点
oldVnode = oldChildren[j];
newVnode = newChildren[j];
}

// 更新相同的后置节点
// 索引 oldEnd 指向旧的一组子节点的最后一个节点
let oldEnd = oldChildren.length - 1;
// 索引 newEnd 指向新的一组子节点的最后一个节点
let newEnd = newChildren.length - 1;
// 获取新,老结束下标对应的节点
oldVnode = oldChildren[oldEnd];
newVnode = newChildren[newEnd];

// 如果新,老的结束节点相同
while(oldVnode.key === newVnode.key) {
// 递归处理子节点
patch(oldVnode, newVnode, container)
// 递减 oldEnd 和 nextEnd
oldEnd--
newEnd--
// 获取递减对应的节点
oldVnode = oldChildren[oldEnd]
newVnode = newChildren[newEnd]
}
// 预处理完毕后,如果满足如下条件,则说明从 j --> newEnd 之间的节点应该作为新节点插入
if (j > oldEnd && j <= newEnd) {
// 锚点的索引
const anchorIndex = newEnd + 1;
// 锚点元素
const anchor = anchorIndex < newChildren.length ? newChildren[anchorIndex].el : null;
// 采用 while 循环, 调用 patch 函数逐个挂载新增节点
while (j <= newEnd) {
patch(null, newChildren[j++], container, anchor)
}
} else if (j > newEnd && j <= oldEnd) {
// 如果满足如下条件以上条件,那么j --> oldEnd 之间的节点应该被卸载
while (j <= oldEnd) {
// 循环卸载多余节点
unmount(oldChildren[j++])
}
} else {
// 获取剩余新的一组子节点的个数
const count = newEnd - j + 1;
// 定义个长度为 count 的 数组,用于存放新的一组子节点在老的组中位置,果然没有的话就存-1
const source = new Array(count);
// 初始化都存放-1
source.fill(-1);

// oldStart 和 newStart 分别为起始索引,即j
const oldStart = j;
const newStart = j;
// 用于最后判断是否有要移动的节点
let moved = false;
// 用于存放寻找过程中找递增序列中最大索引值
let pos = 0;
// 循环新的一组的子节点,构建key 和 index 的映射表
const keyIndex = {};
for(let i = newStart; i <= newEnd; i++) {
keyIndex[newChildren[i].key] = i;
}
// 代表更新过的节点数量
let patched = 0;
// 遍历旧的一组子节点中剩余未处理的节点
for(let i = oldStart; i <= oldEnd; i++) {
oldVnode = oldChildren[i];
// 如果更新过的节点数量小于等于需要更新的节点数量,则执行更新
if (patched <= count) {
// 取出老节点在新节点的索引
const k = keyIndex[oldVnode.key];
if (typeof k !== 'undefined') {
newVnode = newChildren[k];
// 递归处理子节点
patch(oldVnode, newVnode, container);
// 每更新一个节点,都将 patched 变量 +1
patched++;
// 存放新的一组子节点在老的组中位置 
source[k - newStart] = i;
// 如果该节点新的位置小于最大的索引值,说明该节点往前移了
if (k < pos) {
moved = true
} else {
// 如果不是就把该位子存到pos,目前k是递增子节点中最大的索引
pos = k
}
} else {
// 没找到, 卸载该节点
unmount(oldVnode)
}
} else {
// 如果更新过的节点数量大于需要更新的节点数量,则卸载多余的节点
unmount(oldVnode)
}
}
}
// moved 为 true 时说明需要移动节点 
if (moved) {
// 计算最长递增子序列
const seq = lis(source);
// 最长递增子序列中最后一个值的索引
let s = seq.length - 1;
// 新的一组子节点的最后一个节点的索引
let i = count - 1;
// 新的一组子节点从后往前遍历
for (i; i >=0; i--) {
if (source[i] === -1) {
// 说明索引为 i 的节点是全新的节点,应该将其插入
// 该节点在新 children 中的真实位置索引
const pos = i + newStart;
const newVnode = newChildren[pos];
// 该节点的下一个节点的位置索引;
const nextPos = pos + 1;
// 锚点
const anchor = nextPos < newChildren.length ? newChildren[nextPos].el : null;
// 挂载
patch(null, newVnode, container, anchor);
} else if(i !== seq[s]) {
// 如果节点的索引 i 不等于 seq[s] 的值, 说明该节点需要移动
// 该节点在新的一组子节中的真实位置索引
const pos = i + newStart;
const newVnode = newChildren[pos];
// 该节点的下一个节点的位置索引
const nextPos = pos + 1;
// 锚点
const anchor = nextPos < newChildren.length ? newChildren[nextPos].el : null;
// 移动
insert(newVnode.el, container, anchor)
} else {
// 当 i === seq[s] 时, 说明该位置的节点不需要移动,只需要让 s 指向下一个位置
s--
}
}
}

}
复制代码
vue3 快速diff算法流程图(放大查看)

vue2 和 vue3 diff 算法的区别
vue2是全量进行diff,而vue3使用了静态标记,只对打标记的节点进行diff
vue2中的虚拟dom是进行全量的对比,在运行时会对所有节点生成一个虚拟节点树,当页面数据发生变更好,会遍历判断虚拟dom所有节点(包括一些不会变化的节点)有没有发生变化;vue3在diff算法中相比vue2增加了静态标记, 在模版编译时,编译器会在动态标签末尾加上 /* Text*/ PatchFlag。也就是在生成VNode的时候,同时打上标记,patch 过程中就会判断这个标记来 Diff 优化流程,跳过一些静态节点对比
// 源码中所有 PatchFlags 值
export const enum PatchFlags {
TEXT = 1 , // 动态文本节点
CLASS = 1 << 1, // 2 动态class
STYLE = 1 << 2, // 4 动态style
PROPS = 1 << 3, // 8 除去class/style以外的动态属性
FULL_PROPS = 1 << 4, // 16 有动态key属性的节点,当key改变时,需进行完整的diff比较
HYDRATE_EVENTS = 1 << 5, // 32 有监听事件的节点
STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的fragment (一个组件内多个根元素就会用fragment包裹)
KEYED_FRAGMENT = 1 << 7, // 128 带有key属性的fragment或部分子节点有key
UNKEYEN_FRAGMENT = 1 << 8, // 256 子节点没有key的fragment
NEED_PATCH = 1 << 9, // 512 一个节点只会进行非props比较
DYNAMIC_SLOTS = 1 << 10, // 1024 动态slot
HOISTED = -1, // 静态节点 
BAIL = -2 // 表示 Diff 过程中不需要优化
}