React16、17、18优化点大汇总
一.Fiber 架构
主要有九点
新的调度方式~Fiber;生命周期;context;严格模式;portal;refs;fragment;hook;suspense;
一.Fiber 架构
背景知识
一)react Fiber 在 react 中的角色
react 核心理念分为三个部分:
- 1.React Core:处理最核心的 APIs,与终端平台解耦
- 2.React Render:渲染器定义了一个 React Tree,来处理如何接轨不同的平台
比如, react-dom 渲染为浏览器 DOM;React Native 渲染为原生视图; - 3.Reconciler: 负责 diff 算法,进行 patch 行为。
可以被各种 render 公用,提供基础的计算能力。目前主要有两类 reconciler:
(1) stack reconciler,15 以及更早期的版本使用
(2) Fiber reconciler,新一代的架构;
Fiber 就是为了解决之前 react 的问题,进行组件渲染时,从 setState 开始到渲染完成整个过程是同步的。如果需要渲染的组件比较庞大,js 执行会阻塞主线程,会导致页面响应度变差,使得 react 在动画、手势等应用中效果比较差。
核心思想~~1.让渲染有优先级;2.可中断
二)单核处理器(模拟 JS)并发和并行的区别
(1)并发,Concurrent
操作系统会按照一定的调度策略,将 CPU 的执行权分配给多个进程,让它们交替执行,形成一种“同时在运行”假象, 因为 CPU 速度太快,人类感觉不到。实际上在单核的物理环境下同时只能有一个程序在运行。
(2)并行
真正实现同时处理多个任务,这就是并行,严格地讲这是 Master-Slave 架构,分身虽然物理存在,但应该没有独立的意志;
三)调度策略
1.单核处理器常见的调度策略
(1)先到先得(First-Come-First-Served, FCFS);对短进程、IO 密集型不利;
(2)轮询,要设置好时间片的长度;
最好符合大部分进程完成一次典型交互所需的时间;对 IO 不利;
(3)最短进程优先(Shortest Process Next, SPN)
可能长进程得不到响应;长进程阻塞后面;
(4)最短剩余时间(Shortest Remaining Time, SRT)
增加抢占机制,剩余时间短,新的就会抢占旧的进程;
(5)最高响应比优先(HRRN)
响应比 = (等待执行时间 + 进程执行时间) / 进程执行时间
(6)反馈法
每个进程一开始都有相同的优先级,每次被抢占(需要配合其他抢占策略使用,如轮转),优先级就会降低一级。因此通常它会根据优先级划分多个队列。
2.浏览器没有抢占机制,React fiber 用的主动出让机制
(1)合作式调度 cooperative scheduling
(2)用户代码向浏览器申请时间片,由浏览器给我们分配执行时间片(类似 requestIdleCallback 实现,),我们要按照约定在这个时间内执行完毕,并将控制权还给浏览器。
四)Fiber 协程
1.和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程),
它只是一种控制流程的出让机制。
2.ES6 的 Generator
普通函数无法中断,generator 可以;
3.为啥不用 generator 实现
(1)Generator 不能在栈中间让出。
比如你想在嵌套的函数调用中间让出, 首先你需要将这些函数都包装成 Generator,另外这种栈中间的让出处理起来也比较麻烦,难以理解。
除了语法开销,现有的生成器实现开销比较大,所以不如不用。
(2)Generator 是有状态的, 很难在中间恢复这些状态。
一.时间切片 Time Slicing
1.作用
(1)在执行任务过程中,不阻塞用户与页面交互,立即响应交互事件和需要执行的代码;
(2)实现高优先级插队;
2.源码细节
(1)let yieldInterval = 5;
五毫秒,目前单位时间切片的长度;
(2)源码注释中有说明,一个帧中会有多个时间切片(一帧~=16.67ms,包含 3 个时间切片还多)
切片时间不会与帧对齐,如果要与帧对齐,则使用 requestAnimationFrame~~RAF,因为浏览器自动在每帧开头调用,就跟帧对齐了;
二.requestIdleCallback ~ RIC
1.概念
(1)超时检查机制
目前浏览器无法判断当前是否有更高优先级的任务等待被执行,只能换一种思路,通过浏览器提供的 requestIdleCallback API 超时检查机制来让出控制权;
确定一个合理的运行时长~上一点的时间切片 time slicing,每隔 5 毫秒,检测是否超时(比如每执行一个小任务),如果超时就停止执行,将控制权交换给浏览器。
(2)理想的一帧时间是 16ms (1000ms / 60),如果浏览器处理完布局和绘制等任务之后,还有盈余时间,浏览器就会调用 requestIdleCallback 的回调
(3)在浏览器繁忙的时候,可能不会有盈余时间,这时候 requestIdleCallback 回调可能就 不会被执行 。
为了避免其他任务饿死,可以通过 requestIdleCallback 的第二个参数指定一个超时时间 timeout,超过 timeout 时长后,该回调函数会被 立即执行 ;
(4)类似于 rAF 返回一个句柄,可以把它传入 cancelIdleCallback 取消掉任务
2.使用
(1)接口
(2)伪代码:
3.使用建议
(1)只在低优先级的任务中使用它,因为你无法控制它的执行时机。比如给后台发送一些不怎么重要的监控数据,或者进行某种页面检查。
(2)不要在其中修改 DOM 元素,因为它在一个任务周期的 layout 结束之后才执行,如果你修改了 DOM,则会再次引发重排,这会对性能产生一定的影响。
推荐的做法是创建一个 documentFragment 保存对 dom 的修改,并注册 requestAnimationFrame 来应用这些修改。
(3)不在其中执行难以预测执行时间的任务,比如以 Promise 的形式执行某个接口请求。
(4)只在必需时使用 timeout 选项,浏览器会花费额外的开销在检查是否超时上,产生一些性能损失。
4.兼容性
(1)目前 requestIdleCallback 目前只有 Chrome 支持。
(2)而且 RIC 调用频率是 20 次/秒,远低于页面流畅度的要求,每次是 1000ms/20=50ms 的计算时间,如果完全用这 50ms 来计算,同样会带来交互上的卡顿;
(3)所以目前 React 自己实现了一个;
三. React Scheduler 的源码实现
1. MessageChannel
(1)利用 MessageChannel (宏任务)模拟将回调延迟到一帧的最后再执行:
(2)源码中使用 setTimeout 作为降级方案;
(3)MessageChannel 构造函数
返回一个带有两个 MessagePort 属性的 MessageChannel 新对象。
允许我们创建一个新的消息通道,并通过它的两个 MessagePort 属性发送数据。
2.React Scheduler 使用 MessageChannel 的原因
(1)生成宏任务
将主线程还给浏览器,以便浏览器更新页面。浏览器更新页面后继续执行未完成的任务。
(2)为什么不使用微任务呢?
微任务将在页面更新前全部执行完,所以达不到「将主线程还给浏览器」的目的。
(3)为什么不使用 setTimeout(fn, 0)
递归的 setTimeout() 调用会使调用间隔变为 4ms,导致浪费了 4ms。
(4)为什么不使用 rAF()
如果上次任务调度不是 rAF() 触发的,将导致在当前帧更新前进行两次任务调度。
页面更新的时间不确定,如果浏览器间隔了 10ms 才更新页面,那么这 10ms 就浪费了。
3. unstable_scheduleCallback
unstable_scheduleCallback(priorityLevel/delay, callback, { timeout: number })
(1)维护两个小顶堆 taskQueue 和 timerQueue ,前者保存 等待 被调度的任务,后者保存 调度中 的任务
(2) unstable_scheduleCallback 提供两个参数
delay 表示任务的 超时 时长;
timeout 表示任务的 过期 时长;如果没有指定,根据优先程度任务会被分配默认的 timeout 时长。
(3)如果没有提供 delay,则任务被直接放到 taskQueue 中等待处理;
(4)如果提供了 delay,则任务被放置在 timerQueue 中,
(5)此时如果 taskQueue 为空,且当前任务在 timerQueue 的堆顶(当前任务的超时时间最近),则使用 requestHostTimeout 启动定时器(setTimeout),在到达当前任务的超时时间时执行 handleTimeout ,此函数调用 advanceTimers 将 timerQueue 中的任务转移到 taskQueue 中,
(6)此时如果 taskQueue 没有开启执行,则调用 requestHostCallback 启动它,否则继续递归地执行 handleTimeout 处理下一个 timerQueue 中的任务。
(7)时间分片每 5 毫秒检查一下,运行一次异步的 MessageChannel 的 port.postMessage(…)方法,检查是否存在事件响应、更高优先级任务或其他代码需要执行;如果有则执行,如果没有则重新创建工作循环,执行剩下的工作中 Fiber。
(8)5ms 在频繁交互的页面不错;如果页面基本是静态的,可以将 时间分片拉长 ;
react 中利用了一个支持度不算太高的 BOM API ~ navigator.scheduling. isInputPending , 表示用户的输入是否被挂起,也就是用户输入没有及时得到反馈。
如果页面没有发生交互,且不需要重绘(needsPaint === false,这是程序内的一个全局变量),则 React 会把时间分片提升到 300ms(maxYieldInterval)
虽然这个时间远超反应延迟,但是 taskQueue 中每一项任务执行完成后都会去检测有没有用户交互和重绘,如果有则立即把资源回交给浏览器,所以不用担心会因此发生卡顿。
四.过期时间 ExpirationTime
1.会根据当前优先级和当前时间标记生成对应过期时间标记 2.两种类型
(1)时间标记:一个极大值,如 1073741121
(2)过期时间:从网页加载开始计时的实际过期时间,单位为毫秒
(3)过期时间标记
简单理解为和过期时间成反比;
过期时间 = 当前时间 + 优先级对应过期时长
过期时间标记 = 极大数值 - 过期时间 / 10
五.任务优先级
1.优先级 从高到底 ,0-5
(1)No Priority,初始化、重置 root、占位用;
(2)Immediate Priority(-1),这个优先级的任务会立即同步执行, 且不能中断,用来执行过期任务;
(3)UserBlocking Priority(250ms), 会阻塞渲染的优先级,用户交互的结果, 需要及时得到反馈
(4)Normal Priority(5s), 默认和普通的优先级,应对哪些不需要立即感受到的任务,例如网络请求
(5)Low Priority (10s), 低优先级,这些任务可以放后,但是最终应该得到执行. 例如分析通知
(6)Idle Priority(没有超时时间),空闲优先级,用户不在意、一些没必要的任务 (e.g. 比如隐藏的内容), 可能会被饿死;
2.react17 有个新的更细级别的优先级, lanes 车道,
指定一个连续的优先级区间,如果 update 的优先级在这个优先级区间内,则处理区间内包含的优先级生成对应页面快照。
lanes 模型使用 31 位的二进制代表 31 种可能性。可以分别给 IO 任务, cpu 任务不同的 lane,最后可以并发的执行这几种类型的优先级。
(1)注意:时间碎片未用完,高优先级的也抢不了,因为没有释放控制权;
六.流程图
七.React 为 Fiber 做的改造
1.数据结构的调整
(1)Stack Reconciler 到 Fiber Reconciler
React16 之前 ,Reconcilation 是同步的、递归执行的;
现在使用扁平化的链表(单链表)的数据存储结构,使用 循环迭代 来代替之前的 递归 ;
链表比顺序结构数据更占用空间,空间换时间,更方便按照优先级操作,根据当前节点找到下一个节点,方便挂起和恢复;
例子: 这样的父子组件的生命周期执行顺序
1)react16 之前的协调算法,是递归算法
先子元素 B 再父元素 A,b.willMount b.didMount a.willMount a.didMount
2)react16 之后的 Fiber 架构,是扁平化的链表的数据存储结构,FIFO
先进先出,先是父 A 再是子 B,a.willMount a.didMount b.willMount b.didMount
(2)每个 VirtualDOM 节点内部现在使用 Fiber node 表示
模拟函数调用栈,保存了节点处理的上下文信息,方便中断和恢复;
最核心的三个指针
- child: 父节点指向第一个子元素的指针。
- sibling:从第一个子元素往后,指向下一个兄弟元素。
- return:所有子元素都有的指向父元素的指针。
(3)Fiber 也可以理解为工作单元
performUnitOfWork 负责对 Fiber 进行操作,并按照深度遍历的顺序返回下一个 Fiber。两层循环,一个从上往下,一个从下往上;
因为使用了链表结构,每个节点存了各种状态和数据,即使处理流程被中断了,我们随时可以从上次未处理完的 Fiber 继续遍历下去。
2.两个阶段的拆分
之前是一边 Diff 一边提交的,现在分为两个阶段
reconciliation 协调阶段 和 commit 提交阶段 。
##### (1)协调阶段,可以打断的
· constructor
· componentWillMount 废弃
· componentWillReceiveProps 废弃
· static getDerivedStateFromProps
· shouldComponentUpdate
· componentWillUpdate 废弃
· render
因为 Reconciliation 阶段能被打断,会出现函数多次调用的情况,所以这些生命周期函数应该避免使用,16 版之后标记为不安全的;
##### (2)提交阶段,不能暂停,一直到界面更新完成
getSnapshotBeforeUpdate 严格来说,这个是在进入 commit 阶段前调用
componentDidMount
componentDidUpdate
componentWillUnmount
该阶段为了正确地处理各种副作用,包括 DOM 变更、还有你在 componentDidMount 中发起的异步请求、useEffect 中定义的副作用;
因为有副作用,所以必须保证按照次序只调用一次,况且会有用户可以察觉到的变更, 所以不能中断
3.Reconcilation 协调阶段(DOM diff 流程)
简单理解 Reconcilation 就是 虚拟 Dom 的 diff,新的 DOM diff 跟之前类似,区别 3 点
不用递归去对比;
不用 diff 完马上提交变更;
每一个 work loop 结束,需要判断剩余时间够不够;
详细流程如下:
1.如果当前节点不需要更新,直接把子节点 clone 过来,跳到 5;
要更新的话打个 tag,没有变化标记完成即可;2.更新当前节点状态(props, state, context 等)
3.调用 shouldComponentUpdate(),false 的话,跳到 5
4.调用 render()获得新的子节点,并为子节点创建 fiber(创建过程会尽量复用现有 fiber,子节点增删也发生在这里)
5.如果没有产生 child fiber,该工作单元结束,把 effect list 归并到 return,并把当前节点的 sibling 兄弟节点,作为下一个工作单元;否则把 child 作为下一个工作单元
6.work loop 结束,判断剩余时间,如果没有剩余可用时间,等到下一次主线程空闲时才开始下一个工作单元;否则,立即开始做
7.如果没有下一个工作单元了,第 1 阶段结束,进入 pendingCommit 状态;
8.其他细节:
(1)协调阶段会构建一个 workInProgress tree,WIP 树
(2)react 维护一个 effect list 副作用列表,存储有变化打了标记的元素;之间也是用链表存起来的;
(3)所有节点标记完成,react 将 WIP 树标记为 pendingCommit,意思是可以进入 commit 阶段了。
(4)进入阶段 2~提交阶段,会根据 effect-list 来更新 DOM,交换 fiber-tree 和 WIP tree 的指针;
(5)react 大部分时间都在维持两个树(Double-buffering)。
缩减下次更新时,分配内存、垃圾清理的时间。
(6)commit 阶段 完成后,执行 componentDidMount 函数
4.双缓冲
(1)上面提到的 WIP 树(workInProgress tree)
类似图形领域的“双缓存”技术,防止屏幕抖动,优化渲染性能;
react 的 WIP,在 Reconciliation 完毕后一次性提交给浏览器进行渲染,可以减少内存分配和垃圾回收,
(2)WIP 的节点不完全是新的,比如某颗子树不需要变动,React 会克隆复用旧树中的子树;
(3)异常处理,有异常可以继续用旧树的节点,避免整个树挂了;
(4)类似 git 功能,WIP 是从旧树 fork 出来的,所以第二个阶段也叫 commit 阶段;
八.任务的中断和恢复
1.之前的问题
Fiber 之前的数据结构是一棵树,父节点的 children 指向了子节点,但是只有这一个指针是不能实现中断继续的。
比如我现在有一个父节点 A,A 有三个子节点 B,C,D,当我遍历到 C 的时候中断了,重新开始的时候,其实我是不知道 C 下面该执行哪个的,因为只知道 C,并没有指针指向他的父节点,也没有指针指向他的兄弟。
所以 Fiber 就是改造了这样一个结构,加上了指向 父节点和兄弟节点 的指针:
2.中断
检查当前正在处理的工作单元,保存当前成果(firstEffect, lastEffect),修改 tag 标记一下,迅速收尾并再开一个 requestIdleCallback,下次有机会再做;
3.断点恢复
下次再处理到该工作单元时,看 tag 是被打断的任务,接着做未完成的部分或者重做;
4.中断之后,按照优先级安排任务,不一定按照顺序执行,可能导状态不一致;
(1)问题:低优先级任务将 a 设置为 0,而高优先级任务将 a 递增 1, 两个任务的执行顺序会影响最终的渲染结果。
因此要让高优先级任务插队, 首先要保证状态更新的时序。
(2)解决办法
所有更新任务按照顺序插入一个队列, 状态必须按照插入顺序进行计算,但任务可以按优先级顺序执行
目的:保证状态一致性和视图一致性;
5.恢复的注意点,重新完整执行一遍
一次更新分为很多个分片完成, 可能一个任务还没有执行完, 就被另一个优先级更高的更新过程打断;
因为 WIP 树已经更新了,所以低优先级的工作就 完全作废 , 然后等待机会重头到来.
二.生命周期函数的更新
1.废弃并标记为不安全的生命周期函数
(1)componentWillMount(nextProps, nextState)
升级方案:改为 componentDidMount ,异步请求放这里,而不是 willMount 可能会请求多次;
例子:react 的异步请求比如 ajax 请求,为什么放在 componentDidMount 中而不是 wilMount
1)要在已经挂载好了的组件中 setState 才有效,否则无效;
2)react16.0 以后,componentWillMount 可能会被执行多次。
3)跟服务器端渲染(同构)有关系,如果在 componentWillMount 里面获取数据,fetch data 会执行两次,一次在服务器端一次在客户端。在 componentDidMount 中可以解决这个问题。
(2)componentWillReceiveProps(nextProps)
升级方案:静态方法 getDerivedStateFromProps 和 componentDidUpdate 来代替;
补充:如果用到了 this,静态方法也要用怎么处理?
存到 state 中;函数组件可以用 useRef 保存下;与内部变量无关放到 class 之外;
(3)componentWillUpdate(nextProps, nextState)
升级方案:componentDidUpdate 代替;
如涉及大量计算,可在 getSnapshotBeforeUpdate 完成,再在 componentDidUpdate 一次完成更新
2.新增
(1)static getDerivedStateFromProps (nextProps, prevState)
将原本 componentWillReceiveProps(nextProps) 功能进行划分 —— 更新 state 和 操作 props,很大程度避免了职责不清而导致过多的渲染, 从而影响应该性能。
(2)getSnapshotBeforeUpdate (prevProps, prevState)
在组件更新之前获取一个 snapshot —— 可以将计算得的值或从 DOM 得到的信息传递到 componentDidUpdate(prevProps, prevState, snapshot) 周期函数的第三个参数,常常用于 scroll 位置的定位等;
(3)componentDidCatch(error, info)
让开发者可以自主处理错误信息,诸如展示,上报错误等。原理还是 try catch。
3.生命周期三个阶段
(1)挂载阶段
1)constructor
2)static getDerivedStateFromProps
3)render
4)componentDidMount
(2)更新阶段
1)static getDerivedStateFromProps
2)shouldComponentUpdate
3)render,dom diff 计算的
4)getSnapshotBeforeUpdate
5)componentDidUpdate
(3)卸载阶段
1)componentWillUnmount
4.react 更新、重新渲染的方法
(1)setState,常用
注意,传入 null 时,并不会触发 render;
(2)父组件更新,会使子组件重新渲染;
(3)forceUpdate,不推荐
跳过本组件的 shouldComponentUpdate 直接调用 render();
触发正常的生命周期,子组件的 shouldComponentUpdate 会触发的;
(4)setProps
(5)shouldComponentUpdate
三.全新的 Context API
1.旧的问题
是一个实验性的产品,破坏了 React 的分层结构。
如果在穿透组件的过程中,某个组件的 shouldComponentUpdate 返回了 false, 则 Context API 就不能穿透
2.全新 Context API 成了一等 API,可以很容易穿透组件而无副作用
3.完全可以取代部分 Redux 应用
全新的 Context API 带来的穿透组件的能力对于需要状态全局共享的场景十分有用,无需进入额外的依赖就能对状态进行管理,代码简洁明了;
四.React Strict Mode 严格模式
发现开发阶段的潜在问题,主要检测四个问题
1.识别被标志为不安全的生命周期函数
2.检测是否采用了老的 Context API
3.对弃用的 API 进行警告
4.探测某些产生副作用的方法
五.Portal
允许将组件渲染到其他 DOM 节点上。
这对大型应用或者独立于应用本身的渲染很有帮助。弹窗,提示,toast 等等;
六.Refs 属性,操作真正的 DOM
(1)React.createRef 来取得 ref 对象
(2)React.forwardRef,让父组件可以访问到子组件的 Ref,从而操作子组件的 DOM
七.Fragment
(1)类似 document.createDocumentFragment 文档片段,减少 DOM 操作;
(2)render 的再也不用再最外层包裹一层无用的元素了,比如套个 div 元素;
(3)ReactDOM 的 render 函数可以数组形式返回 React Component
八.react 16.8 加入 hooks
一)Hook 为了解决什么问题?
1.在组件之间复用状态逻辑困难
复杂组件的 render props
高阶组件,容易造成“嵌套地狱”
2.复杂组件变得难以理解
比如各种各样的生命周期把业务逻辑拆分的七零八碎;
3.难以理解的 class 组件;this 问题;
4.class 组件的优缺点
优点:面向对象特点
缺点:就是 hook 要解决的问题;
二)hook 的理解
本质是 自变量和因变量 ;
1.useState 定义 自变量 ;
2.useMemo 和 useCallback 定义 无副作用 的 因 变量
3.useEffect 定义 有副作用 的因变量;
4.useReducer 为了方便 操作更多的自变量 ;理解为高级版的 useState,使用 redux 的理念把多个 state 合并为一个;
5.useContext 为了 跨组件层级 操作自变量;
6.useRef 为了增加 组件逻辑的灵活性 ;作为标记变量可作用于自变量和因变量的不同路径中;
三)hook 作用和基本 API
1.作用
把面向生命周期编程变成了面向业务逻辑编程。
业务组件的封装方式可以修改成 Hooks + UI Component。
2.基本 API
(1)useState 保存组件状态
(2)useEffect 处理副作用,比如异步请求;
第二个参数,表示监听的数据,发生变化就触发;
如果第二个参数是空数组,不监听,只在初始化和销毁才触发,代替 DidMount 和 WillUnmount
如果第二个参数不设置空数组,会触发死循环执行,每次更新都触发;
(3)useLayoutEffect ,同步执行副作用
比如 DOM 操作,在 render DOM 更新之后同步触发函数,优先于 useEffect
DOM 改变后同步触发,使用它来从 DOM 读取布局并同步重新渲染
(4)useMutationEffect 更新兄弟组件之前,它在 React 执行其 DOM 改变的同一阶段同步触发
(5)useContext 减少组件层级,用来在多层组件间传递数据;
(6)useReducer,类似 react-redux
(7)useCallback 记忆组件
缓存数据,防止不必要的渲染;
(8)useMemo, 记忆组件
缓存数据,防止不必要的渲染;
和 useCallback 的区别,useCallback 不执行第一个参数函数而是直接返回给你,useMemo 执行把结果返回;
(9)useRef 保存引用值,作为标记变量可作用于自变量和因变量的不同路径中;增加组件逻辑的灵活性;
(10)use Imperative Handle 透传 Ref, 类似 forwardRef
等
四)React Hooks 的优缺点
1.优点
(1)解决嵌套问题,简洁,代码量更少: React Hooks 解决了 HOC 和 Render Props 的嵌套问题,更加简洁
(2)解耦: React Hooks 可以更方便地把 UI 和状态分离,做到更彻底的解耦
(3)组合: Hooks 中可以引用另外的 Hooks 形成新的 Hooks,组合变化万千
(4)解决类组件的 3 个问题: React Hooks 为函数组件而生,从而解决了类组件的几大问题:
1)this 指向容易错误
2)业务逻辑被分割在不同声明周期中,使得代码难以理解和维护
3)代码复用成本高(高阶组件容易使代码量剧增)
2.缺点
(1)还有两个类组件的生命周期函数不能用 hooks 替代
getSnapshotBeforeUpdate 和 componentDidCatch
(2)写法上有限制
因为 hook 是一个 链式的数据结构 而不是数组,所以不能嵌套,不能在条件判断、循环中使用,会破坏链式结构;
只能在函数顶层使用,增加了重构旧代码的成本;
react hook 需要利用调用顺序来更新状态和调用钩子函数,放到循环或条件分支中,可能导致调用顺序不一致,导致奇怪的 bug;
(3)使用 useState 时,数组对象,使用 push、pop、splice 直接更新,无效;
比如 let [nums, setNums] = useState([0,1,2]);
nums.push(1) 无效,必须使用 nums=[…nums, 1],再 setNums(nums);
类组件中直接 push 是没问题的;
(4)不能使用装饰器
(5)额外的学习成本
(6)破坏了 PureComponent、React.memo 浅比较的性能优化效果;
为了取最新的 props 和 state,每次 render()都要重新创建事件处理函数;
(7)在闭包场景可能会引用到旧的 state、props 值;
解决方法
1.不要在 useEffect 里面写太多的依赖项,划分这些依赖项成多个单一功能的 useEffect。“单一职责模式”
2.如果你碰到状态不同步的问题,可以考虑下手动传递参数到函数,而不是取父级作用域的值;
3.使用 useRef,保证同一个引用;
五).hook 的原理和实现
1.useState 的实现
2.useEffect
其他的后续再补上
六).关于 hook 常见的面试题
1.React 的 useState 和类组件的 state 区别?
(1)类组件的 state 必须是对象,而 useState 可以是 基本类型或者对象 ;
(2)类组件新的 state 会和旧的进行 merge,而 useState 新的 state 是 直接替换旧的 state;
2.useState 为什么使用数组而不是对象
(1)useState 使用了 JS 的解构赋值思想
数组是有序的,解构顺序也是有序的,但变量名可以任意命名;
对象是无序的,解构是必须变量名和属性名相同才行;
数组会更加灵活,任意命名
(2)一个组件可以有多个 useState,为了避免冲突确保准确性,使用数组而不是对象;
3.在无状态组件每一次函数上下文执行的时候,react 用什么方式记录了 hooks 的状态?
无论是类组件调用 setState,还是函数组件的 dispatchAction ,都会产生一个 update 对象,里面记录了此次更新的信息和状态,然后将此 update 放入待更新的 pending 队列中;
4.useEffect,useMemo 中,为什么 useRef 不需要依赖注入,就能访问到最新的改变值?
函数组件更新 useRef 做的事情更简单,就是返回了缓存下来的值,也就是无论函数组件怎么执行,执行多少次,hook.memoizedState 内存中都指向了一个对象,所以 useEffect,useMemo 中 useRef 不需要依赖注入,就能访问到最新的改变值。
5.useMemo 是怎么对值做缓存的?
原理很简单,就是判断两次 deps 是否相等,如果不想等,证明依赖项发生改变,那么执行 useMemo 的第一个函数,得到新的值,然后重新赋值给 hook.memoizedState,如果相等 证明没有依赖项改变,那么直接获取缓存的值。
不过这里有一点,值得注意,nextCreate()执行,如果里面引用了 usestate 等信息,变量会被引用,无法被垃圾回收机制回收,就是闭包原理,那么访问的属性有可能不是最新的值,所以需要把引用的值,添加到依赖项 dep 数组中。每一次 dep 改变,重新执行,就不会出现问题了。
九.suspense
React 16.6 新增了 组件,它主要是解决运行时的 IO 问题:
代码分片,异步获取数据
一.代码分片、异步加载
1.使用
只打包条件为 true 的组件;
2.原理
(1)react 目前还是同步渲染,一路走到黑,如上要画 clock, 现在没有,会抛一个异常出来, componentDidCatch 和 getDerivedStateFromError , 这两个函数就是来捕获异常;
(2)下载资源的时候会抛出一个 promise, 会有地方(这里是 suspense)捕捉这个 promise;suspense 实现了 getDerivedStateFromError
(3)捕获到异常的时候, 然后就 等这个 promise resolve 等这个 promise resolve, reRender,它会尝试重新画一下子组件。
伪代码:
简易 demo
二.异步获取数据
1.使用
2.原理
resource.read()会调 api, 返回一个 promise, 上面会有 suspense 抓住, 等 resolve 的时候,再画一下, 就达到看起来是同步的样子;
React17
1.规划
(1)去掉不安全的生命周期。
(2)concurrent mode 并发模式
2.concurrent mode 并发模式、优点和缺点
并发模式主要由下面两部分组成
- 基于 fiber 实现的可中断更新的架构
- 基于调度器的优先级调度
【优点】
1.快速响应用户操作和输入,提升用户交互体验
2.让动画更加流畅,通过调度,可以让应用保持高帧率
3.利用好 I/O 操作空闲期或者 CPU 空闲期,进行一些预渲染。比如离屏(offscreen)不可见的内容,优先级最低,可以让 React 等到 CPU 空闲时才去渲染这部分内容。这和浏览器的 preload 等预加载技术差不多。
4.用 Suspense 降低加载状态(load state)的优先级,减少闪屏。比如数据很快返回时,可以不必显示加载状态,而是直接显示出来,避免闪屏;如果超时没有返回才显式加载状态。
【缺点】
因为浏览器无法实现抢占式调度,无法阻止开发者做傻事的,开发者可以随心所欲,想挖多大的坑,就挖多大的坑。
React Fiber 本质上是为了解决 React 更新低效率的问题,不要期望 Fiber 能给你现有应用带来质的提升, 如果性能问题是自己造成的,自己的锅还是得自己背.
react18 的更新点
1.Root API 更新
(1)之前是
(2)React 18 之后改成
可以为一个 React App 创建多个根节点,甚至在未来可以用不同版本的 React 来创建。
(3)React18 保留了上述两种用法,老项目不想改仍然可以用 ReactDOM.render() ;
新项目想提升性能,可以用 ReactDOM.createRoot() 借并发渲染的东风。
2.startTransition API(用于非紧急状态更新)
UI 更新分紧急和不紧急,给不紧急的加 startTransition(() => {}),剩下更多资源留给紧急更新,可以让渲染更顺畅;
1.作用
用于非紧急状态更新;也就是把传入的函数设置为低优先级;
2.使用
3.源码类似如下的伪代码
当调用 startTransition,在其上下文中获取到的全局变量 isInTransition 为 true。
如果 startTransition 的回调函数 fn 中包含更新状态的方法(比如上文 Demo 中的 setSomeData),那么这次更新就会被标记为 isTransition,代表这是一个低优先级的过渡更新;然后走内部的调度、批处理、更新流程了;
3.渲染的自动批处理 Automatic batching 优化
主要解决异步回调中无法批处理的问题
4.SSR 架构(Server-Side Rendering )
(1)Streaming HTML,不用等 HTML 全部返回来就可以渲染,
支持 Suspense 组件,遇到
这种,会先渲染 Spinner,等数据好了后再通过行内 script 的方式动态添加;
(2)选择性 Hydration,不用等所有 JavaScript 加载完一次性 Hydration
5.生命周期
之前只触发一次可能会触发多次。
v18 的 Strict Mode,由于包含了 Strict Effect 规则,mount 时的 useEffect 逻辑会被重复执行。