Vue3 响应式依赖收集与更新之effect

关于Vue3reactive 的响应实现与Proxy的代理实现,可以参考前篇文章Vue3 响应式之reactive:深入理解 Proxy代理。今天这篇文章主要讲解响应式数据的依赖收集与数据变化时是如何保障数据的一致性与触发对应的函数依赖执行。

在 Vue 3 中,effect (副作用函数)是响应式系统的核心机制之一。它负责 依赖收集响应式更新 触发,是 watch、watchEffect、computed、组件渲染等功能的底层基础。简单来说,effect 是一个 带有响应式感知能力的函数,当在一个 effect 函数中访问响应式数据(如 reactive 包裹的数据),Vue 会自动记录这些依赖;当这些依赖发生变化时,该 effect 会自动重新执行。在源码层面,effect 由 ReactiveEffect 类封装,通过全局变量 activeEffect 实现依赖追踪。

1. effect函数的实现

javascript 复制代码
/**
 * effect实现
 * @param {function} fn - 执行函数
 * @param {object}  options - 配置参数
 * @returns {function} effect
 */
function effect(fn, options = {}) {
    const effect = createReativeEffect(fn, options)
    return effect
}
var id = 0 // 标识当前effect
var activeEffectStack = [] // 使用Stack记录嵌套effect的调用顺序
var activeEffect = null // 当前执行的effect,用于依赖收集
function createReativeEffect(fn, options) {
    const effect = function reativeEffect() {
        activeEffectStack.push(effect) // 嵌套effect调用时进行记录
        activeEffect = effect
        try {
             return fn() // 访问fn时会进行依赖收集,例如访问reactive数据时
            } finally{
                activeEffectStack.pop()
                activeEffect = activeEffectStack[activeEffectStack.length - 1]
           }
    }
    effect.id = ++id
    effect.raw = fn
    effect.options = options
    effect.deps = [] // 记录响应式属性的依赖,在某阶段进行cleanup,后续再讲解,非本文的重点
    if (!options.lazy) { // 非懒加载则立即执行
        effect();
    }
    return effect
}

2. reactive下的依赖收集之track函数

javascript 复制代码
/*
* createGetter: proxy第二个参数mutablehandle中的get方法
* @param - 无
* @returns {function} 返回get函数
*/
function createGetter() {
    return function get(target, key, receiver) {
        track(target, key)
        const res = Reflect.get(target, key, receiver)
        // console.log(`get ${key}`)
        if(isObject(res)) {
            return reactive(res)
        }
        return res // 返回对应的值,例如访问obj.property
    }
}
/*
* 依赖收集函数
* param {object} target -例如reactive声明的数据
* param {string|number} key -例如reactive声明数据下的某个属性
*/
function track(target, key) {
    if(!activeEffect) return // 未存在调用的effect
    let depsMap = targetMap.get(target) // 获取对应target对map
    if(!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
    }
    let deps = depsMap.get(key) // 获取对应target对应map下的key进行依赖收集
    if(!deps) {
        depsMap.set(key, (deps = new Set()))
    }
   
    if(!deps.has(activeEffect)) {
        deps.add(activeEffect)
        activeEffect?.deps?.push(deps) // 后续 cleanup(清理旧依赖)做准备
    }
}

以上在target对应的key访问时自动进行对应effect的收集,保障后续target下某个key更新时对应的effect进行调用从而触发更新

3. reactive下的依赖触发更新之trigger函数

javascript 复制代码
/*
* createGetter: proxy第二个参数mutablehandle中的set方法
* @param - 无
* @returns {function} 返回set函数
*/
function createSetter() {
    return function set(target, key, value, receiver) {
        const oldValue = target[key]
        const res = Reflect.set(target, key, value, receiver)
        // console.log(`set ${key}`)
        const hadkey = target instanceof Array ? Number(key) < target.length : key in target // 判断是否新增属性,优化vue2中新增属性无触发响应更新的问题
        if(!hadkey) { // key为新增
            trigger(target, key, value, 'ADD')
        }else if(!isEqual(oldValue, value)){ // 非新增直接进行值的比较
            trigger(target, key, value, 'SET') // 触发更新
        }
        return res
    }
}
/*
* 依赖触发更新函数
* param {object} target -例如reactive声明的数据
* param {string|number} key -例如reactive声明数据下的某个属性
* param {any} value - 设置的值
* type {string} type - 触发类型
*/
function trigger(target, key, value, type = 'SET') {
    const depsMap = targetMap.get(target)
    if(!depsMap) return
    const deps = depsMap.get(key)
    const effects = new Set() // 收集key对应的所有依赖
    const addEffect = (deps) => {
        if(deps) {
            deps.forEach(effect => {
                effects.add(effect)
            })
        }
    }
    if(Array.isArray(target) && key === 'length') { // 访问数组的length属性
        addEffect(depsMap.get('length')) // 收集length对应的Effect
        depsMap.forEach((deps, key) => {
            if(typeof key === "number" || key >= value) {  // 触发被删除项的依赖(索引 >= newLength)
                addEffect(deps)
            }
        })
    }else{
        if(key !== void 0) {
            addEffect(deps) //正常触发对应effect
        }
        switch(type) {
            case 'ADD':
                if(Array.isArray(target) && isIntegerKey(key)) {
                    addEffect(depsMap.get('length')) // 新增长度需要触发length对应Effect
                }
                break;
        }
    }
    effects.forEach(effect => {
        effect() // 触发所有已收集到的effect进行执行
    })
}

4.具体案例

javascript 复制代码
const obj = reactive({
    name: 'zs',
    age: 18,
    address: {
        city: 'beijing',
        info: {
            name: 'ls'
        }
    }
})
effect(() => {
    console.log(obj.name, '--obj.name--');
    setTimeout(() => {
        obj.name = 'ls'
    }, 1000);
}, {
    lazy: false
})
// 打印如下:
// zs --obj.name--
//1秒后 ls --obj.name--

5.总结

以上是关于响应式对象例如reactive下数据访问和更改时对应的依赖函数收集和更新,本篇文章主要是基于vue3源码reactive基本响应式内容的实现,让大家能够了解到vue3下的响应式机制的运行流程,在vue3的源码中还会针对许多边界和其它响应式api进行适配和处理,我也会在后续更新中把更多流程完善后再把代码上传至git进行分享给大家,也欢迎大家在评论区交流!

相关推荐
x-cmd5 小时前
[x-cmd] jsoup 1.22.1 版本发布,引入 re2j 引擎,让 HTML 解析更安全高效
前端·安全·html·x-cmd·jsoup
天下代码客5 小时前
使用electronc框架调用dll动态链接库流程和避坑
前端·javascript·vue.js·electron·node.js
weixin199701080165 小时前
【性能提升300%】仿1688首页的Webpack优化全记录
前端·webpack·node.js
冰暮流星5 小时前
javascript之数组
java·前端·javascript
晚霞的不甘6 小时前
Flutter for OpenHarmony天气卡片应用:用枚举与动画打造沉浸式多城市天气浏览体验
前端·flutter·云原生·前端框架
weixin79893765432...6 小时前
Vue 渲染体系“三件套”(template 模板语法、h 函数和 JSX 语法)
vue.js·h函数·template 模板·jsx 语法
xkxnq6 小时前
第五阶段:Vue3核心深度深挖(第74天)(Vue3计算属性进阶)
前端·javascript·vue.js
三小河6 小时前
Agent Skill与Rules的区别——以Cursor为例
前端·javascript·后端
Hilaku6 小时前
不要在简历上写精通 Vue3?来自面试官的真实劝退
前端·javascript·vue.js