Vue

响应式原理

  • Vue2

    底层原理使用 Object.defineProperties 中的 getset 来拦截对象上的属性,在 get 钩子进行依赖收集,在 set 钩子中进行依赖的触发。

    • 该方案有个缺点是无法监听原来不在对象中是后续添加上的属性,因此 Vue 给我们提供了$set 来供我们手动触发依赖的收集与触发;
    • 针对数组的处理方案是,Vue 重写了数组上的可以改变原数组的方法,并在其中进行了依赖的触发操作,例如重写了 splice、 push 等方法,在方法改变原数组时触发已收集的依赖。因为数组无法监听到针对索引的修改,所以我们修改某个响应式数组的某个索引上的值时,Vue 此时并不会帮我们触发依赖,我们仍然需要使用$set 来告诉 Vue 此时需要手动触发依赖。
  • Vue3

    底层使用了 es6 的 Proxy 中的 getset 来进行拦截对象上的属性,在 get 钩子进行依赖收集,在 set 钩子中进行依赖的触发。

    • 由于 Proxy 代理的是整个对象,因此可以监听到该对象的 key 的变化,无论是初始化时已有的 key 还是后续手动添加的 key 都可以进行到监听。
    • 使用数组时,我们可以使用 ref 来进行包裹,如果我们使用 reactive 的话,我们也可以监听该数组,但是我们无法从服务端获取到一个数组然后赋值给我们的响应式对象,因为我们的响应式数组是原数组,并不是我们的赋值过去的新数组。使用 ref 时,Vue 底层会帮我们自动包裹一层,将其包装成我们的响应式对象,类似于包裹的对象中有一个 value 属性,我们所操作的一切都是在这个响应式对象上的 value 属性上,所以我们需要使用 xxx.value 来进行获取值或是修改响应式对象的值。因此我们可以将从服务端获取到的数组直接进行赋值,因为我们相当于修改了响应式对象上的某个 value 属性,原响应式对象还是原来的响应式对象没有发生变化。

虚拟 DOM diff 原理

  • Vue2

    在 Vue2 中虚拟 DOM 的 diff 操作是使用的双端 diff 算法,原理为老 DOM 树与新的 DOM 树先从双端进行比较,如果双端的都一样直到找到一个不一样的才将中间的部分进行替换与修改,无法处理中间部分的 diff 逻辑。

  • Vue3

    在 Vue3 中虚拟 DOM 的 diff 算法是先使用双端 diff 算法后在中间部分无法 diff 的地方使用了最长递增子序列算法,该算法可以将中间部分最长的递增部分保留下来,进而修改更少的中间部分元素就可以达到将老 DOM 变成新 DOM 的结果。

    • 其中最长递增子序列实现与思路如下:

      • 使用了贪心算法 + 二分算法来实现最长递增子序列将原本动态规划的 n^2 时间复杂度降低到 n*logn
      • 实现原理为,我们维护一个数组 res,其中存放的是我们递增的值的初始化数组的索引,我们只需要让该数组增长的慢,那么我们就可以保证最后的最长递增子序列长度是正确的但是其中的索引值是不准确的。我们除此之外还维护了一个数组 p,该数组表示 res 数组变化时,它的前一个位置在哪,后续我们进行从后往前回溯即可把 res 数组还原成正确的索引值。
    • 代码实现如下:

      function getSequence(nums: number[]) {
        /* 初始化增长数组为nums中的第一个值的索引 */
        const res = [0];
        /* 初始化p 为nums的拷贝 */
        const p = nums.slice();
        /* 开启循环进行遍历 */
        for (let i = 0; i < nums.length; i++) {
          /* 获取res中的最后一位的索引 */
          const resLastIndex = res.length - 1;
          /* 当前位置的值大于res中最后一位的值(最后一位就是最大的) */
          if (nums[i] > nums[res[resLastIndex]]) {
            /* res中添加当前的nums的索引 */
            res.push(i);
            /* p数组中记录当前位置的上一个位置的索引 */
            p[i] = res[resLastIndex];
            /* 使用了continue就不用使用else */
            continue;
          }
          /* 开始二分 */
          /* 左指针为0 */
          let left = 0;
          /* 右指针为res的最后一位 */
          let right = res.length - 1;
          /* 当左指针小于右指针时继续二分 */
          while (left < right) {
            /* 取中间值 */
            const mid = left + ((right - left) >> 1);
            /* 当前值大于中间值的时候那么左指针变为中间指针的索引+1 */
            if (nums[i] > nums[res[mid]]) {
              left = mid + 1;
            } else {
              /* 其他情况下中间指针的索引赋值到右指针 */
              right = mid;
            }
          }
          /* 跳出循环时左指针的位置的值要么是第一个大于当前位置的值,要么是等于当前位置的值,只有为大于的情况下才进入下面替换逻辑 */
          if (nums[res[left]] > nums[i]) {
            /* 将第一个大于当前位置的值的索引赋值为i */
            res[left] = i;
            /* 当第一个大于当前值的位置为0时没有前驱指针不需要考虑 */
            /* 当第一个大于当前值的位置大于0时,他的前驱指针就是res的当前位置的前一位所存储的nums中的索引 */
            if (left > 0) {
              p[i] = res[left - 1];
            }
          }
        }
        /* 最后开始倒序回溯 */
        /* u 代表res的长度 */
        let u = res.length;
        /* v代表res中最大一个值的再nums中的索引 */
        let v = res[u - 1];
        while (u--) {
          /* 第一次时候  res[u] 就是 v 的值  */
          /* p[v]代表的是比nums[v]小的位置的索引 */
          /* 将 res 依次还原为正确的值 */
          res[u] = v;
          v = p[v];
        }
        /* 最后返回res  res中存储的是最长递增的nums数组中的数的索引*/
        return res;
      }
      
Last Updated:
Contributors: zhaoyuqiqi