Vue3响应式原理-初级

125 阅读4分钟

前言

Vue的响应式系统中,组件状态都是由响应式的 JavaScript 对象组成的。当更改它们时,视图会随即自动更新,这是单向数据流。众所周知,Vue2中使用Object.defineProperty(数据劫持)实现响应式数据,这个方案的弊端是无法监听对象新增属性和删除属性、不能监听数组的变化。在ES6中出现的Proxy(数据代理)解决了Object.defineProperty的问题,它可以做到更好的监听对对象的操作,同时结合Refelct对象,可以提供更好的实现响应式数据方案。

一、Proxy基本实现响应式数据

Vue3采用Proxy实现响应式数据。Proxy可以在目标对象之前架设一层“拦截”,外界对该对象的访问(比如获取属性的值、对属性赋值等等),都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。使用Proxy创建一个对象的代理,对对象的操作通过操作代理对象来完成,而不是操作原来的对象。

// target是目标对象,handler也叫捕获器(trap)
var proxy = new Proxy(target, handler);
  1. 基本实现
let obj = {
  name: 'why',
  age: 18
}
let proxyObj = new Proxy(obj, {
  get: function (target, key) {
    console.log('监听获取:', target[key]);
    return target[key]
  },
  set: function (target, key, value) {
    console.log('监听赋值:', value);
    // 操作了源对象
    target[key] = value
  },
  deleteProperty: function (target, key) {
    delete target[key];
    console.log('监听删除:', target[key]);
    return true;
  }
})
console.log(proxyObj.name); // 获取已有属性
proxyObj.name = 'why1' // 已有属性重新赋值
console.log(proxyObj.name); // 查看已有属性重新赋值
console.log(proxyObj.age); // 获取已有属性
delete proxyObj.age // 删除已有属性
console.log(proxyObj.age); // 查看删除已有属性
console.log(proxyObj.newPro); // 获取不存在的属性
proxyObj.newPro = 'newPro' // 给不存在的属性赋值
console.log(proxyObj.newPro); // 查看新增属性

缺点:操作了源对象。

  1. Proxy捕获器
    • 一共有13个,常用的有get/set/has/deleteProperty
    • 用于函数对象的有construct(拦截 Proxy 实例作为函数调用的操作,如proxy.call(object, ...args))和apply(拦截 Proxy 实例作为构造函数调用的操作,如new proxy(...args))。

二、认识Reflect

1.基本概念

Reflect是一个内置对象,译作反射。它提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法,与 proxy handler的方法相同。Reflect的所有属性和方法都是静态的(就像Math对象)。设计目的:

  • 早期的规范没有考虑对对象本身的操作如何设计会更规范,所以将这些API放到了Object上。而Object作为一个构造函数,并不适合这么做。
  • 一些类似in、delete的操作符,看起来奇奇怪怪。
  • 所以新增Reflect,把上面的操作都集中到Relect上。
  • 在使用Proxy时,可以做到不操作对象。
let obj = {
  name: 'why',
  age: 18
}
if (Reflect.deleteProperty(obj, 'age')) {
  console.log('删除成功'); 
} else {
  console.log('删除失败');
}
// 删除成功

一句话总结:堆在Object上的东西太多了,造个新对象来更优雅的使用那些API。用反射这个词挺到位的,功能一样,只是用法不一样。

2.常见方法

Proxy中的方法一一对应。一共有13个,常用的有get/set/has/deleteProperty

3.结合Proxy实现响应式数据

使用Reflect的好处:

  1. 代理对象的目的就是不直接操作源对象
  2. Reflect.set返回Boolean值,可以判断本次操作是否成功。
  3. 如果源对象有setter/getter访问器属性,那么可以通过receiver改变里面的this
    • Reflect.getReflect.set可以设置最后一个receiver参数,指定接收者 receiverreceiver就是外层Proxy对象。
let obj = {
  _name: 'why',
  age: 18,
  set name(newValue) {
    console.log(this, 'this');
    this._name = newValue
  },
  get name() {
    return this._name
  }
}
let proxyObj = new Proxy(obj, {
  get: function (target, key, receiver) {
    console.log('监听获取:', target[key]);
    return Reflect.get(target, key, receiver)
  },
  set: function (target, key, value, receiver) {
    console.log('监听赋值:', value);
    // const isSuccess = Reflect.set(target, key, value, receiver)
    const isSuccess = Reflect.set(target, key, value, { name: 1, age: 2 })
    if (!isSuccess) {
      throw new Error(`set ${key} failure`)
    }
    
    // 使用观察者模式,发布者发布消息,让订阅者更新
    // dep.notify()
  },
})
console.log(proxyObj.name); // 获取已有属性
proxyObj.name = 'why1' // 已有属性重新赋值
console.log(proxyObj.name); // 查看已有属性重新赋值

一句话总结:Reflect的用法更优雅,功能更全。

三、观察者模式

  • 观察者模式:

    • 只有发布者订阅者,它们之间存在依赖,并且要知道彼此的存在。
  • 发布者(Dep):

    • 需要一个数组保存所有的订阅者。
    • 提供一个添加订阅者的方法。
    • 提供一个发送通知方法,执行订阅者的更新。
  • 订阅者(Watcher):
    • 提供一个更新方法,供发布者调用。
// 发布者
class Dep {
  constructor() {
    this.subs = []
  }
  // 添加订阅者
  addSub(sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // 发布通知
  notify() {
    this.subs.forEach((sub) => {
      sub.update()
    })
  }
}
// 订阅者/观察者
class Watcher {
  update() {
    console.log('update');
  }
}
let dep = new Dep()
let watcher = new Watcher()
// 添加订阅者
dep.addSub(watcher)
// 发布通知
dep.notify()

四、总结

What:

  • Vue3基于ProxyReflect实现响应式数据,结合观察者模式实现响应式。通过Proxy生成一个代理对象,不直接操作源对象而操作代理对象。Reflect对对象的操作比Object更优雅。

Why:

  • 可以直接监听对对象的操作而不只是某个属性。
  • 代码简洁,不需要遍历所有属性才能达到监听所有属性的目的。有性能提升作用。
  • 有13种捕获器,直接起飞。

附录

  1. 文章:ES6入门-Proxy
  2. 文章:ES6入门-Reflect