本篇文章行文思路如下
React 是一个用来构建用户界面的前端库,从执行的角度来说,在 Web 语境下,就是帮我们操作 DOM 的库。
在 JQuery 时代,我们用 JQuery 或 类似的库在满足兼容性的同时,命令式的操作 DOM 来完成用户界面的变化。
但在 React、Vue 等框架的帮助下,我们与 DOM 之间多了层代理,开发者只用告诉需要什么样的 DOM,预期 DOM 要变化成什么样子,框架则帮我们找出差异,处理变化,进行 DOM 操作。这是一种声明式的代码。
举个例子,假如我要实现下面的需求
CODE01 - 获取 id 为 app 的 div 标签 02 - 它的文本内容为 hello world 03 - 为其绑定点击事件 04 - 当点击时弹出提示:ok
如果我们使用 JQuery 来做
JavaScriptconst div = document.querySelector("#app") // 获取 div02 div.innerText = "hello world" // 设置文本内容 div.addEventListener("click", () => { alert("ok") }) // 绑定点击事件
对于 声明式的框架呢? 以 React 为例子
React JSX<div onClick={() => alert("ok")}>Hi</div>
命令式更加关注过程,而声明式更加关注结果。命令式在理论上可以做到极致优化,但是用户要承受巨大的心智负担;而声明式能够有效减轻用户的心智负担,但是性能上有一定的牺牲。
在用户体验愈发重要、前端交互愈加复杂的趋势下,声明式的框架逐渐成了主流。
在设计一个这样的视图层框架时,能力加点有俩方向,如下图。
所谓编译时,可以理解为不依赖用户打开网页时 JavaScript 的执行构建渲染所需的数据和指令,而是在前端代码 构建「build」 的过程中提前进行代码分析生成渲染所需的数据和指令。
对于这块感兴趣的同学,安利 《Vue.js 设计与实现》 ,其中 第 1 章权衡的艺术 就很好的描述了这块内容,限于篇幅不进行展开了。 截取一段总结给大伙瞅瞅
相较于 Vue 和 Svelte,React 是一个没有太多 编译行为 的框架,表达和写法基本和 JavaScript 一致,灵活的写法也导致 React 很难在编译时提前做太多的事情,因此我们可以看到 React 几个大版本的的优化主要都在运行时。
计算被集中放在了运行时,有影响、被占用的是浏览器的渲染进程
渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
而且 JavaScript 是 单线程运行 的,而且从上面可知在浏览器环境麻烦事非常多,它要负责页面的 JS 解析和执行、绘制、事件处理、静态资源加载和处理
它只是一个’JavaScript’,同时只能做一件事情,这个和 DOS
的单任务操作系统一样的,事情只能一件一件的干。要是前面有一个任务长期霸占 CPU,后面什么事情都干不了,浏览器会呈现卡死的状态,这样的用户体验就会非常差。
计算资源不够,卡顿,从技术上如何度量呢?
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/
主流浏览器的刷新频率一般是 60Hz
,也就是每秒刷新 60
次,大概 16.6ms
浏览器刷新一次。由于 GUI
渲染线程和 JS
线程是互斥的,所以 JS
脚本执行和浏览器布局、绘制不能同时执行。
在这 16.6ms
的时间里,浏览器既需要完成 JS
的执行,也需要完成样式的重排和重绘,如果 JS
执行的时间过长,超出了 16.6ms
,这次刷新就没有时间执行样式布局和样式绘制了,于是在页面上就会表现为卡顿。
如何更好的协调好 JavaScript 执行、渲染、用户行为响应等 合理的利用宝贵的 CPU 资源,是 React 这个运行时框架要去解决的问题
下文的主要参考是 React 已有的所有 blog 和 Evolution of React on a Timeline,挑一些我觉得关键的节点和内容
React 这样的库的需求诞生于 Facebook 的广告组织,随着 Facebook 的规模越来越大,一个开始简单的代码库也在增长,功能的数量增加了,代码的复杂度增加的更多,变的不好维护。
三个背景进行理解
搞的 Server Component 也算是不忘初心了 (😄
在 2013 5 月 29 日至 31 日举行的 JS ConfUS 期间,Jordan Walke 向全世界介绍了 React。
https://www.youtube.com/watch?v=GW0rj4sNH2w
当时已经有很多框架了,如 Angular。React 选用 diff 的方案 来实现最小的 DOM 更新。
https://legacy.reactjs.org/blog/2013/06/05/why-react.html
React.createClass
同一事件回调函数上下文中的多次 setState 只会触发一次更新。
JavaScriptclass 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 也需要进行复杂的递归更新,更新一旦开始,中途就无法中断,直到遍历完整颗树,才能释放主线程。
在 A Cartoon Intro to Fiber - React Conf 2017 给了这种 reconciler 名字,叫作 stack reconciler
对于 16 这个版本,最重要的 改动是 引入了 Fiber 这一结构来解决 stack reconciler 中的问题,这也是后续 React 其他 feature 的基石。
fiber
是一种流程让出机制,它能让react
中的同步渲染进行中断,并将渲染的控制权让回浏览器,从而达到不阻塞浏览器渲染的目的。fiber
能细化成一种数据结构,或者一个执行单元。存着与其他 Fiber 的关系。传统递归,一条路走到黑
react fiber
,灵活让出控制权保证渲染与浏览器响应
结合上文提到的 # 运行时要考虑哪些问题 进行理解,Diff 工作可以分解成更小的单元,拆分在多个 Frame 中执行,这样的处理不会卡 Frame
这里推荐读下 原文 中 ## What is Concurrent React?
的内容,因为大部分 18 版本的 新 Feature 能力,都是在这个 Concurrent Mode 的基础上才能 work
https://sedationh.notion.site/Concurrent-40c33ff7e7c643cb98965606200e3906?pvs=4
这里总结下 Concurrent 可以让 React 在同一时刻准备多个版本的 UI
特点
https://sedationh.notion.site/React-18-createRoot-62cd00627e5340a2a8f585397af34a0a?pvs=4 https://github.com/facebook/react/blob/main/CHANGELOG.md#react-dom-client
ReactDOM.render
. New features in React 18 don’t work without it.JavaScript// 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 />)
https://sedationh.notion.site/React-18-Automatic-Batching-462af57a7ff74b3798a6a3a43918c84f?pvs=4
JavaScript// 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 进行强制更新
React JSXimport { 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 is a React Hook that lets you update the state without blocking the UI. https://sedationh.notion.site/Transition-7b2702649a9a4a43930a951fff0f03ba?pvs=4
React JSXfunction 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 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
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