Vue.js 3 渐进式实现之响应式系统——阶段性总结一:基础的响应式系统

93 阅读4分钟

往期回顾

  1. 系列开篇与响应式基本实现
  2. effect 函数注册副作用
  3. 建立副作用函数与被操作字段之间的联系
  4. 封装 track 和 trigger 函数
  5. 分支切换与 cleanup
  6. 嵌套的 effect 与 effect 栈
  7. 避免无限递归循环
  8. 调度执行
  9. 懒执行的 effect
  10. 计算属性与缓存
  11. 计算属性的 track 和 trigger
  12. watch 的基本实现原理
  13. 立即执行的 watch 与回调执行时机
  14. 竞态问题与过期的副作用

阶段性总结一:基础的响应式系统

成果展示

在这一阶段中,我们着重讨论了响应系统的概念与实现,并简单介绍了响应式数据的基本原理。

至此,我们已经完全实现了基础的响应式系统,欣赏一下完整代码吧:

let activeEffect

const effectStack = []

function effect(fn, options = {}) {
    const effectFn = () => {
        cleanup(effectFn)

        activeEffect = effectFn
        effectStack.push(effectFn)

        const res = fn()

        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]

        return res
    }

    effectFn.options = options
    effectFn.deops = []

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

    return effectFn
}

function cleanup(effectFn) {
    effectFn.deps.forEach( deps => {
        deps.delete(effectFn)
    })

    effectFn.deps.length = 0
}

const bucket = new WeakMap()

const data = { text1: 'text1', text2: 'text2' }

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

    set(target, key, newVal) {
        target[key] = newVal
        trigger(target, key)
        return true
    }
})

function track(target, key) {
    if (!activeEffect) return target[key]

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

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

    deps.add(activeEffect)
    activeEffect.deps.push(deps)
}

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

    const effects = depsMap.get(key)
    const effectsToRun = new Set()

    effects?.forEach(effectFn => {
        if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn)
        }
    })

    effectsToRun.forEach(effectFn => {
        if (effectFn.options.scheduler) {
            effectFn.options.scheduler(effectFn)
        } else {
            effectFn()
        }
    })
}

function computed(getter) {
    let value
    let dirty = true

    const effectFn = effect(getter, {
        lazy: true,
        scheduler() {
            dirty = true
            trigger(obj, 'value')
        }
    })

    const obj = {
        get value() {
            if (dirty) {
                value = effectFn()
                dirty = false
            }

            track(obj, 'value')
            return value
        }
    }
}

function watch(source, cb, options = {}) {
    let getter
    if (typeof source === 'function') {
        getter = source
    } else {
        getter = () => traverse(source)
    }

    let oldValue, newValue

    let cleanup
    function onInvalidate(fn) {
        cleanup = fn
    }

    const job = () => {
        newValue = effectFn()
        if (cleanup) {
            cleanup()
        }

        cb(newValue, oldValue, onInvalidate)
        oldValue = newValue
    }

    const effectFn = effect(
        () => getter(),
        {
            lazy: true,
            scheduler: () => {
                if (options.flush === 'post') {
                    const p = Promise.resolve()
                    p.then(job)
                } else {
                    job()
                }
            }
        }
    )

    if (options.immediate) { 
        job()
    } else {
        oldValue = effectFn()
    }
}

function traverse(value, seen = new Set()) {
    if (typeof value !== 'object' || value === null || seen.has(value)) {
        return
    }

    seen.add(value)
    
    for (const key in value) {
        traverse(value[key], seen)
    }

    return value
}

完整实现步骤

回顾一下我们是如何从0开始一步步实现完整的响应式系统的:

  1. 首先,我们使用 Proxy 拦截对象属性的读和写,实现了最基础的响应式原理:数据变化时,依赖了数据的函数自动重新运行。
  2. 新增 effect 函数用来注册副作用函数,可以正确地收集任何名字甚至是匿名的副作用函数。
  3. 使用 WeakMap 套 Map 套 Set 的数据结构,建立起了副作用函数与被操作字段之间的关联。
  4. 单独封装了用于依赖收集的 track 和用于触发更新的 trigger 函数,提升灵活性与可扩展性。
  5. 通过每次执行副作用函数时重新进行一次依赖收集,解决了分支切换产生遗留副作用函数的问题。
  6. 引入 effect 栈以支持 effect 函数的嵌套调用。
  7. 通过在每次触发更新前判断移除当前正激活的副作用函数,解决了无限递归循环问题。
  8. 为 effect 函数新增 options 参数,并通过其 scheduler 属性实现了副作用函数的调度执行。后续的 computed 和 watch 正是基于此功能才得以实现。
  9. 用 options 参数的 lazy 属性实现了懒执行的 effect,并且能够在外部手动调用副作用函数获取到返回值。
  10. 基于前两节实现的功能,我们初步实现了 computed,并且带有缓存功能。
  11. 通过在 computed 内手动调用 track 和 trigger,为 computed 实现了和普通响应式数据一样的依赖收集和触发更新功能。
  12. 基于 options 参数的 scheduler 和 lazy 属性,实现了基本的 watch,支持监听对象的任意属性变化,支持接收 getter 函数,回调函数中能获取到新值和旧值。
  13. 进一步实现了 watch 的两个特性:立即执行的 watch 和回调执行时机。
  14. 通过为 watch 的回调函数新增 onInvalidate 参数,实现了在回调中注册清理函数以清理过期副作用的功能,解决了竞态问题。

下一阶段

下一阶段中,我们将把目光聚焦在响应式数据本身,深入探讨实现响应式数据都需要考虑哪些内容,其中的难点又是什么。

实际上,实现完整的响应式数据要比想象中难很多,并不是像我们目前这样,单纯地拦截 get/set 操作即可。如何拦截 for...in 循环?如何代理数组、Map、Set 甚至 WeakMap 和 WeakSet?解决这些问题,需要深入 ECMAScript 语言的规范。