惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

雷峰网
雷峰网
T
The Blog of Author Tim Ferriss
Scott Helme
Scott Helme
P
Proofpoint News Feed
D
Docker
The Hacker News
The Hacker News
云风的 BLOG
云风的 BLOG
Vercel News
Vercel News
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Project Zero
Project Zero
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
GbyAI
GbyAI
Jina AI
Jina AI
P
Proofpoint News Feed
P
Privacy & Cybersecurity Law Blog
T
Threat Research - Cisco Blogs
C
CERT Recently Published Vulnerability Notes
博客园 - 叶小钗
U
Unit 42
博客园_首页
Apple Machine Learning Research
Apple Machine Learning Research
Latest news
Latest news
T
The Exploit Database - CXSecurity.com
博客园 - 三生石上(FineUI控件)
博客园 - 聂微东
T
Threatpost
V
Vulnerabilities – Threatpost
C
Cisco Blogs
Spread Privacy
Spread Privacy
Cisco Talos Blog
Cisco Talos Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
V
Visual Studio Blog
G
GRAHAM CLULEY
Microsoft Azure Blog
Microsoft Azure Blog
博客园 - Franky
G
Google Developers Blog
Know Your Adversary
Know Your Adversary
F
Fortinet All Blogs
H
Hackread – Cybersecurity News, Data Breaches, AI and More
NISL@THU
NISL@THU
N
Netflix TechBlog - Medium
Y
Y Combinator Blog
L
Lohrmann on Cybersecurity
C
CXSECURITY Database RSS Feed - CXSecurity.com
Recent Announcements
Recent Announcements
量子位
S
Schneier on Security
I
Intezer
酷 壳 – CoolShell
酷 壳 – CoolShell
D
Darknet – Hacking Tools, Hacker News & Cyber Security

动物的光合作用

使用 Peer Relay 提高 Tailscale 的速度 内网穿透下 NAS 使用 DNS 轮循作为负载均衡 <众安车保>上当指南 - 动物的光合作用 JS 中的 Shebang/Hashbang - 动物的光合作用 我的自建 NAS - 动物的光合作用 React 组件中复用代码的方式 - 动物的光合作用 负载均衡下的前端资源更新策略 - 动物的光合作用 CSS 基于视区的长度单位 - 动物的光合作用 Excalidraw 支持自定义字体 - 动物的光合作用 我用 Docker 部署的一些服务 - 动物的光合作用 CSS 书写模式和逻辑属性 - 动物的光合作用 大量活动页面持续性维护的解决方案 - 动物的光合作用 Mac mini 搭建外网可访问的简易 NAS - 动物的光合作用 React Fragment 添加事件监听? - 动物的光合作用 使用脚本备份 Github 仓库 - 动物的光合作用 高性能 React 应用的几个小技巧 - 动物的光合作用 复习 DOM 事件 - 动物的光合作用 我的博客架构变迁之路 - 动物的光合作用 浏览器和 JavaScript 的一些新特性 - 动物的光合作用 useCallback 的误区 - 动物的光合作用 在 React 中使用事件分离状态 - 动物的光合作用 如何处理 RESTFUL 数据异常导致的前端错误 - 动物的光合作用 基于 Node.js 的 WebFont 解决方案
如何在 React 解决竞态条件 - 动物的光合作用
mebtte · 2022-06-19 · via 动物的光合作用

最近看了一篇文章「解决前端常见问题:竞态条件」(PDF), 解释了什么是竞态条件以及如何解决这个问题, 不过觉得例子不是很完美, 所以用自己的例子复述一遍.

blog

在上面这个博客中, 通过点击标题跳转到文章内容, 而文章内容需要发送网络请求才能拿到, 因为网络环境复杂, 所以请求成功与否与耗时都无法预估, 为了简化, 假设请求都会成功, 且获取「浏览器内存」内容需要 3s, 其他都是 1s.

大多数情况下上面的博客都没有问题. 如果点击「浏览器内存」后快速点击「monorepo 简介」, 就会发现展示「monorepo 简介」后变成了展示「浏览器内存」:

快速点击文章 1 后点击文章 2
快速点击文章 1 后点击文章 2
路由是文章 2, 展示的内容是文章 1
路由是文章 2, 展示的内容是文章 1

如上图所示, 路径和 ID 都是 monorepo, 展示的却是「浏览器内存」的内容. 如果我们把请求的时间线画出来可以很容易的发现问题:

两次请求的时间线
两次请求的时间线

因为在切换到「monerepo 简介」后, 「浏览器内存」请求没有被取消, 所以当请求响应时会把当前内容替换掉. 这就是前端的竞态条件问题.

在 React 中解决这个问题通常有以下几个方法:

key

我们知道在 React 中渲染列表每个列表项指定 key 值可以优化性能以及避免一些 bug, 其实 key 除了用在列表项外也可以用于普通节点, 节点添加 key props 后, key 值发生变化 React 会卸载旧的节点然后生成新的节点. 上面例子中导致问题的是两次请求的 setArticleContent 是同一个 state, 如果把 articleId 作为 ArticleContent 的 key, 那么每次请求的 setArticleContent 都是不同的 state, 就不会产生竞态条件问题.

function ArticleContentWrapper() {
  const { articleId } = useParams<{ articleId: string }>();
  return (
    <ArticleContent key={articleId} articleId={articleId!} />
  );
}
blog_with_key

setArticleContent 之前进行判断

如果能在请求响应后判断请求是否已经过时, 如果过时的话则跳过 setArticleContent. 一种可行的方法是通过 useRef:

const reqIdRef = useRef(0); // 永远指向最新的 reqId
const getArticleContent = useCallback(async () => {
  /** 每次请求都会生成一个 reqId */
  const reqId = Math.random();
  reqIdRef.current = reqId;

  setArticleContent({
    error: null,
    loading: true,
    content: null,
  });
  try {
    const content = await requestArticleContent(articleId);

    /** 如果 reqId 是最新的则更新 */
    if (reqIdRef.current === reqId) {
      setArticleContent({
        error: null,
        loading: false,
        content,
      });
    }
  } catch (error) {
    /** 如果 reqId 是最新的则更新 */
    if (reqIdRef.current === reqId) {
      setArticleContent({
        error: error as Error,
        loading: false,
        content: null,
      });
    }
  }
}, [articleId]);
blog_request_with_ref

每次发起请求都会生成一个 reqId, 然后赋值给外面的 reqIdRef, 这样 reqIdRef 永远指向最新的 reqId, 所以每当有新的请求发起, 旧的请求永远都符合 reqId !== reqIdRef.current 所以会跳过 setArticleContent.

通过 AbortController 取消请求

上面都是通过 hack 方式解决竞态条件问题的, 真正的解决方案应该是切换文章取消之前未完成的请求. 取消请求的话我们可以使用 AbortController:

const abortControllerRef = useRef(new window.AbortController());
const getArticleContent = useCallback(async () => {
  /** 发起新的请求之前取消上一次的请求 */
  abortControllerRef.current.abort();

  setArticleContent({
    error: null,
    loading: true,
    content: null,
  });
  try {
    const content = await window.fetch(
      `https://example.com/api/article?id=${articleId}`,
      {
        /** 将 AbortController 注入 fetch */
        signal: abortControllerRef.current.signal,
      },
    );

    setArticleContent({
      error: null,
      loading: false,
      content,
    });
  } catch (error) {
    setArticleContent({
      error: error as Error,
      loading: false,
      content: null,
    });
  }
}, [articleId]);

AbortController 同样支持 axios 之类的请求库.

进一步阅读