Vue3 响应式系统实现原理学习总结(二)

94 阅读4分钟

上一篇总结,讲到响应式系统的最基础实现。除了最基础实现之外,其实还有一些细节问题需要处理。第二部分,我会针对这些细节问题,进行总结。这些细节问题,说实话,如果不是作者书中讲解,我自己很难想到,我想,这个这就是学习的价值。

解决比较难想到的细节问题

分支切换问题

分支切换问题,我的理解是,当一个响应式属性为某个值的时候,副作用不再和另一个属性有关联。但是目前的实现,即使无关联,也会导致副作用函数触发。

这个阐述有点抽象,直接看代码例子最好。

effect(() => {
    const result = obj.ok ? obj.text : 'not'
    console.log(result)
})
obj.ok = false
obj.text = 'changed'

代码例子中,预期是希望obj.okfalse的时候,不再触发effect注册的副作用函数,因为默认都是返回not

为了解决这个问题,核心是使用cleanup函数,在执行handler.set的时候,先解除所有的副作用和bucket的对应关系,然后触发依赖收集,建立新的对应关系。这样就可以保证分支切换的时候,不会用冗余的副作用函数触发。

要解除所有的副作用和bucket的对应关系,则需要知道effect注册的副作用函数,被哪些Set所收集,所以需要在effect注册的副作用函数上,增加一个deps属性,用于反向收集。

为了方便阅读,我还是会把完整的代码写出来。

const bucket = new WeakMap()

const data= { ok: true, text: 'hello world' }

function track(target, key) {
    let depsMap
    if (!bucket.get(target)) {
        bucket.set(target, new Map())
    }
    depsMap = bucket.get(target)
    let deps
    if (!depsMap.get(key)) {
        depsMap.set(key, new Set())
    }
    deps = depsMap.get(key)
    if (activeEffect) {
        deps.add(activeEffect)
        activeEffect.deps.push(deps)
    }
}

function trigger(target, key) {
    let depsMap
    if (!bucket.get(target)) return
    depsMap = bucket.get(target)
    let deps
    if (!depsMap.get(key)) return
    deps = depsMap.get(key)

    // 修复无限循环的问题
    const effectToRun = new Set(deps)
    effectToRun.forEach(effect => {
        effect()
    })
}

const obj = new Proxy(data, {
    get(target, key) {
        track(target, key);
        return target[key]
    },
    set (target, key, newVal) {
        target[key] = newVal
        trigger(target, key, newVal);
    }
})

function cleanup (effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
        const deps = effectFn.deps[i]
        deps.delete(effectFn)
    }
    effectFn.deps.length = 0
}

let activeEffect
function effect (fn)  {
    const effectFn = () => {
        cleanup(effectFn)
        activeEffect = effectFn
        fn()
    }
    effectFn.deps = []
    effectFn()
}

effect(() => {
    const result = obj.ok ? obj.text : 'not'
    console.log(result)
})

这里简单说一个细节。

// 修复无限循环的问题
const effectToRun = new Set(deps)
effectToRun.forEach(effect => {
    effect()
})

trigger函数中,如果没有用一个新的Set来维护需要执行的副作用,会陷入无限循环。核心原因是因为,Set中移除了一个副作用函数之后,又新增同一个副作用函数,会导致这个Set永远无法遍历结束。这个是Set的规范规定,想要深入可以检索一下资料。

effect嵌套问题

由于渲染函数是使用effect,而渲染函数可能会有嵌套,本质上来说也就是组件的嵌套。那么effect的注册函数,也是要支持嵌套。现在的实现,如果嵌套effect,会出现内层effect影响外层effct的问题。

原因也比较简单,因为只有一个activeEffect变量去存储当前的副作用函数。当effect发生嵌套,且内层effect先触发依赖收集,那么activeEffect是内层effect注册的副作用,而到了外层的时候,仍然收集了这个内层副作用依赖。

能还原问题的代码如下:

effect(() => {
  effect(() => {
    console.log(obj.text)
  })
  console.log(obj.ok)
})
obj.ok = false
// 打印
// hello world
// true
// hello world <= 这里应该打印多一行,hello world

为了解决这个问题,熟悉数据结构的话,很容易想到要用栈来解决。

核心修改如下

let activeEffect
let effectStack = []
function effect (fn)  {
    // 修改
    const effectFn = () => {
        cleanup(effectFn)
        activeEffect = effectFn
        // 1. 执行副作用函数前,入栈
        effectStack.push(effectFn)
        fn()
        // 2. 执行完出栈
        effectStack.pop()
        // 3. 出栈之后,还原activeEffect
        activeEffect = effectStack[effectStack.length - 1]
    }
    effectFn.deps = []
    effectFn()
}

当前完整代码如下:

const bucket = new WeakMap()
const data= { ok: true, text: 'hello world' }
function track(target, key) {
    let depsMap
    if (!bucket.get(target)) {
        bucket.set(target, new Map())
    }
    depsMap = bucket.get(target)
    let deps
    if (!depsMap.get(key)) {
        depsMap.set(key, new Set())
    }
    deps = depsMap.get(key)
    if (activeEffect) {
        deps.add(activeEffect)
        activeEffect.deps.push(deps)
    }
}
function trigger(target, key) {
    let depsMap
    if (!bucket.get(target)) return
    depsMap = bucket.get(target)
    let deps
    if (!depsMap.get(key)) return
    deps = depsMap.get(key)
    const effectToRun = new Set(deps)
    effectToRun.forEach(effect => {
        effect()
    })
}
const obj = new Proxy(data, {
    get(target, key) {
        track(target, key);
        return target[key]
    },
    set (target, key, newVal) {
        target[key] = newVal
        trigger(target, key, newVal);
    }
})
function cleanup (effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
        const deps = effectFn.deps[i]
        deps.delete(effectFn)
    }
    effectFn.deps.length = 0
}
let activeEffect
let effectStack = []
function effect (fn)  {
    // 修改
    const effectFn = () => {
        cleanup(effectFn)
        activeEffect = effectFn
        // 1. 执行副作用函数前,入栈
        effectStack.push(effectFn)
        fn()
        // 2. 执行完出栈
        effectStack.pop()
        // 3. 出栈之后,还原activeEffect
        activeEffect = effectStack[effectStack.length - 1]
    }
    effectFn.deps = []
    effectFn()
}

解决无限循环问题

上一次提到无限循环问题,是在分支切换问题的时候有讲到,核心原因是Set的delete和add的共同作用。现在说到的无限循环问题,是指在副作用函数中,对响应式数据有做变更。当前的实现,由于没有拦截在Set中执行这个变更操作,会触发依赖收集和依赖触发的无限循环。

触发这个问题的代码如下:

effect(() => {
  obj.text = obj.text + 'test'
})

为了解决这个问题,只需要在执行trigger的时候,排除当前activeEffect即可。

核心修改代码如下:

function trigger(target, key) {
    
    ...
    
    // 不执行当前的注册的activeEffect即可
    effects.forEach(effect => {
        if (effect !== activeEffect) {
            effectToRun.add(effect)
        }
    })
    effectToRun.forEach(effect => {
        effect()
    })
}

完整代码如下:

const bucket = new WeakMap()
const data= { ok: true, text: 'hello world' }
function track(target, key) {
    let depsMap
    if (!bucket.get(target)) {
        bucket.set(target, new Map())
    }
    depsMap = bucket.get(target)
    let deps
    if (!depsMap.get(key)) {
        depsMap.set(key, new Set())
    }
    deps = depsMap.get(key)
    if (activeEffect) {
        deps.add(activeEffect)
        activeEffect.deps.push(deps)
    }
}
function trigger(target, key) {
    let depsMap
    if (!bucket.get(target)) return
    depsMap = bucket.get(target)
    let effects
    if (!depsMap.get(key)) return
    effects = depsMap.get(key)
    const effectToRun = new Set()

    // 不执行当前的注册的activeEffect即可
    effects.forEach(effect => {
        if (effect !== activeEffect) {
            effectToRun.add(effect)
        }
    })
    effectToRun.forEach(effect => {
        effect()
    })
}
const obj = new Proxy(data, {
    get(target, key) {
        track(target, key);
        return target[key]
    },
    set (target, key, newVal) {
        target[key] = newVal
        trigger(target, key, newVal);
    }
})
function cleanup (effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
        const deps = effectFn.deps[i]
        deps.delete(effectFn)
    }
    effectFn.deps.length = 0
}
let activeEffect
let effectStack = []
function effect (fn)  {
    const effectFn = () => {
        cleanup(effectFn)
        activeEffect = effectFn
        effectStack.push(effectFn)
        fn()
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
    }
    effectFn.deps = []
    effectFn()
}