第四章 响应式系统的作用于实现
4.1 响应式数据与副作用函数
effect
函数的执行会直接或间接影响其他函数的执行,这时我们说effect
函数产生了副作用。- 修改了全局变量
- 所谓的响应式,修改
obj.text
的值,同时希望副作用函数会重新执行
const obj = { text: 'hello world' }
function effect(){
// effect函数的执行会读取 obj.text
document.body.innerText = obj.text
}
obj.text = 'hello vue3'
4.2 响应式数据的基本实现
- 响应式基本实现
- 当副作用函数
effect
执行时,会触发字段obj.text
的读取操作。 - 当修改
obj.text
的值时,会触发字段obj.text
的设置操作。
- 当副作用函数
- 实现思路
- 创建桶
- 读取时候将
effect
放入桶 - 设置时候将
effect
拿出桶执行 - Proxy来代理对象,从而实现最简单的响应式
// 存储副作用函数的桶
const bucket = new Set()
// 原始数据
const data = { text: 'hello world' }
// 对原始数据的代理
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())
// 返回 true代表设置操作成功
return true
}
})
- 缺陷:我们直接通过名字 effect 来获取副作用函数,这中硬编码方式很不灵活。
4.3 设计一个完善的响应式系统
- 解决上一节缺陷:首先解决 我们传入函数名字自定义问题
- 解决方法: 用一个全局变量来存储被注册的副作用函数
// 用一个全局变量存储被注册的副作用函数
let activeEffect
// effect 函数用来注册副作用函数
function effect(fn){
// 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
activeEffect = fn
// 执行副作用函数
fn()
}
- 进而,可以将一个匿名的副作用函数按照如下传入
effect
使用
effect(() => {
// 一个匿名的副作用函数
() => {
document.body.innerText = obj.text;
}
})
- 如此,我们对
Proxy
代理的对象进行修改
const obj = new Proxy(data,{
get(target,key){
// 将activeEffect 中存储的副作用函数收集到桶中
if(activeEffect){ //新增
bucket.add(activeEffect)//新增
}//新增
return target[key]
},
set(target,key,newValue){
target[key] = newVal
bucket.forEach(fn => fn())
return true
}
})
- 如此以来,我么发现函数在小部分情况下能正常运行,但是如果我们对函数稍作测试。比如在
obj
上设置一个不存在的属性时。请看如下代码: - 从代码中可以看出,在匿名副作用函数内部读取字段
obj.text
值,这时匿名副作用函数与字段obj.text
之间会建立响应联系。然而,obj.notExist
这个不存在的属性被延迟访问时,并没有和匿名副作用函数之间建立响应联系。但是,这个副作用函数同样被执行了(这是我们不愿意看到的)。
effect(() => {
// 匿名副作用函数
() => {
console.log('effect run') // 会打印两次
document.body.innerText = obj.text
}
})
setTimeout(() => {
// 副作用函数并没有读取 notExist 属性的值
obj.notExist = 'hello vue3'
},1000)
- 解决上述问题: 只需要在副作用函数与被操作的目标字段之间建立明确的关系。
- 思考下数据结构
- 被操作(读取)的代理对象obj
- 被操作(读取)的字段名text
- 使用
effect
函数注册的副作用函数effectFn
target
|
|_________ key
|
| _________ effectFn
- 咱们先看实现结果的数据结构图示:
- 再三考虑,我们使用
WeakMap来代替旧桶
,用Map
来存储key
,用Set
来存储副作用函数effect
// 存储桶 存储副作用函数的桶
const bucket = new WeakMap()
const obj = new Proxy(data, {
// 拦截读取操作
get(target,key){
// 没有 activeEffect,直接 return
if(!activeEffect) return
// 根据 target 从 ”桶“ 中取得 depsMap,它也是一个 Map 类型, key ---> effect
let depsMap = bucket.get(target)
// 如果不存在 depsMap ,那么新建一个Map与 target 关联上
if(!depsMap) {
bucket.set(target,(depsMap = new Map()))
}
// 再根据 key 从 depsMap 中取得 deps, 它是一个 Set 类型
// 里面存储着所有与当前key想关联的副作用函数: effects
let deps = depsMap.get(key)
if(!deps){
// 如果 deps 不存在, 新建一个 Set 并与 key关联上
depsMap.set(key,(deps = new Set()))
}
// 最后将当前激活的副作用函数添加到桶中
deps.add(activeEffect)
return target[key]
},
// 拦截设置操作
set(target,key,newValue){
// 设置属性值
target[key] = newVal
// 根据 target 从同种取得 depsMap,它是key ---> effects
const depsMap = bucket.get(target)
if(!depsMap) return
// 根据 key 取得所有副作用函数 effects
const effects = depsMap.get(key)
// 执行副作用函数
effects && effect.forEach(fn => fn())
}
})
- 还差一步:进一步优化代码,我们将其封装为
track
函数表达追踪,trigger
用于 set拦截函数来触发变化。 - 封装后的代码如下:
const obj = new Proxy(data,{
// 拦截读取操作
get(target,key){
// 将副作用函数 activeEffect 添加到存储副作用的函数桶中
track(target,key)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target,key,newValue){
// 设置属性值
target[key] = newValue
// 把副作用函数从桶里取出并执行
trigger(target,key)
}
})
// 在get拦截函数内调用 track 函数追踪变化
function track(target,key){
// 没有activeEffect,直接return
if(!activeEffect) return
let depsMap = bucket.get(target)
if(!depsMap){
bucket.set(target,(depsMap => new Map()))
}
let deps = new depsMap.get(key)
if(!deps){
depsMap.set(key,(deps => new Set()))
}
deps.add(activeEffect)
}
// 在set拦截函数内调用 trigget 函数触发变化
function trigger(target,key){
// 根据 target 从同种取得 depsMap,它是key ---> effects
const depsMap = bucket.get(target)
if(!depsMap) return
// 根据 key 取得所有副作用函数 effects
const effects = depsMap.get(key)
// 执行副作用函数
effects && effect.forEach(fn => fn())
}
4.4 分支切换与cleanup
- 首先我们看一段代码
- 当
effect
函数内部存在一个三元表达式,根据字段obj.ok
的值不同会执行不同的代码分支。因而当obj.ok
值发生改变,代码执行就会跟着变化。 - 痛点: 分支切换会导致遗留副作用函数。代码下方则是副作用函数与响应式数据之间建立的关系。
- 在
obj.ok
从true变为false的后,此时触发副作用函数执行,理想情况下副作用函数effectFn
不应该被字段obj.text
所对应的依赖集合收集。
const data = { ok: true, text: "hello world" }
const obj = new Proxy(data, {/* ... */})
effect(function effectFn(){
document.body.innerText = obj.ok ? obj.text : 'not'
})
data
|
|______ok
|
|_____ effectFn
|
|______ text
|
|_____ effectFn
- 解决问题: 每次副作用函数执行时,可以把它从所有与之关联的依赖集合中删除。
// 用一个全局变量存储被注册的副作用函数
let activeEffect
function effect(fn){
const effectFn = () => {
// 当 effectFn 执行时,将其设置为当前激活的副作用函数
activeEffect = effectFn
fn()
}
// activeEffect.deps 用来存储所有与该副作用函数想关联的依赖集合
effectFn.deps = []
effectFn()
}
- 此时使用
track
函数收集effectFn.deps
数组中的依赖集合。- 在
track
函数中,将当前执行的副作用函数activeEffect
添加到依赖集中deps
中,这就说明deps
就是一个与当前副作用函数存在联系的依赖集合。 - 我们再将
deps
添加到activeEffect.deps
中,来完成依赖收集。
- 在
function track(target,key){
// 没有 activeEffect,直接return
if(!activeEffect) return
let depsMap = bucket.get(target)
if(!depsMap){
bucket.set(target,(depsMap = new Map()))
}
let deps = depsMap.get(key)
if(!deps){
despMap.set(key,(deps => new Set()))
}
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 activeEffect.deps数组中
activeEffect.deps.push(deps) // 新增
}
- 进而,就可以在每次副作用函数执行时,根据
effectFn.deps
获取所有想关联的依赖集合,进而将副作用函数从依赖集合中移除:
// 用一个全局变量存储被注册的副作用函数
let activeEffect
function effect(fn){
const effectFn = () => {
// 调用 cleanup 函数完成清除工作
cleanup(effectFn) // 新增
fn()
}
// activeEffect.deps 用来存储所有与该副作用函数想关联的依赖集合
effectFn.deps = []
effectFn()
}
- cleanup
- cleanup函数接受副作用函数作为参数,遍历副作用函数的
effectFn.deps
数组,该数组的每一项都是一个依赖集合,然后将该副作用函数从依赖集合中移除,最终重置effectFn.deps
数组。
- cleanup函数接受副作用函数作为参数,遍历副作用函数的
function cleanup(effectFn){
// 遍历 effectFn.deps 数组
for(let i = 0;i < effectFn.deps.length; i++){
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
- 缺陷: 由于我们在
trigger
函数中使用forEach来遍历执行副作用函数。格局ECMA规范,在调用forEach
遍历Set
集合时,如果一个值已经被访问过了,但是该值被删除并重新添加到集合中,如果此时forEach
遍历没有结束,那么该值会重新访问,因此就出现了无限循环的BUG。
// 修改前 trigger
function trigger(target,key){
const depsMap = bucket.get(target)
if(!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(fn => fn()) // 问题代码
}
// 修改后:trigger,构造另外一个Set集合来遍历它
function trigger(target,key){
const depsMap = bucket.get(target)
if(!despMaap) return
const effects = depsMap.get(key)
const effectsToRun = new Set(effects) // 新增
effectsToRun.forEach(effectFn => effectFn()) // 新增
effects && effects.forEach(fn => fn()) // 删除
}
4.5 嵌套的effect与effect栈
effect
是可以发生嵌套的,比如我们嵌套渲染组件。父组件会渲染会调用effect
,而当子组件渲染的时候,同样执行了effect
。- 缺点:上面实现的响应式会存在嵌套渲染组件的时候,依赖收集后,副作用函数被覆盖。即
activeEffect
副作用函数在嵌套effect
的时候会出现被里层覆盖。
// 用一个全局变量存储当前激活的 effect 函数
let activeEfffect
function effect(fn){
const effectFn = () => {
cleanup(effectFn)
// 当调用effect注册副作用函数时,将副作用函数复制给 activeEffect
activeEffect = effectFn
fn()
}
// activeEffect.deps 用来存储所有与该副租用函数相关的以来集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
- 上述代码问题所在:即使这个响应式数据是在外层副作用函数中读取的,它们收集到的副作用函数也都会是内层副作用函数,这就是问题所在。
- 解决办法:我们使用一个副作用函数栈来存储副作用函数。
activeEfffect
指向栈顶,当内层函数执行完成,从栈顶弹出函数,并且将栈顶指向前一个副作用函数。
// 用一个全局变量存储当前激活的 effect 函数
let activeEfffect
// effect 栈
const effectStack = [] // 新增
function effect(fn){
const effectFn = () => {
cleanup(effectFn)
// 当调用effect注册副作用函数时,将副作用函数复制给 activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将副作用函数
effectStack.push(effect) // 新增
fn()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
effectStack.pop() // 新增
activeEffect = effectStack[effectStack.length - 1] //新增
}
// activeEffect.deps 用来存储所有与该副租用函数相关的以来集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
4.6 避免无限递归循环
- 我们需要在响应式函数加入条件守卫,否则
effect
函数会出现无限递归调用。
const data = { foo:1 }
const obj = new Proxy(data, {/* *** */})
effect(() => obj.foo++) // 会导致无限循环
- 导致无限循环原因,读取obj.foo会触发track,然后执行副作用函数。将其加1赋值给obj.foo,又会触发 trigger,此时执行副作用函数。当副作用函数还没执行完成,又要开始下一次执行。所以导致的了无限递归。
- 解决问题: 添加条件守卫,当track和trigger执行的副作用函数相同时,则不触发执行。
// 修改后:trigger
function trigger(target,key){
const depsMap = bucket.get(target)
if(!despMaap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach((effectFn) => {
if(effectFn != activeEffect){
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => effectFn())
}
4.7 调度执行
- 书中提到的调度执行,可以理解为给
effect
函数传入第二个option参数,通过对option参数将副作用函数传入到调度器scheduler
中,从而达到控制执行时机、次数以及方式。 - 例子: 实现不同顺序的输出
const data = { foo : 1}
const obj = new Proxy(data, {/* **** */})
effect(() => {
console.log(obj.foo)
})
obj.foo++
console.log('结束了')
// 执行结果 1 2 '结束了'
// 期望:不调整代码,使其输出 1 '结束了' 2
effect(() => {console.log(obj.foo)},
// options
{
// 调度器 scheduler 是一个函数
scheduler(fn)
}
)
- 只需要将
option
挂在到effect
上。在触发副作用函数之前对effect.option
的有无进行判断。
function trigger(target,key){
/* ***** 省略 ***** */
const effectsToRun = new Set()
effects && effects.forEach((effectFn) => {
if(effectFn != activeEffect){
effectsToRun.add(effectFn)
}
})
// 主要代码
effectsToRun.forEach((effectFn) => {
// 查询是否存在调度器,如果一个副作用函数存在调度器,则调度该调度器,并将副作用函数当成当成参数传入
if(effect.options.scheduler){
effectFn.options.scheduler(effectFn)
} else {
// 否则直接执行 effectFn 函数
effectFn()
}
})
}
// 完成上面打印输出需求
const data = { foo : 1}
const obj = new Proxy(data, {/* **** */})
effect(() => {
console.log(obj.foo),
// options
{
scheduler(fn){
// 函数放入宏任务队列
setTimeout(fn)
}
}
})
obj.foo++
console.log('结束了')
// 结果 1 '结束' 2
- 书中提到了我们有时只需要关注结果并非过程,此时也可以使用调度器配合完成,这里就不详细列出来了。(p63,p64)
4.8 计算属性 computed 与 lazy
- 我们可以像调度器一样,给
effect
添加一个lazy
参数,进一步对副作用函数执行的值先保存一份 。因此实现如下代码:
function effect(fn,option = {}){
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
// 将fn 的执行结果存储到res 中
const res = fn()
activeEffect = effectStack[effectStack.length - 1]
// 将res作为 effectFn 的返回值
return res
}
effectFn.options = options
effectFn.deps = []
// lazy执行
if(!options.lazy){
effectFn()
}
return effectFn
}
- 实现简单的computed,但仍然有缺陷
function computed(getter){
// 把 getter 作为副作用函数, 创建一个lazy的effect
const effectFn = effect(getter, {
lazy:true
})
const obj = {
// 当读取 value 时才执行effectFn
get value(){
return effectFn()
}
}
return obj
}
// 使用
const data = {foo:1,bar:2}
const obj = new Proxy(data,{/* **** */})
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value) // 3
- computed存在缺陷:多次访问
sumRes.value
的值,会导致effectFn
多次执行,即使此时obj.foo
和obj.bar
的值本身没有变化。
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
- 为了解决多次访问effectFn重新问题,我们现在对值进行缓存
function computed(getter){
// value 用来缓存上一次计算的值
let value
// dirty标志,用来表示是否需要重新计算值,为true 则意味着 脏, 需要计算
let dirty = true
const effectFn = effect(getter,{
lazy:true
})
const obj = {
get value(){
// 只有脏 才计算值,并将得到的值缓存到value中
if(dirty){
value = effectFn()
// 将dirty设置为 false, 下一次访问直接使用缓存的 value中的值
dirty = false
}
}
}
}
-
缺陷:根据上述代码我们很明显可以看出,
dirty
没有在每次执行后进行重置为false,那么第一次访问sumRes.value
,此时dirty
会变为false,以后再也不会变化。因此后面访问的值都将是第一次访问的值。 -
解决办法: 当值发生变化,在effect中添加调度器,将
dirty
设置为true。
function computed(getter){
// value 用来缓存上一次计算的值
let value
// dirty标志,用来表示是否需要重新计算值,为true 则意味着 脏, 需要计算
let dirty = true
const effectFn = effect(getter,{
lazy:true,
// 添加调度器,在调度器中 dirty 重置为 true
scheduler(){
dirty = true
}
})
const obj = {
get value(){
// 只有脏 才计算值,并将得到的值缓存到value中
if(dirty){
value = effectFn()
// 将dirty设置为 false, 下一次访问直接使用缓存的 value中的值
dirty = false
}
}
}
}
- 上述代码,
scheduler
会在 getter 函数中所依赖的响应式数据变化时执行。那么下一次访问值,就会重新调用effectFn 函数了。 - 缺点:当我们在另外一个
effect
中访问数据的并且再修改数据的值,此时不会重新计算。 - 原因: 一个计算属性内部拥有自己的effect,并且它是懒执行的,只有当真正读取计算属性的值才会执行。对于计算属性的getter函数来说,它里面访问的响应式数据只会把computed内部的effect收集为依赖。而当计算属性用于另外一个 effect时,就会发生 effect嵌套,外层的effect不会被内层 effect中的响应式数据收集。
- 解决问题: 手动调用 track 函数进行追踪
function computed(getter){
// value 用来缓存上一次计算的值
let value
// dirty标志,用来表示是否需要重新计算值,为true 则意味着 脏, 需要计算
let dirty = true
const effectFn = effect(getter,{
lazy:true,
// 添加调度器,在调度器中 dirty 重置为 true
scheduler(){
dirty = true
// 当计算属性依赖响应式数据变化时,手动调用 trigger 函数 触发响应式
trigger(obj,value)
}
})
const obj = {
get value(){
// 只有脏 才计算值,并将得到的值缓存到value中
if(dirty){
value = effectFn()
// 将dirty设置为 false, 下一次访问直接使用缓存的 value中的值
dirty = false
}
// 当读取 value 时,手动调用 track 函数进行追踪
track(obj,value)
return value
}
}
return obj
}
- 读取先track,计算属性依赖的数据变化时,会执行调度器函数,在调度器中手动调用trigger函数触发响应。
4.9 watch 的实现原理
- Watch 本质就是观测一个响应式数据,当数据发生改变通知并执行相应的回调函数。
- watch实现本质,就是利用
effect
和options.scheduler
- 如果
effect
传入scheduler
调度器,则副作用函数不会立即执行,而是等待调度器调度。本质scheduler
调度函数就相当于一个回调函数。
function watch(source, cb){
effect(
// 触发读取操作,从而建立联系
() => source.foo,
{
scheduler(){
// 当数据变化时,调用回调函数 cb
cb()
}
}
)
}
- 缺点: 目前只能对
source.foo
改变进行监听,下面代码封装一个通用的读取操作 - 解决: 在watch内部的
effect
中调用traverse
函数进行递归的读取操作,从而来代替source.foo
这种硬编码方式。
function watch(source, cb){
effect(
// 触发读取操作,从而建立联系
() => traverse(source),
{
scheduler(){
// 当数据变化时,调用回调函数 cb
cb()
}
}
)
}
function traverse(value, seen = new Set()){
// 如果要读取的数据是原始值,或者已经被读取过了,那么什么都不做
if(typeof value !== 'object' || value === null || seen.has(value)) return
// 将数据添加到seen中,代表遍历地读取过了,避免循环引用引起的死循环
seen.add(value)
// 暂时不考虑数组等其他数据结构
// 假设 value 就是一个对象,使用 for ... in 读取对象的每一个值,并递归地调用 traverse 进行处理
for(const k in value){
traverse(value[k],seen)
}
return vlaue
}
- 上述代码实现了,能读取一个对象上的任意属性,从而当任意属性变化时,都能触发回调函数执行。
- 继续扩展watch 函数:watch函数还可以接受一个getter函数
watch(
// getter函数
() => obj.foo,
// 回调函数
() => {
console.log('obj.foo 的值变了')
}
)
- 如果需要实现,支持传入getter的watch,那么在getter函数内部,用户就可以指定该watch依赖哪些响应式数据,只有当这些数据变化时候,才会触发回调函数执行。
function watch(source, cb){
// 定义 getter
let getter
// 如果 source 是函数,说明用户传递的是 getter,所以直接把 source 赋值给 getter
if(typeof source === 'function'){
getter = source
} else{
// 否则按照原来实现调用
getter = () => traverse(source)
}
effect(
// 触发读取操作,从而建立联系
() => getter(source), // 变更
{
scheduler(){
// 当数据变化时,调用回调函数 cb
cb()
}
}
)
}
- 此时已经实现watch可以传入getter函数
- 最后,我们实现watch获得新旧值。利用 effect 函数的 lazy选项。
function watch(source, cb){
// 定义 getter
let getter
// 如果 source 是函数,说明用户传递的是 getter,所以直接把 source 赋值给 getter
if(typeof source === 'function'){
getter = source
} else{
// 否则按照原来实现调用
getter = () => traverse(source)
}
// 定义旧值和新值
let oldValue,newValue
// 使用effect注册副作用函数时,开启lazy,并把返回值存储到effectFn中以便后续手动调用
const effectFn = effect(
// 触发读取操作,从而建立联系
() => getter(source), // 变更
{
lazy: true,
scheduler(){
// 在 scheduler 中重新执行副作用函数获得是新值
newValue = effectFn()
// 将旧值和新值作为回调函数的参数
cb(newValue, oldValue)
// 更新旧值,不然下一次会得到错误的旧值
oldValue = newValue
}
}
)
// 手动调用副作用函数,拿到的就是旧值
oldValue = effectFn()
}
- 核心新增了
lazy
创建了一个懒执行的effect
。最下面手动调用 effectFn函数获得返回值就是旧值,即第一次执行得到的值。当变化发生并触发scheduler
调度函数执行时,会重新调用effectFn
函数执行得到新值。因而拿到的新旧值,将其当做参数传递给回到函数cb
。 - 一定要注意,将新值更新旧值,否则下一次变更时候就会得到错误的旧值。
4.11 过期的副作用
- 假设一个场景: 在使用watch时候,先对obj更改,导致 请求A执行。当 请求A返回之前,我们再对obj进行更改,此时发出 请求B,那么当B比A先返回,A后返回,最终我们会得到A请求返回值。(这不是我们的想要的,A为过期请求值。)
- 解决问题: 让watch接受第三个参数
onInvalidate
,它是一个函数,类似于事件监听器。
watch(obj,async (newValue,oldValue,onInvalidate) => {
// 定义一个标志,代表当前副作用函数是否过期,默认为false,代表没有过期
let expired = false
// 调用 onInvalidate() 函数注册一个过期回调
onInvalidate(() => {
// 当过期时,将expired 设置为true
expired = true
})
// 发送网络请求
const res = await fetch('/path/to/request')
if(!expired){
finalData = res
}
})
- 上述代码简单实现怎样使用onInvalidate 函数来通知过期执行。通过定义
expired
标志变量,同来表示当前执行的副作用函数是否过期。接着调用onInvalidate
函数注册一个过期回调,当该副作用函数的执行过期时减expired
标志变量设置为true。最后才赋值请求结果。 - 在vue中,每次副作用函数重新执行之前,会先调用我们通过
onInvalidate
注册的过期回调。
function watch(source, cb,options = {}){
// 定义 getter
let getter
// 如果 source 是函数,说明用户传递的是 getter,所以直接把 source 赋值给 getter
if(typeof source === 'function'){
getter = source
} else{
// 否则按照原来实现调用
getter = () => traverse(source)
}
// 定义旧值和新值
// 提取 scheduler 调度韩式为一个独立的 job函数
let oldValue,newValue
// cleanup用来存储用户注册的过期回调
let cleanup // 新增
function onInvalidate(fn){
// 将过期回调存储到 cleanup中
cleanuo = fn
}
const job = () => {
newValue = effectFn()
// 在调用回调函数cb之前,先调用过期回调函数
cb(newValue,oldValue,onInvalidate)
oldValue = newValue
}
// 使用effect注册副作用函数时,开启lazy,并把返回值存储到effectFn中以便后续手动调用
const effectFn = effect(
// 触发读取操作,从而建立联系
() => getter(), // 变更
{
lazy: true,
// 使用 job 函数作为调度器函数
scheduler: () => {
if(option.flush === 'post'){
const p = Promise.resolve()
p.then(job)
}else {
job()
}
}
}
)
// 新增
if(options.immediate){
job()
}else{
// 手动调用副作用函数,拿到的就是旧值
oldValue = effectFn()
}
//使用
watch(obj,async(newValue, oldValue, onInvalidate) => {
let expired = false
onInvalidate(() => {
expired = false
})
const res = await fetch('/path/to/request')
if(!expired){
finalData = res
}
})
// 第一次修
obj.foo++
setTimeOut(() => {
// 200 秒做第二次修改
obj.foo++
},200)
- 修改第一次,立即执行wathc回调函数。由于在回调函数中调用了
onInvalidate
,所以会注册一个过期回调,然后发送请求A - 假设请求A 1000ms 才返回结果。而我们在200ms第二次修改了 obj.foo 的值,这又会导致watch回调执行。注意,实现中,每次执行回调函数之前要想先检查过期回调是否存在。如果存在,会优先执行过期回调。
- 由于在watch回调函数第一次执行时候,我们已经注册了一个过期函数,所以在watch的回调函数中第二次执行之前,会优先执行之前注册的过期回调。
- 这会使得第一次执行的副作用函数内的变量
expired
的值变为true
,即副作用函数的执行过期。 - 于是等待请求A的返回结果时,其结果会被抛弃。
END
个人博客汇总地址:github.com/codehzy/blo…