详解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()
	    }
    }
}
相关推荐
摸鱼的鱼lv6 小时前
🔥 Vue.js组件通信全攻略:从父子传值到全局状态管理,一篇搞定所有场景!🚀
前端·vue.js
IT_陈寒6 小时前
Java性能优化:10个让你的Spring Boot应用提速300%的隐藏技巧
前端·人工智能·后端
lichong9516 小时前
【混合开发】vue+Android、iPhone、鸿蒙、win、macOS、Linux之dist打包发布在Android工程asserts里
android·vue.js·iphone
whysqwhw6 小时前
Hippy 跨平台框架扩展原生自定义组件的完整实现方案对比
前端
dasseinzumtode7 小时前
nestJS 使用ExcelJS 实现数据的excel导出功能
前端·后端·node.js
子兮曰7 小时前
🔥C盘告急!WSL磁盘暴增?三招秒清20GB+空间
前端·windows·docker
Jinuss7 小时前
Vue3源码reactivity响应式篇之EffectScope
前端·vue3
stoneship7 小时前
网页截图API-Npm工具包分享
前端
Jedi Hongbin7 小时前
Three.js shader内置矩阵注入
前端·javascript·three.js