往期回顾
- 系列开篇与响应式基本实现
- effect 函数注册副作用
- 建立副作用函数与被操作字段之间的联系
- 封装 track 和 trigger 函数
- 分支切换与 cleanup
- 嵌套的 effect 与 effect 栈
- 避免无限递归循环
- 调度执行
- 懒执行的 effect
- 计算属性与缓存
- 计算属性的 track 和 trigger
- watch 的基本实现原理
- 立即执行的 watch 与回调执行时机
- 竞态问题与过期的副作用
阶段性总结一:基础的响应式系统
成果展示
在这一阶段中,我们着重讨论了响应系统的概念与实现,并简单介绍了响应式数据的基本原理。
至此,我们已经完全实现了基础的响应式系统,欣赏一下完整代码吧:
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开始一步步实现完整的响应式系统的:
- 首先,我们使用 Proxy 拦截对象属性的读和写,实现了最基础的响应式原理:数据变化时,依赖了数据的函数自动重新运行。
- 新增 effect 函数用来注册副作用函数,可以正确地收集任何名字甚至是匿名的副作用函数。
- 使用 WeakMap 套 Map 套 Set 的数据结构,建立起了副作用函数与被操作字段之间的关联。
- 单独封装了用于依赖收集的 track 和用于触发更新的 trigger 函数,提升灵活性与可扩展性。
- 通过每次执行副作用函数时重新进行一次依赖收集,解决了分支切换产生遗留副作用函数的问题。
- 引入 effect 栈以支持 effect 函数的嵌套调用。
- 通过在每次触发更新前判断移除当前正激活的副作用函数,解决了无限递归循环问题。
- 为 effect 函数新增 options 参数,并通过其 scheduler 属性实现了副作用函数的调度执行。后续的 computed 和 watch 正是基于此功能才得以实现。
- 用 options 参数的 lazy 属性实现了懒执行的 effect,并且能够在外部手动调用副作用函数获取到返回值。
- 基于前两节实现的功能,我们初步实现了 computed,并且带有缓存功能。
- 通过在 computed 内手动调用 track 和 trigger,为 computed 实现了和普通响应式数据一样的依赖收集和触发更新功能。
- 基于 options 参数的 scheduler 和 lazy 属性,实现了基本的 watch,支持监听对象的任意属性变化,支持接收 getter 函数,回调函数中能获取到新值和旧值。
- 进一步实现了 watch 的两个特性:立即执行的 watch 和回调执行时机。
- 通过为 watch 的回调函数新增 onInvalidate 参数,实现了在回调中注册清理函数以清理过期副作用的功能,解决了竞态问题。
下一阶段
下一阶段中,我们将把目光聚焦在响应式数据本身,深入探讨实现响应式数据都需要考虑哪些内容,其中的难点又是什么。
实际上,实现完整的响应式数据要比想象中难很多,并不是像我们目前这样,单纯地拦截 get/set 操作即可。如何拦截 for...in 循环?如何代理数组、Map、Set 甚至 WeakMap 和 WeakSet?解决这些问题,需要深入 ECMAScript 语言的规范。