《Vue.js 设计与实现》4.6 避免无线递归循环 阅读理解

272 阅读2分钟

4.6 避免无线递归循环

当前章节阶段的响应系统在如下场景中会出现栈溢出,原因在于 obj.foo++ 即是读取操作也是赋值操作,拆开来看:obj.foo = obj.foo + 1

const data = { foo: 1 }
const obj = new Proxy(data, {/*...*/}
effect(() => obj.foo++)

最终完整代码(星号中为关键代码):

// 存储副作用函数的桶
const bucket = new WeakMap()
let temp1, temp2

// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 栈
const effectStack = []

const effect = (fn) => {
    const effectFn = () => {
        // 调用 cleanup 函数完成清除工作
        cleanup(effectFn)

        // 当调用 effect 注册副作用函数时,将副作用函数赋值给 activeEffect
        activeEffect = effectFn

        // 在调用副作用函数之前将当前副作用函数压入栈中
        effectStack.push(effectFn)
        
        fn()

        // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
        effectStack.pop() 
        activeEffect = effectStack[effectStack.length - 1]
    }

    // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
    effectFn.deps = []

    // 执行副作用函数
    effectFn()
}

function cleanup (effectFn) {
    // 遍历 effectFn.deps 数组
    for (let i = 0; i < effectFn.deps.length; i++) {
        // deps 是依赖集合
        const deps = effectFn.deps[i]
        // 将 effectFn 从依赖集合中移除
        deps.delete(effectFn)
    }

    // 最后需要重置 effectFn.deps 数组
    effectFn.deps.length = 0
}

// 原始数据
const data = { foo: 1 }

const obj = new Proxy(data, {
    // 拦截读取操作
    get (target, key) {
        // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
        track(target, key)

        // 返回属性值
        return target[key]
    },

    // 拦截设置操作
    set (target, key, newVal) {
        // 设置属性值
        target[key] = newVal

        // 把副作用函数从桶里取出并执行
        trigger(target, key)
    }
})

// 在 get 拦截函数内调用 track 函数追踪变化
function track (target, key) {
    // 没有 activeEffect ,直接 return
    if (!activeEffect) return

    let depsMap = bucket.get(target)

    if (!depsMap) {
        bucket.set(target, (depsMap = new Map()))
    }

    let deps = depsMap.get(key)
    if (!deps) {
        depsMap.set(key, (deps = new Set()))
    }

    // 把当前激活的副作用函数添加到依赖集合 deps 中
    deps.add(activeEffect)

    // deps 就是一个与当前副作用函数存在联系的依赖集合
    // 将其添加到 activeEffect.deps 数组中
    activeEffect.deps.push(deps)
}

// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger (target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) return

    const effects = depsMap.get(key)

    const effectsToRun = new Set()
    // ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
    effects && effects.forEach(effectFn => {
        if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn)
        }
    })
    // ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
    effectsToRun.forEach(effectFn => effectFn())
}




// 注册副作用函数
effect(() => {
    obj.foo++ // obj.foo = obj.foo + 1 即是读取操作也是赋值操作
})

梳理一下注册流程以及当前形成死循环的原因:

缩进代表进入了另一个函数体

/**
 * 梳理一下注册流程以及当前形成死循环的流程:
 * effect(function fn ()=>{...}) 注册副作用函数
 * --重新包装 fn 为 effectFn
 * --为副作用函数添加 deps 属性,值为数组,存储包含此副作用函数的依赖集合的引用
 * --★执行副作用函数 effectFn 
 * ----effectFn 函数体中首先执行 cleanup 清除所有依赖集合中的当前副作用函数
 * ----将当前执行的 effectFn 赋值给 activeEffect
 * ----将当前执行的 effectFn 压入栈 effectStack
 * ----执行原始副作用函数 fn
 * ------fn 函数体中执行读取操作 obj.foo + 1 ,随即进入代理中的 get 操作
 * --------get 函数体执行
 * ----------track 对当前读取的 key 收集副作用函数集合 deps.add(activeEffect)
 * ----------将当前 deps 依赖集合 push 进当前执行的副作用函数的 activeEffect.deps 数组中
 * ----------返回当前属性值 return target[key],读取操作结束
 * 
 * ************************正常情况下注册流程结束,执行到 ▲ 步骤*************************************
 * 
 * ------fn 执行赋值操作 obj.foo = 值,进入代理的 set 操作
 * --------set 函数体执行
 * ----------赋值操作
 * ----------trigger 开始逐个执行副作用函数操作
 * ------------执行 effectFn 此时进入上面标 ★ 号的步骤,这样就形成了一个死循环,引起栈溢出
 * 
 * ***********************以下步骤永远无法执行,因此当前 activeEffect === effectFn*******************
 * 
 * ------▲ 执行出栈操作 effectStack.pop()
 * ------清除或还原当前执行的副作用函数 activeEffect = effectStack[effectStack.length - 1]
 */