Vue 3 的响应式系统,实现思路是通过 Proxy 对对象进行拦截,在 get 拦截中完成依赖收集,在 set 拦截中触发依赖更新,然后结合 effect (副作用函数)实现响应式的自动追踪与更新。
简单实现Proxy代理对象
根据以上的实现思路,先去实现一个简单的proxy。
js
function reactive(target){
return new Proxy(target, {
get(target, key, receiver){
return target[key]
},
set(target, key, value, receiver){
const oldValue = target[key]
if(oldValue !== value){
target[key] = value
}
return true
}
})
}
const state = reactive({name: 'zhangsan', age: 10})
这样实现看上去没什么问题,但如果代理的对象中某个属性是一个函数的话,就会出现this指向问题。
举个🌰
js
const obj = {
name: 'zhangsan',
get otherName(){
return this.name + '_other' // 这里我们读取一个属性为函数,在函数中读取name
}
}
const state = reactive(obj)
在这个例子中,如果在 Proxy 的 get 拦截里直接用 target[key]
来取值,那么当我们访问 state.otherName 时,实际上调用的是 obj.otherName 的 getter,getter 内部的 this 会指向原始对象 obj。
这样一来,this.name 实际等价于 obj.name,它绕过了代理对象 state,不会再触发 Proxy 的 get 拦截逻辑,也就无法进行依赖收集。
因此Vue3中采用了Reflect配合Proxy保证this的指向的正确性。
使用 Reflect.get(target, key, receiver),并传入当前的代理对象 receiver(这里就是 state),那么 getter 内部的 this 会被正确绑定到代理对象 state。此时 this.name 相当于访问 state.name,就会再次走一遍 Proxy 的 get,从而触发依赖收集,保证响应式系统正常工作。
js
function reactive(target){
return new Proxy(target, {
get(target, key, receiver){
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver){
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
return result
}
})
}
副作用函数effect
Effect在vue中是一个非常重要的概念。什么是effect?你可以把它理解为:一段需要被"追踪"的代码,一旦它依赖的数据变了,这段代码就会自动重新运行。
js
// @例子1
const state = reactive({name:'zhangsan', age:10})
effect(()=>{
app.innerHtml = state.name
})
setTimeout(()=>{ state.name = 'lisi' }, 1000)
在上面这个例子中,app元素设置state.name,因为state是个响应式对象,那么当设置state.name为lisi时,就应该触发effect重新执行,重新渲染app元素中的内容。
实现effect:
js
function effect(fn, options){
const _effect = new ReactiveEffect(fn)
_effect.run() // 当使用effect时,先执行一次
return _effect
}
class ReactiveEffect {
constructor(fn){
this.fn = fn
}
run(){
return this.fn()
}
}
Effect实现以后如何跟响应式数据结合起来?可以在全局定义变量activeEffect,用来表示当先正在执行的effect函数。
实现思路:
- 当effect函数执行时,将effect函数赋值给全局变量activeEffect
- 如果effect中有响应式数据,则会触发proxy的get方法,在get方法中判断activeEffect是否存在,存在即收集
- 当响应式数据修改时,会触发proxy的set方法,将收集到的effect依次执行即可。
js
let activeEffect;
function effect(fn){
const _effect = new ReactiveEffect(fn)
_effect.run() // 当使用effect时,先执行一次
return _effect
}
class ReactiveEffect {
_trackId = 0 // 这个在依赖收集时进行解释
constructor(fn){
this.fn = fn
}
run(){
try{
activeEffect = this; // 将当前正在执行的effect赋值给activeEffect
this._depsLength = 0
this._trackId ++
return this.fn()
} finally {
activeEffect = undefined; // 当effect执行完后,再置空
}
}
}
依赖收集与依赖触发:
js
function reactive(target){
return new Proxy(target, {
get(target, key, receiver){
track(target, key) // 依赖收集
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver){
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
trigger(target, key, value, oldValue) // 依赖触发
}
return result
}
})
}
依赖收集: 在收集依赖时,要考虑到收集的副作用函数与响应式数据之间的关系,以上面@例子1 为例,副作用函数中只读取了state.name,那么副作用函数只需要跟name属性进行绑定即可,而不需要考虑age。因此我们需要一个数据结构来表达这个关联关系。
在vue的实现中,采用的是 weakMap -> Map -> Map的结构。

js
const targetMap = new WeakMap()
function track(target, key){
// 判断activeEffect是否存在,不存在则表示没有正在执行的effect,就不用收集
if(activeEffect){
// 按照图示结构
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Map()
depsMap.set(key, dep)
}
// 在创建effect的类中有一个私有属性_trackId,目的是用于标识此effect是否已被收集。
// 比如在同一个effect中重复读取同一个属性,如果不加限制,effect就会被重复收集。
if(dep.get(activeEffect) !== activeEffect._trackId){
// 将activeEffect 与 _trackId存起来
dep.set(activeEffect, _trackId)
}
}
}
触发依赖:
根据上面的图示的数据结构依次取出来执行即可。
js
function trigger(target, key, newValue, oldValue) {
let depsMap = targetMap.get(target)
if (!depsMap) {
return
}
let dep = depsMap.get(key)
if (dep) {
for (const effect of dep.keys()) {
effect.run()
}
}
}