4. 响应系统的作用与实现
4.1 响应式数据与副作用函数
什么是副作用函数
函数的执行会直接或间接影响其他函数的执行,这时我们说函数产生了副作用。比如修改了全局变量
// 全局变量
let val = 1
function effect() {
val = 2 // 修改全局变量,产生副作用
}4.2 响应式数据的基本实现
现在我们有个effect函数,我们希望obj.text改变的时候,会自动重新运行effect函数,达到响应式数据更新的目标
// 原始数据
const data = { text: 'hello world' }
function effect() {
document.body.innerText = data.text
}现在的两个重点操作就是
- 在读取
data.text的值的时候,把effect函数收集起来放在一个桶里面 - 在设置
data.text的值的时候,即值被重新赋值了,执行桶里面的effect函数
转换成代码就是
// 存储副作用函数的桶
const bucket = new Set()
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 effect 添加到存储副作用函数的桶中
bucket.add(effect)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal
// 把副作用函数从桶里取出并执行
bucket.forEach(fn => fn())
}
})4.3 设计一个完善的响应系统
从上面不难看出,目前有很多缺陷
代理中的
get,硬编码取了全局的effect函数,不够灵活改造一下
effect函数,新增一个参数接收副作用函数,新建一个全局变量activeEffect来记录当前活动的副作用函数js// 用一个全局变量存储当前激活的 effect 函数 let activeEffect function effect(fn) { // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect activeEffect = fn // 执行副作用函数 fn() }副作用函数没有和具体的
key所关联,只要改变了obj的任意一个key,都会触发副作用函数的执行假设原始对象为
target,target对象有多个key,然后每一个key对应有副作用函数effect先把
bucket的类型更改为WeakMap,以target作为WeakMap的key,而他的value是一个map结构使用
WeakMap,因为利于垃圾回收,WeakMap对于key的引用是弱引用,当target不存在时候,就会进行垃圾回收上面的
Map结构我们命名为depsMap,他的key是原始对象target的属性,而他的value就是和这个key所关联的副作用函数集合**(Set结构,目的是去重)**,命名为deps

- 为了优化代码,应该封装两个函数
track和trigger。在get中追踪(track)依赖;在set中触发(trigger)依赖
代码如下
js// 原始数据 const data = { text: 'hello world' } // 对原始数据的代理 const obj = new Proxy(data, { // 拦截读取操作 get(target, key) { // 将副作用函数 activeEffect 添加到存储副作用函数的桶中 track(target, key) // 返回属性值 return target[key] }, // 拦截设置操作 set(target, key, newVal) { // 设置属性值 target[key] = newVal // 把副作用函数从桶里取出并执行 trigger(target, key) } }) function track(target, key) { let depsMap = bucket.get(target) if (!depsMap) { bucket.set(target, (depsMap = new Map())) } let deps = depsMap.get(key) if (!deps) { depsMap.set(key, (deps = new Set())) } deps.add(activeEffect) } function trigger(target, key) { const depsMap = bucket.get(target) if (!depsMap) return const effects = depsMap.get(key) effects && effects.forEach(fn => fn()) } // 用一个全局变量存储当前激活的 effect 函数 let activeEffect function effect(fn) { // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect activeEffect = fn // 执行副作用函数 fn() } effect(() => { console.log('effect run') document.body.innerText = obj.text })
4.4 分支切换与 cleanup
所谓分支切换,是指有条件影响哪部分代码的执行,比如一个三元表达式
effect(() => {
console.log('effect run')
document.body.innerText = obj.ok ? obj.text : 'not'
})可以看到,副作用函数 分别被字段 data.ok 和字段 data.text 所对应的依赖集合收集。
但当字段 obj.ok 的值修改为 false,并触发副作用函数重新执行后,不应该被字段 obj.text 所对应的依赖集合收集
目前我们没有做到这一点,解决方案也很简单:每次副作用函数执行时,我们可以先把它从所有与之关联的依赖集合中删除
cleanup
副作用函数要明确知道哪些依赖集合关联到它
重构一下
effect函数,新建effectFn函数来做之前的事情,在effectFn函数上新增一个数组,来储存当前副作用函数的依赖集合deps因为函数本质上也是一个对象
jsfunction effect(fn) { const effectFn = () => { // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect activeEffect = effectFn fn() } // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合 effectFn.deps = [] // 执行副作用函数 effectFn() }上面只是初始化了数组,具体要在
track的时候收集jsfunction track(target, key) { let depsMap = bucket.get(target) if (!depsMap) { bucket.set(target, (depsMap = new Map())) } let deps = depsMap.get(key) if (!deps) { depsMap.set(key, (deps = new Set())) } // 当前key所关联的副作用函数 deps.add(activeEffect) // 收集当前副作用函数关联的依赖集合 activeEffect.deps.push(deps) }
