详解Vue3的响应式系统

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函数。

实现思路:

  1. 当effect函数执行时,将effect函数赋值给全局变量activeEffect
  2. 如果effect中有响应式数据,则会触发proxy的get方法,在get方法中判断activeEffect是否存在,存在即收集
  3. 当响应式数据修改时,会触发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()
	    }
    }
}
相关推荐
恋猫de小郭3 分钟前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端