美文网首页
React 实验阶段的 Concurrent 模式

React 实验阶段的 Concurrent 模式

作者: 弱冠而不立 | 来源:发表于2021-01-26 10:48 被阅读0次

    参考文章:
    React技术揭秘——React理念
    官方文档——Concurrent 模式介绍 (实验性)

    为什么 React 要推出这个模式

    我们可以从官网看到React的理念:

    用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。

    那制约快速响应的因素有哪些呢?

    • 当遇到大计算量的操作或者设备性能不足使页面掉帧,导致卡顿。
    • 发送网络请求后,由于需要等待数据返回才能进一步操作导致不能快速响应。

    这两类场景可以概括为

    • CPU的瓶颈
    • IO的瓶颈

    那 React 是如何解决这俩个瓶颈的呢?

    突破CPU的瓶颈

    我在 React15 和 React16 的架构对比 有对于屏幕刷新的介绍:

    市场上主流的屏幕刷新率都是60HZ(一秒刷新60次),也就是(1000ms / 60)。即16.6ms浏览器刷新一次。在每16.6ms时间内,需要完成JS脚本执行 ----- 样式布局 ----- 样式绘制。当JS执行时间过长,超出该帧,那么这次刷新就没有时间执行样式布局和样式绘制了。

    那如何解决这个问题呢?
    在每帧时,留出一点固定时间给JS。当预留的时间不够用时,React将线程控制权交还给浏览器使其有时间渲染UI,React则等待下一帧时间到来继续被中断的工作。
    看下初次渲染情况下非Concurrent 模式和 Concurrent 模式的区别。

    非 Concurrnet 模式 开启了 Concurrent 模式的记录

    能明显地看到,Concurrent 模式下,原本的同步更新,通过时间切片(每帧时,留出一点固定时间给JS)变为可中断的异步更新。

    突破IO的瓶颈

    网络延迟是前端开发者无法解决的。如何在网络延迟客观存在的情况下,减少用户对网络延迟的感知?
    可以先看一下,业界人机交互最顶尖的苹果举例,在IOS系统中:

    点击“设置”面板中的“通用”,进入“通用”界面(点击“通用”后的交互是同步的,直接显示后续界面) 作为对比,再点击“设置”面板中的“Siri与搜索”,进入“Siri与搜索”界面(点击“Siri与搜索”后的交互是异步的,需要等待请求返回后再显示后续界面)

    虽然点击“通用”后的交互直接跳转到后续页面是同步的,点击“Siri与搜索”后的交互因为需要请求数据所以是异步的,但是你能感受到两者体验上的区别么?

    这里的窍门在于:点击“Siri与搜索”后,先在当前页面停留了一小段时间,这一小段时间被用来请求数据。

    当“这一小段时间”足够短时,用户是无感知的。如果请求时间超过一个范围,再显示loading的效果。

    试想如果我们一点击“Siri与搜索”就显示loading效果,即使数据请求时间很短,loading效果一闪而过。用户也是可以感知到的。

    React 为了达到这样的效果,在实验性阶段推出了 用于数据获取的SuspenseConcurrent UI 模式

    首先介绍一下用于数据获取的 Suspense(试验阶段)

    首先强调:用于数据获取的 Suspense 是一个新特性,你可以使用 <Suspense> 以声明的方式来“等待”任何内容,包括数据。
    “等待”目标代码加载,并且可以直接指定一个加载的界面,,让它在用户等待的时候显示。示例:

    const ProfilePage = React.lazy(() => import('./ProfilePage')); // 懒加载
    
    // 在 ProfilePage 组件处于加载阶段时显示一个 Loading 组件
    <Suspense fallback={<Loading />}>
      <ProfilePage />
    </Suspense>
    

    先抛出问题,在React中哪个阶段获取数据合适?为什么要使用Suspense用于数据获取?

    • fetch-on-render(渲染之后获取数据,如:在 useEffect 中 fetch)
    // 在函数组件中:
    useEffect(() => {
      fetchSomething();
    }, []);
    
    // 或者,在 class 组件里:
    componentDidMount() {
      fetchSomething();
    }
    

    React 称这种方法为“fetch-on-render”(渲染之后获取数据),因为数据的获取是发生在组件被渲染到屏幕之后。这种方法会导致“瀑布”的问题。
    codesandbox 代码示例链接:React 在渲染完成后请求数据造成的瀑布问题

    假设获取父级组件数据需要 2 秒,那么在这个方法中,我们只能在 2 秒之后,才开始获取子级组件数据。这就是上面提到的“瀑布”问题:本该并行发出的请求无意中被串行发送出去。
    • fetch-then-render(接收到全部数据之后渲染,不使用 Suspense),先尽早获取下一屏需要的所有数据,数据准备好后,渲染新的屏幕。但在数据拿到之前,我们什么事也做不了。
      codesandbox 代码示例链接:接收到全部数据之后渲染
      利用 Promise.all() 来等待所有数据,解决了之前出现的网络“瀑布”问题,却意外引出另外一个问题。这就导致了,我们即使先接收完子组件数据,也不能先渲染子组件,还得等到父组件也接收完才行
    • Render-as-you-fetch(获取数据之后渲染,使用 Suspense)
      在上面方法 2 中,我们是在调用 setState 之前就开始获取数据:
    1. 开始获取数据
    2. 结束获取数据
    3. 开始渲染

    有了 Suspense,我们依然可以先获取数据,而且可以给上面流程的 2、3 步骤调换顺序:

    1. 开始获取数据
    2. 开始渲染
    3. 结束获取数据

    有了 Suspense,我们不必等到数据全部返回才开始渲染。实际上,我们是一发送网络请求,就马上开始渲染:codesandbox代码示例

    // 一早就开始数据获取,在渲染之前!
    const resources = fetchAll();
    // ...
    function Father() {
      // 尝试读取用户信息
      const fatherName = resources.fatherName.read();
      return <h1>FatherName: {fatherName} </h1>;
    }
    
    需要将 resource 对象设计成在数据开始请求之前无法被获取,来实现数据请求先发生于组件渲染。 本文中所有使用“伪 API” 的演示仿照官方示例都实现了对请求和渲染的顺序控制。

    再次表示:用于数据获取的 Suspense 只是一个新特性,你可以使用 <Suspense> 以声明的方式来“等待”任何内容,包括数据。本文重点介绍它在数据获取的用例,它也可以用于等待图像、脚本或其他异步的操作。

    同时需要注意到的是:Suspense 模式并不与苹果在 IOS 给出的教科书级别优化中的逻辑一样,貌似它并没有在请求时,在当前页面停留一下的机制,而是在请求时渲染一下预设的 Loading 组件。此时就需要介绍 Concurrent UI 模式了。

    Concurrent UI 模式 (试验阶段)

    给出一个关于 Suspense 用于数据获取的代码示例:在codesandbox上查看

    当我们点击 “Next” 按钮来切换激活的页面,现存的页面立刻消失了,然后我们看到整个页面只有一个加载提示。可以说这是一个“不受欢迎”的加载状态。

    如果我们可以“跳过”加载提示这个过程,并且等到内容加载后再过渡到新的页面,效果会更好。
    React 提供了一个新的内置的 useTransition() 的 Hook 可以实现这个设计。
    我们通过几个步骤来使用它。

    1. 保项目中正在使用 Concurrent 模式,需要使用 ReactDOM.unstable_createRoot()或者也可能是ReactDOM.createRoot()(需要React的实验版本) 而非 ReactDOM.render()
    ReactDOM.unstable_createRoot(
      document.getElementById('root')
      ).render(<App />);
    
    1. 增加一个从 React 引入 useTransition Hook 的 import:
    import React, { useState, useTransition, Suspense } from "react";
    

    3.我们在 App 组件中使用它:

    function App() {
      const [resource, setResource] = useState(initialResource);
      const [startTransition, isPending] = useTransition({
        timeoutMs: 3000
      });
    

    就这段代码而言,它还什么都做不了。我们需要使用这个 Hook 的返回值来配置我们的界面切换。useTransition 包含两个返回值:

    • startTransition 类型为function。我们用它来告诉 React 我们希望的延迟的是哪个 state 的更新
    • isPending 类型为 boolean。此变量在 React 中用于告知我们该转换是否正在进行。
    1. 把期望不跳出加载页面的 state 更新包裹在 startTransition 中
      例如,案例中点击 Next 按钮,不希望它显示Loading字样,而是希望能暂时停留。在 CodeSandbox 中尝试
    <button
      onClick={() => {
        startTransition(() => {
          const nextUserId = getNextId(resource.userId);
          setResource(fetchProfileData(nextUserId));
        });
      }}
    >
    
    当点击时,我们没有直接切换到一个空白的页面,而是在前一个页面停留了一段时间。当数据加载好的时候 React 会帮我们切换到新的界面。

    还是有地方体验不友好。最好不要显示加载中。但是如果没有这个过程提示的话体验会更糟糕!当我们点击 “Next”按钮,什么都没有发生,就好像整个应用卡死一样。
    我们需要注意的是,调用 useTransition() 包含两个值返回值:startTransition 和 isPending。

     const [startTransition, isPending] = useTransition({ timeoutMs: 3000 });
    

    我们已经使用了 startTransition 来包裹 state 更新。现在我们要使用 isPending 了。React 提供了这个布尔值来告诉我们当前我们是否正在等待界面切换完成。在 CodeSandbox 中尝试

    return (
      <>
        <button
          disabled={isPending}
          onClick={() => {
            startTransition(() => {
              const nextUserId = getNextId(resource.userId);
              setResource(fetchProfileData(nextUserId));
            });
          }}
        >
          Next
        </button>
        {isPending ? " Loading..." : null}
      </>
    );
    
    现在,这感觉好多了!当我们点击 Next 按钮的时候,它变得不可用,因为点击它很多次并没有意义。而且新增的“Loading…”提示让用户知道程序并没有卡住。
    总结:

    我们分四步步实现了这个功能

    1. 我们引入了 useTransition Hook 并在更新 state 的组件中使用了它。
    2. 我们传入了 {timeoutMs: 3000} 使得前一个页面在屏幕上最多保持3秒钟。
    3. 我们把 state 更新包裹在 startTransition 中,以通知 React 可以延迟这个更新。
    4. 我们使用 isPending 来告诉用户界面切换的进展并禁用按钮。

    一些小技巧

    把过慢的组件用 <Suspense> 把它包裹起来使其惰性加载。

    如果有些特性并不是下个界面所必要的部分,用 <Suspense> 把它包裹起来使其惰性加载。这样就确保了我们能够尽可能快的给用户显示其他的内容,相反的,如果有些组件缺少了这个界面就不值得显示,就使用 useTransition,这样 transition 就会“等待”直到它准备好。

    你可以在 这里 查看这个示例。“文章”和“趣闻”响应相差了 100ms。但是 React 把他们的变化合并在一起并同时更新了他们的 Suspense 的区域。

    延迟显示过快的等待提示

    在上面的示例中,当我们点击 Button 组件时它会立刻显示一个 Pending 状态提示,这让用户知道有什么事情正在发生。但是,如果这个 transition 过程相对较短的时候(小于 500ms),它有可能过于分散注意力,并且使得 transition 本身感觉上更慢。
    一个可能的方法就是延迟等待提示的显示:

    .DelayedSpinner {
      animation: 0s linear 0.5s forwards makeVisible;
      visibility: hidden;
    }
    
    @keyframes makeVisible {
      to {
        visibility: visible;
      }
    }
    
    const spinner = (
      <span className="DelayedSpinner">
        {/* ... */}
      </span>
    );
    
    return (
      <>
        <button onClick={handleClick}>{children}</button>
        {isPending ? spinner : null}
      </>
    );
    

    通过这个更改(通过 CSS 动画延迟 Loading 组件的显示),即使我们进入了 Pending 状态,在 500ms 过去之前我们都不会给用户显示任何提示。

    在 API 响应快的情况下对比感受下 使用前使用后。即使其他的代码并没有更改,通过不在延迟上吸引用户注意,隐藏掉“过快”的加载状态以达到提升感官体验。

    官网和参考文章还有一些其他内容,这里仅是一些我所关注的重点。还是希望能早日使用上这个稳定的 Concurrent 模式。

    相关文章

      网友评论

          本文标题:React 实验阶段的 Concurrent 模式

          本文链接:https://www.haomeiwen.com/subject/nxeszktx.html