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

53 阅读4分钟

上两篇文章,分别总结了响应式系统的基础实现和细节问题处理。接下来还剩下两个重点实现需要总结,一是围绕computed相关的实现原理,二是围绕watch相关的实现。先讲computed是因为,watch实现有部分基于computed的实现。先看computed部分。

computed相关实现

调度器实现

所谓调度器,是指控制副作用函数如何执行的函数。比如,可以让副作用函数异步执行。通过effect函数中增加一个options对象参数,来传递scheduler调度器。

核心实现主要涉及两个地方的修改,一个是调度器函数在effect中的传参,一个是调度器函数在handler.setter中的执行。

修改代码如下:

function effect (fn, options = {})  {
    const effectFn = () => {
        cleanup(effectFn)
        activeEffect = effectFn
        effectStack.push(effectFn)
        fn()
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
    }
    effectFn.deps = []
    // 增加options
    effectFn.options = options
    effectFn()
}

function trigger(target, key) {

    ...
    
    effectToRun.forEach(effect => {
        if (effect.options.scheduler) {
            effect.options.scheduler(effect)
        } else {
            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()

        effects.forEach(effect => {
        if (effect !== activeEffect) {
            effectToRun.add(effect)
        }
    })
    effectToRun.forEach(effect => {
        if (effect.options.scheduler) {
            effect.options.scheduler(effect)
        } else {
            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, options = {})  {
    const effectFn = () => {
        cleanup(effectFn)
        activeEffect = effectFn
        effectStack.push(effectFn)
        fn()
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
    }
    effectFn.deps = []
    // 增加options
    effectFn.options = options
    effectFn()
}

effect(() => {
    console.log(obj.text)
}, {
    scheduler: effect => setTimeout(effect)
})

经过改造,effect已经实现了调度器功能。这个调度器的实现,是后续很多computed功能实现的基础。

lazy实现

为了减少不必要的副作用执行,保证可以懒执行副作用函数,在实现computed之前,我们着手实现lazy。

lazy功能的修改主要在两个地方,一是effect函数,lazy的情况下,要返回一个副作用函数引用,而非执行副作用函数。二是为了lazy执行的副作用函数,可以直接获取getter的返回值(getter作为副作用函数),要在effectFn中把执行结果返回。

直接看代码如下:

function effect (fn, options = {})  {
    const effectFn = () => {
        let res
        cleanup(effectFn)
        activeEffect = effectFn
        effectStack.push(effectFn)
        res = fn()
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
        return res // 增加返回值
    }
    effectFn.deps = []
    effectFn.options = options

    if (!effectFn.options.lazy) {
        effectFn()
    }
    return effectFn // lazy的情况下,返回effectFn,而非执行
}

经过这样的修改,支持lazy的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()

    effects.forEach(effect => {
        if (effect !== activeEffect) {
            effectToRun.add(effect)
        }
    })
    effectToRun.forEach(effect => {
        if (effect.options.scheduler) {
            effect.options.scheduler(effect)
        } else {
            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, options = {})  {
    const effectFn = () => {
        let res
        cleanup(effectFn)
        activeEffect = effectFn
        effectStack.push(effectFn)
        res = fn()
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
        return res
    }
    effectFn.deps = []
    effectFn.options = options

    if (!effectFn.options.lazy) {
        effectFn()
    }
    return effectFn
}

computed基础实现

computed基础实现非常简单,就是通过一个lazy的effect函数,作为一个对象的value值,并返回这个对象。

function computed (getter) {
    const effectFn = effect(getter, { lazy: true })
    const obj = {
        get value() {
            return effectFn()
        }
    }
    return obj
}

由于computed实现的代码非常清晰简单,且暂时不涉及其他部分代码,这里就不列出来。

computed增加缓存

为了给computed增加缓存,需要用一个dirty布尔值来标志是否需要重新计算。因此,dirty的变更时机就是缓存实现的重点。当计算了第一次的时候,dirty变更为false;当副作用函数被响应式数据通知变更的时候,dirty要恢复为truedirty恢复为true的实现,就依赖最初实现的调度器。

这部分代码也比较独立,直接看computed代码:

function computed (getter) {
    let value
    let dirty = true
    const effectFn = effect(getter,
      { lazy: true,
        scheduler() { dirty = true } // dirty恢复为true
      })
    const obj = {
        get value() {
            if (dirty) {
                value = effectFn()
                dirty = false // 计算完之后,闭包缓存value,dirty为false
            }
            return value
        }
    }
    return obj
}

computed的值不会触发依赖收集

最后解决computed不会触发依赖收集的问题。

问题代码如下:

const sumRes = computed(() => obj.text)

effect(() => {
    console.log(sumRes.value)
})

当obj.text变更的时候,console.log(sumRes.value)并不会执行。原因也毕竟清晰,因为sumRes本质是一个没有被经过Proxy代理的对象,并没有被bucket收集。为了解决这个问题,可以手动触发依赖的收集和触发。

function computed (getter) {
    let value
    let dirty = true
    const effectFn = effect(getter, {
        lazy: true,
        scheduler() {
            dirty = true
            trigger(obj, 'value') // 手动trigger
        }
    })
    const obj = {
        get value() {
            if (dirty) {
                value = effectFn()
                dirty = false
            }

            track(obj, 'value') // 手动track
            return value
        }
    }
    return obj
}

经过上面的修复,这个问题就解决了,computed的实现也完成。简单总结一下computed的实现过程,首先是调度器和lazy的实现作为铺垫,后面实现了computed和缓存的功能,最后解决了computed内部对象的依赖收集和触发的问题。