likes
comments
collection
share

📢你要在watch中执行异步方法,那你就得明白什么是竟态问题!

作者站长头像
站长
· 阅读数 4

前言

大家好,最近有朋友发出疑问说在vue的响应式API文档中,watch监听函数里面执行异步方法时,为什么需要手动调用watch回调函数返回的第三个参数onCleanup方法,它的原理又是什么?这其实就是为了解决竟态问题。下面我将先说明什么是竟态问题,以及出现问题的场景,并给出解决办法。对竟态问题详细了解后再手写一个简单的watch监听函数,探索onCleanup的实现原理,帮助你更好的了解vue在封装watch方法时是怎么解决竟态问题的。

竟态问题定义

竞态问题,也叫竞态条件(Race Condition),是指一个系统或进程的输出依赖于不受控制的事件出现顺序或出现时机。

换一种角度来理解就是存在多个相同请求时,无法保证先请求的先返回就导致了返回结果错乱问题。

竟态问题场景

举个例子——输入框搜索

📢你要在watch中执行异步方法,那你就得明白什么是竟态问题!

当我们每次输入每一个字母时,都会发送一个请求获取当前关键词的相关结果,但如果这时由于网络原因,前面的请求还没完成,继续输入关键词发送请求,则会存在多个并发请求,这时如果后面发送的请求比前面的请求先完成了,就会导致返回结果顺序错乱,拿不到预期的结果,也就是所谓的竞态问题。

解决方案

这里不讨论在请求中间层做处理的解决方案,因为这不是本文的重点,下面解决方案将结合vue中的watch监听方法进行讲解。

1.中断请求:在下一个请求开始前取消上一个请求;

2.忽略请求结果:在下一个请求开始前,标记忽略上一个请求的返回结果;

在前面例子中,也许大家会想到利用防抖,在指定时间内如果连续触发请求,只执行一次。但这并不能解决根本问题。因为你无法预知网络延迟时间,超出设定的防抖时间范围再次触发时,如果前面的请求还没有拿到返回结果还是会出现问题。

watch执行异步方法如何解决竞态问题

请求中断

下图例子来自vue官方文档,其实就是在用到了中断请求的方法。

📢你要在watch中执行异步方法,那你就得明白什么是竟态问题!

如果你使用的是最新版的axios,以下代码可以模拟以上例子的doAsyncWork方法

export const doAsyncWork = (newId) => {
  // 创建中断请求控制器
  const controller = new AbortController();
  const response = axios.get('/user', {
    params: {
      id: newId
    },
    signal: controller.signal
  })
  return {
    // 返回中断请求方法
    cancel: () => controller.abort(),
    response
  }
}

这个例子其实就是每次id改变时,发送请求前先中断前一个请求。这时可能会有同学问,你明明传入onCleanup方法中的cancel方法不是本次请求的吗?为什么会取消上一次的请求?了解watch方法实现原理的同学应该都知道怎么一回事,不了解也没关系,下面我们简单手撸一个watch方法说明其中的原理。

watch监听方法中的参数onCleanup方法解析

// 手写一个简单的watch监听方法
function watch(source, cb, options = {}) {
  // ...省略前面watch实现细节代码
  // 定义新值和旧值
  let newValue, oldValue
  // 定义保存执行onCleanup时传入的回调方法,在本例中就是保存取消请求方法cancel
  let cleanup
  // 清除副作用方法
  function onCleanup(fn) {
    cleanup = fn
  }
  // 假设watch监听值改变时会触发这个方法
  const job = () => {
    newValue = getNewValue()
    /**
     * 执行watch的回调方法前先看看有没有需要执行的副作用方法(在这为中断请求方法)
     * 第一次触发watch监听回调时cleanup为空,所以不会中断请求
     * cb回调执行后,onCleanup(cancel)调用会保存本次中断请求方法到cleanup变量中
     * 当第二监听回调触发前,执行的cleanup保存的为上一次的请求中断方法
     * 执行后会重新赋值为本次的请求中断方法
     */
    if(cleanup) {
      cleanup()
    }
    // 执行传入的回调方法,cb方法为传入watch的第二个参数
    cb(newValue, oldValue, onCleanup)
    oldValue = newValue
  }
  // 获取新值
  function getNewValue() {
    // ...省略实现细节
  }
  // ...省略后面watch实现细节代码
}

重点: 其中主要是理解onCleanup方法在watch方法中只是担任保存用户传入的中断请求方法的角色。

忽略请求

忽略请求主要是利用闭包的原理,在每次监听回调执行时都会在该方法中创建一个expired变量标记当前请求是否过期,然后把过期方法传入onCleanup中,待下次再次触发回调时,修改上一次回调闭包中的expired变量为true

watch(keyword, (async (newValue, oldValue, onCleanup) => {
  // 标记上一个请求是否过期
  let expired = false
  
  onCleanup(() => {
    expired = true
  })

  const res = await getSeachResult(newValue)
  
  // 赋值时判断请求是否过期,如果过期则忽略赋值
  if(!expired) {
    result.value = res
  }
}), {
  immediate: true
})

总结

以上我们通过vuewatch API引入了什么是竞态问题,并给出了竞态问题的定义,接着例举了一个可能会出现该问题的场景,并说明可用于解决该问题的两个方案,最后回到本文主题,说明在watch监听回调方法中如何解决该问题,以及手写一个简单的watch方法说明在解决该问题上的实现原理,最后列举了在使用watch时,如何使用提到的两种方案解决该问题的例子

转载自:https://juejin.cn/post/7340109872384491583
评论
请登录