Vue3中的副作用过期问题(watchEffect篇、下)

2,284 阅读4分钟

watch中的onCleanup:

无论是watch还是watchEffect,可以发现在函数中都可以接受一个onCleanup,watch的cleanup是回调的第三个参数,watchEffect()是回调的第一个参数,看看文档对cleanUp这个函数的描述:

watch: image.png watchEffect: image.png

文档的描述是注册清理副作用函数的回调函数,那么为什么要注册清理副作用函数的回调函数呢?为什么副作用函数要被清理呢?

副作用函数过期问题:

用watch看一个例子:

watch(source, async (old, new, OnCleanup) => {
  // 是否过期是标志
  let expired = false
  // 注册过期回调
  OnCleanup(()=> {
    expired = true
  })
 const res = await fetch('something')
 // 如果未过期,那么可以取res为finalData
 if (!expired) {
   finalData = res
 }
})

从上面代码可以看到,source改变后会执行回调,但是回调中有异步任务。假设这一次fetch的返回值为A,在异步任务完成之前很有可能source又一次改变了,这时又会触发callback重新执行,又会发送一次fetch请求,假设这次fetch的返回值为B,那么A和B哪个会先返回呢,答案是不确定的,很有可能B比A先返回也有可能A比B先返回。但是无论是谁先返回结果,都会造成最终finalData的混乱,我们并不知道finalData的值是哪次请求得到的。

但是毕竟B是比A更新的请求,应该保留的结果是B。所以理想情况下是,无论执行多少次callback,都要取最新的那一次请求。

所以这就是通过Onleanup注册的清理回调的作用,每次source改变之后,在运行callback之前都会执行清理回调。运行清理回调之后,通过闭包保存的expired会被设置为true,所以这一次的请求结果就不会被赋给finalData。只有没过期时才会采用请求结果。

那么Vue是如何实现这一点呢?

注册清理回调:

基本的原理就和之前说过的那样,需要在每次响应式数据变更之后,callback执行之前,调用注册过的清理回调,清理上一次过期的副作用函数,比如上面例子中的等待中的异步请求。把上篇文章的watch拿过来改造一下:

function watch(source, callback) {
  let getter

  if (typeof source === 'function') {
    // 如果source是getter
    if (callback) {
      getter = source
    } else {
      // source为getter还可能是watchEffect(无callback)
      // 将cleanup和source封装为getter
      getter = () => {
        if (cleanup) {
          cleanup()
        }
        source()
      }
    }
  } else if (Array.isArray(source)) {
    // 如果是数组
    getter = () =>
      source.map((val) => {
        // 我只处理了数组中只有普通对象和getter
        if (typeof source === 'function') {
          return val()
        } else {
          traverse(val)
        }
      })
  } else {
    getter = () => {
      traverse(source)
    }
  }

  let value
  let prevValue
  let cleanup

  // 清理回调注册函数
  function Oncleanup(fn) {
    cleanup = fn
  }

  // 前面讲到过,如果lazy为true,
  // 就返回副作用函数的包装函数,这个包装函数返回副作用函数的值
  const effectFn = effect(
    () => {
      // 执行封装好的getter,读取数据
      getter()
    },
    {
      lazy: true,
      scheduler() {
        // 说明是watch
        if (callback) {
          // 调度函数内执行callback
          value = effectFn()
          // watch为懒执行,只有在第一次数据变化触发callback时才会注册cleanup
          // callback执行之前清理回调,清理回调只会在第一次callback执行之后注册
          if (cleanup) {
            cleanup()
          }
          callback(value, prevValue, Oncleanup)
          prevValue = value
        } else {
          // watch为了兼容watchEffect,在调度函数内嵌套执行effectFn()
          // cleanup与source封装在一起
          effectFn(Oncleanup)
        }
      }
    }
  )
  if (callback) {
    // 因为懒执行,所以手动调用进行读取操作建立响应联系
    // 数据变更调用scheduler的时候,会重新拿到新值
    // 这里不会注册cleanup,因为watch的effectFn只是它的getter
    prevValue = effectFn()
  } else {
    // watchEffect首次运行并收集依赖
    // wachEffect执行的effectFn本身就是他自己的callback,所以会立刻注册回调
    effectFn(Oncleanup)
  }
}

在上面的代码中我们分别对watch和watchEffect的部分做了改造,让他们在首次执行时能够注册清理回调, 同时在每次响应式依赖改变之后,副作用函数运行之前,触发注册的清理回调。这样用户就有机会对过期的回调“做标记”,实现清理过期副作用的目的。