react15慢在哪里
React15之前的协调过程是同步的,也叫stack reconciler,同时因为js的执行是单线程的,这就导致了在更新比较耗时的任务时,不能及时响应一些高优先级的任务,比如用户的输入,所以页面就会卡顿,这就是cpu的限制
,还有一个是IO限制
。
CPU限制
我们都知道,GUI渲染线程
与JS线程
是互斥的。所以JS脚本执行和浏览器布局、绘制不能同时执行。
在每16.6ms时间内,需要完成如下工作:
JS脚本执行 ----- 样式布局 ----- 样式绘制
当JS执行时间过长,超出了16.6ms,这次刷新就没有时间执行样式布局和样式绘制了,就会造成页面的卡顿,那么如何解决这个问题呢?
解决
试想一下,如果我们在日常的开发中,在单线程的环境中,遇到了比较耗时的代码计算会怎么做呢,首先我们可能会将任务分割,让它能够被中断,在其他任务到来的时候让出执行权,当其他任务执行后,再从之前中断的部分开始异步执行剩下的计算。所以关键是实现一套异步可中断的方案。
在浏览器每一帧的时间中,预留一些时间给JS线程,
React
利用这部分时间更新组件,可以看到,在源码中,预留的初始时间是5ms,当预留的时间不够用时,React
将线程控制权交还给浏览器使其有时间渲染UI,React
则等待下一帧时间到来继续被中断的工作
开启ConCurrent Mode
启用时间切片
// 通过使用ReactDOM.unstable_createRoot开启Concurrent Mode
// ReactDOM.render(<App/>, rootEl);
ReactDOM.unstable_createRoot(rootEl).render(<App/>);
此时我们的长任务被拆分到每一帧不同的task
中,JS脚本
执行时间大体在5ms
左右,这样浏览器就有剩余时间执行样式布局和样式绘制,减少掉帧的可能性。
没有开启ConCurrent Mode
开启ConCurrent Mode
Io瓶颈
当我们从一个页面进入另一个页面的时候,如果请求的时间过长,就会让用户觉得明显觉得很卡顿,所以,React
让请求时间超过一个范围,那么会显示一个Loading
效果。
解决
React
实现了Suspense 功能及配套的hook
——useDeferredValue ,而在源码内部,为了支持这些特性,同样需要将同步的更新变为可中断的异步更新。
react三大概念
-
Fiber
:react15的更新是同步的,因为它不能将任务分割,所以需要一套数据结构让它既能对应真实的dom又能作为分隔的单元,这就是Fiber。 -
Scheduler
:有了Fiber,我们就需要用浏览器的时间片异步执行这些Fiber的工作单元,我们知道浏览器有一个api叫做requestIdleCallback
,它可以在浏览器空闲的时候执行一些任务,我们用这个api执行react的更新,让高优先级的任务优先响应不就可以了吗,但事实是requestIdleCallback
存在着浏览器的兼容性和触发不稳定的问题,所以我们需要用js实现一套时间片运行的机制,在react中这部分叫做scheduler
。 -
Lane
:有了异步调度,我们还需要细粒度的管理各个任务的优先级,让高优先级的任务优先执行,各个Fiber工作单元还能比较优先级,相同优先级的任务可以一起更新,想想是不是更cool呢。
代数效应
除了cpu的瓶颈问题,还有一类问题是和副作用相关的问题,比如获取数据、文件操作等。不同设备性能和网络状况都不一样,react怎样去处理这些副作用,让我们在编码时最佳实践,运行应用时表现一致呢,这就需要react有分离副作用的能力,为什么要分离副作用呢,因为要解耦,这就是代数效应。
代数效应是函数式编程中的一个概念,用于将副作用从函数调用中分离。
代数效应
能够将副作用
(比如请求图片数量
)从函数逻辑中分离,使函数关注点保持纯粹
解耦副作用在函数式编程的实践中非常常见,例如redux-saga
,将副作用从saga中分离,自己不处理副作用,只负责发起请求。
function* fetchUser(action) {
try {
const user = yield call(Api.fetchUser, action.payload.userId);
yield put({type: "USER_FETCH_SUCCEEDED", user: user});
} catch (e) {
yield put({type: "USER_FETCH_FAILED", message: e.message});
}
}
严格上讲react不支持Algebraic Effects
的,但react有Fiber
啊,执行完Fiber的更新之后交还执行权给浏览器,让浏览器决定后面怎么调度,由此可见Fiber得是一个链表结构才能达到这样的效果,Suspense
也是这种概念的延伸。
React 16架构
react的核心可以用ui=fn(state)
来表示,更详细可以用
const state = reconcile(update);
const UI = commit(state);
上面的fn可以分为如下一个部分:
Scheduler
(调度器): 排序优先级,让优先级高的任务先进行reconcile
Reconciler
(协调器): 负责找出变化的组件,并打上不同的Flags
(旧版本react叫Tag)Renderer
(渲染器): 将Reconciler
中打好标签的节点渲染到视图上
相较于React15,React16中新增了Scheduler(调度器)
Scheduler (调度器)
其实部分浏览器已经实现了这个API,这就是requestIdleCallback 。但是由于以下因素,React
放弃使用:
- 浏览器兼容性
- 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的
requestIdleCallback
触发的频率会变得很低
基于以上原因,React
实现了功能更完备的requestIdleCallbackpolyfill
,这就是Scheduler。除了在空闲时触发回调的功能外,Scheduler还提供了多种调度优先级供任务设置
Reconciler(协调器)
react-reconciler/src/ReactFiberWorkLoop.new.js
line 1646
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
更新工作从递归变成了可以中断的循环过程。每次循环都会调用shouldYield
判断当前是否有剩余时间
React16是如何解决中断更新时DOM渲染不完全的问题?
Reconciler与Renderer不再是交替工作。当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记,类似这样
react/packages/reactreconciler/src/ReactFiberFlags.js
export const Placement = /* */ 0b0000000000010;
export const Update = /* */ 0b0000000000100;
export const PlacementAndUpdate = /* */ 0b0000000000110;
export const Deletion = /* */ 0b0000000001000;
整个Scheduler与Reconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer
Renderer(渲染器)
Renderer根据Reconciler为虚拟DOM打的标记,同步执行对应的DOM操作
render阶段遍历Fiber树类似dfs的过程,捕获
阶段发生在beginWork函数中,该函数做的主要工作是创建Fiber节点,计算state和diff算法,冒泡
阶段发生在completeWork中,该函数主要是做一些收尾工作,例如处理节点的props、和形成一条effectList的链表,该链表是被标记了更新的节点形成的链表
Fiber
Fiber对象上面保存了包括这个节点的属性、类型、dom等,Fiber通过child、sibling、return(指向父节点)来形成Fiber树。
// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;
举个例子:
function App() {
return (
<div>
你好啊
<span>李银河</span>
</div>
)
}
对应的Fiber架构为:
还保存了更新状态时用于计算state的updateQueue,updateQueue是一种链表结构,上面可能存在多个未计算的update,update也是一种数据结构,上面包含了更新的数据、优先级等,除了这些之外,上面还有和副作用有关的信息。
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 作为静态数据结构的属性
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// 用于连接其他Fiber节点形成Fiber树
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
// 作为动态的工作单元的属性
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;
}
Fiber 双缓存
Q: 什么是双缓存?
双缓存
是指存在两颗Fiber树,current Fiber树
描述了当前呈现的dom树,workInProgress Fiber
是正在更新的Fiber树,这两颗Fiber树都是在内存中运行的,在workInProgress Fiber
构建完成之后会将它作为current Fiber
应用到dom上。这种在内存中构建并直接替换的技术叫做双缓存
Fiber树与双缓存
current Fiber树
中的Fiber节点
被称为current fiber
,workInProgress Fiber树
中的Fiber节点
被称为workInProgress fiber
,他们通过alternate
属性连接。
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
React
应用的根节点通过使current
指针在不同Fiber树
的rootFiber
间切换来完成current Fiber
树指向的切换。即当workInProgress Fiber树
构建完成交给Renderer
渲染在页面上后,应用根节点的current
指针指向workInProgress Fiber树
,此时workInProgress Fiber树
就变为current Fiber树
。
每次状态更新都会产生新的workInProgress Fiber树
,通过current
与workInProgress
的替换,完成DOM
更新。
例如,我们吧上面的代码改成
function App() {
return (
<div>
你好啊
<span>YogLn</span>
</div>
)
}
对应的workInProgress Fiber
和current Fiber
在mount时(首次渲染),会根据jsx对象(Class Component或的render函数者Function Component的返回值),构建Fiber对象,形成Fiber树,然后这颗Fiber树会作为current Fiber
应用到真实dom上
在update(状态更新时如setState)的时候,会根据状态变更后的jsx对象和current Fiber
做对比形成新的workInProgress Fiber
,然后workInProgress Fiber
切换成current Fiber
应用到真实dom就达到了更新的目的,而这一切都是在内存中发生的,从而减少了对dom好性能的操作。