从零到一打造 Vue3 响应式系统 Day 18 - Reactive:深入 Proxy 的设计思路

在之前的文章中,我们已经完成了 ref 的实现,它能将原始值包装成响应式对象。现在,我们要接着完成响应式系统核心的另一部分:reactive 函数。我们的目标是接收一个完整的对象,并返回一个代理对象,使其所有属性都具备响应性。

目标设定

我们的目标很明确:完成一个 reactive 函数,让其行为和 Vue 的官方示例一样。

环境搭建

JavaScript 复制代码
// import { reactive, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
import { reactive, effect } from '../dist/reactivity.esm.js'

const state = reactive({
  a: 0
})
effect(() => {
  console.log(state.a)
})

setTimeout(() => {
  state.a = 1
}, 1000)

我们期待初始化页面时输出 0,一秒钟后输出 1。

使用被注释掉的官方示例,我们可以很明显地看到正确的输出值。

我们先在 src 目录下新建一个 reactive.ts

TypeScript 复制代码
export function reactive(target){
}

并且在 index.ts 中引入。

TypeScript 复制代码
export * from './ref'
export * from './effect'
export * from './reactive'

另外,我们在存放工具函数的 shared/src/index.ts 中编写一个对象判断函数。

TypeScript 复制代码
export function isObject(value) {
  return typeof value === 'object' && value !== null
}

核心思路

我们再另外编写一个函数 createReactiveObject,我们实际的逻辑并不直接放在 reactive 函数中。

主要是因为 createReactiveObject 之后在其他地方也会用到,像是 shallowReactive 之类的 API。

TypeScript 复制代码
export function reactive(target){
  return createReactiveObject(target)
}

接下来思考 createReactiveObject 本身的限制,以及我们的需求:

  1. 它只能接收对象类型,所以我们要去判断它的类型。

  2. reactive 的核心是使用一个 Proxy 对象来处理。

  3. Proxy 对象中会需要 getset 处理器来收集依赖、触发更新。

    • 收集依赖target 的每个属性都是一个依赖,因此我们需在收集依赖时,把 target 的属性跟 effect (也就是 sub) 建立关联关系。
    • 触发更新:通知之前为该属性收集的依赖,让它们重新执行。

为什么 Vue 3 的 reactive() 特别适合使用 Proxy?

主要是因为 Proxy 有几个关键特性:

  • Proxy 可以拦截并自定义对象的各种操作,不只是属性的读取和设置。
  • 与 Vue 2 使用 Object.defineProperty() 相比,Proxy 的最大优势是可以侦测到新增的属性
  • Proxy 可以直接拦截数组的索引操作和 length 变更。
  • Proxy 可以处理 MapSetWeakMapWeakSet 等集合类型。

看来针对对象类型的 reactiveProxy 对象确实是一个更好的解决方案,那我们开始实现吧!

初步实现 - 借鉴 Ref 的实现

TypeScript 复制代码
import { isObject } from '@vue/shared'

function createReactiveObject(target){
  // reactive 只处理对象
  if (!isObject(target)) return target

  // 创建 target 的代理对象
  const proxy = new Proxy(target, {
    get(target, key, receiver){
      // 收集依赖:绑定 target 的属性与 effect 的关系
      console.log('get:', target, key)
      return Reflect.get(target, key, receiver)
    },
    set(target, key, newValue, receiver){
      // 触发更新:通知之前收集的依赖,重新执行 effect
      console.log('set:', target, key, newValue)
      return Reflect.set(target, key, newValue, receiver)
    }
  })

  return proxy
}

我们来看一下,实际的输出值:

看来好像挺接近的,但依照我们编写 ref 的经验,我们还需要处理链表相关的逻辑。

先回顾一下我们的 ref 之前是怎么写的:

TypeScript 复制代码
export function trackRef(dep) {
  if (activeSub) {
    link(dep, activeSub)
  }
}

export function triggerRef(dep) {
  if (dep.subs) {
    propagate(dep.subs)
  }
}
  • get 中有一个 trackRef 函数,trackRef 函数判断是否存在 effect (activeSub),如果存在,就将依赖 (dep) 以及 effect (activeSub) 传入 link 函数建立链表关联关系。
  • set 中有一个 triggerRef 函数,triggerRef 函数判断该依赖是否收集过 effect,如果存在,就传入 propagate 进行触发更新。

看来这个依赖 (dep) 很重要,那什么是依赖呢?

TypeScript 复制代码
class RefImpl {
  _value;
  [ReactiveFlags.IS_REF] = true

  subs: Link
  subsTail: Link
  // ...
  get value() {
    if (activeSub) {
      trackRef(this) // 这里的 this (RefImpl 实例) 就是 dep
    }
    return this._value
  }

  set value(newValue) {
    this._value = newValue
    triggerRef(this) // 这里的 this (RefImpl 实例) 就是 dep
  }
}

我们可以看到传入 trackReftriggerRefdep 必须包含 subssubsTail 属性。

那我们可以创建一个 Dep 类,其他逻辑可以照搬 reftrackReftriggerRef 并进行修改。

TypeScript 复制代码
import { activeSub } from './effect'
import { link, propagate, Link } from './system'

function createReactiveObject(target){
  // reactive 只处理对象
  if(!isObject(target)) return target

  // 创建 target 的代理对象
  const proxy = new Proxy(target, {
    get(target, key, receiver){
      track(target, key)
      return Reflect.get(target, key, receiver)
    },
    set(target, key, newValue, receiver){
      const res = Reflect.set(target, key, newValue, receiver)
      trigger(target, key)
      return res
    }
  })

  return proxy
}

class Dep {
  subs: Link
  subsTail: Link
  constructor(){}
}

function track(target, key){
  if (!activeSub) return
  // link(dep, activeSub) // dep 从哪里来?
}

function trigger(target, key){
  // if (dep.subs) {
  //   propagate(dep.subs) // dep 从哪里来?
  // }
}

注:在 set 处理器中,我们应该先完成赋值操作,再触发更新通知。

感觉创建一个 Dep 类的实例,传入 track 就可以了。不过用户传入的 target 对象跟我们新建的 Dep 似乎没有直接关系。

看起来我们遇到了一些问题:

  • 我们不能再用一个 Dep 实例来管理所有属性的依赖,必须为对象的每个属性 都维护一个独立的 Dep
  • 如何建立 target.aDep for a 的对应关系?
  • 如何在不污染原始 target 对象的情况下,存储 targetkeyDep 之间的关联?

为了解决这个问题,我们需要引入一个更复杂的数据结构来存储这些关系,明天我们再接着探讨。


想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。

相关推荐
你的人类朋友2 小时前
🍃说说Base64
前端·后端·安全
ze_juejin2 小时前
Node.js 全局变量完整总结
前端
ttyyttemo2 小时前
Learn-Jetpack-Compose-By-Example---TextFieldValue
前端
_AaronWong2 小时前
多页面应用登录状态共享:基于弹出窗口的通用解决方案
前端·javascript·vue.js
六月的可乐2 小时前
Vue接入AI聊天助手实战
前端·vue.js·人工智能
degree5202 小时前
Webpack 与 Vite 构建速度对比:冷启动、HMR、打包性能实测分析
前端
猩猩程序员2 小时前
下一版本 MCP 协议将于2025年11月25日发布
前端
熊猫_豆豆2 小时前
用MATLAB画一只可爱的小熊
前端·matlab·画图
凯哥19702 小时前
Vue 3 + Supabase 认证与授权时序最佳实践指南
前端·后端