Last updated on

React 演进 及 18 新特性解析

本篇文章行文思路如下

  1. React 的产生背景、要解决的问题
  2. 梳理下 React 发展的历史,理解 React 的设计思路「从哪来、到哪去」
  3. 过一些 18 版本的新能力

构建用户界面

image

React 是一个用来构建用户界面的前端库,从执行的角度来说,在 Web 语境下,就是帮我们操作 DOM 的库。

在 JQuery 时代,我们用 JQuery 或 类似的库在满足兼容性的同时,命令式的操作 DOM 来完成用户界面的变化。

但在 React、Vue 等框架的帮助下,我们与 DOM 之间多了层代理,开发者只用告诉需要什么样的 DOM,预期 DOM 要变化成什么样子,框架则帮我们找出差异,处理变化,进行 DOM 操作。这是一种声明式的代码。

举个例子,假如我要实现下面的需求

01 - 获取 id 为 app 的 div 标签
02 - 它的文本内容为 hello world
03 - 为其绑定点击事件
04 - 当点击时弹出提示:ok

如果我们使用 JQuery 来做

const div = document.querySelector("#app") // 获取 div02
div.innerText = "hello world" // 设置文本内容
div.addEventListener("click", () => {
  alert("ok")
}) // 绑定点击事件

对于 声明式的框架呢? 以 React 为例子

<div onClick={() => alert("ok")}>Hi</div>

命令式更加关注过程,而声明式更加关注结果。命令式在理论上可以做到极致优化,但是用户要承受巨大的心智负担;而声明式能够有效减轻用户的心智负担,但是性能上有一定的牺牲。

在用户体验愈发重要、前端交互愈加复杂的趋势下,声明式的框架逐渐成了主流。

在设计一个这样的视图层框架时,能力加点有俩方向,如下图。

image

所谓编译时,可以理解为不依赖用户打开网页时 JavaScript 的执行构建渲染所需的数据和指令,而是在前端代码 构建「build」 的过程中提前进行代码分析生成渲染所需的数据和指令。

对于这块感兴趣的同学,安利 《Vue.js 设计与实现》 ,其中 第 1 章权衡的艺术 就很好的描述了这块内容,限于篇幅不进行展开了。 截取一段总结给大伙瞅瞅 image

相较于 Vue 和 Svelte,React 是一个没有太多 编译行为 的框架,表达和写法基本和 JavaScript 一致,灵活的写法也导致 React 很难在编译时提前做太多的事情,因此我们可以看到  React  几个大版本的的优化主要都在运行时。

运行时要考虑哪些问题

计算被集中放在了运行时,有影响、被占用的是浏览器的渲染进程

image

渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

而且 JavaScript 是 单线程运行 的,而且从上面可知在浏览器环境麻烦事非常多,它要负责页面的 JS 解析和执行、绘制、事件处理、静态资源加载和处理

image 图片来自

它只是一个’JavaScript’,同时只能做一件事情,这个和  DOS  的单任务操作系统一样的,事情只能一件一件的干。要是前面有一个任务长期霸占 CPU,后面什么事情都干不了,浏览器会呈现卡死的状态,这样的用户体验就会非常差

image JavaScript 就像单行道,这幅图很好的描绘了计算资源不够用的场景 😄

计算资源不够,卡顿,从技术上如何度量呢?

Most devices today refresh their screens 60 times a second. If there’s an animation or transition running, or the user is scrolling the pages, the browser needs to match the device’s refresh rate and put up 1 new picture, or frame, for each of those screen refreshes. Each of those frames has a budget of just over 16ms (1 second / 60 = 16.66ms). In reality, however, the browser has maintenance work to do, so all of your work needs to be completed inside 10ms. When you fail to meet this budget the frame rate drops, and the content judders on screen. This is often referred to as jank, and it negatively impacts the user’s experience. — 上面内容取自 https://web.dev/rendering-performance/

image

主流浏览器的刷新频率一般是  60Hz,也就是每秒刷新  60  次,大概  16.6ms  浏览器刷新一次。由于  GUI  渲染线程和  JS  线程是互斥的,所以  JS  脚本执行和浏览器布局、绘制不能同时执行。

在这  16.6ms  的时间里,浏览器既需要完成  JS  的执行,也需要完成样式的重排和重绘,如果  JS  执行的时间过长,超出了  16.6ms,这次刷新就没有时间执行样式布局和样式绘制了,于是在页面上就会表现为卡顿。

如何更好的协调好 JavaScript 执行、渲染、用户行为响应等 合理的利用宝贵的 CPU 资源,是 React 这个运行时框架要去解决的问题

React 的时间线

下文的主要参考是 React 已有的所有 blogEvolution of React on a Timeline,挑一些我觉得关键的节点和内容

2013 年之前

React 这样的库的需求诞生于 Facebook 的广告组织,随着 Facebook 的规模越来越大,一个开始简单的代码库也在增长,功能的数量增加了,代码的复杂度增加的更多,变的不好维护。

三个背景进行理解

  1. Jordan Walke 搞了个  FaxJS 来降低新功能加入导致的复杂度不可控。
  2. 此时,Facebook 在使用 XHP,它是一种用于 PHP 的 HTML 组件框架。XHP 在 Facebook Lite 中充当了 UI 渲染层,并且在创建自定义和可重用的 HTML 元素方面表现良好。
  3. Facebook 新收购 Instagram 对 Facebook 的广告组织 的已有方案有兴趣,并推动与现有业务解耦和进行开源

搞的 Server Component 也算是不忘初心了 (😄

2013

在 2013 5 月 29 日至 31 日举行的 JS ConfUS 期间,Jordan Walke 向全世界介绍了 React。

https://www.youtube.com/watch?v=GW0rj4sNH2w

image

image

当时已经有很多框架了,如 Angular。React 选用 diff 的方案 来实现最小的 DOM 更新。

  • Declarative Components
  • No observable data binding
  • Embedded XML Syntax 「JSX」

Why did we build React?

https://legacy.reactjs.org/blog/2013/06/05/why-react.html

  1. 鼓励创建可重用的 UI 组件、而不是使用模板
  2. 提供 JSX
  3. reconciliation 找出 应用于 DOM 的最小更改
  4. it’s a lightweight description of what the DOM should look like 「虚拟 DOM」

2014

2015

  • v0.13 - v0.14
  • The most notable new feature is support for ES6 classes
    • 就是我们现在用的 class 组件,这个版本之前用的都是 React.createClass
  • v0.13
    • 在上周的 EmberConf 和 ng-conf 上,我们很高兴地看到 Ember 和 Angular 一直在努力提高速度,现在它们的性能都可以与 React 相媲美。我们一直认为性能不是选择 React 的最重要原因,但我们仍在计划更多的优化以使 React 更快。

2016

整体设计

image

存在的问题

不完美的批处理

同一事件回调函数上下文中的多次 setState 只会触发一次更新。

class Example extends React.Component {
  constructor() {
    super()
    this.state = {
      val: 0,
    }
  }

  componentDidMount() {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    setTimeout(() => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)
    }, 0)
  }

  render() {
    return null
  }
}

15 版本上面代码的打印顺序是  0、0、2、3

递归同步更新,无法中途停止,很容易在复杂场景出现长任务导致渲染卡顿

React 15  本身的架构是递归同步更新的,如果节点非常多,即使只有一次  state  变更,React  也需要进行复杂的递归更新,更新一旦开始,中途就无法中断,直到遍历完整颗树,才能释放主线程。

image

A Cartoon Intro to Fiber - React Conf 2017 给了这种 reconciler 名字,叫作 stack reconciler

2017

  • v16.0.0 - v16.2.0
  • Fiber
  • 提供 ErrorBoundary
  • New render return types: fragments and strings
  • 传送门
  • Fragment 支持
  • 改进仓库结构

Fiber

对于 16 这个版本,最重要的 改动是 引入了 Fiber 这一结构来解决 stack reconciler 中的问题,这也是后续 React 其他 feature 的基石。

image

  • 从运行机制上来解释,fiber是一种流程让出机制,它能让react中的同步渲染进行中断,并将渲染的控制权让回浏览器,从而达到不阻塞浏览器渲染的目的。
  • 从数据角度来解释,fiber能细化成一种数据结构,或者一个执行单元。存着与其他 Fiber 的关系。

传统递归,一条路走到黑

image

react fiber,灵活让出控制权保证渲染与浏览器响应

image

结合上文提到的 # 运行时要考虑哪些问题 进行理解,Diff 工作可以分解成更小的单元,拆分在多个 Frame 中执行,这样的处理不会卡 Frame

2018

2019

  • v16.8
  • 开始支持 hooks

2020

2022

  • v18
  • 全面开启 Concurrent Mode
  • 细节看下文吧

React 18

Concurrent

这里推荐读下 原文 ## What is Concurrent React? 的内容,因为大部分 18 版本的 新 Feature 能力,都是在这个 Concurrent Mode 的基础上才能 work

https://sedationh.notion.site/Concurrent-40c33ff7e7c643cb98965606200e3906?pvs=4

这里总结下 Concurrent 可以让 React 在同一时刻准备多个版本的 UI

特点

  1. 渲染行为可中断,可恢复
  2. 处理任务有优先级(如 离屏渲染< 当前渲染 < 用户行为)
  3. Concurrent 是 实现很多 feature 的基础能力(如 Suspense)

createRoot

https://sedationh.notion.site/React-18-createRoot-62cd00627e5340a2a8f585397af34a0a?pvs=4 https://github.com/facebook/react/blob/main/CHANGELOG.md#react-dom-client

  1. 支持并发模式
  2. Use it instead of ReactDOM.render. New features in React 18 don’t work without it.
  3. 复用根节点
// old
const rootElement = document.getElementById("root")
ReactDOM.render(<App />, rootElement)

// new
const rootElement = document.getElementById("root")
const root = ReactDOM.createRoot(rootElement)
root.render(<App />)
root.render(<App2 />)

Automatic Batching

https://sedationh.notion.site/React-18-Automatic-Batching-462af57a7ff74b3798a6a3a43918c84f?pvs=4

// Before: only React events were batched.
setTimeout(() => {
  setCount((c) => c + 1)
  setFlag((f) => !f)
  // React will render twice, once for each state update (no batching)
}, 1000)

// After: updates inside of timeouts, promises,
// native event handlers or any other event are batched.
setTimeout(() => {
  setCount((c) => c + 1)
  setFlag((f) => !f)
  // React will only re-render once at the end (that's batching!)
}, 1000)

Batching 是指 React 将多个状态更新分组到单个重新渲染中以获得更好的性能。18 之前,只在 React 事件处理程序中 Batching,promises、setTimeout、本机事件处理程序或任何其他事件中的更新不会在 React 中 Batching。

看 Demo https://codesandbox.io/s/automatic-batching-g8jg63?file=/src/index.js

可以利用 flushSync 进行强制更新

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
}

useTransition

useTransition is a React Hook that lets you update the state without blocking the UI. https://sedationh.notion.site/Transition-7b2702649a9a4a43930a951fff0f03ba?pvs=4

function TabContainer() {
  const [isPending, startTransition] = useTransition()
  const [tab, setTab] = useState("about")

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab)
    })
  }
  // ...
}

体验 Demo The pythagoras tree is a fractal. A deeply nested data structure that brings any rendering framework to its knees。「复杂的 DOM 渲染场景」 https://react-fractals-git-react-18-swizec.vercel.app

再体会下切换 Tab 的场景 https://react.dev/reference/react/useTransition

useSyncExternalStore

useSyncExternalStore is a React Hook that lets you subscribe to an external store. 给状态库用的

比较这俩,看着的使用场景

https://github.com/sedationh/debug-zustand/commit/ce8c6df83cdecef657be71da87f95de6125e291c#r124976497 https://github.com/sedationh/debug-zustand/commit/ce8c6df83cdecef657be71da87f95de6125e291c#r124976321

useDeferredValue

Call useDeferredValue at the top level of your component to get a deferred version of that value. https://sedationh.notion.site/useDeferredValue-cde018f201e34be3a0af0745ce556ded?pvs=4

useDeferredValue 可以让一个 state 延迟生效,只有当前没有紧急更新时,该值才会变为最新值。useDeferredValue 和 startTransition 一样,都是标记了一次非紧急更新。

其他

还有一些特性并没有介绍、尤其是和服务端渲染相关的(业务还没有用的场景),感兴趣的同学可去官网进行进一步的了解。

如何升级呢?

https://react.dev/blog/2022/03/08/react-18-upgrade-guide

整体回顾

  1. 复杂的业务催生「框架」的诞生
  2. React 经历了从 Facebook Ads 团队解决问题产生,到开源给社区用,再到被广泛应用的过程
  3. JSX、Vitrual DOM、Hooks、Fiber、Concurrent Mode … React 专注于运行时的能力,变的越来越像一个操作系统,React OS,处理不同资源的调度、优先级 …
  4. 随着 React 越来越复杂、被用的越来越多,React 的发布和更新也变的更加缓慢和谨慎。