kasuie page cover

Vue 源码笔记

发表于

浏览量564

评论数0

Vue 源码笔记

记录总结一些自己阅读Vue源码的一些笔记,也方便后续回顾,主要是响应式和Diff算法,可能存在偏差,欢迎指正~ 砂糖

响应式原理

众所周知,Vue最大的特点之一就是数据驱动视图,简单来说就是数据变化引起视图变化,那么第一步就是先要知道数据什么时候发生变化,也就是说对数据的变化要进行侦测,实现数据的响应式。

Vue2响应式原理

vue2 中,使用 JS 内置对象方法developer.mozilla.orgObject.defineProperty,它可以直接在一个对象上定义一个新属性,或修改其现有属性,数据的响应式首先就是监听数据的变化,借助defineProperty这个方法 vue 给数据添加了getset方法,当数据被读和写分别使用get()set()进行拦截自动执行一些逻辑,使得数据变得可观测,也就是响应式对象。

当然这只是最基本的,在平常开发中,组件与组件都不是毫无关系单一存在的,数据也不可能只有一个,它们之间是存在依赖关系。因此在上面提到的拦截处执行一些逻辑,这里的逻辑就是处理这些依赖关系的。

理所应当的每一个数据都会有一个管理自己依赖的依赖管理器(Dep类实例),这是一个依赖数组:

get方法中可以监听到读取的操作,当读取时,把是谁读取的通过创建一个Watcher实例,然后保存在属性的依赖管理器中,也就是依赖收集。

set方法中可以获取到get方法中收集在依赖管理器中的依赖,调用每一个Watcher的更新方法,从而进行依赖的更新。

其代码对应流程一些细节:

  1. 数据通过实例为observer对象,其构造函数会执行defineReactive方法,在该方法内会实例一个自己的依赖管理器,在 get 函数中通过 dep.depend 做依赖收集,在set函数中通过dep.notify做依赖更新。
  2. 当是谁读取数据时,会实例化Watcher,构造函数会执行pushTarget(this)将自身保存,在由这个Watcher实例触发原本数据的get方法,在该方法中触发dep.depend()方法,从而将Watcher添加数据的依赖管理器中。
  3. 当数据发生了变化时,会触发setter,触发dep.notify()方法,在该方法中遍历依赖数组,调用其更新方法更新数据。

以上,是针对于对象的处理方式,由于Object.defineProperty是对象上的方法,有一些局限性,对于数组的处理,它无法像上面一样在set方法中进行进行依赖更新。

vue2 中,为了解决这个问题,它重写了对数组原型链中操作数组的一些方法,包括:push,pop,shift,unshift,splice,sort,reverse,这样在对数组进行操作时,就能监听到了,从而进行依赖的更新

不足之处:不管是对象还是数组,都有一些不足的地方,当向对象数据里添加一对新的key/value或删除一对已有的key/value时,当用数组的下标来操作数据时,都是无法监测到的,也就无法进行依赖更新。(为了解决这个问题,vue2 提供Vue.setVue.delete全局 api)

Vue3响应式原理

vue3 中,使用的是 ES6 新增的developer.mozilla.orgProxy对象,它用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。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中。

  • 需要删除节点时,先获取父节点,再删除该节点。

  • 更新节点时,稍微比较复杂一点,会先进行节点类型判断,有以下逻辑:

    1. 新旧节点类型都是静态节点,则直接跳过
    2. 新节点是文本节点时,如果旧节点也是文本节点,对比文本内容,如果不同则修改为相同的,如果旧节点不是文本节点,则直接调用setTextNode方法创建一个相同的文本节点替换掉。
    3. 新节点是元素节点时,这个时候又需要判断是否含有子节点:
      • 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,如果相同则将下标往前移,如果不同或者一个数组遍历完成则跳出循环。
  • 经过双向的循环对比之后,根据记录的下标判断,又分为一下情况:
    1. 下标i已经大于了oldEndIndex但是小于newEndIndex说明oldchildren已经遍历完了,但是newchildren还没有,则将还未遍历的元素,新增到oldchildren
    2. 下标i大于newEndIndex,说明未遍历完oldChildren,则需要将未遍历到的元素进行删除
    3. 最后是新旧都有剩余元素,首先生成一个newchildren中未处理节点一样长度的数组source,用-1填充,然后需要获取新旧剩余元素,都要遍历一遍,newchildren中剩余元素遍历是为了获取一个 未处理的每个元素为key,其对应原newchildren中的下标为value 的映射表,然后遍历oldChildren中为未处理的元素,尝试在映射表中查找,如果未找到,说明需要移除该元素,如果找到了就将source数组中把当前迭代的索引下标替换掉对应的位置中的-1
    4. 在经过上步骤处理,我们得到一个关于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

心情:哎呦真的好烦,事情好多,羡慕小时候!

芙宁娜
最后修改:2024年04月22日

留下你的评论吧

http(s)://

回到顶部