
在之前的文章中,我们已经完成了 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
本身的限制,以及我们的需求:
-
它只能接收对象类型,所以我们要去判断它的类型。
-
reactive
的核心是使用一个Proxy
对象来处理。 -
Proxy
对象中会需要get
和set
处理器来收集依赖、触发更新。- 收集依赖 :
target
的每个属性都是一个依赖,因此我们需在收集依赖时,把target
的属性跟effect
(也就是sub
) 建立关联关系。 - 触发更新:通知之前为该属性收集的依赖,让它们重新执行。
- 收集依赖 :
为什么 Vue 3 的 reactive()
特别适合使用 Proxy?
主要是因为 Proxy
有几个关键特性:
Proxy
可以拦截并自定义对象的各种操作,不只是属性的读取和设置。- 与 Vue 2 使用
Object.defineProperty()
相比,Proxy
的最大优势是可以侦测到新增的属性。 Proxy
可以直接拦截数组的索引操作和length
变更。Proxy
可以处理Map
、Set
、WeakMap
、WeakSet
等集合类型。
看来针对对象类型的 reactive
,Proxy
对象确实是一个更好的解决方案,那我们开始实现吧!
初步实现 - 借鉴 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
}
}
我们可以看到传入 trackRef
和 triggerRef
的 dep
必须包含 subs
和 subsTail
属性。
那我们可以创建一个 Dep
类,其他逻辑可以照搬 ref
的 trackRef
和 triggerRef
并进行修改。
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.a
→Dep for a
的对应关系? - 如何在不污染原始
target
对象的情况下,存储target
、key
与Dep
之间的关联?
为了解决这个问题,我们需要引入一个更复杂的数据结构来存储这些关系,明天我们再接着探讨。
想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。