理解 React Fiber & Concurrent Mode

抛出问题

React 目前主要的问题有两个html

react-cpu-io.jpg

  1. 页面在大量 DOM 节点同时更新的状况下,会出现延迟很严重的现象,具体表现为交互/渲染的卡顿现象
  2. 异步操做(好比 data fetching)须要在组件 mount 以后进行,致使子组件的 fetching 有可能被父组件的 fetching 阻塞(下一篇会细讲)

其中第一个问题会在设备计算能力差(好比 mobile device)的状况下被放大,咱们能够把这个问题和 CPU 绑在一块儿(CPU-bound)react

第二个问题会在设备网络状况差(好比 3G slow network)的状况下被放大,咱们能够把这个问题和 IO 绑在一块儿(IO-bound)git

今天咱们主要来说下,React concurrent mode 是如何解决第一个问题的,第二个问题会在后面的代码实战文章再仔细讲下。github

分析问题

首先,这个问题并非一个 performance 问题,而是一个调度 (scheduling) 问题,因此提升处理速度并不能彻底解决这个问题,就像提升网速并不能解决 HTTP/1.1 协议的线头阻塞 (Head-of-line blocking) 问 题。浏览器

no-debounce.gif

这个问题的本质是,浏览器的 main thread 是单线程的,短期大量 CPU consuming 的 task 被加到了 call stack,致使 event loop 在好几个周期都没有空闲去处理 queue 里面的用户交互 (click, scroll, etc.),从而用户感知到了卡顿。网络

react-event-loop.gif

这两个问题在目前能不能解决呢?其实是能够的,可是解决的方式不够优雅,或者说,没有彻底解决。好比第一个问题在 Table 搜索的时候很常见,若是 Table 数据特别多,搜索的时候的卡顿是必定的。解决方法也很简单,使用 debounce 函数在用户连续输入的时候,不进行操做,等用户输入暂停再搜索。这种 workaround 至关于解决了一半的问题,用户能接受最好,不能接受,其实也没有别的更好的方法了。异步

debounce.gif

解决问题 - React Fiber

解决这个问题的原理其实很简单,HTTP/2 采用分帧 (Frame) 的方式解决的 HTTP/1.1 的线头阻塞问题,咱们也能够将大量的更新任务 (Batch update task) 拆分红一个个小的 task,从而让 event loop 有机会处理用户交互的 callback。async

http2-frame.jpg

React Fiber 就是 React Team 一直在持续开发用来解决上面问题的新 algorithm 或者叫 reconciler。ide

Fiber 的原理挺复杂的,简单说就是原来须要一口气放到 call stack 里面执行的一堆任务,被放到了 heap 里面由一个 scheduler 来调度执行。函数

react-concurrent-mode-img.jpg

其实 React Team 应该在一开始就意识到这个问题了,因此 setState() 被设计成了异步的,但就像官方文档里说的,他们并无彻底利用 asynchronous update (not taking advantage of this) 。仅仅异步是不够的,blocking 仍是会 blocking,只有 scheduling the update 才能实现可中断渲染 (interruptible rendering)

React Concurrent Mode

react-concurrent-mode-demo.gif

Concurrent Mode 指的就是 React 利用上面 Fiber 带来的新特性的开启的新模式 (mode)。目前 React 实验版本容许用户选择三种 mode

  • Legacy Mode: 就至关于目前稳定版的模式
  • Blocking Mode: 应该是之后会代替 Legacy Mode 而长期存在的模式
  • Concurrent Mode: 之后会变成 default 的模式

Concurrent Mode 其实开启了一堆新特性,其中有两个最重要的特性能够用来解决咱们开头提到的两个问题

  • Suspense
  • useTrasition

其中 Suspense 能够用来解决请求阻塞的问题,UI 卡顿的问题其实开启 concurrent mode 就已经解决的,但如何利用 concurrent mode 来实现更友好的交互仍是须要对代码作一番改动的,后面的文章会用实际代码的方式讲下这两个的使用,这里给一个大数据下的不一样 mode 下的页面更新速度的对照 demo

须要注意

上面 Fiber 的 update 机制,有个细节问题就是,既然如今的更新是能够被打断的,那若是打断的操做影响了更新怎么办?好比说有个 background-color 我原来要变成绿色,可是用户点击了一个按钮,background-color 要变成红色,那最终结果会怎样?指望确定是红色,否则就是 bug 了。所以 React 这里作了处理,更新到一半的 work 是能够被丢弃而从新开始的。

这样的作法没错,可是就带来一个问题,有些生命周期函数可能会被屡次调用

react-lifecycle-methods.jpg

咱们能够将全部的生命周期函数分红两类,分别运行在 Render 阶段和 Commit 阶段

  • Render phase 简单说就是决定究竟渲染什么的阶段,涉及绝大多数生命周期。

  • Commit phase 简单说就是将须要渲染的内容推到 DOM (若是是 DOM 环境) 的阶段。

componentDidMount(), componentDidUpdate(), componentWillUnmount() 这三个生命周期函数 (lifecycle method) 运行在 commit 阶段,其他的生命周期函数均可以简单理解为运行在 render 阶段。

咱们上面说更新到一半的 work 有可能被从新启动,这就意味着,像是 rendershouldComponentUpdate 等等这些在 render 阶段的生命周期函数并非像你指望的那样只执行一个,而是有可能被执行屡次。

解决方式有两种:

  1. 把在这些生命周期执行的逻辑放到 commit phase 执行,若是能彻底作到,这里能够直接将组件改为 React hook 的形式。
  2. 保证 render phase 执行的逻辑是幂等的 (idempotent),不理解的能够查下,这个很简单很少讲

总结

React Concurrent Mode 经过启用 Fiber reconciler 实现了可中断渲染 (interruptible render),从而让一系列的交互优化变成了可能。下篇文章会使用实际代码来说解究竟如何正确使用 Concurrent Mode API

Ref

相关文章
相关标签/搜索