解决前端常见问题:竞态条件

更新日期: 2022-05-18阅读: 204标签: 问题

| 导语 

竞态条件一词翻译自英语 "race conditions"。当我们在开发前端 web 时,最常见的逻辑就是从后台服务器获取并处理数据然后渲染到浏览器页面上,过程中有不少的细节需要注意,其中一个就是数据竞态条件问题,本文会基于 react 并结合一个小 demo 来解释何为竞态条件,以及循序渐进地介绍解决竞态条件方法。框架不同解决的方式会不一样,但不影响理解竞态条件。

获取数据

下面是一个小 demo:前端获取文章数据,并渲染到页面上

App.tsx

import React from 'react';
import { Routes, Route } from 'react-router-dom';
import Article from './Article';

function App() {
return (
<Routes>
<Route path="/articles/:articleId" element={<Article />} />
</Routes>
);
}

export default App;

Article.tsx

import React from 'react';
import useArticleLoading from './useArticleLoading';

const Article = () => {
const { article, isLoading } = useArticleLoading();

if (!article || isLoading) {
return<div>Loading...</div>;
}

return (
<div>
<p>{article.id}</p>
<p>{article.title}</p>
<p>{article.body}</p>
</div>
);
};

export default Article;

在上述的 Article 组件中,我们把相关的数据请求封装到了自定义 hook "useArticleLoading" 中,为了页面的使用体验,我们要么显示获取的数据,要么显示加载中。这里加上了加载态的判断。

useArticleLoading.tsx

import { useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';

interface Article {
 id: number;
 title: string;
 body: string;
}

function useArticleLoading() {
 const { articleId } = useParams<{ articleId: string }>();
 const [isLoading, setIsLoading] = useState(false);
 const [article, setArticle] = useState<Article | null>(null);

 useEffect(() => {
setIsLoading(true);
fetch(`https://get.a.article.com/articles/${articleId}`)
     .then((response) => {
       if (response.ok) {
         return response.json();
       }
       return Promise.reject();
     })
     .then((fetchedArticle: Article) => {
       setArticle(fetchedArticle);
     })
     .finally(() => {
       setIsLoading(false);
     });
 }, [articleId]);

 return {
   article,
   isLoading,
 };
}

export default useArticleLoading;

在这个自定义 hook 中,我们管理了加载态以及数据请求

当我们 url 访问 /articles/1 时,会发出 get 请求获取对应 articleId 为 1 的文章内容

竞态条件出现场景

上面是我们非常常见的获取数据的方法,但是让我们考虑以下情况(时间顺序):

  • 访问 articles/1 查看第一个文章内容

    • 浏览器开始请求后台服务器,获取文章 1 的内容

    • 网络连接出现问题

    • articles/1 请求未响应,数据未渲染到页面中

  • 不等待 articles/1 了,访问 articles/2

    • 浏览器开始请求后台服务器,获取文章 2 的内容

    • 网络连接没有问题

    • articles/2 请求立即响应了,数据渲染到页面中

  • articles/1 的请求响应了

    • 通过 setArticles (fetchedArticles) 覆盖了当前的文章内容

    • 当前 url 应该显示 articles/2,却显示了 articles/1

需要理解的一点就是,网络请求的过程是复杂的,且响应时间是不确定的,访问同一个目的地址,请求经过的网络链路不一定是一样的路径。所以先发出的请求不一定先响应,如果前端以先发请求先响应的规则来开发的话,那么就可能会导致错误的数据使用,这就是竞态条件问题。

解决

解决方法也很简单,当收到响应后,只要判断当前数据是否需要,如果不是则忽略即可。

在 React 中可以很巧妙的通过 useEffect 的执行机制来简洁、方便地做到这点:

useArticlesLoading.tsx

useEffect(() => {
 let didCancel = false;

 setIsLoading(true);
 fetch(`https://get.a.article.com/articles/${articleId}`)
   .then((response) => {
     if (response.ok) {
       return response.json();
     }
     return Promise.reject();
   })
   .then((fetchedArticle: Article) => {
     if (!didCancel) {
       setArticle(fetchedArticle);
     }
   })
   .finally(() => {
     setIsLoading(false);
   });

 return () => {
   didCancel = true;
 }
}, [articleId]);

根据 hook 的执行机制:每次切换获取新文章时,执行 useEffect 返回的函数,然后再重新执行 hook,重新渲染。

现在 bug 不会再出现了:

  • 访问 articles/1 查看第一个文章内容

    • 浏览器开始请求后台服务器,获取文章 1 的内容

    • 网络连接出现问题

    • articles/1 请求未响应,数据未渲染到页面中

  • 不等待 articles/1 了,访问 articles/2

    • useArticleLoading 重新渲染执行,重新渲染前执行了上一次的 useEffect 返回函数,把 didCancel 设置为 true

    • 网络连接没有问题

    • articles/2 请求立即响应了,数据渲染到页面中

  • articles/1 的请求响应了

    • 由于 didCancel 变量,setArticles (fetchedArticles) 没有执行。

处理完后,当我们再次切换文章时,didCancel 为 true,就不会再处理上一个文章的数据,以及 setArticles。

AbortController 解决

虽然上述通过变量的解决方案解决了问题,但它并不是最优的。浏览器仍然等待请求完成,但忽略其结果。这样仍然浪费占用着资源。为了改进这一点,我们可以使用 AbortController。

通过 AbortController,我们可以中止一个或多个请求。使用方法很简单,创建 AbortController 实例,并在发出请求时使用它:

useEffect(() => {
const abortController = new AbortController();

setIsLoading(true);
fetch(`https://get.a.rticle.com/articles/${articleId}`, {
signal: abortController.signal,
})
.then((response) => {
if (response.ok) {
return response.json();
}
return Promise.reject();
})
.then((fetchedArticle: Article) => {
setArticle(fetchedArticle);
})
.finally(() => {
setIsLoading(false);
});

return () => {
abortController.abort();
};
}, [articleId]);

通过传递 abortController.signal,我们可以很容易的使用 abortController.abort() 来终止请求(也可以使用相同的 signal 传递给多个请求,这样可以终止多个请求)

使用 abortController 后,再来看看效果:

  • 访问 articles/1

    • 请求服务器获取 articles/1 数据

  • 不等待响应,再访问 articles/2

    • 重新渲染 hook,useEffect 执行返回函数,执行 abortController.abort ()

    • 请求服务器获取 articles/2 数据

    • 获取到 articles/2 数据并渲染到页面上

  • 第一个文章从未完成加载,因为我们手动终止了请求

可以在开发工具中查看手动中断的请求:


调用 abortController.abort () 有一个问题,就是其会导致 promise 被拒绝,可能会导致未捕获的错误:


为了避免,我们可以加个捕获错误处理:

useEffect(() => {
 const abortController = new AbortController();

 setIsLoading(true);
 fetch(`https://get.a.article.com/articles/${articleId}`, {
   signal: abortController.signal,
 })
   .then((response) => {
     if (response.ok) {
       return response.json();
     }
     return Promise.reject();
   })
   .then((fetchedArticle: Article) => {
     setArticle(fetchedArticle);
   })
   .catch(() => {
     if (abortController.signal.aborted) {
       console.log('The user aborted the request');
     } else {
       console.error('The request failed');
     }
   })
   .finally(() => {
     setIsLoading(false);
   });

 return () => {
   abortController.abort();
 };
}, [articleId]);

停止其他 promises

AbortController 不止可以停止异步请求,在函数中也是可以使用的:

function wait(time: number) {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, time);
});
}
wait(5000).then(() => {
console.log('5 seconds passed');
});
function wait(time: number, signal?: AbortSignal) {
return new Promise<void>((resolve, reject) => {
const timeoutId = setTimeout(() => {
resolve();
}, time);
signal?.addEventListener('abort', () => {
clearTimeout(timeoutId);
reject();
});
});
}
const abortController = new AbortController();

setTimeout(() => {
abortController.abort();
}, 1000);

wait(5000, abortController.signal)
.then(() => {
console.log('5 seconds passed');
})
.catch(() => {
console.log('Waiting was interrupted');
});

传递 signal 给 wait 来终止 promise。

其他

关于 AbortController 兼容性:


除了 IE,其他可以放心使用。

总结

本文讨论了 React 中的竞态条件,解释了竞态条件问题。为了解决这个问题,我们学习了 AbortController 背后的思想,并扩展了解决方案。除此之外,我们还学习了如何将 AbortController 用于其他目的。它需要我们更深入地挖掘并更好地理解 AbortController 是如何工作的。对于前端,可以选择自己最合适的解决方案。

以上文章来源于腾讯IMWeb前端团队 ,作者fly
原文:https://mp.weixin.qq.com/s/GryL1QVARtMB8-WIzd7xQQ

链接: https://www.fly63.com/article/detial/11517

解决mac上每次升级nodejs都要重新安装扩展包的问题

以前用起来没注意到这个现象,最近一段时间发现,每次随着使用brew upgrade自动升级了nodejs版本,原来安装的nodejs扩展包就不起作用了,还需要重新安装一遍。再加上一些扩展包存储网站被墙的问题,这个过程真是令人痛不欲生

点击form 表单中的button导致页面刷新问题

做点击按钮切换背景样式与内容的时候每次都刷新页面 ,发现button写在了form 标签当中,将input或者button的type属性改为button,点击按钮页面就不会自动重新加载了。

解决ios端点击出现闪烁或黑色背景的问题

最近开发一个简单的H5页面,在IOS端测试的时候发现,点击按钮会闪动,出现一个黑色的背景一闪而过,影响用户体验。在ios端,safari浏览器上触发click事件有300ms的延迟相应

前端开发中遇到的一些问题

页面两个标签(非block样式)之间有一个小margin,样式怎么改都去不掉 ;app上点击有背景;手机端input边框阴影;chrome模拟器里点击元素错位;git push 报错RPC failed;

在ios端点击按钮闪烁解决方法

在ios端,safari浏览器上触发click事件有300ms的延迟响应,为touch添加的样式会和click冲突而出现闪烁问题,在safari中触摸事件的相应顺序如下:

js中使用append应注意自动补全问题

一般的标签是如<td></td>、<tr></tr>、<div></div>以成对出现,如果缺少一个都会出错,而append为了保证页面代码的准确性,会对标签进行自动补全,如

如何问一个有效的问题

提问前最好自己过一遍,看有哪里描述不清晰的,哪里表达不通顺,以及是否可以通过现有的这套描述可以让对方很清晰的知道问题所在。 问题:XXX 版本的 XXX 接口调用返回的数据不符合预期。

Node中url.parse(url, true).query:[Object: null prototype]问题

在node v13.5.0中通过url模块解析get请求参数的时候,遇到query: [Object: null prototype],如果我们在node中console.log一个null prototype,就会出现[Object: null prototype]

vue常见问题汇总及解决方案

找不到某些依赖或者模块这种情况一般报错信息可以看到是哪个包抛出的信息.,一般卸载这个模块,安装重新安装下即可。为什么要 return 一个数据对象呢? 官方解释如下: data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。

6 个意想不到的 JavaScript 问题

作为前端开发工程师,JavaScript 是我们的主要开发语言,它本身语法比较简单,并且生态系统也非常完善,在社区的影响力越来越大。在我们使用过程中,经常会遇到各种奇怪的问题

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!