最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 前端的“Race Condition”

    正文概述 掘金(连续普通拳)   2021-03-11   463

    关于 Race Condition

    关于 Race Condition,维基上有具体介绍(英文版的更详细): 前端的“Race Condition”

    举个例子,大概就是两个线程去修改全局资源,理想的情况: 前端的“Race Condition”

    但在缺少同步锁的情况下,实际的情况可能是这样: 前端的“Race Condition”

    如何去解决:
    前端的“Race Condition”

    大意是大部分语言都提供了资源锁/同步锁这种东西,根据不同的语言选择不同的方法去处理这个问题

    在前端的表现形式

    javascript 是单线程的,理应不会出现上面的情况。但是在异步渲染的时候,还是会出现渲染的时序问题,表现形式,大概是,一个详情组件,watch/useEffect传进来的id,然后根据id向后端发送请求,然后异步的渲染。

    因为是异步的,你没法保证先发出的请求就一定是最先返回的,就会出现了页面展示的id和详情对不上的情况: 前端的“Race Condition”

    运用同步锁的概念,可以定义一个blocked的变量,在请求时,阻止后续请求的发送: 前端的“Race Condition” 看似解决了渲染的时序问题,但仔细观察会发现,这样处理会导致新的问题产生:

    1. 整体的渲染周期变长了很多
    2. 前端是重交互和UI的,这种“锁”也会导致用户的操作被阻塞,对用户的使用影响也是不好的
    3. 场景太过单一,打个比方,如果处理的是输入框智能提示的时序问题,你不可能在前一个请求未返回前阻止用户继续输入

    前端的“Race Condition”

    现实中的案例

    登陆/切换账户场景

    1. 用户未登陆的状况下,点击登陆,在登陆成功前点了取消,请求是异步的,取消是同步的,会导致,即使用户点击取消,用户仍然登陆成功了。
    2. 用户切换账号,场景类似,就是切换成功前取消切换,但是切换成功后的操作还是会执行的,如果用户感知不到账户切换,会出现比较大的 bug。

    切换 tab/搜索

    点击什么字母就会返回什么字母,这块对接口做了处理,早先的请求响应更慢,当你连续点击a -> ab -> abc,会出现:先展示abc->ab->a,搜索的场景同理 前端的“Race Condition” 结果: 前端的“Race Condition”

    原因

    前端的“Race Condition” 产生这种时序问题的原因很多,简单概括包含以下几点:

    1. 当前所处的网络环境差,不稳定,没法保证请求返回的稳定性
    2. 后端的处理逻辑不同。打个比方,不同的两个接口都能触发组件的更新,但后端对这两个接口的处理策略不同,或者这两个接口访问的数据量不同,就会导致请求的处理周期不同,也就没法保证时序
    3. 此时的用户是个倒霉蛋,第1个请求就是比第2个请求返回慢

    如何去解决

    测试案例

    前端的“Race Condition”

    前端的“Race Condition”

    一个简单的Vue组件,根据输入的内容展示不同的结果,这块的接口做了处理,先发送的请求依然是响应最慢,会出现搜索和结果不匹配的情况

    方案1:从最底层出发,“取消”请求

    目前的请求方式大概两种:XMLHttpRequest + Fetch,目前主流的方案还是XMLHttpRequestFetch因为兼容性的问题使用的还是不多,基于XMLHttpRequest,用的最多的大概是axios,这种一般都会把取消请求的方法封装好了

    前端的“Race Condition” 我们还是以Fetch为例子。Fetch还是比较尴尬,本身就有兼容性的问题,对于请求控制的 AbortController 的兼容性相比更差,这块先不考虑这些。关于AbortControllerMDN上有详细说明: 前端的“Race Condition” 按照官方的例子这样处理就好了:

    async handleSearch() {
      try {
        this.isCanceled = false;
        if (this.controller) {
          this.controller.abort();
          this.isCanceled = true;
        }
        this.controller = new AbortController();
        const { result } = await fetch(
          `http://localhost:3000/list?search=${this.text}`,
          {
            signal: this.controller.signal,
          }
        ).then((response) => response.json());
        this.result = result;
    
        console.log("result", result);
      } catch (err) {
        console.log("err", err);
        // this.controller.signal.aborted
        if (this.isCanceled) {
          console.log("aborted");
        } else {
          this.$message("请求出错了");
        }
      }
    }
    

    ⚠️需要注意的点:

    1. 取消的请求会走到catch,会和一些异常场景耦合,所以需要单独处理
    2. 这块每次都去生成新的实例,我没有找到相对应的reset方法
    3. error拿不到取消请求的信息,controller.signal.aborted能够判断请求是否aborted,但因为每次生成新实例的原因,只能用变量去控制

    前端的“Race Condition”

    1. 百度,只会保留最新的请求,前面的请求都会被取消:

    前端的“Race Condition” 2. 谷歌,谷歌会保留最大4个的并行请求,然后取消前面的所有请求: 前端的“Race Condition”

    取消 Promise

    取消Promise,其实就是让Promise提前resolved或者rejected。关于取消的具体姿势,可以看下how-to-cancel-your-promise

    就是下面几点:

    1. Pure Promises
    2. Switch to generators
    3. Note on async/await

    简单写法:

    const request = (...arg) => {
      let cancel;
      const promise = new Promise((resolve, reject) => {
        cancel = () => reject("aborted");
        fetch(...arg).then(resolve, reject);
      });
    
      return [promise, cancel];
    };
    // ...
    async handleSearch() {
      try {
        if (this.cancel) {
          this.cancel();
        }
        const [promise, cancel] = request(
          `http://localhost:3000/list?search=${this.text}`
        );
        this.cancel = cancel;
        const result = (await promise.then((response) => response.json()))
          .result;
        this.result = result;
    
        console.log("result", result);
      } catch (err) {
        if (err === "aborted") {
          console.log(err);
        } else {
          this.$message("请求出错了");
        }
      }
    }
    

    前端的“Race Condition”

    匹配请求

    只有当前处理的是请求匹配时才处理,否则不管,这里分为两种情况:

    1. 有唯一key区分的,例如商品详情:
      // 存在 id
      async handleSearch() {
        try {
          const detail = await fetch(`xx/${this.id}`);
          if (detail.id === this.id) {
              this.detail = detail;
          }
        } catch (err) {
          this.$message("请求出错了");
        }
      }
      
    2. 不存在唯一key,记录最后Promise引用,再匹配
      async handleSearch() {
        try {
          const curPromise = fetch(`xx/${this.id}`);
          this.promiseRef = curPromise;
          
          const detail = await curPromise;
          
          if (this.promiseRef === curPromise) {
              this.detail = detail;
          }
        } catch (err) {
          this.$message("请求出错了");
        }
      }
      

    我用过的库

    redux-saga

    redux-saga,我以前使用React的时候喜欢用,是Redux的一个中间件,主要就是处理副作用的,即请求。感觉这个库实现了个小型的IO系统,这块内容感兴趣的同学自行了解,我只说下解决方法,redux-saga提供了TakeLatest的辅助辅助函数去处理这种问题:

    function* loadStarwarsHeroSaga() {
      yield* takeLatest(
        'LOAD_STARWARS_HERO',
        function* loadStarwarsHero({ payload }) {
          try {
            const hero = yield call(fetchStarwarsHero, [
              payload.id,
            ]);
            yield put({
              type: 'LOAD_STARWARS_HERO_SUCCESS',
              hero,
            });
          } catch (err) {
            yield put({
              type: 'LOAD_STARWARS_HERO_FAILURE',
              err,
            });
          }
        },
      );
    }
    

    rx-js

    rx-js是一个响应式的库,官方说了,算是异步的lodash。把所有的数据封装成流的形式进行处理。用到的操作方法主要就是SwitchMap

    import { Subject, merge, of } from "rxjs";
    import { ajax } from "rxjs/ajax";
    import { switchMap, catchError, tap } from "rxjs/operators";
    
    export default {
      name: "HelloWorld",
      data() {
        return {
          text: "",
          result: "holder",
        };
      },
      mounted() {
        this.subject = new Subject();
    
        this.subject
          .pipe(
            tap(() => {
              console.log("text:", this.text);
            }),
            switchMap((str) =>
              ajax(`http://localhost:3000/list?search=${this.text}`)
            ),
            catchError((err, caught$) => {
              return merge(of({ err }), caught$);
            })
          )
          .subscribe((response) => {
            if (response.err) {
              this.$message("请求失败");
            } else {
              const result = response.response.result;
              console.log("result:", result);
              this.result = result;
            }
          });
      },
      beforeDestroy() {
        this.subject.unsubscribe();
      },
      methods: {
        handleSearch() {
          this.subject.next();
        },
      },
    };
    

    前端的“Race Condition” 因为把数据当作流去处理,避免了时序的问题:

    前端的“Race Condition”

    结束语

    我整理的大概这么多,解决方式不止这些,还有像GraphQL等,了解的不多,就没写了。“竞态”问题出现在一些简单应用中的概率相对小很多,但在一些复杂应用中就会比较容易出现,自从我从B端项目切换到活动页以后,就再也没有碰到这种问题了(活动页赛高),只是我朋友碰到了这个问题,所以就简单整理了下,大概这么多,谢谢阅读。


    起源地下载网 » 前端的“Race Condition”

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元