前言

Vue3 已经发布许久,官网推荐的版本也已经直接改成了 v3,Vue3 的响应式原理是通过什么实现的呢?

正文

Vue2

在了解 V3 之前,我们先来了解一下 Vue2 的原理:Object.defineProperty.

Vue2 使用的是 Object.defineProperty 来实现响应式原理。

Object.defineProperty(obj, prop, descriptor) 接受三个参数,而且都是必填的
obj: 要定义属性的对象
prop: 要定义或修改的属性的名称
descriptor: 要定义或修改的属性描述符

当一个普通的 js 对象传入 Vue 实例作为 data 选项时,Vue 将遍历 data 的所有属性,并使用 Object.defineProperty 重写这些属性的 getter/setter 方法用来追踪依赖,在属性值被访问和修改时通知变更。每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中将访问过的属性设置为依赖,之后当属性的 setter 触发时,会通知 watcher 对关联的组件进行重新渲染。

在 Vue2 中,Vue 的响应式系统是基于 数据拦截 + 发布订阅模式,包含了四个模块:

  • Observer: 通过 Object.defineProperty 拦截 data 的 getter/setter 方法,从而使得每一个 property 都拥有一个 Dep,当触发 getter 的时候收集依赖(使用该 property 的 watcher),当触发 setter 的时候通知更新;
  • Dep: 依赖收集器,用户维护 data property 的所有 watcher;
  • Watcher: 将视图依赖的 property 绑定到 Dep 中,当数据修改时触发 setter,调用 Dep 的 notify 方法,通知所有依赖该 property 的 watcher 进行 update,使 property 与 视图绑定;
  • Compile: 模板指令解析器,对模板的每个元素节点的指令进行扫描解析,根据指令模板替换 property 的值,同时注入 watcher 更新数据的回调方法

总结: Observer 通过重写 data 各个 property 的 getter/setter 方法,对每个 property 都维护一个 Dep,用于收集依赖该 property 的所有 watcher,当该 property 触发 setter 的时候,派发更新通知。

Vue3

与 Vue2 的实现不同,Vue3 的响应式原理是用 ES6 的 Proxy 方法来实现的。
Proxy 用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如:属性查找、赋值、枚举、函数调用等)。可以理解为:在目标对象之前有一层“拦截”,外界对该对象的访问都必须先通过这层拦截。因此提供了一种机制:可以对外界的访问进行过滤和改写

// target: 目标对象,待要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
// handler: 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 proxy 的行为
const proxy = new Proxy(target, handler);

常见代理函数:

  • get: 拦截对象属性的读取

    let obj = {
      'name': "张三",
    }
    const proxy = new Proxy(obj, {
      // target:代理对象;key:当前的属性名
      get(target, key) {
        return key in target ? target[key] : key
      }
    })
    console.log(proxy["name"]) // 张三
    console.log(proxy["age"]) // age
  • set: 拦截对象属性的设置;(必须有返回值,返回值为布尔类型)

    let obj = {
      'name': "张三",
    }
    const proxy = new Proxy(obj, {
      // target:代理对象;key:当前的属性名;value:新的属性值
      set(target, key, value) {
        target[key] = value
        return true
      }
    })
    proxy.name = '李四'
    console.log(proxy["name"]) // 李四
  • has: 拦截 key in proxy 的操作

    let range = {
       start: 1,
       end: 5
     }
     const proxy = new Proxy(range, {
       // target:代理对象;key:当前的属性名
       has(target, key) {
         return key >= proxy.start && key <= proxy.end
       }
     })
     console.log(2 in proxy) // true
     console.log(9 in proxy) // false
  • deleteProperty: 拦截对象属性的删除

    let obj = { name: '李四', age: '18' }
    const proxy = new Proxy(obj, {
      // target:代理对象;key:当前的属性名
      deleteProperty(target, key) {
        console.log('当前删除 :', target[key])
        return delete target[key]
      }
    });
    delete proxy.name
    Reflect.deleteProperty(proxy, 'age')
  • ownKeys: 拦截对象键值的读取

Proxy 支持 13 种拦截操作。

总结

  • Object.defineProperty(obj, prop, descriptor) 中的第一个参数是需要定义的对象,当我们使用后,这个对象就会被数据劫持
  • Proxy 实际是一个构造函数,用来生成 Proxy 实例。而且 Proxy 定义的是拦截,只是对外界访问是的过滤和改变
  • defineProperty 的局限性最大原因是它只能针对单例属性做监听。Vue2 中的响应性实现正是基于 defineProperty 中的 descriptor,对 data 中的属性做了遍历 + 递归,为每个属性设置了 getter、setter。这也就是为什么 Vue 只能对 data 中预定义过的属性做出响应的原因,在 Vue 中使用下标的方式直接修改属性的值或者添加一个预先不存在的对象属性是无法做到 setter 监听的(一般使用 this.$set(…) 显式声明一下),这是 defineProperty 的局限性
  • Proxy 的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性,将会带来很大的性能提升和更优的代码。Proxy 可以理解成,在目标对象之前架设一层 “拦截” ,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写
  • 在 Vue2 中,对于一个深层属性嵌套的对象,要劫持它内部深层次的变化,就需要递归遍历这个对象,执行 Object.defineProperty 把每一层对象数据都变成响应性的,这无疑会有很大的性能消耗。而在 Vue3 中,使用 Proxy 并不能监听到对象内部深层次的属性变化,因此它的处理方式是在 getter 中去递归响应性,这样做的好处是真正访问到的内部属性才会变成响应性,简单的可以说是按需实现响应性,减少性能消耗

参考链接: