举个栗子,简易代码,对比Vue2和3的响应式原理

387 阅读5分钟

前言

Vue3历时两年开发,99位贡献者,2600次提交,628次PR

Vue3支持2的大多数特性。

性能方面的提升:

1.打包大小减少41%

2.初次渲染快55%,更新快133%

3.内存使用减少54%

新增了Composition API以及其他特性,而且有更好的typescript支持。

src=http___5b0988e595225.cdn.sohucs.com_images_20181116_a677a422791a413290810061f9d1682a.jpeg&refer=http___5b0988e595225.cdn.sohucs.jpg

学习和使用Vue3势在必行。

这篇文章,先来简单的对比一下Vue2和Vue3的响应式原理。

Vue2.0响应式原理

src=http___www.uml.org.cn_ajax_images_201712072.jpeg&refer=http___www.uml.org.jpg

1.初识Object.defineProperty

Vue2借助原生js的api中Object.defineProperty,拦截对data对象的name属性的访问。

当访问时,执行get函数。属性变化时,监听这个变化,通过set函数。

src=http___upload-images.jianshu.io_upload_images_10374438-86a54fc9adc592fb.png&refer=http___upload-images.jianshu.jpg

举个栗子:

const data = {};
let name = 'Vue';
Object.defineProperty(data, 'name',{
    get: function(){
      console.log('get');
      return name;
    },
    set: function (newValue){
      console.log('set');
      name = newValue;
      // 视图重新渲染
    }
})

2.基本的响应式实现

u=4272933827,2488839667&fm=26&gp=0.jpg

我还有很多栗子,响应式的过程,代码如下:

const data = {
    name: 'OrzR3',
    age: 30
}
// 变成响应式数据
observer(data);
​
function observer(target){
    if(typeof target !== 'object' || target === null){
        return target;
    }
    for(let key in target){
        defineReactive(target, key, target[key])
    }
}
​
function defineReactive(target, key, value){
    Object.defineProperty(target, key,{
      get(){
          return value;
      },
      set(newValue){
          if(newValue !== value){
              value = newValue();
              console.log('更新视图');
          }
      }
    })
}
​
data.name = 'Test';
// 控制台打印,更新视图

Vue源码更加复杂一点,判断更多情况,但是核心逻辑,就是这个逻辑。

3.处理值为复杂对象情况

如果对象中的属性,还是一个对象,继续调用observe,在set方法中,监听新设置的value

 const data = {
    name: 'OrzR3',
    age: 30,
    friend:{
      friendName: 'sven'
    }
  }
  // 变成响应式数据
  observer(data);
  
  function observer(target){
    if(typeof target !== 'object' || target === null){
      return target;
    }
    for(let key in target){
      defineReactive(target, key, target[key])
    }
  }
​
  function defineReactive(target, key, value){
    // 如果对象中的属性,还是一个对象,继续调用observe
    observer(value);
    Object.defineProperty(target, key,{
      get(){
        return value;
      },
      set(newValue){
        // 监听新设置的value
        observer(newValue);
        if(newValue !== value){
          value = newValue();
          console.log('更新视图');
        }
      }
    })
  }
​
  data.name = 'Test';
  data.age = { number: 40 }
  // 控制台打印,更新视图

4.处理值为数组的情况

根据索引改变时,会更新视图。

使用push之类的方法,操作数组时,不会更新视图。

需要重写原本的数组的方法。

const oldArrayProto = Array.prototype;
const newArrProto = Object.create(oldArrayProto);
console.log('old', oldArrayProto);
console.log('new', newArrProto);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName =>{
    newArrProto[methodName] = function(){
        console.log('更新视图');
        oldArrayProto[methodName].call(this, ...arguments);
    }
})

const data = {
    name: 'OrzR3',
    age: 30,
    friend:{
      friendName: 'sven'
    },
    colors: ['red', 'orange', 'green']
}
​
  // 保存数组原型
const oldArrayProto = Array.prototype;
const newArrProto = Object.create(oldArrayProto);
console.log('old', oldArrayProto);
console.log('new', newArrProto);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName =>{
newArrProto[methodName] = function(){
  console.log('更新视图');
  oldArrayProto[methodName].call(this, ...arguments);
}
})
// 变成响应式数据
observer(data);
  
function observer(target){
    if(typeof target !== 'object' || target === null){
      return target;
    }
    // 把数据变成响应式数据的时候判断
    // 如果数据是数组,修改原型为新创建的原型
    if(Array.isArray(target)){
      target.__proto__ = newArrProto;
    }
    for(let key in target){
      defineReactive(target, key, target[key])
    }
}
​
function defineReactive(target, key, value){
    // 如果对象中的属性,还是一个对象,继续调用observe
    observer(value);
    Object.defineProperty(target, key,{
      get(){
        return value;
      },
      set(newValue){
        // 监听新设置的value
        observer(newValue);
        if(newValue !== value){
          value = newValue();
          console.log('更新视图');
        }
      }
    })
}
​
data.name = 'Test';
data.age = { number: 40 }
// 根据索引改变时,会更新视图
data.colors[0] = 'blue';
// 使用push之类的方法,操作数组时,不会更新视图
// 需要重写原本的数组的方法
data.colors.push('blue');
// 控制台打印,更新视图

缺点

  • Object.defineProperty深度监听,性能差。

  • 使用Object.defineProperty做响应式数据时的问题:

  • 如果数据为对象,而且层级很深,则不断的进行深度监听,直到属性是个普通的值。当数据复杂的时候,会卡死。

因此,在vue3中使用proxy来解决。proxy在使用到数据时,才启用监听。

  • 此外,使用Object.defineProperty进行监听,当数据删除时,在data上新增属性,视图不会更新。

  • Object.defineProperty没有办法处理数据删除和新增属性。

  • 因此,当数据删除时,使用Vue.delete方法。数据新增属性时,使用Vue.set方法。

src=http___pic4.zhimg.com_v2-db7221c0d7ca752b2f88d7ca94939976_1440w.jpg&refer=http___pic4.zhimg.jpg

这一系列问题,在Vue3.0中得到了优化解决方案。

Vue3.0响应式原理

Vue3.0 使用proxy代替了vue2.0版本中的defineProperty,监测机制改变,性能更好。弥补了Vue2.0的一些缺点。

举个例子,用proxy实现响应式

 // proxy代理存放在WeakMap中
 // toProxy存放代理后的对象
  const toProxy = new WeakMap();
  // toProxy存放代理前的对象
  const toRaw = new WeakMap();
​
  function trigger() {
    console.log("触发视图更新");
  }
​
  function isObject(target) {
    return typeof target === "object" && target !== null;
  }
​
  function reactive(target) {
    if (!isObject(target)) {
      return target;
    }
    // 如果代理表中已经存在了,就把这个结果返回
    let proxy = toProxy.get(target);
    if (proxy) {
      return proxy;
    }
    // 如果这个对象已经被代理过了,则原封不动返回
    if (toRaw.get(target)) {
      return target;
    }
    const handlers = {
      set(target, key, value, receiver) {
        // 如果触发的是私有属性,可以直接触发视图更新
        if (target.hasOwnProperty(key)) {
          trigger();
        }
        return Reflect.set(target, key, value, receiver);
      },
      get(target, key, receiver) {
        const res = Reflect.get(target, key, receiver);
        if (isObject(target[key])) {
          // 如果属性是对象,递归调用
          return reactive(res);
        }
        return res;
      },
      deleteProperty(target, key) {
        return Reflect.deleteProperty(target, key);
      },
    };
    // proxy es6方法
    let observed = new Proxy(target, handlers);
    toProxy.set(target, observed); //原对象,代理后的结果
    toRaw.set(observed, target);
    return observed;
  }
​
  let obj = {
    name: "OrzR3",
    list: [1, 2, 3],
  };
  let p = reactive(obj);
  p.name = "sven";
  p.list.push(4);

附录

WeakMap

WeakMap 是es6语法,表示弱引用的对象。

在 JavaScript 中,一般我们创建一个对象,都是建立一个强引用:

var obj = new Object(); 只有当我们手动设置 obj = null 的时候,才有可能回收 obj 所引用的对象。

而如果我们能创建一个弱引用的对象:

// 假设可以这样创建一个 var obj = new WeakObject(); 我们什么都不用做,只用静静的等待垃圾回收机制执行,obj 所引用的对象就会被回收。

WeakMap 可以帮你省掉手动删除对象关联数据的步骤,所以当你不能或者不想控制关联数据的生命周期时就可以考虑使用 WeakMap。

Reflect 与 Proxy

Proxy 与 Reflect 是 ES6 为了操作对象引入的 API 。

Proxy 可以对目标对象的读取、函数调用等操作进行拦截,然后进行操作处理。它不直接操作对象,而是像代理模式,通过对象的代理对象进行操作,在进行这些操作时,可以添加一些需要的额外操作。

Reflect 可以用于获取目标对象的行为,它与 Object 类似,但是更易读,为操作对象提供了一种更优雅的方式。它的方法与 Proxy 是对应的。

详见文档:

www.runoob.com/w3cnote/es6…