React 并发原理(一)

简介: React 并发原理(一)

赛内卡说,折磨我们的,不是事实,而是恐惧

大家好,我是柒八九

前言

就在前几天,我们讲了两篇关于React 18性能优化React Server Componment的文章介绍。其中大部分篇幅,都是基于RSC的.

而,今天我们来讲点不一样的东西。React 并发原理

又很凑巧,最近在做一个需求,有一些操作也是比较耗时和影响页面响应,您猜怎么着,只从有了新useTransiton高钙片啊..一次吃一片..腰不疼啦,腿不痛啦..上六楼啊也有劲勒..我们瞧准啦...新useTransiton高钙片!!!


你能所学到的知识点

  1. 前置知识点
  2. 丝滑般用户体验
  3. 在没有使用startTransition时,浏览器为什么会出现卡顿
  4. startTransition如何工作的
  5. 可视化并发渲染过程
  6. 耗时任务应该分割成组件,以便过渡正常工作

好了,天不早了,干点正事哇。



1. 前置知识点

前置知识点,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略

什么是 useTransition?

useTransition 是一个 React Hook,允许你在不阻塞用户界面的情况下更新状态

使用 useTransition

首先,确保你的项目已经升级到 React 18 或更高版本。

并且,在你的组件的顶层调用useTransition,以将某些状态更新标记为过渡。

import { useTransition } from 'react';
function Container() {
  const [isPending, startTransition] = useTransition();
  // ...
}

参数

useTransition 不接受任何参数。

返回值

useTransition 返回一个包含两个项的数组:

  1. isPending 标志,用于告诉你是否有待处理的过渡。
  2. startTransition 函数,允许你将状态更新标记为过渡。

Run-to-completion VS Preemptive Multitasking

Run-to-completion

运行至完成(Run-to-completion) 是计算机科学中的一个概念,通常用于描述在单线程执行任务时的行为。具体来说,它表示一个任务或操作会一直执行,直到完成,而不会被中断或被其他任务打断。

特点

  1. 连续执行:Run-to-completion 意味着一个任务或操作在开始执行后将连续执行,不会在执行过程中被中断。
  2. 单线程环境: 这个概念通常用于描述单线程编程环境,其中只有一个执行线程,负责按照顺序执行任务和操作。
  3. 任务不被打断:Run-to-completion 模型中,一个任务的执行不会被其他任务或事件所打断。一旦开始执行,任务将一直执行,直到完成或返回结果
  4. 保证顺序性: 任务的执行顺序是按照它们被调度的顺序进行的。这意味着在执行任务期间,不会有其他任务插入或中断,从而确保了任务的有序执行。
  5. 避免竞态条件: 由于任务的连续执行性质,Run-to-completion 有助于避免竞态条件(Race Conditions)和并发问题,因为在单线程中没有多个任务可以同时访问共享资源。

像我们的老朋友JavaScript就是一个典型的单线程编程语言,所有代码都运行在一个主线程中。JavaScript 中的事件循环(Event Loop)遵循 Run-to-completion 模型,确保在同一时刻只有一个任务在执行。

像我们平时用不到的Ruby/Lua也属于Run-to-completion语音

上面的语言虽然采用 Run-to-completion 模型,但它们也支持异步编程模式,例如使用回调函数、Promiseasync/await 等,以在需要时引入非阻塞操作,确保响应性和性能。


Preemptive Multitasking

抢占式多任务处理Preemptive Multitasking)是一种多任务处理模型,其中操作系统具有能力中断当前正在执行的任务,并在需要时将控制权转移到其他任务。这种模型允许操作系统管理多个任务并有效地共享 CPU 时间,以实现更高的系统并发性和响应性。

特点

  1. 任务调度: 抢占式多任务处理依赖于任务调度器(Task Scheduler),它负责管理各个任务的执行。任务调度器按照一定的策略,如优先级、时间片轮转等,来决定哪个任务应该获得 CPU 时间。
  2. 中断机制: 抢占式多任务处理的核心是中断机制。当操作系统决定切换到另一个任务时,它会发送一个中断信号,将当前任务的执行状态保存起来,然后将控制权切换到另一个任务。这种切换是无缝的,用户通常不会察觉到。
  3. 优先级: 抢占式多任务处理支持任务的优先级,高优先级任务可以在低优先级任务之前获得执行时间。
  4. 并行性: 由于任务可以在任何时刻被中断和切换,多个任务可以并行执行,以提高系统的性能和响应速度。

常用的支持抢占式多任务处理的编程语言:

  1. C/C++: C 和 C++ 是支持抢占式多任务处理的流行编程语言。通过使用线程库(如POSIX线程库),开发人员可以创建和管理多个线程,每个线程代表一个任务,操作系统会在不同线程之间进行抢占式调度。
  2. Java: Java 提供了多线程支持,开发人员可以使用 Java 的 Thread 类来创建多个线程,而 Java 虚拟机(JVM)负责抢占式任务调度。
  3. Rust: Rust 是一门系统级编程语言,具有强大的并发和线程支持,可以用于创建高性能的多任务应用程序。

抢占式多任务处理对于需要实现高度并发、响应速度要求高的应用程序非常有用,它允许操作系统有效地管理和调度任务,确保任务能够及时响应外部事件和请求。


Web Workers 简介

Web Workers 是一项用于在浏览器中执行多线程 JavaScript 代码的技术,它们旨在改善 Web 应用程序的性能和响应性。Web Workers 允许我们在主线程之外创建一个或多个工作线程,这些线程可以并行运行,执行计算密集型任务而不会阻塞用户界面的响应。

  1. 类型: 浏览器中的 Web Workers 主要有三种类型:
  • 专用工作线程(Dedicated Web Worker)通常简称为工作者线程、Web WorkerWorker,是一种实用的工具,可以让脚本单独创建一个 JS 线程,以执行委托的任务。只能被创建它的页面使用
  • 共享工作线程(Shared Web Worker):可以被多个不同的上下文使用,包括不同的页面。任何与创建共享工作者线程的脚本同源的脚本,都可以向共享工作者线程发送消息或从中接收消息
  • 服务工作线程(Service Worker):主要用途是拦截、重定向和修改页面发出的请求,充当网络请求的仲裁者的角色
  1. 用途: Web Workers 可以用于各种用途,包括但不限于:
  • 计算密集型任务,如图像处理、数据加密、数学计算等。
  • 处理后台数据同步和定期轮询。
  • 加载和处理大型数据集,以减轻主线程的负担。
  • 处理网络请求以避免阻塞用户界面。
  1. 创建: 创建 Web Workers 非常简单。我们可以使用以下代码创建一个 Dedicated Worker:
const worker = new Worker('worker.js');
  1. 其中 'worker.js' 是 Worker 脚本的文件路径。在 Worker 脚本中,我们可以监听事件来处理消息和执行工作。
  2. 通信Web Workers与主线程之间通过消息传递进行通信。我们可以使用以下方法在主线程和 Worker 之间发送和接收消息:
  • 在主线程中,使用 worker.postMessage(data) 来向 Worker 发送消息。
  • 在 Worker 中,使用 self.postMessage(data) 来向主线程发送消息。
  1. 我们还可以在主线程和 Worker 中监听消息事件,以便处理接收到的消息。
    主线程中的监听方式:
worker.addEventListener('message', (event) => {
  // 处理来自 Worker 的消息
  const data = event.data;
});
  1. Worker 中的监听方式:
self.addEventListener('message', (event) => {
  // 处理来自主线程的消息
  const data = event.data;
});
  1. 限制和注意事项
  • Web Workers 不能访问 DOM,因为它们在独立的上下文中运行。
  • 由于数据传递是通过消息进行的,因此需要序列化和反序列化数据,这可能会导致性能开销。
  • Shared Workers 可能会引入竞态条件和同步问题,因此需要小心处理共享状态。

更过更详细的内容,翻看我们之前写的关于


MessageChannel的简览

MessageChannelHTML5 中的一个 API,它允许你在不同的 JavaScript 线程之间传递消息。这对于在主线程和 Web Workers 之间进行通信非常有用。

下面是一个使用 MessageChannel 用于主线程和worker之间数据通信的的示例代码:

// 创建一个新的 MessageChannel
const channel = new MessageChannel();
// 获取消息的两个端口
const mainPort = channel.port1;
const workPort = channel.port2;
// 在主线程中监听来自workPort的消息
mainPort.onmessage = (event) => {
  console.log(`主线程中接收到的消息: ${event.data}`);
};
// 在 Web Worker 中监听来自port1的消息
// 我们利用Blob 进行Web Worker的实例化处理
const workerCode = `
  self.onmessage = (event) => {
    const port = event.ports[0];
    console.log('在Web Worker中接收到信息:', event.data.message);
    port.postMessage('来自Web Worker的问候')
  };
`;
// 创建一个新的 Web Worker,并将端口workPort传递给它
const blob = new Blob([workerCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob), { type: 'module' });
worker.postMessage({message:'来自主线程的问候!'},[workPort]);

这段代码做了以下事情:

  1. 创建了一个新的 MessageChannel,它包含两个端口:mainPortworkPort
  2. 在主线程中,我们通过 mainPort.onmessage 事件监听来自 workPort 的消息,一旦有消息到达,就会触发回调函数,打印消息内容。
  3. Web Worker 中,我们利用Blob 进行Web Worker的实例化处理,它监听来自 self.onmessage 的消息,并在收到消息时打印出来。
  4. 我们创建了一个新的 Web Worker,并将上述代码传递给它。然后,我们使用 worker.postMessageWeb Worker 发送消息。这里需要注意第二个参数。

最终,你会在浏览器的控制台中看到类似以下内容的输出:

在Web Worker中接收到信息: 来自主线程的问候!
主线程中接收到的消息: 来自Web Worker的问候

这证明了通过 MessageChannel 实现了主线程和 Web Worker 之间的双向通信。


好了,天不早了,干点正事哇。



2. 丝滑般用户体验

以下是该文章将基于的CodeSandbox应用程序链接。这部分代码是从React官网的useTransition文档的变种。

image.png

这里存在三个标签页,About/Posts (slow)/Contact 这不就是典型的公司官网介绍页面。我们通过点击对应的Button进行内容的切换。(setTab(nextTab))。

App.js

import { useState, useTransition } from "react";
import TabButton from "./TabButton.js";
import AboutTab from "./AboutTab.js";
import PostsTab from "./PostsTab.js";
import ContactTab from "./ContactTab.js";
export default function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState("about");
  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }
  return (
    <>
      <TabButton 
        isActive={tab === "about"} 
        onClick={() => selectTab("about")}
      >
        About
      </TabButton>
      <TabButton 
        isActive={tab === "posts"} 
        onClick={() => selectTab("posts")}
      >
        Posts (slow)
      </TabButton>
      <TabButton
        isActive={tab === "contact"}
        onClick={() => selectTab("contact")}
      >
        Contact
      </TabButton>
      <hr />
      {tab === "about" && <AboutTab />}
      {tab === "posts" && <PostsTab />}
      {tab === "contact" && <ContactTab />}
    </>
  );
}

PostTab.js: 在渲染时间方面较慢

const PostsTab = memo(function PostsTab() {
  // 只记录一次。真正的耗时任务发生在SlowPost内部。
  console.log("[ARTIFICIALLY SLOW] Rendering 500 <SlowPost />");
  let items = [];
  for (let i = 0; i < 1000; i++) {
    items.push(<SlowPost key={i} index={i} />);
  }
  return <ul className="items">{items}</ul>;
});

SlowPost.js:真正耗时的组件

function SlowPost({ index }) {
  console.log("rendering post " + index);
  let startTime = performance.now();
  while (performance.now() - startTime < 1) {
    // 每项等待1毫秒不执行任何操作,以模拟耗时操作。
  }
  return <li className="item">Post #{index + 1}</li>;
}


PostsTab 组件充当多个 SlowPost 组件的容器,每个 SlowPost 组件需要 1 毫秒进行渲染。因此,如果有 1000 篇帖子需要渲染,并且每篇帖子对应一个 SlowPost 组件,那么 PostsTab 组件的总渲染时间将为 1 秒。在这 1 秒的时间内,浏览器在用户交互方面可能会变得迟钝。然而,由于在 startTransition 回调中进行处理,通常会导致明显页面卡顿的现象,此时却销声匿迹

function selectTab(nextTab) {
  startTransition(() => {
    setTab(nextTab);
  });
}

为了能更直观的体验这种如德芙般丝滑的感觉,我们可以按照下面的步骤操作一下:

  1. About 页面上,选择Posts (slow)选项卡。
  2. 立即(即在页面未显示帖子页面时)点击Contact页面。

如果Posts页面显示得过快,我们可以将帖子数量从 1000(即 1 秒渲染时间)增加到更大的数量。

正如我们可以注意到的,选择Posts页面后立即选择Contact页面时,没有出现延迟。使用 startTransition 就是使这种流畅用户体验成为可能的关键。

为了感受 startTransition 的神奇之处,我们可以尝试注释掉 startTransition 部分,并按照上述步骤进行操作:

function selectTab(nextTab) {
//    startTransition(() => {
      //当nextTab ==='post'时,页面明显出现卡顿现象 
      setTab(nextTab);
//    });
  }

现在,如果需要渲染 2000 篇帖子,我们应该会注意到在点击Posts (slow)选项卡后会出现 2 秒的冻结时间。

这就是startTransition的魅力所在。接下来,我们将其抽丝剥茧。看看它到底用了何种魔法。

相关文章
|
7月前
|
算法 前端开发 JavaScript
React的diff算法原理
React的diff算法原理
128 0
|
7月前
|
JSON 缓存 前端开发
【React】React原理面试题集锦
本文集合一些React的原理面试题,方便读者以后面试查漏补缺。作者给出自认为可以让面试官满意的简易答案,如果想要了解更深刻,可以点击链接查看对应的详细博文。在此对链接中的博文作者非常感谢🙏。
141 0
|
2月前
|
监控 前端开发 UED
在 React 18 中利用并发渲染提高应用性能
【10月更文挑战第12天】利用并发渲染需要综合考虑应用的特点和需求,合理运用相关特性和策略,不断进行优化和调整,以达到最佳的性能提升效果。同时,要密切关注 React 的发展和更新,以便及时利用新的技术和方法来进一步优化应用性能。你还可以结合具体的项目实践来深入理解和掌握这些方法,让应用在 React 18 的并发渲染机制下发挥出更好的性能优势。
110 59
|
2月前
|
存储 前端开发 测试技术
React Hooks 的工作原理
【10月更文挑战第1天】
|
4月前
|
前端开发 算法 JavaScript
React原理之Diff算法
【8月更文挑战第24天】
|
4月前
|
前端开发 JavaScript 算法
如何学习react原理
【8月更文挑战第9天】 如何学习react原理
47 6
|
4月前
|
开发者 安全 UED
JSF事件监听器:解锁动态界面的秘密武器,你真的知道如何驾驭它吗?
【8月更文挑战第31天】在构建动态用户界面时,事件监听器是实现组件间通信和响应用户操作的关键机制。JavaServer Faces (JSF) 提供了完整的事件模型,通过自定义事件监听器扩展组件行为。本文详细介绍如何在 JSF 应用中创建和使用事件监听器,提升应用的交互性和响应能力。
38 0
|
4月前
|
前端开发 Java UED
瞬间变身高手!JSF 与 Ajax 强强联手,打造极致用户体验的富客户端应用,让你的应用焕然一新!
【8月更文挑战第31天】JavaServer Faces (JSF) 是 Java EE 标准的一部分,常用于构建企业级 Web 应用。传统 JSF 应用采用全页面刷新方式,可能影响用户体验。通过集成 Ajax 技术,可以显著提升应用的响应速度和交互性。本文详细介绍如何在 JSF 应用中使用 Ajax 构建富客户端应用,并通过具体示例展示 Ajax 在 JSF 中的应用。首先,确保安装 JDK 和支持 Java EE 的应用服务器(如 Apache Tomcat 或 WildFly)。
44 0
|
4月前
|
缓存 JavaScript 前端开发
【React生态进阶】React与Redux完美结合:从原理到实践全面解析构建大规模应用的最佳策略与技巧分享!
【8月更文挑战第31天】React 与 Redux 的结合解决了复杂状态管理的问题,提升了应用性能。本文详细介绍了在 React 应用中引入 Redux 的原因、步骤及最佳实践,包括安装配置、状态管理、性能优化等多方面内容,并提供了代码示例,帮助你构建高性能、易维护的大规模应用。
66 0
|
4月前
|
前端开发 JavaScript 中间件
【前端状态管理之道】React Context与Redux大对决:从原理到实践全面解析状态管理框架的选择与比较,帮你找到最适合的解决方案!
【8月更文挑战第31天】本文通过电子商务网站的具体案例,详细比较了React Context与Redux两种状态管理方案的优缺点。React Context作为轻量级API,适合小规模应用和少量状态共享,实现简单快捷。Redux则适用于大型复杂应用,具备严格的状态管理规则和丰富的社区支持,但配置较为繁琐。文章提供了两种方案的具体实现代码,并从适用场景、维护成本及社区支持三方面进行对比分析,帮助开发者根据项目需求选择最佳方案。
64 0