专栏首页Teobler的开发日记React 18探秘(上)
原创

React 18探秘(上)

React 17 那篇没有任何新特性的博客还历历在目,半年多后,终于等来了 17 铺路许久的 18 发布计划,本来想赶紧看看都有些啥,无奈事情略多,一直拖到现在,最近有点点时间,看看 18 给我们带来了什么。

17 发布消息出来的那会我一直好奇这个没有新特性的发布目的是啥,一通搜索之后得到了一些答案:17 在给未来的 Concurrent Mode 铺路,为大家做好未来渐进式升级的准备。 React 的 Concurrent Mode 在下一盘大棋,一盘包括了 RN / Web / SSR / Server Component 的大棋。而这次 18 的发布计划虽然还是没能发布 Concurrent Mode,但也透露了一些未来 Concurrent Mode 的样子。

根据发布计划来看,这次 18 的主要功能可以分成三类:

  1. 一些开箱即用的改进,比如自动批量更新
  2. 一些新的 API,比如 startTransition
  3. 以及新的流式服务端渲染

值得注意的是,这次虽然是一个大版本更新,但是 Concurrent Mode 是一个可选项,博客中提到大部分项目可以做到只消耗一个下午的时间就能完成升级。

自动批量更新

自动批量更新(Automatic batching)是里面最容易理解和使用的新功能。在聊这个功能之前,我们得先理解什么是批量更新(batching)。

我们假设有一段下面的代码:

const App = () => {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    setCount(c => c + 1); // Does not re-render yet
    setFlag(f => !f); // Does not re-render yet
    // React will only re-render once at the end (that's batching!)
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}

如果你在更新 state 的时候是在同一个事件回调里的,那么 React 不会依次更新这些 state,因为这样的更新意味着有多少 state 就会有多少次 re-render。从性能角度考虑,由于这些 state 都是在同一个事件回调中更新的,所以可以认为他们可以一起更新,于是 React 就让这些 state 一次性一起更新了。

但是如果此时的更新发生在 fetch data 或者是 setTimeout 的回调里,那么 React 就不会做这样的优化了,即使那个更新依然在事件回调里:

const App = () => {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    fetchSomething().then(() => {
      // React 17 and earlier does NOT batch these because
      // they run *after* the event in a callback, not *during* it
      setCount(c => c + 1); // Causes a re-render
      setFlag(f => !f); // Causes a re-render
    });
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}

这是因为 React 18 之前仅仅只会在浏览器事件发生的过程中进行批量更新,而不会在事件结束后(比如 fetch data 的回调里面)批量更新。而如果你升级到 18 之后并且使用了 createRoot,那么这些更新都将自动批量更新,这无疑在框架层面提升了性能。

但如果你说我就是有特殊的情况需要依次更新 state,这咋办?

React 团队也考虑了这种特殊情况,提供了一个 flushSync API 来应对这种情况:

import { flushSync } from 'react-dom'; // Note: react-dom, not react

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  // React has updated the DOM by now
  flushSync(() => {
    setFlag(f => !f);
  });
  // React has updated the DOM by now
}

startTransition

这个 API 的目的也是为了提升性能问题,一句话来描述的话,startTransition 能够让你的应用持续响应用户的交互,哪怕这个时候页面正在进行重量级的更新。

这句话有些抽象,我们来举个实际例子。

来需求了

假设网页上有个实时搜索框,用户可以在里面输入任意字符,然后前端应用用这些关键字发送请求到后端实时渲染从后端拿到的结果。

一个很通用的需求,做过这个需求的同学都知道这个需求如果不做任何处理会有性能问题。浏览器需要同时处理用户的输入和页面的渲染,如果渲染量比较大,用户的输入能够感受到明显的卡顿。

映射到代码里,我们的(伪)代码可能长这样:

const App = () => {
  const [inputValue, setInputValue] = useState("");
  const [data, setData] = useState(null);
  
  const onChange = (event) => {
    // Urgent: Show what was typed
    setInputValue(event.target.value);
  }
  
  useEffect(() => {
    // Not urgent: Show the results
    setData(fetchData(inputValue))
  }, [inputValue]);

  return (
    <div>
      <input onChange={onChange} value={inputValue} />
      {data}
    </div>
  );
}

在这个例子里显示渲染结果的优先级并没有显示用户输入高。在 Web 应用中,响应用户交互的优先级几乎是最高的,因为这决定了你的应用是否是实时可用的,卡顿将带来不好的用户体验。

咋办呢

那么在 React 18 之前我们如何解决这个问题呢?没错,通用解是 debounce 和 throt。在这个场景下虽然 throt 优于 debounce,但是他们依然有一个绕不开的问题:假如渲染时间片的确很大,虽然降低了渲染次数,但是在渲染期间如果用户再次输入,这次输入依然会被渲染阻塞,卡顿依然会出现。

那么 React 18 可以怎么做?

import { startTransition } from "react";

useEffect(() => {
  startTransition(() => {
    setData(fetchData(inputValue));
  });
}, [inputValue])

startTransition 能将某一个更新标记为“不紧急”,在该更新进行中如果有更加紧急的更新发生,那么这个“不紧急”的更新将被打断,去更新优先级更高的任务。

这里有一个官方实例从浏览器的角度详细解析了这个 API 带来的性能优化有多少。

什么是 transion

所以,在 React 上下文中, transition 是个啥?

实际上,React 将 state 的更新分成了两类:

  • 紧急更新 (Urgent updates)将直接作用于用户交互,比如输入、点击等等
  • 过渡更新 (Transition updates)将 UI 从一个视图过渡到另一个视图

页面交互的反馈需要与物理反馈一一对应,比如用户在键盘上输入了一串字符,那么理论上页面上也应该立马出现一串对应的字符,否则用户就会认为你的网页有问题,不好用 -- 毕竟他的键盘是好好的。

而搜索结果的实时反馈相对而言没有这么重要,不管是用户输入第一个字符时的搜索结果,还是第三个字符时的搜索结果都不重要,因为用户想要输入五个字符,只要五个字符一输入完毕,页面就显示正确的结果即可。这些都只是 UI 的过渡

但同时你又不能阻塞我的删除操作,毕竟我输完五个字符后,可能发现第三个字符输错了。即 UI 的过渡不能阻塞用户的交互

怎么做到的

在代码运行时,如果一个函数被包裹在 startTransion 中,这个函数的执行并不是被延迟了,这也是它与 setTimeout 最大的不同。相反,这个函数会被立即执行:

console.log('1')
startTransition(() => {
  console.log('2')
  setSearchQuery('hello')
})
console.log('3')
// output:
// 1
// 2
// 3

只不过在执行前 React 会标记一个 transion 开始了。然后在这个 transion 期间的 state 更新也会被标记,这些标记决定了在渲染阶段 React 如何处理这些更新:

let isInTransition = false

function startTransition(fn) {
  isInTransition = true
  fn()
  isInTransition = false
}

function setState(value) {
  stateQueue.push({
    nextState: value,
    isTransition: isInTransition
  })
}

而它的源码实现也同样整洁。

亿点小细节

需要注意的是,如果有多个 transion 他们会被自动批量更新,而不是独立更新:

startTransition(() => {
  navigateToNewTab();
});

startTransition(() => {
  dismissModal();
});

这是因为在进行设计的时候 transion 是可以相互嵌套的,那么就会出现这样的情况:

// Outer transition, added by some wrapper  function
startTransition(() => {
  // Nested transitions, called by some inner function that is wrapped by
  // an abstraction
  startTransition(() => {
    navigateToNewTab();
  });
  
  startTransition(() => {
    dismissModal();
  });
});

外层的 transion 希望里面的函数批量自动更新,但是里面的两个 transion 却希望各自独立执行,为了避免冲突,所有的 transion 将自动批量更新。

面向未来的 startTransion

这个 API 的目的不止于此,就现在来说它还能配合 Suspense 支持 data fetching 的渲染的优化,稍后 React 团队将放出更多例子和文章。

在未来,React 想要将计划中的动画效果也包含在这个 API 里,也就是在未来只要使用了这个 API,React 可以自动帮你解决页面渲染,动画淡入淡出等问题,但是这个计划要想实现应该是在很久以后了,看看那个时候在看文章的你还有没有在写代码吧 :)

参考

  1. The Plan for React 18
  2. React 的 Concurrent Mode 是否有过度设计的成分?
  3. New feature: automatic batching
  4. New feature: startTransition
  5. Question: startTransition behavior

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 不能因为你没有新产出,就意味着你没有提供价值

    文章是好文章,能一窥大厂顶级开源项目团队的日常。细读后发现,从文章隐隐透露出作者:

    公众号@魔术师卡颂
  • [直播] Hooks从理念到实现到源码

    可以说,从出生伊始,React的使命就不是讨好用户,而是Facebook探索前端UI开发最佳实践的一次尝试。

    童欧巴
  • 直播报名 | 携程三端通用框架中的RNWEB框架,6月28日晚8点

    携程技术
  • 「开发提效」从页面直接打开代码文件

    对于 React 项目,有这样一个款插件:react-dev-inspector。

    皮小蛋
  • 「React进阶」探案揭秘六种React‘灵异’现象

    今天我们来一期不同寻常的React进阶文章,本文我们通过一些不同寻常的现象,以探案的流程分析原因,找到结果,从而认识React,走进React的世界,揭开Rea...

    用户6835371
  • 直播报名 | 携程三端通用框架中的CRN-WEB框架,6月28日晚8点

    携程技术
  • 从 0 到 1 实现 React 系列 —— 5.PureComponent 实现 && HOC 探幽

    本系列文章在实现一个 cpreact 的同时帮助大家理顺 React 框架的核心内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/Pur...

    牧云云
  • 来来来,尝试一下 React 18 !

    React 团队最近发布了 React 18 的 alpha 版本。这个版本主要是增强 React 应用程序的 并发渲染 能力,你可以在 React 18 中尝...

    桃翁
  • 【React源码笔记】setState原理解析

    点击上方蓝字,发现更多精彩 导语 大家都知道React是以数据为核心的,当状态发生改变时组件会进行更新并渲染。除了通过React Redux、React Ho...

    腾讯VTeam技术团队
  • Redis 6.0多线程探秘(上)

    我们在生产环境一般都会选择稳定版本来部署,在每个大版本之间还会有若干个小版本,目前最新的版本是6.2.2。

    PHP开发工程师
  • 前沿技术解密——VirtualDOM

    作为React的核心技术之一Virtual DOM,一直披着神秘的面纱。 实际上,Virtual DOM包含: Javascript DOM模型树(VTre...

    IMWeb前端团队
  • 前沿技术解密——VirtualDOM

    差异算法是Virtual DOM的核心,实际上该差异算法是个取巧算法(当然你不能指望用O(n^3)的复杂度来解决两个树的差异问题吧),不过能解决Web的大部分问...

    IMWeb前端团队
  • VsCode插件之Live Serve探秘.(上)

    云深无际
  • React 框架运行时优化方案的演进

    上周刚在公司进行了一次 React 运行时优化方案的分享,以下是分享的文字版,文章比较长,干货也很多,相信你看完后会对 React 有不一样的理解。

    前端森林
  • React 框架运行时优化方案的演进

    上周刚在公司进行了一次 React 运行时优化方案的分享,以下是分享的文字版,文章比较长,干货也很多,相信你看完后会对 React 有不一样的理解。

    ConardLi
  • 从源码深入探究React 运行时优化方案的演进

    上周刚在公司进行了一次 React 运行时优化方案的分享,以下是分享的文字版,文章比较长,干货也很多,相信你看完后会对 React 有不一样的理解。

    Nealyang
  • Next.js 12 发布!迄今以来最大更新!

    就像在 Next.js Conf 上宣布的那样,Next.js 12 是 Next.js 有史以来最大的版本,更新概览如下:

    玖柒的小窝
  • Next.js 12 发布!迄今以来最大更新!

    就像在 Next.js Conf 上宣布的那样,Next.js 12 是 Next.js 有史以来最大的版本,更新概览如下:

    ConardLi
  • 从Context源码实现谈React性能优化

    Context的实现与组件的render息息相关。在讲解其实现前,我们先来了解render的时机。

    公众号@魔术师卡颂

扫码关注云+社区

领取腾讯云代金券