最近,我重新动手实践了《Vue.js设计与实现》这本书中关于响应式系统的部分。其中有一些细节表述可能有所瑕疵,还有代码的实现上,也有值得探讨的细节。整体瑕不掩瑜,作者循序渐进又娓娓道来的文风,让人读过之后如沐春风。
对代码而言,终归还是要实践,我会通过自己的实践版本,来阐述自己对响应式系统的理解,具体代码和书中可能有所出入,以我自己的理解为主。
响应式系统的实现过程
参考小节标题,用我自己的话总结,就是分成11个部分来讲解。
- 副作用函数的介绍
- 副作用函数和响应式数据的结合
- 解决副作用函数硬编码问题
- 解决响应式数据字段和副作用函数对应问题
- 分支切换问题
- effect嵌套问题
- 无限循环问题
- effect调度器实现
- computed的实现
- watch的实现
- 过期的副作用处理
如果觉得11个部分过于多,可以简单分组。第1点到第4点,可以实现一个相对基础的响应式系统。第5点到第7点,是解决比较难想到的细节问题。第8点到第11点,是主要是computed和watch的实现。接下来,也会按这个分组,来逐步讲解响应式系统的实现。
基础的响应式系统实现
什么是副作用函数
所谓的副作用函数,我的理解,简单来说就是会对函数之外有内容有影响的函数。比如,触发网络请求,触发控制台打印,还有更改了某个全局变量,都算是副作用。
function effect () {
console.log('hi')
}
副作用函数和响应式数据结合
所谓的副作用函数和响应式数据结合,是说当响应式数据发生变更的时候,副作用函数会触发。这里我们只做一个最简单的实现。
主要使用到Proxy,Set两个API。Proxy用于对象代理,Set数据结构用于副作用函数的收集。
const bucket = new Set
const data = { text: 'hello world' }
const obj = new Proxy(data, {
get (target, key) {
bucket.add(effect)
return target[key]
},
set (target, key, newVal) {
target[key] = newVal
bucket.forEach(fn => {
fn()
})
}
})
function effect () {
console.log(obj.text)
}
effect()
obj.text = 'changed'
// 打印结果: hello world changed
如上所示,原理很简单,就是Proxy的handler.get用于收集副作用函数,而触发变更的时候,通过Proxy的handler.set触发收集的副作用函数。所以当obj的text变更的时候,会触发effect函数,打印最新的obj.text。
解决副作用函数硬编码问题
副作用函数和响应式数据结合的实现中,可以看到,handler.get收集副作用函数的时候,是硬编码收集effect函数。为了处理硬编码的问题,我们可以通过实现一个注册副作用函数的函数来解决,同时用全局变量activeEffect来存储注册的副作用函数,供handler.get收集。
const bucket = new Set()
const data = { text: 'hello world' }
const obj = new Proxy(data, {
get(target, key) {
if (activeEffect) {
bucket.add(activeEffect)
}
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
bucket.forEach(fn => {
fn()
})
}
})
let activeEffect
function effect (fn) {
activeEffect = fn
fn()
}
effect(() => {
console.log(obj.text)
})
obj.text = 'changed'
// 打印结果: hello world changed
解决响应式数据字段和副作用函数对应问题
解决完硬编码问题,还需要考虑响应式数据属性变化,和副作用函数一一对应的问题。现在的实现,还不能做到这一点。任何属性的变更,都会触发和这个响应式数据相关的副作用函数触发。因为只有一个Bucket统一管理所有的副作用。
为了解决这个问题,我们需要重新设计bucket的数据结构,使其支持对应多个响应式数据,且每个副作用函数,都要只是对应上相应的响应式数据属性,只有对应的属性变更,才会触发和这个属性有关联的副作用函数。
可以用WeakMap,Map,Set三种数据结构来设计。WeakMap下面,每一个key是一个响应式数据,value是一个Map,Map里面存储该响应式数据下面属性和对应的副作用Set。
const bucket = new WeakMap()
const data = { text: 'hello world', name: 'Mike' }
const obj = new Proxy(data, {
get(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)
}
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
let depsMap
if (!bucket.get(target)) return
depsMap = bucket.get(target)
let deps
if (!depsMap.get(key)) return
deps = depsMap.get(key)
deps = depsMap.get(key)
deps.forEach(fn => fn())
}
})
let activeEffect
function effect (fn) {
activeEffect = fn
fn()
}
effect(() => {
console.log(obj.text)
})
obj.text = 'changed'
obj.name = 'John'
// 打印结果: hello world changed
经过改造,可以看到,即使obj.name变更,也不会触发到只涉及obj.text的副作用函数触发。
bucket之所以要用WeakMap,是可以更加便捷解除没有引用的依赖,当响应式对象没有引用的时候,会被垃圾回收。如果没有用WeakMap而使用Map,这个回收的动作,就需要手动实现,不方便。