阅读Vuejs设计与实现(第四章)

1,390 阅读19分钟

第四章 响应式系统的作用于实现

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数组。
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.fooobj.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实现本质,就是利用 effectoptions.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为过期请求值。)
image-20220327201303336
  • 解决问题: 让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的返回结果时,其结果会被抛弃
image-20220327205958325

END

个人博客汇总地址:github.com/codehzy/blo…