2.1.响应系统 响应系统的作用与实现
响应系统的作用与实现
微型响应式系统
- 副作用函数:执行会直接或间接的影响其他函数的执行。
- 响应式数据:当目标数据改变之后,和这个数据绑定的副作用函数自动执行。通过拦截对象属性的读写,可以实现响应式数据。Vue3 使用
Proxy
来实现对象属性的拦截。
响应式数据最简单的例子:
HTML |
---|
| <div id="app"></div>
<script>
const app = document.getElementById('app');
// 定义包含函数的“桶”,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, value) {
target[key] = value;
// 执行函数
bucket.forEach(fn => fn())
return true
}
})
// 数据变化后应该执行的函数
function effect() {
app.innerText = obj.text
}
effect()
setTimeout(() => {
obj.text = 'Hello world 2'
}, 1000)
</script>
|
实现匿名函数
上面的代码有一个主要问题:我们硬编码了副作用函数名字(effect),我们可以改进上述代码,通过一个匿名函数的形式来实现这个功能:
JavaScript |
---|
| // 创建一个变量存储被注册的副作用函数
let activeEffect
// effect 用于注册副作用函数
function effect(fn) {
// 注册副作用函数
activeEffect = fn
// 执行副作用函数
fn()
}
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())
return true
}
})
// 定义匿名的副作用函数
effect(()=>{
console.log('effect run')
document.body.innerText = obj.text
})
setTimeout(() => {
// 这里有个问题,副作用函数中并没有读取 notExist 属性值,但副作用和函数仍然执行了
obj.notExist = 'hello, vue3'
}, 1000)
|
副作用函数与被操作的字段之间的联接
我们没有在副作用函数与被操作的目标字段间建立明确的联系,所以当更新其他字段时,副作用函数仍被执行了。要解决这个问题, 就不能使用 Set 数据结构,而要改为 Map,由于 WeakMap 可以更好的回收不用的内存(弱引用),我们使用 WeakMap。
- WeakMap
- Map
target
(对象:data
)
- Set
key
(该对象的键:text
)
- Function
activeEffect
(副作用函数:匿名函数)
- Function
- ...
- Set
- Map
- ...
JavaScript |
---|
| let activeEffect
function effect(fn) {
activeEffect = fn
fn()
}
// 使用 WeakMap 构建树状数据结构
const bucket = new WeakMap()
const data = {
text: 'Hello world',
}
const obj = new Proxy(data,{
get(target, key) {
if (!activeEffect) {
return target[key]
}
// 取得键为目标对象的 Map,如果不存在就创建
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
// 取得当前对象操作的key的副作用函数 Set 桶,不存在就创建
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
// 向桶中添加副作用函数
deps.add(activeEffect)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
// 尝试取出副作用函数并执行
const depsMap = bucket.get(target)
if (!depsMap) {
return
}
const effects = depsMap.get(key)
effects && effects.forEach(fn=>fn())
}
})
effect(()=>{
console.log('effect run')
document.body.innerText = obj.text
})
// 现在就不会有重复执行问题了
setTimeout(()=>{
obj.notExist = 'hello, vue3'
}
, 1000)
|
分支切换与cleanup
什么是分支切换:
JavaScript |
---|
| const data = {
ok: false,
text: 'Hello world',
}
effect(()=>{
console.log('effect run')
// 这个三元表达式会造成分支切换,即 obj.ok 的值发生变化时,代码执行的分支会跟着变化。
// 分支切换可能会造成遗留的副作用函数。
// 当 obj.ok 由 true 设置为 false 后,再次更新 obj.text 仍然会造成不必要的更新,
// 我们期望的效果是:当 obj.ok 为 false 时,不对 obj.text 的变化进行更新
document.body.innerText = obj.ok ? obj.text : 'not'
})
|
要解决这个问题, 我们可以在每次副作用函数执行之前,将其从相关联的依赖集合中移除。
- WeakMap
- Map
data
- Set
ok
= true
- Set
text
= "hello world"
- Function
activeEffect
- Array
deps
= [Set, Set]
(这两个 activeEffect
是相同的)
分支切换与 cleanup:
JavaScript |
---|
| let activeEffect
function effect(fn) {
const effectFn = ()=>{
cleanup(effectFn)
// 当 effectFn 执行时,将其设置为激活的副作用和函数
activeEffect = effectFn
fn()
}
// 在函数上挂载一个数组,用来存储所有与该副作用函数相关联的依赖 Set
effectFn.deps = []
effectFn()
}
// 清除副作用函数避免不必要的更新
function cleanup(effectFn) {
// console.warn('cleanup', [...effectFn.deps])
// 遍历依赖数组
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
// 从依赖集合中删除该函数
deps.delete(effectFn)
}
// 重置数组
effectFn.deps.length = 0
}
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)
// console.log('getter track', key, deps)
// deps 是一个与当前副作用函数关联的 Set
// 添加到函数的 deps 数组中
activeEffect.deps.push(deps)
}
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) {
return
}
const effects = depsMap.get(key)
// 创建 Set 的副本,防止无限循环
const effectToRun = new Set(effects)
effectToRun.forEach(effectFn=>effectFn())
}
const bucket = new WeakMap()
const data = {
ok: true,
text: 'Hello world',
}
const obj = new Proxy(data,{
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
trigger(target, key)
}
})
effect(()=>{
console.log('effect run')
document.body.innerText = obj.ok ? obj.text : 'not'
}
)
// 更新值
setTimeout(()=>{
console.log('set text 1')
obj.text = 1
}
, 1000)
setTimeout(()=>{
console.log('set ok false')
obj.ok = false
}
, 2000)
setTimeout(()=>{
console.log('set text 2')
// 此处不触发更新,很好。
obj.text = 2
}
, 3000)
|
副作用函数的嵌套
副作用函数是可以嵌套的,执行顺序是 effectFn1, effectFn2
JavaScript |
---|
| const data = {
foo: true,
bar: true
}
// const obj = new Proxy(data,{...
let temp1, temp2
effect(function effectFn1() {
console.log('effectFn1 executed')
effect(function effectFn2() {
console.log('effectFn2 executed')
temp2 = obj.bar
})
temp1 = obj.foo
})
|
但是,当我们修改 obj.foo
的值,期望执行的是 effectFn1,实际上却执行的是 effectFn2
JavaScript |
---|
| setTimeout(()=>{
obj.foo = false
}
, 1000)
// effectFn2 executed
|
原因是:当副作用函数发生嵌套时,内层副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的值。
只需要将函数保存在栈中就可以控制执行顺序:
JavaScript |
---|
| let activeEffect
const effectStack = [] // 副作用函数栈
function effect(fn) {
const effectFn = ()=>{
cleanup(effectFn)
activeEffect = effectFn
// 将当前副作用函数压入栈中
effectStack.push(effectFn)
fn()
// 在副作用函数执行完后将其弹出
effectStack.pop()
// 还原为之前的值
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.deps = []
effectFn()
}
|
避免无限递归
JavaScript |
---|
| effect(function effectFn1() {
obj.foo++
})
|
这个语句会造成无限递归,原因是 foo++
既进行了读又进行了写操作。读的时候副作用函数正在执行中,还没执行完毕就开始下一次执行,导致无限递归调用。
通过简单的修改 trigger
方法可以解决无限递归:
JavaScript |
---|
| function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) {
return
}
const effects = depsMap.get(key)
const effectToRun = new Set()
effects && effects.forEach(effectFn => {
// 只有在当前副作用函数与要执行的函数不同的情况下,才将其加入要执行的函数桶中
if (effectFn !== activeEffect) {
effectToRun.add(effectFn)
}
})
effectToRun.forEach(effectFn => effectFn())
}
|
调度器
通过给开放调度器配置, 可以指定函数的执行时机或次数
JavaScript |
---|
| function effect(fn, options={}) {
const effectFn = ()=>{
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
// 挂载配置
effectFn.options = options
effectFn.deps = []
effectFn()
}
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) {
return
}
const effects = depsMap.get(key)
const effectToRun = new Set()
effects && effects.forEach(effectFn=>{
if (effectFn !== activeEffect) {
effectToRun.add(effectFn)
}
})
effectToRun.forEach(effectFn=>{
// 如果有调度器,则执行调度器
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
}
|
执行以下代码,成功改变了 effectFn
的执行顺序,让其在全部代码执行结束之后执行。也可以在调度器中自行实现队列、节流、防抖等功能,以优化大量数据变化时的性能。
JavaScript |
---|
| const data = {
foo: 1,
}
// const obj = new Proxy(data,{ ...
effect(()=>{
console.log(obj.foo)
}
, {
scheduler(fn) {
setTimeout(fn)
}
})
obj.foo++
console.log('end.')
// 1
// end.
// 2
|
使用防抖函数优化执行性能:JS 的节流与防抖函数
JavaScript |
---|
| // 包装防抖函数
const debouncedFn = debounce((fn) => {
fn()
}, 100)
effect(()=>{
console.log(obj.foo)
}
, {
// 使用防抖函数作为调度器
scheduler: fn => debouncedFn(fn)
})
// 这里执行了3次,但调度器执行的防抖函数的回调只执行1次
obj.foo++
obj.foo++
obj.foo++
console.log('end.')
// 1
// end.
// 4
|
任务队列
JavaScript |
---|
| // 定义一个任务队列
const jobQueue = new Set()
// 创建一个Promise实例,用它将任务添加到微任务队列
const p = Promise.resolve()
// 是否正在刷新队列
let isFlushing = false
function flushJob() {
// 如果正在刷新,不进行下一步操作
if (isFlushing) {
return
}
isFlushing = true
// 执行微任务队列
p.then(() => {
jobQueue.forEach(job => job())
}).finally(() => {
isFlushing = false
})
}
function print() {
console.log('boom')
}
function scheduler(fn) {
jobQueue.add(fn)
flushJob()
}
// 无论调度执行多少次,回调函数只执行一次
for (let index = 0; index < 10; index++) {
scheduler(print)
}
|
计算属性computed与lazy
2.1.x 计算属性computed与lazy
watch的实现原理
在#计算属性computed与lazy的基础上增加watch函数
JavaScript |
---|
| 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.deps = []
// 只有在不为lazy的时候立即执行
if (!options.lazy) {
effectFn()
}
return effectFn
}
// 清除副作用函数避免不必要的更新
function cleanup(effectFn) {
// console.warn('cleanup', [...effectFn.deps])
// 遍历依赖数组
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
// 从依赖集合中删除该函数
deps.delete(effectFn)
}
// 重置数组
effectFn.deps.length = 0
}
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)
// console.log('getter track', key, deps)
// deps 是一个与当前副作用函数关联的 Set
// 添加到函数的 deps 数组中
activeEffect.deps.push(deps)
}
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) {
return
}
const effects = depsMap.get(key)
const effectToRun = new Set()
effects && effects.forEach(effectFn=>{
if (effectFn !== activeEffect) {
effectToRun.add(effectFn)
}
}
)
effectToRun.forEach(effectFn=>{
// 如果有调度器,则执行调度器
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
}
)
}
const bucket = new WeakMap()
const data = {
foo: 1,
bar: 1,
}
const obj = new Proxy(data,{
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
trigger(target, key)
}
})
function watch(source, cb, options={}) {
// 递归遍历读取对象的所有属性
function traverse(value, seen=new Set()) {
// 如果是原始值或null或已经读取过了,什么都不做
if (typeof value !== 'object' || value === null || seen.has(value)) {
return
}
// 添加到已读取的集合中,避免死循环
seen.add(value)
// 暂时不考虑数组的情况
for (const k in value) {
// 读取
traverse(value[k], seen)
}
}
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()
// 在调用回调函数cb之前,先调用过期回调
if (cleanup) {
cleanup()
}
// onInvalidate设置用户的过期回调函数
cb(newValue, oldValue, onInvalidate)
// 更新旧值
oldValue = newValue
}
const effectFn = effect(()=>getter(), {
lazy: true,
scheduler: ()=>{
// 判断flush是否为post,如果是,将job放到微任务中执行
if (options.flush === 'post') {
const p = Promise.resolve()
p.then(job)
} else {
job()
}
}
})
if (options.immediate) {
job()
} else {
oldValue = effectFn()
}
}
watch(()=>obj.foo, (nv,ov)=>{
console.log('obj 更新了', nv, ov)
}
, {
immediate: true,
// flush: 'post'
})
obj.foo++
console.log('end')
|