Vue.js设计与实现—响应式的设计与实现学习笔记(一)
副作用函数
副作用函数,就是执行之后会产生副作用的函数
const obj = { text: 'hello world' }
// effect是一个副作用函数,它执行后会修改body,
// 而其他任何函数都可以读取或写入body,
// 故effect函数的执行会直接或间接影响其他函数
function effect() {
document.body.innerText = obj.text
}
响应式数据的基本实现
Proxy实现简单响应式
// 存储副作用函数的存储桶
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, newV) {
target[key] = newV
bucket.forEach(fn => fn())
return true
}
})
// 副作用函数
function effect() {
// 读取obj.text,收集依赖
document.body.innerText = obj.text
}
// 设置obj.text,执行副作用函数
setTimeout(() => {
obj.text = 'hello vue3'
}, 2000)
存在的问题:上述代码中,我们使用了一个具名为effect的副作用函数,是不切合实际的。我们希望用户能够自定义副作用函数(具名/匿名)
用户自定义副作用函数的实现
const bucket = new Set()
// 用于存储用户注册的副作用函数
let activeEffect
// 用于注册副作用函数
function effect(fn) {
activeEffect = fn
fn()
}
const data = {text: 'hello world'}
const obj = new Proxy(data, {
get(target, key) {
if (activeEffect) {
bucket.add(activeEffect)
}
return target[key]
},
set(target, key, newV) {
target[key] = newV
bucket.forEach(fn => fn())
return true
},
})
// 注册副作用函数
effect(() => {
console.log('执行副作用函数')
document.body.innerHTML = obj.text
})
setTimeout(() => {
obj.text = 'hello vue3'
// 给obj赋一个不存在的属性值
obj.ok = true
}, 2000)
存在的问题:上述代码中,我们给obj赋了一个不存在的属性ok值,会发现副作用函数执行了。我们希望给target赋一个不存在的属性值时,不会触发副作用函数。即需要建立target-key-fn(副作用函数)明确的绑定关系
建立target-key-fn绑定关系
// 使用WeakMap替代Set
const bucket = new WeakMap()
let activeEffect
function effect(fn) {
activeEffect = fn
fn()
}
const data = { text: 'hello world' }
const obj = new Proxy(data, {
get(target, key) {
// 没有activeEffect,则return
if (!activeEffect) return target[key]
let depsMap = bucket.get(target)
if (!depsMap) {
// bucket是一个WeakMap结构,与target关联
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
// depsMap上一个Map结构,与key关联
depsMap.set(key, (deps = new Set()))
}
// deps是一个Set集合,与副作用函数关联
deps.add(activeEffect)
return target[key]
},
set(target, key, newV) {
target[key] = newV
// 从桶中取出与target关联的depsMap
let depsMap = bucket.get(target)
if (!depsMap) return
// 从depsMap中取出与key关联的Set(副作用函数集合)
const effects = depsMap.get(key)
effects && effects.forEach(fn => fn())
return true
},
})
// 注册副作用函数
effect(() => {
console.log('执行副作用函数')
document.body.innerHTML = obj.text
})
setTimeout(() => {
obj.text = 'hello vue3'
// 给obj赋一个不存在的属性值
obj.ok = true
}, 2000)
运行上述代码,我们给obj赋了一个不存在的属性ok值,副作用函数没有再执行。对上述代码做一下封装
// 使用WeakMap替代Set
const bucket = new WeakMap()
let activeEffect
function effect(fn) {
activeEffect = fn
fn()
}
const data = { text: 'hello world' }
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newV) {
target[key] = newV
trigger(target, key)
return true
},
})
function track(target, key) {
// 没有activeEffect,则return
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) {
// bucket是一个WeakMap结构,与target关联
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
// depsMap上一个Map结构,与key关联
depsMap.set(key, (deps = new Set()))
}
// deps是一个Set集合,与副作用函数关联
deps.add(activeEffect)
}
function trigger(target, key) {
// 从桶中取出与target关联的depsMap
let depsMap = bucket.get(target)
if (!depsMap) return
// 从depsMap中取出与key关联的Set(副作用函数集合)
const effects = depsMap.get(key)
effects && effects.forEach(fn => fn())
}
// 注册副作用函数
effect(() => {
console.log('执行副作用函数')
document.body.innerHTML = obj.text
})
setTimeout(() => {
obj.text = 'hello vue3'
// 给obj赋一个不存在的属性值
obj.ok = true
}, 2000)
副作用函数内分支切换
// 使用WeakMap替代Set
const bucket = new WeakMap()
let activeEffect
function effect(fn) {
activeEffect = fn
fn()
}
// 新增ok属性,用于副作用函数内分支切换
const data = { text: 'hello world', ok: true }
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newV) {
target[key] = newV
trigger(target, key)
return true
},
})
function track(target, key) {
// 没有activeEffect,则return
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) {
// bucket是一个WeakMap结构,与target关联
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
// depsMap上一个Map结构,与key关联
depsMap.set(key, (deps = new Set()))
}
// deps是一个Set集合,与副作用函数关联
deps.add(activeEffect)
}
function trigger(target, key) {
// 从桶中取出与target关联的depsMap
let depsMap = bucket.get(target)
if (!depsMap) return
// 从depsMap中取出与key关联的Set(副作用函数集合)
const effects = depsMap.get(key)
effects && effects.forEach(fn => fn())
}
// 注册副作用函数
effect(() => {
console.log('执行副作用函数')
document.body.innerHTML = obj.ok ? obj.text : 'not!'
})
setTimeout(() => {
// 修改ok属性
obj.ok = false
obj.text = 'hello vue3'
}, 2000)
存在的问题:上述代码中,我们在副作用函数内进行了分支切换,我们希望obj.ok赋值为false时,无论text属性如何变化,都不应该执行副作用函数。
但实际上还是执行了,因为text属性的依赖集合已经在初始ok为true时收集起来了,修改text后就会触发副作用函数。
我们希望在执行副作用函数前,先清除上一次关联的依赖集合,执行副作用函数时再重新收集依赖
分支切换与清除遗留副作用函数
在执行副作用函数前,先清空依赖集合,然后会在执行副作用函数的时候重新进行依赖收集。
这样当重新给ok赋值为false时,会先清空依赖集合,然后再执行副作用函数,重新进行依赖收集,根据分支切换语句,就不会再收集到text的依赖。
const bucket = new WeakMap()
let activeEffect
// 重新设计副作用函数注册器
function effect(fn) {
const effectFn = () => {
// 在执行副作用函数前,先清空依赖集合,然后会在执行副作用函数的时候重新进行依赖收集
cleanup(effectFn)
activeEffect = effectFn
fn()
}
// effectFn.deps用于存储与副作用函数相关联的依赖Set
effectFn.deps = []
effectFn()
}
const data = { text: 'hello world', ok: true }
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newV) {
target[key] = newV
trigger(target, key)
return true
},
})
function track(target, key) {
if (!activeEffect) return
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)
// 将依赖集合存储到当前激活副作用函数deps中
activeEffect.deps.push(deps)
}
function trigger(target, key) {
let depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(fn => fn())
}
// 清除遗留副作用函数集合
function cleanup(effectFn) {
for (let i = 0, len = effectFn.deps.length; i < len; i++) {
const deps = effectFn.deps[i]
// 将副作用函数从依赖集合中移除
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
// 注册副作用函数
effect(() => {
console.log('副作用函数执行了')
// 副作用函数内的分支切换
document.body.innerHTML = obj.ok ? obj.text : 'not!'
})
setTimeout(() => {
obj.ok = false
obj.text = 'hello vue3'
}, 2000)
存在的问题:上述代码,看似已经解决了分支切换的问题。运行代码发现死循环了。因为Set结构的特性,在Set forEach中先删除了一个item,再添加了删除的这个item,就会导致死循环。可以创造一个新的Set结构来遍历就可以解决这个问题。
const bucket = new WeakMap()
let activeEffect
// 重新设计副作用函数注册器
function effect(fn) {
const effectFn = () => {
// 在执行副作用函数前,先清空依赖集合,然后会在执行副作用函数的时候重新进行依赖收集
cleanup(effectFn)
activeEffect = effectFn
fn()
}
// effectFn.deps用于存储与副作用函数相关联的依赖Set
effectFn.deps = []
effectFn()
}
const data = { text: 'hello world', ok: true }
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newV) {
target[key] = newV
trigger(target, key)
return true
},
})
function track(target, key) {
if (!activeEffect) return
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)
// 将依赖集合存储到当前激活副作用函数deps中
activeEffect.deps.push(deps)
}
function trigger(target, key) {
let depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
// 用一个新的Set集合来遍历
const effectsToRun = new Set(effects)
effectsToRun && effectsToRun.forEach(effectFn => effectFn())
}
// 清除遗留副作用函数集合
function cleanup(effectFn) {
for (let i = 0, len = effectFn.deps.length; i < len; i++) {
const deps = effectFn.deps[i]
// 将副作用函数从依赖集合中移除
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
// 注册副作用函数
effect(() => {
console.log('副作用函数执行了')
// 副作用函数内的分支切换
document.body.innerHTML = obj.ok ? obj.text : 'not!'
})
setTimeout(() => {
obj.ok = false
obj.text = 'hello vue3'
}, 2000)
存在的问题:上述代码已经解决了一些问题,但在实际应用中副作用函数嵌套的场景,例如一个组件中渲染另一个组件。
const data = { text: 'hello world', ok: true, msg: '111' }
// 嵌套副作用函数
effect(function effectFn1() {
console.log('effectFn1执行')
effect(function effectFn2() {
console.log('effectFn2执行')
effect(function effectFn3() {
console.log('effectFn3执行')
obj.msg
})
obj.text
})
obj.ok
})
setTimeout(() => {
obj.ok = false
}, 2000)
我们期望重新给ok赋值时,打印出来的是‘effectFn1执行 effectFn2执行 effectFn3执行’,但实际打印出来‘effectFn3执行’。是因为原先使用activeEffect存储当前激活的函数,当发生effect嵌套时,内层会覆盖外层,所以无论set哪一个属性,都只会执行最内层的副作用函数。
可嵌套的effect
function effect(fn) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.deps = []
effectFn()
}
到此我们解决了effect嵌套的问题。还有副作用函数中同时读取和设置的情况,就会产生无限递归循环,导致栈溢出。
const data = {foo: 1}
effect(() => {
obj.foo = obj.foo + 1
})
此时我们发现track收集和trigger触发执行副作用函数都是activeEffect,故只需要增加一个trigger守卫条件:trigger触发的副作用函数与当前激活的副作用函数是否相同,相同则不执行副作用函数。
避免无限循环
function trigger(target, key) {
// 从桶中取出与target关联的depsMap
let depsMap = bucket.get(target)
if (!depsMap) return
// 从depsMap中取出与key关联的Set(副作用函数集合)
const effects = depsMap.get(key)
// 这是书中的源码
// TODO:这里为什么要遍历两遍呢?我改成下面注释的这段代码了,运行起来没什么问题。
const effectsToRun = new Set()
effects &&
effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => effectFn())
// 非书中源码
// const effectsToRun = new Set(effects)
// effectsToRun &&
// effectsToRun.forEach(effectFn => {
// if (effectFn !== activeEffect) {
// effectFn()
// }
// })
}
小结
- 了解了什么是副作用函数
- 实现了基本响应式
- 解决了分支切换,遗留副作用函数绑定问题
- 解决了副作用函数嵌套问题
- 解决了副作用函数内同时读取和设置导致无限循环栈溢出的问题