Vue 源码笔记
发表于
浏览量836
评论数0
Vue 源码笔记
记录总结一些自己阅读Vue源码的一些笔记,也方便后续回顾,主要是响应式和Diff算法,可能存在偏差,欢迎指正~
响应式原理
众所周知,Vue
最大的特点之一就是数据驱动视图,简单来说就是数据变化引起视图变化,那么第一步就是先要知道数据什么时候发生变化,也就是说对数据的变化要进行侦测,实现数据的响应式。
Vue2响应式原理
vue2 中,使用 JS 内置对象方法Object.defineProperty
,它可以直接在一个对象上定义一个新属性,或修改其现有属性,数据的响应式首先就是监听数据的变化,借助defineProperty
这个方法 vue 给数据添加了get
和set
方法,当数据被读和写分别使用get()
和set()
进行拦截自动执行一些逻辑,使得数据变得可观测,也就是响应式对象。
当然这只是最基本的,在平常开发中,组件与组件都不是毫无关系单一存在的,数据也不可能只有一个,它们之间是存在依赖关系。因此在上面提到的拦截处执行一些逻辑,这里的逻辑就是处理这些依赖关系的。
理所应当的每一个数据都会有一个管理自己依赖的依赖管理器(Dep
类实例),这是一个依赖数组:
在get
方法中可以监听到读取的操作,当读取时,把是谁读取的通过创建一个Watcher
实例,然后保存在属性的依赖管理器中,也就是依赖收集。
在set
方法中可以获取到get
方法中收集在依赖管理器中的依赖,调用每一个Watcher
的更新方法,从而进行依赖的更新。
其代码对应流程一些细节:
- 数据通过实例为
observer
对象,其构造函数会执行defineReactive
方法,在该方法内会实例一个自己的依赖管理器,在get
函数中通过dep.depend
做依赖收集,在set
函数中通过dep.notify
做依赖更新。 - 当是谁读取数据时,会实例化
Watcher
,构造函数会执行pushTarget(this)
将自身保存,在由这个Watcher
实例触发原本数据的get
方法,在该方法中触发dep.depend()
方法,从而将Watcher
添加数据的依赖管理器中。 - 当数据发生了变化时,会触发
setter
,触发dep.notify()
方法,在该方法中遍历依赖数组,调用其更新方法更新数据。
以上,是针对于对象的处理方式,由于Object.defineProperty
是对象上的方法,有一些局限性,对于数组的处理,它无法像上面一样在set
方法中进行进行依赖更新。
vue2 中,为了解决这个问题,它重写了对数组原型链中操作数组的一些方法,包括:push
,pop
,shift
,unshift
,splice
,sort
,reverse
,这样在对数组进行操作时,就能监听到了,从而进行依赖的更新
不足之处:不管是对象还是数组,都有一些不足的地方,当向对象数据里添加一对新的key/value
或删除一对已有的key/value
时,当用数组的下标来操作数据时,都是无法监测到的,也就无法进行依赖更新。(为了解决这个问题,vue2 提供Vue.set
和Vue.delete
全局 api)
Vue3响应式原理
vue3 中,使用的是 ES6 新增的Proxy
对象,它用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。Proxy
劫持的是整个对象,不需要做特殊处理,解决了在 vue2 中的一些不足之处。数据的响应式大体思路还是不变,将数据实例为proxy
对象,在实例读取和更新时触发自定义的拦截器,在对应的拦截器实现依赖更新和依赖收集操作。反正思路是这样,我也偷懒只了解了差别比较大的地方。
Diff 算法
Diff
算法针对的是虚拟Dom
,而虚拟Dom
就是用一个JS
对象来描述一个Dom
节点,我们知道一个页面渲染离不开DOM
节点树,在节点树上有非常多的Dom
节点,每一个节点上的数据量都是非常多的,而虚拟Dom
只包含一些节点的必要信息,当数据发生变化时,我们对比变化前后的虚拟Dom
节点,通过Diff
算法计算出需要更新的地方,然后再去更新需要更新的视图,这样大大节省了操作真实Dom
带来的性能。
vue 中通过VNode
类可以实例化不同类型的虚拟Dom
节点,其中包含tag
表示节点的标签名,text
表示节点中包含的文本,children
表示该节点包含的子节点等。通过属性之间不同的搭配,就可以描述出各种类型的真实Dom
节点,类型包含如下:
- 注释节点
- 文本节点
- 元素节点
- 组件节点
- 函数式组件节点
- 克隆节点
Vue2 的 Diff 算法
Diff
算法核心的地方是新旧都有节点,需要更新时,这里是采用了双端对比的算法,进行对比更新。
核心代码大致实现逻辑:
-
需要创建新节点时,会先判断节点类型,依次判断是否是元素节点,注释节点和文本节点,只有这三种节点才能被创建并插入到
dom
中。 -
需要删除节点时,先获取父节点,再删除该节点。
-
更新节点时,稍微比较复杂一点,会先进行节点类型判断,有以下逻辑:
- 新旧节点类型都是静态节点,则直接跳过
- 新节点是文本节点时,如果旧节点也是文本节点,对比文本内容,如果不同则修改为相同的,如果旧节点不是文本节点,则直接调用
setTextNode
方法创建一个相同的文本节点替换掉。 - 新节点是元素节点时,这个时候又需要判断是否含有子节点:
- 3.1. 当新节点有子节点,旧节点没有子节点,则可以说明是空节点或者是文本节点,空节点的话会直接把新节点的子节点创建一份插入到空节点下面,而是文本节点会先清空文本内容,再插入。
- 3.2. 当新节点没有子节点时,不管旧节点是什么,直接清空。
- 3.3. 当新节点和旧节点都有子节点时,则需要递归对比更新。通过新旧节点两端逐步迭代两个子节点数组,判断更新子节点(双端对比),分为了四种情况:
- 创建子节点:如果
newchildren
的某个子节点在oldchildren
没有,就创建该节点插入到所有未处理节点之前。 - 删除子节点:如果把
newchildren
都循环完了,oldchildren
还存在未处理的节点,就删除。 - 更新子节点:如果
newchildren
的某个子节点在oldchildren
找到了,并且位置也相同,就重复上述操作再次进行对比更新。 - 移动子节点:如果
newchildren
的某个子节点在oldchildren
找到了,但是位置不相同,则需要移动旧的节点到所有未处理节点之前。
- 创建子节点:如果
虽然情况分的蛮多的,但是总的来说就是对比新旧两份vnode
,使旧的vnode
和新的一样。
概括来讲其实就干了三件事:
- 新的
vnode
有的节点,而旧的没有就创建节点 - 新的
vnode
没有的节点,而旧的有就删除节点 - 新的
vnode
和旧的vnode
都有的节点,就以新的节点为准,更新旧的节点
Vue3 的 Diff 算法
vue3 中,也是一样没有看全部 😕 ,大致了解了核心Diff
的一些变化,也就是上述当新旧vnode
节点都有子节点需要更新时,类似 vue 2 的算法,首先进行双向的比较,再加上最长子序列算法来减少节点的移动操作,提高了Diff
的效率。
核心代码大致逻辑:
- 从新旧
children
数组前面开始迭代比对,记录下标为i
,如果相同则将下标往后移,如果不同或者一个数组遍历完成则跳出循环。 - 从新旧
children
数组后面开始迭代比对,记录下标分别为newEndIndex
,oldEndIndex
,如果相同则将下标往前移,如果不同或者一个数组遍历完成则跳出循环。 - 经过双向的循环对比之后,根据记录的下标判断,又分为一下情况:
- 下标
i
已经大于了oldEndIndex
但是小于newEndIndex
说明oldchildren
已经遍历完了,但是newchildren
还没有,则将还未遍历的元素,新增到oldchildren
中 - 下标
i
大于newEndIndex
,说明未遍历完oldChildren
,则需要将未遍历到的元素进行删除 - 最后是新旧都有剩余元素,首先生成一个
newchildren
中未处理节点一样长度的数组source
,用-1
填充,然后需要获取新旧剩余元素,都要遍历一遍,newchildren
中剩余元素遍历是为了获取一个 未处理的每个元素为key
,其对应原newchildren
中的下标为value
的映射表,然后遍历oldChildren
中为未处理的元素,尝试在映射表中查找,如果未找到,说明需要移除该元素,如果找到了就将source
数组中把当前迭代的索引下标替换掉对应的位置中的-1
- 在经过上步骤处理,我们得到一个关于
source
新的一组子节点在老的组中位置的数组,再使用最长增长子序列算法可以得到一个最长索引递增的数组,再配合迭代未处理的newChildren
,可以使移动最少次数就更新完成。(说实话有点不好懂,我刚想了半天这块儿都不理解,意思是这个意思,可能需要实际上手写代码操作好理解一点
- 下标
可用下面代码尝试理解
// 求最长递归子序列的下标
function getSequence(arr) {
const p = arr.slice();
const result = [0];
let i, j, u, v, c;
const len = arr.length;
for (i = 0; i < len; i++) {
const arrI = arr[i];
if (arrI !== 0) {
j = result[result.length - 1];
if (arr[j] < arrI) {
p[i] = j;
result.push(i);
continue;
}
u = 0;
v = result.length - 1;
while (u < v) {
c = (u + v) >> 1;
if (arr[result[c]] < arrI) {
u = c + 1;
} else {
v = c;
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1];
}
result[u] = i;
}
}
}
u = result.length;
v = result[u - 1];
while (u-- > 0) {
result[u] = v;
v = p[v];
}
return result;
}
getSequence([102, 103, -1, 105, 106, -1, 107, 109, -1]);
// 打印出来是这样的一个数组 [0, 1, 3, 4, 6, 7]
[102, 103, -1, 105, 106, -1, 107, 109, -1]
可以看作是source
数组,通过求得最长递增子序列,这些位置上的元素不进行更新,就能使的旧节点与新节点同步。
大概有这些吧......
写在最后;
地点: 成都
时间: 2024-04-16 21:50:00
心情:哎呦真的好烦,事情好多,羡慕小时候!