从 proxy 到 Vue3 的响应式系统漫谈

大家可能知道 Vue2 是通过 object.defineProperty 监听对象的数据变化以实现数据驱动视图的同步更新,也叫双向绑定、响应式系统。初接触 Vue3 时,可能会对 Vue3 的响应式系统感到困惑,例如:Vue3 的响应式系统是怎么实现的?Vue2 和 Vue3 的实现有什么区别?不同的实现方式带来了什么样的优劣处?

我也是带着这些疑问,重新整理了相关知识,让我们从基础内置对象 proxy 说起

理解内置对象 proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等) — MDN

proxy 的使用语法:

// target 目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
// handler 通常以函数作为属性的对象,包含有 Proxy 的各个捕获器(trap)
const p = new Proxy(target, handler)

例子:

const handler = {
  get: function (obj, prop) {
    return prop in obj ? obj[prop] : 37;
  },
};

const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b); // 1, undefined
console.log("c" in p, p.c); // false, 37

以上的例子,在读取 p 的属性时会触发 handler.get 。如果 p 有这个属性,返回这个属性,否则返回37。

通过了解 proxy 内置对象,我们能对对象设置多种捕获器,以监听对象的各类操作。支持的捕获器多种多样,可以根据需求自由选择配置

proxy 在 Vue3 中的应用

官网用以下伪代码解释了 Vue 中的响应性是如何工作的,也就是 reactive 和 ref 的伪代码实现:

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key)
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      trigger(target, key)
    }
  })
}

function ref(value) {
  const refObject = {
    get value() {
      track(refObject, 'value')
      return value
    },
    set value(newValue) {
      value = newValue
      trigger(refObject, 'value')
    }
  }
  return refObject
}

可以看到,reactive 方法使用了 proxy 的特性,返回了包装后的代理对象。注意这个对象虽然在使用的时候和原对象没什么不同,但是通过 === 号还是可以比对出来和原对象不同。

const obj = {}
const p = new Proxy(obj, {})
// false
console.log(p === obj)

Vue3 的响应式系统

由此我们可以看出,Vue 通过追踪对象属性的读写,实现了数据与视图之间的通知和更新。

在 Vue3 中,还有一个概念叫 响应式副作用 ,比较常用的就是 watchEffect()computed 。常见的响应式副作用的用例就是更新 DOM,也叫做“响应式渲染”:

import { ref, watchEffect } from 'vue'

const count = ref(0)

watchEffect(() => {
  document.body.innerHTML = `计数:${count.value}`
})

// 更新 DOM
count.value++

响应式副作用的实现原理依赖于上文中 ref 、 reactive 追踪对象属性变化的钩子函数中(getter、setter 和 捕获器里的 track 与 trigger )。在 track 执行时将 副作用 记录在该属性的订阅者集合里,在 trigger 执行时同时执行这些订阅副作用。以实现 计算属性 的功能

总结

从上面的知识中我们可以找到开头几个问题的答案:

  1. Vue3 的响应式系统是怎么实现的?

答:ref 通过包装成对象,将初始值绑定在对象的 value 中,同时对 value 属性的 getter 和 setter 进行了监听,也就是使用了 get set 语法在读取和设置 value 时绑定了钩子函数。官网是这样解释的:

该 .value 属性给予了 Vue 一个机会来检测 ref 何时被访问或修改。在其内部,Vue 在它的 getter 中执行追踪,在它的 setter 中执行触发。 — Vue官网

而 reactive 通过 proxy 的特性,拦截对象所有属性的访问和修改。但 reactive 有着一定的局限性如:只能绑定对象类型的值、不能替换整个对象、解构会导致与代理对象的连接断开。

  1. Vue2 和 Vue3 的实现有什么区别?

答:Vue2 通过ES5语法 object.defineProperty; Vue3 通过 proxy 进行代理、通过 get、set 语法绑定监听函数

  1. 不同的实现方式带来了什么样的优劣处

答: object.defineProperty 在一些场景下不能监听数组元素变动(使用索引修改数组元素、使用 splice 等方法改变数组元素),不能监听动态添加的属性变化; proxy 能够支持数组元素的变动和动态添加到属性,但是也让我们在使用的时候需要 .value 来解包,同时打印在控制台时看到的是 proxy 对象而不是原始值,需要适应

参考文章

  1. Proxy – JavaScript | MDN (mozilla.org)

  2. 响应式基础 | Vue.js (vuejs.org

  3. 深入响应式系统 | Vue.js (vuejs.org)

原文链接:https://juejin.cn/post/7355321162530521122 作者:木雁之间

(0)
上一篇 2024年4月9日 上午10:18
下一篇 2024年4月9日 上午10:28

相关推荐

发表回复

登录后才能评论