过期的副作用

过期的副作用

竞态问题

竞态问题比较常见于多线程或者多进程编程中,所以前端领域很少讨论这个问题。但是在开发中,这个问题也是挺常见的:

let finalData

watch(obj, async () => {
  // 发送并等待网络请求
  const res = await fetch('/path/to/request')
  // 将请求结果赋值给 data
  finalData = res
})

这段代码中,我们用watch观测obj的变化,每次变化都会发一次请求,然后把finalData的值重置为请求的结果。这一个逻辑乍一看似乎是一个正常的watch逻辑。但是仔细想一想,fetch伴随的网络请求可能会伴随着网络阻塞等种种因素,那么进而可能产生竞态问题。

假设我前后做了A,B两次修改,按照watch的回调函数那么会发送A,B两次请求。倘若没有额外处理,那么从代码看就会是谁的请求晚回来,finalData就会是哪个。例如下面这个状况就会把finalData设给A。

在这里插入图片描述

然而这并不合理,理想状态下,我们希望finalData是B请求返回的数据,不论哪个比较晚回来。所以我们需要一个让副作用过期的手段。

已有的解决方案

那么在vue中,已经有一个现成的方案:watch函数中的第三个函数onInvalidate,他是一个函数,类似于事件监听器,当过期事件触发的时候,就会调用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是如何使前边的副作用过期:

假设第一次修改obj的请求为A,第二次修改obj的请求为B。那么我们现在假设请求A返回来比请求B晚,如上图所示。那么在第二次请求回来的时候会触发回调设置finalData,于此同时,通过触发onInvalidate设置第一次的expired为true,那么在请求A回来的时候并不会修改finalData。

vue是如何实现onInvalidate的

onInvalidate的大致原理如下:

function watch(source, cb, options = {}) {
  let getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }

  let oldValue, newValue

  // cleanup 用来存储用户注册的过期回调
  let cleanup
  // 定义 onInvalidate 函数
  function onInvalidate(fn) {
    // 将过期回调存储到 cleanup 中
    cleanup = fn
  }

  const job = () => {
    newValue = effectFn()
    // 在调用回调函数 cb 之前,先调用过期回调
    if (cleanup) {
      cleanup()
    }
    // 将 onInvalidate 作为回调函数的第三个参数,以便用户使用
    cb(newValue, oldValue, onInvalidate)
    oldValue = newValue
  }

  const effectFn = effect(
    // 执行 getter
    () => getter(),
    {
      lazy: true,
      scheduler: () => {
        if (options.flush === 'post') {
          const p = Promise.resolve()
          p.then(job)
        } else {
          job()
        }
      }
    }
  )

  if (options.immediate) {
    job()
  } else {
    oldValue = effectFn()
  }
}

就是在wath函数中定义了一个onInvalidate的函数,内部逻辑就是将全局的cleanup设置为传进来的函数参数。那么在触发的副作用函数job中增加一个if分支:当存在cleanup的时候,执行cleanup再执行回调函数。

可能会有一点点抽象,其实cleanup是存在于watch对象中的一个内部变量,那么在回调函数中必须要执行onInvalidate才能进行过期副作用。还是以请求A和请求B的例子来讲,当请求A设置完之后,此时cleanup已经被赋值

cleanup = () => {
    expired = true;
}

那么在B设置的时候,那么cleanup会被执行,那么此时的expired在作用域链中,指向的是请求A的函数栈,那么此时对于请求A的回调函数中,expired已经为true了。至此,我们成功让A过期了。

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值