vue2.x与vue3.x的响应差别

191 阅读4分钟

当前2.x的vue响应是基于Object.defineProperty的,其存在一些局限

  • 无法跟踪响应对象属性的添加 / 删除。
  • 无法监控到数组的下标变化 -- vue里hack了八大数组的操作方法,像push\pop\shift等。
  • 在进行响应绑定时,如果属性值是对象,还需要进行深度遍历。

如何解决? 使用 Vue.set 和 Vue.delete 来保证。

     * Object.defineProperty(obj, prop, descriptor)
     * 定义或修改的属性描述符
     * https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

下面我们来验证一下以上提到的几点。 案例:

    /**
     * Object.defineProperty(obj, prop, descriptor)
     * 定义或修改的属性描述符
     * https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
     */
    function definePropertyFn(data, prop, value) {
      Object.defineProperty(data, prop, {
        enumerable: true,
        configurable: true,
        get: function () {
          console.log(`get key--- ${prop} ---- value ${value}`);
          return value;
        },
        set: function (newVal) {
          console.log(`set key--- ${prop} ---- value ${newVal}`);
          value = newVal;
        },
      });
    }
    function observe(data) {
      // 由于 Object.defineProperty 只能对属性进行劫持,因此需要遍历对象的每个属性
      if (!data || typeof data !== "object") {
        return false;
      }
      Object.keys(data).forEach((key) => {
        definePropertyFn(data, key, data[key]);
      });
    }

这个时候,如何你observe一个数组,可以看出是没法正常触发getter or setter 的。

    let arr = [1, 2, 3];
    observe(arr);
    document.getElementById("arrPush1").onclick = function () {
      console.log("-------arrPush-----");
      arr.push(4);
    };
    document.getElementById("arrPush2").onclick = function () {
      console.log("------arr赋值-----");
      arr[0] = new Date().getTime();
    };

因而需要对数组shift push 等进行特殊处理。利用Object.create() 与 属性重定义,实现hack 处理。

    (function () {
      const arrayProto = Array.prototype;
      const arrayMethods = Object.create(arrayProto);
      const methodsToPatch = [
        "push",
        "pop",
        "shift",
        "unshift",
        "splice",
        "sort",
        "reverse",
      ];
      //   对特定的方法进行拦截,特殊通知
      methodsToPatch.forEach((method) => {
        const original = arrayProto[method];
        Object.defineProperty(arrayMethods, method, {
          enumerable: true,
          writable: true,
          configurable: true,
          value: function (...args) {
            // const result = original.apply(this, args);
            const result = Reflect.apply(original, this, args);
            switch (method) {
              case "push":
                console.log("拦截通知了push操作");
                // emit & notify update
                break;
              case "splice":
              case "pop":
                console.log("拦截通知了xxx操作");
                break;
            }
            return result;
          },
        });
      });
      //   看下是否通知
      let testArr = new Array();
      testArr.__proto__ = arrayMethods;
      testArr.push("11111");
      //   打开tool看下原型的展示
      //   console.log(testArr);
    })();

有打开过vue2.x源码的,应该可以看出,上面的大致也是vue2.x 里源码的处理方式。打开node_modules 下的源码 core/observer/index.js

附上官方响应图:

vue3.x 使用的是ES6的Proxy 作为其观察者机制,取代之前的Object.defineProperty

有以下优点:

  • 可以劫持整个对象
  • 有13种形式劫持操作

还有一个与proxy 紧密使用的 Reflect 可以走下面这个test 进一步熟悉proxy

    /**
     * const p = new Proxy(target, handler)
     * target 可以是任何类型的对象,包括原生数组、函数、甚至另一个代理
     * https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
     * https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
     */
    let obj = {
      a: 1,
      ddd: 2222,
      name: "hhh",
    };
    let proxyObj = new Proxy(obj, {
      get: function (target, prop) {
        console.log(`-------get-${prop}----`);
        return target[prop] ? target[prop] : 0;
      },
      set: function (target, prop, value) {
        //   可以做一些赋值校验
        console.log(`-------set-${prop}-----`);
        target[prop] = 8888;
      },
      getPrototypeOf: function (target) {
        console.log(`-------getPrototypeOf------`);
        //   读取代理对象的原型时,会被调用
        return Array.prototype;
      },
      deleteProperty: function (target, prop) {
        //   删除属性
        const WhiteArr = ["name", "phone"];
        if (WhiteArr.indexOf(prop) > -1) {
          console.error("不可以删除该属性");
          return false;
        }
        delete target[prop];
        return true;
      },
      has: function (target, prop) {
        // 判断对象是否具有某个属性
        const WhiteArr = ["money", "phone"];
        if (WhiteArr.indexOf(prop) > -1) {
          console.warn("这是私有属性");
          return false;
        }
        return true;
      },
      apply: function () {
        //   函数调用的拦截 (代理的是函数)
      },
      construct: function () {
        //   new 操作的拦截
      },
      isExtensible: function () {
        // 拦截 判断一个对象是否是可扩展
      },
    });
    // 利用它进行数据的二次处理、可以进行数据合法性的校验
    proxyObj.a;
    proxyObj.b;
    proxyObj.ccc = 666;
    var aaa_prototype = Object.getPrototypeOf(proxyObj);
    console.log(aaa_prototype === Object.prototype);
    console.log(aaa_prototype === Array.prototype);

    // 拦截delete  操作
    delete proxyObj.name;
    console.log(obj);
    // 用 has方法隐藏了属性 money
    console.log("money" in proxyObj);

基本认识proxy 之后,就可以升级个vue3.x 使用一番,可参考这个文章,一步步搭建vue-next 环境。

vue3.x 的各各模块,相对都是比较独立的,可以取单独的一部分进行使用,比如 Vue 3.0 的数据响应式系统是独立的模块,可以完全脱离Vue 而使用

    /**
     * Vue 3.0 的数据响应式系统是独立的模块,可以完全脱离 Vue 而使用
     * https://juejin.cn/post/6844903959660855309
     * https://jrainlau.github.io/#/article?number=20
     * yarn dev reactivity 调试
     */
    const { reactive, effect } = VueReactivity;
    const data = {
      count: 1,
    };
    // 将data转成proxy对象state
    const state = reactive(data);
    const fn = () => {
      const count = state.count;
      document.getElementById("txt").innerHTML = count;
      console.log(`set count to ${count}`);
      9;
    };
    // effect把fn()作为响应的回调,当state.count 发生变化时,便触发了 fn()
    // 默认会立即执行一次,进而把依赖进行收集
    effect(fn);
    // 点击事件
    document.getElementById("btn").addEventListener("click", () => {
      state.count++;
    });
    // 源码
    // ├── compiler-core # 所有平台的编译器
    // ├── compiler-dom # 针对浏览器而写的编译器
    // ├── reactivity # 数据响应式系统
    // ├── runtime-core # 虚拟 DOM 渲染器 ,Vue 组件和 Vue 的各种API
    // ├── runtime-dom # 针对浏览器的 runtime。其功能包括处理原生 DOM API、DOM 事件和 DOM 属性等。
    // ├── runtime-test # 专门为测试写的runtime
    // ├── server-renderer # 用于SSR
    // ├── shared # 帮助方法
    // ├── template-explorer
    // └── vue # 构建vue runtime + compiler

vue3.x 收集的大致流程如下,看源码就在reactivity 目录下。

参考链接 参考链接

下次梳理一下 typescript & hook

tips:

2020 年 Vue.js 的重大变化无疑是 Vue.js3.0 的发布,有了非常多新特性,总结如下:

  • 对 Vue.js 进行了完全 Typescript 重构,让 Vue.js 源码易于阅读、开发和维护;
  • 重写了虚拟 Dom 的实现,对编译模板进行优化、组件初始化更高效, 性能上有较大的提升;Vue.js2 对象式组件存在一些问题:难以复用逻辑代码、难以拆分超大型组件、代码无法被压缩和优化、数据类型难以推倒等问题;而 CompositionAPI 则是基于函数理念,去解决上述问题,使用函数可以将统一逻辑的组件代码收拢一起达到复用,也更有利于构建时的 tree-shaking 检测,这个使用起来有些类似于 React 的 hook;
  • 以上变化都秉持着 VUE 的“渐进式框架“ 理念, Vue.js3.0 持续开发兼容旧的版本,即使升级了 Vue.js3.0 也可以按照之前的组件开发模式进行开发。

vue3介绍