# vue3相关的原理
# 响应式原理
只有通过
proxyObj
进行操作的时候才能通过定义的操作拦截方法进行处理,直接使用原对象则无法触发拦截器。这也是Vue 3
中要求的reactive
声明的对象修改原对象无法触发视图更新的原因。并且Proxy
也只针对 引用类型数据 才能进行代理,所以这也是Vue
的基础数据都需要通过ref
进行声明的原因,内部会建立一个新对象保存原有的基础数据值。
# Diff
算法
那么相比vue2中的双端对比,在vue3中的diff
算法,又做了哪些优化呢?在非理想情况下的节点对比采用了最长递增子序列的算法思想来做处理;
- 从头对比找到有相同的节点
patch
,发现不同,立即跳出。 - 如果第一步没有
patch
完,立即,从后往前开始patch
,如果发现不同立即跳出循环。 - 如果新的节点大于老的节点数 ,对于剩下的节点全部以新的
vnode
处理(这种情况说明已经patch
完相同的vnode
)。 - 对于老的节点大于新的节点的情况 , 对于超出的节点全部卸载(这种情况说明已经
patch
完相同的vnode
)。 - 不确定的元素(这种情况说明没有
patch
完相同的vnode
) 与 3 ,4对立关系。
前面的逻辑跟vue2还是比较像,逐渐向中间收缩,那么关键点就在判断哪些节点是需要变动的。在经历上述操作后,会出现以下节点需要判断(即图中圈起来的节点)
首先,我们以新节点的数量创建一个 source
数组,并用 -1
填满;这个 source
数组就是用来做新旧节点的对应关系的,我们将新节点在旧列表的位置存储在该数组中,我们再根据 source
计算出它的最长递增子序列用于移动 DOM
节点。
其次,我们先建立一个对象存储当前新列表中的节点与 index
的关系:
const newVNodeMap = {
c: '1',
d: '2',
b: '3',
i: '4'
}
2
3
4
5
6
然后再去旧列表中去找相同的节点,并记录其index
的位置。在找节点时,如果旧节点在新列表中没有的话,直接删除就好。除此之外,我们还需要一个数量表示记录我们已经patch
过的节点,如果数量已经与新列表剩余的节点数量一样,那么剩下的旧节点我们就直接删除了就可以了。
# DOM
如何移动
首先,我们需要定义一个 List
数组来存储 source
中的最长连续递增子序列的下标:-
然后从后往前遍历 source
数组;这个过程中会发生三种情况:
- 当前数值为
-1
,也就说明该节点是新增的,我们直接将其插入到队尾就好了,同时i--
。 - 当前的索引和
List
中的值一致,即i == List[j]
,同时i --, j --
。 - 当前的索引不是
List
中的值,那么该节点就需要进行移动,我们只需要将该节点插入到队尾就可以了,因为队尾是排好序的。
具体解析:
首先,
i = 3
,即上图中,值为-1
为第一种情况,节点需要新增,i--
;i = 2
,索引为2 != List[j]
为第三种情况,节点需要移动,直接在旧列表中,将b
节点插入到尾部位置,i --
i = 1
,此时索引i == List[j]
为第二种情况,我们的节点不需要移动;i = 0
,此时索引i == List[j]
为第二种情况,我们的节点也不需要移动;
TIP
在最好的情况下即 i != 0
的条件下,平均的时间复杂度是 O(nlgn)
那么在 i = 0
时,时间复杂度为 O(n^2)
在vue2中关于的这种节点的查找和替换的时间复杂度稳定为 O(n^2)
在vue3中依赖于最长递增子序列去做节点的移动和删除/新增,时间复杂度为 O(nlgn)~O(n^2)
// 最长递增子序列算法实现
const arr = [10,5,6,7,4,1,2,8,9]
function lis(arr) {
let len = arr.length,
dp = new Array(len).fill(1); // 用于保存长度
// i = 0 => O(n^2) ; i != 0 => O(nlogn)
for (let i = len - 1; i >= 0; i--) {
let cur = arr[i]
for(let j = i + 1; j < len; j++) {
let next = arr[j]
// 如果是递增 取更大的长度值
if (cur < next) dp[i] = Math.max(dp[j]+1, dp[i])
}
}
return Math.max(...dp)
}
lis(arr) // 5
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17