requestAnimationFrame 和 requestIdleCallback
通常情况下,显示设备屏幕刷新率为 60Hz,即 1 秒 60 帧,折合每帧平均时间约 16.6ms。
浏览器无法严格控制每16.6ms执行一帧,而是通过vSync标识符动态控制每帧执行时间,(非阻塞情况下)大约为16.6ms左右。
在高刷新率显示设备上每帧时间会缩短。
浏览器对每一帧的执行和渲染的流程分为7个阶段,如下图:
- 第1-3阶段:js执行阶段,分别为用户事件回调、定时器回调、窗口变更事件回调
- 第4阶段:rAF阶段,即window.requestAnimationFrame回调执行阶段
- 第5-6阶段:页面渲染阶段,前3阶段js执行时间过长将阻塞渲染,导致页面卡顿
- 第7阶段:帧空闲阶段,即window.requestIdleCallback回调执行阶段。若前6阶段运行时间超过16.6ms,则该回调不会执行
requestIdleCallback调用示例:
/*
为requestIdleCallback绑定回调函数
第一个参数:回调函数,必选
第二个参数:配置选项,可选
配置选项中timeout为强制执行超时时间毫秒数
如果超过指定时间浏览器仍没有空闲则强制执行
*/
const handle = window.requestIdleCallback(callback, {timeout: 1000});
// callback方法接收一个deadline对象参数
function callback(deadline){
// deadline.timeRemaining() 返回当前帧剩余空闲时间毫秒数
console.log(deadline.timeRemaining())
// deadline.didTimeout 返回当前回调是否已经触发超时
console.log(deadline.didTimeout)
}
// 取消requestIdleCallback回调
window.cancelIdleCallback(handle)
注意:
目前只有chrome浏览器支持requestIdleCallback,React并不使用requestIdleCallback进行调度,而是使用MessageChannel实现了schedule package(调度包),概念上模拟requestIdleCallback。
fiber 和 Virtual DOM
fiber是React最小工作单元,每一个Virtual DOM对应一个fiber工作单元(但不是每一个fiber工作单元都有对应的Virtual DOM)。
在React应用中,通过createElement方法或JSX语法创建Virtual DOM,通过ReactDOM的render方法将Virtual DOM渲染到页面。
在这个过程中,render方法会创建根fiber,将传入的Virtual DOM作为根fiber的children,将传入的页面容器元素作为对应真实DOM,将根fiber作为下一工作单元,开启fiber执行工作循环。
workLoop
workLoop即负责运行fiber工作单元调度的fiber执行工作循环。
React通过fiber架构 将一次更新任务划分为以一个fiber作为一个工作单元的多个子任务,并对子任务设置执行优先级,使用schedule package对子任务执行进行调度,优先执行重要任务,异步执行低优先级任务,减少主任务进程运行(阻塞)时间。
在每一次帧空闲阶段,workLoop会运行一个或多个fiber工作单元,然后让出调度权给浏览器来处理用户事件等高优先级工作,在下一帧空现阶段,workLoop会从上一次帧空闲阶段结束时停止的fiber继续运行,直到fiber tree全部fiber运行完成,或者(如触发重绘时)丢弃之前没运行完的fiber tree,重新从根fiber开始运行并生成新的fiber tree,如下图:
每一个fiber工作单元运行时会执行三项核心工作:
- 为 当前 fiber 创建 真实DOM(如果没有创建)
- 执行 reconcileChildren,遍历 children Virtual DOM,创建 对应的 fiber,生成 fiber tree
- 根据 fiber tree 返回下一个工作单元
随着工作循环执行,会生成完整Virtual DOM树和对应的完整fiber tree。
在组件首次渲染到页面时,React 把对应容器元素创建的根fiber和根据Virtual DOM生成的子fiber为组成一个完整fiber tree。
在之后每次组件更新时,React 把当前Virtual DOM树和上一次渲染创建的fiber tree通过diff算法生成一个新的fiber tree。
fiber tree
fiber代码示例:
// 使用JSX语法<span key="1"></span>创建Virtual DOM,其对应fiber实例可能如下
{
stateNode: new HTMLSpanElement, // 真实DOM
type: "span", // 组件类型:原生组件为HTML标签名,类组件为构造函数,函数组件为函数自身
tag: 5, // fiber类型:原生类组件为5,类组件为1
alternate: null, // 上一次render生成的fiber
key: "1", // key属性用于同级元素列表diff优化
updateQueue: null, // 用于状态更新,回调函数,DOM更新的队列
memoizedState: null,// 用于创建输出的fiber状态
memoizedProps: {children: 0},// 上一次render创建的props
pendingProps: {children: 0}, // 当前已更新且待应用到子组件或DOM元素的props
effectTag: 0, // effect类型
nextEffect: null // 下一个effct指针,用于effect串联为effect list
...
}
所有fiber工作单元以链表形式组成fiber tree,其中每一个fiber工作单元通过child、sibling、return三个指针进行关联。
fiber tree 结构示例:
const A1 = {key:'A1', tag:'div'}
const B1 = {key:'B1', tag:'div'}
const C1 = {key:'C1', tag:'div'}
const C2 = {key:'C2', tag:'div'}
const B2 = {key:'B2', tag:'div'}
A1.child = B1;
B1.return = A1
B1.child = C1;
C1.return = B1
C1.sibling = C2;
C2.return = B1;
B1.sibling = B2
B2.return = A1
const fiberTree = A1;
reconcile 阶段
在运行fiber工作单元过程中,会执行 reconcileChildren方法,React会进行diffing计算,遍历当前虚拟DOM树和上一次更新生成的fiber tree并进行比对,找出需要调用getDerivedStateFromProps和shouldComponentUpdate生命周期函数的组件并执行,找出需要调用其他生命周期函数的组件,找出需要更新的真实DOM,创建出新的fiber tree,在对应fiber上创建effect,将全部effect构建成effect list链表。
该阶段不会导致任何用户可见的更改(如真实DOM更新),允许中断执行或通过调度实现异步执行。
effect list
在构造 fiber tree过程中,React将需要执行的生命周期函数、真实DOM更新操作等effect保存到对应fiber中,所有的effect以链表形式保存为effect list。
在commit 阶段,通过effect list获取全部effect进行批量更新。
// 生成 effect list
// effect list 基于根节点生成,每个fiber的firstEffect指向其第一个子fiber,lastEffect指向其最后一个子effect,nextEffect指向下一个effect,组成单链表结构
function collectFiberEffect(fiber){
let returnFiber = fiber.return;
if(returnFiber){ // 有父级
if(!returnFiber.firstEffect){ // 父级未定义firstEffect
returnFiber.firstEffect = fiber.firstEffect // 定义父级fiber的firstEffect为当前fiber的firstEffect
}
if(fiber.lastEffect){ // 当前fiber已经定义lastEffect
if(returnFiber.lastEffect){ // 父级已经定义lastEffect
// 父级lastEffect已经不是最后一个,
returnFiber.lastEffect.nextEffect = fiber.lastEffect //定义父级fiber的lastEffect的nextEffect为当前fiber的lastEffect
}else{ // 父级未定义lastEffect 即 当前为第一个effect
returnFiber.firstEffect = fiber // 定义firstEffect
}
}
const effectTag = fiber.effectTag
if(effectTag){ // 有effect
if(returnFiber.lastEffect){ // 已经定义lastEffect
// returnFiber.lastEffect已经不是最后一个,定义其nextEffect为当前effect
returnFiber.lastEffect.nextEffect = fiber // 定义nextEffect
}else{ // 未定义lastEffect 即 当前为第一个effect
returnFiber.firstEffect = fiber // 定义firstEffect
}
returnFiber.lastEffect = fiber // 更新lastEffect 到当前 effect
}
}
}
commit 阶段
当所有fiber工作单元运行完成后,新的fiber tree、收集了全部effect的effect list链表、更新后的Virtual DOM树 也随之构建完成,workLoop会将新的fiber tree从根fiber进行commit。
在commit阶段,React将遍历执行effect list中全部effect,通过effectTag执行相应的真实DOM更新或生命周期函数运行操作:
- Snapshot:执行getSnapshotBeforeUpdate生命周期函数
- Placement:执行componentDidMount生命周期函数,创建真实DOM
- Update:执行componentDidUpdate生命周期函数,更新真实DOM
- Deletion:执行componentWillUnmount生命周期函数,删除真实DOM
由于使用effect list实现了仅对需要更新的fiber的遍历,避免了遍历整个fiber tree,减少了实际更新的真实DOM,从而实现性能提升。
该阶段会将发生改变的真实DOM渲染或更新到页面,必须同步执行且一次性执行完成不可中断。
实现原理示例:
// React.createElement方法
function createElement(type, props, ...children) {
// 返回创建的Virtual DOM
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object"
? child
: createTextElement(child) // 文本节点类型
),
},
}
}
// 创建文本节点类型Virtual DOM
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
const React = { createElement }
// 接收key,返回key是否以"on"开头,用来简单判断是否是事件方法属性
const isEvent = key => key.startsWith("on")
// 接收key,返回key是否不等于"children"且不是事件方法属性
const isProperty = key => key !== "children" && !isEvent(key)
// 从fiber创建真实DOM
function createDom(fiber) {
const dom = // 创建真实DOM
fiber.type === "TEXT_ELEMENT" // 判断虚拟DOM类型是否为文本节点
? document.createTextNode("")
: document.createElement(fiber.type)
// 更新真实DOM
updateDom(dom, {}, fiber.props)
// 返回真实DOM
return dom
}
// 接收prev和next,返回一个函数 :接收key,返回next的key是否不等于prev的key
const isNew = (prev, next) => key => prev[key] !== next[key]
// 接收prev和next,返回一个函数:接收key,返回next中是否不存在key
const isGone = (prev, next) => key => !(key in next)
// 更新真实DOM
function updateDom(dom, prevProps, nextProps){
// 移除新props中没有或变更的事件方法属性
Object.keys(prevProps)
.filter(isEvent) // 选择事件方法属性
.filter(
key =>
!(key in nextProps) || // 新props中不存在
isNew(prevProps, nextProps)(key) // 新props和老props不同
)
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.removeEventListener( // 取消事件绑定
eventType,
prevProps[name]
)
})
// 移除老props中存在,新props中不存在的属性
Object.keys(prevProps) // 遍历 老props
.filter(isProperty) // 过滤children属性和事件方法属性
.filter(isGone(prevProps, nextProps)) // 选择新props中不存在的属性
.forEach(name => {
dom[name] = "" // 置空
})
// 设置新props中新增或变更的属性
Object.keys(nextProps) // 遍历新props
.filter(isProperty) // 过滤children属性和事件方法属性
.filter(isNew(prevProps, nextProps))// 选择新props和老props值不同的属性
.forEach(name => {
dom[name] = nextProps[name] // 重新赋值
})
// 重新设置 事件方法属性
Object.keys(nextProps)
.filter(isEvent) // 选择事件方法属性
.filter(isNew(prevProps, nextProps))// 选择新props和老props值不同的属性
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener( // 添加事件绑定
eventType,
nextProps[name]
)
})
}
// 下一个fiber工作单元
let nextUnitOfWork = null
// 当前工作中的根fiber
let wipRoot = null
// 上一次提交的根fiber
let currentRoot = null
// 需要删除的fiber的数组
let deletions = null
// 提交根fiber
function commitRoot() {
// 处理需要删除的fiber
deletions.forEach(commitWork)
// 提交当前工作中的根fiber的第一个子fiber
commitWork(wipRoot.child)
// 将当前工作中的根fiber更新到上一次提交的根fiber
currentRoot = wipRoot
// 置空当前工作中的根fiber
wipRoot = null
}
// 提交fiber
function commitWork(fiber) {
if (!fiber) { // fiber tree 遍历完成
return
}
let domParentFiber = fiber.parent // 父fiber
// 函数组件和类组件自身没有对应的真实DOM
while (!domParentFiber.dom) { // 判断是否存在真实DOM
// 查找最近的具有真实DOM的父fiber,作为当前fiber的真实DOM的容器
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom // 父fiber真实DOM
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom !== null
) {
domParent.appendChild(fiber.dom) // 添加当前fiber真实DOM到父级
} else if (
fiber.effectTag === "UPDATE" &&
fiber.dom !== null
) {
updateDom( // 根据当前fiber更新真实DOM
fiber.dom,
fiber.alternate.props,
fiber.props
)
} else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent) // 移除需要删除的真实DOM
}
/*
通过两次递归调用commitWork,
先后分别传入子fiber和兄弟fiber,
实现深度优先遍历,
即有子fiber则先遍历子fiber,直到fiber tree最深层级子fiber,
此时没有子fiber,开始遍历兄弟fiber,直到同级fiber遍历完成,
此时没有兄弟fiber,如此直到整个fiber tree遍历完成。
React在fiber tree中实际发生改变的fiber上创建effect,
将effect构建成一个收集了全部fiber变更的effect list链表,
在commit阶段执行effect list的全部effect,
实现了只对实际发生改变的fiber的对应DOM进行更新,
避免了遍历整个fiber tree造成的性能浪费。
*/
commitWork(fiber.child) // 提交第一个子fiber
commitWork(fiber.sibling) // 提交下一个兄弟fiber
}
function commitDeletion(fiber, domParent) {
if (fiber.dom) { // 原生组件
domParent.removeChild(fiber.dom)
} else { // 函数组件和类组件,没有对应真实DOM,删除其child
commitDeletion(fiber.child, domParent)
}
}
// ReactDOM.render方法
function render(element, container) {
// 创建根fiber,更新到当前工作中的根fiber
wipRoot = {
dom: container, // 根fiber对应真实DOM即container
props: {
children: [element],
},
alternate: currentRoot, // 记录上一次提交的fiber
}
// 置空需要删除的fiber的数组
deletions = []
// 将根fiber设置为下一个工作单元
nextUnitOfWork = wipRoot
}
const ReactDOM = { render };
// 运行fiber的工作循环
function workLoop(deadline){
let shouldYield = false; // 超时标记
// 有帧空闲时间 且 有工作单元待运行
while(nextUnitOfWork && !shouldYield){
nextUnitOfWork = performUnitOfWork(nextUnitOfWork) // 运行fiber并返回下一个fiber
shouldYield = deadline.timeRemaining() < 1; // 更新超时标记
}
if(!nextUnitOfWork && wipRoot){ // 下一个工作单元为空 且 当前工作中的根fiber不为空
commitRoot() // 提交当前工作中的根fiber
}
if(nextUnitOfWork){ // 下一个工作单元不为空
requestIdleCallback(workLoop, {timeout: 167}) // 注册下一次帧空闲时间回调
}
}
requestIdleCallback(workLoop, {timeout: 167})
// 运行fiber工作单元
function performUnitOfWork(fiber){
/*
原生组件的type为一个通过React.createElement创建的虚拟DOM对象,
类组件的type为组件类自身(组件构造函数),
函数组件的type为组件函数自身。
类组件和函数组件无法通过props直接获取属性,
类组件需要创建组件实例,函数组件需要执行组件方法,
然后各自从返回的虚拟DOM对象来获取其props。
*/
const isFunctionComponent = fiber.type instanceof Function
const isClassComponent = fiber.type instanceof Class
if (isFunctionComponent) { // 判断组件类型
updateFunctionComponent(fiber) // 函数组件
}
else if(isClassComponent){
updateClassComponent(fiber) // 类组件
}
else {
updateHostComponent(fiber) // 原生组件
}
/*
注意:事实上,React源码中并没有完整遍历fiber tree,
而是通过一些提示和试探性判断方法跳过了没有改变的完整子树
*/
if (fiber.child) { // 如果有子fiber
return fiber.child // 第一个子fiber即下一个工作单元
}
// 如果没有子fiber
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) { // 如果有兄弟fiber
return nextFiber.sibling // 下一个兄弟fiber即下一个工作单元
}
// 如果没有兄弟fiber 返回父级fiber查找
nextFiber = nextFiber.parent
}
}
// 原生组件更新方法
function updateHostComponent(fiber) {
if (!fiber.dom) {
// 创建 fiber 对应 真实DOM
fiber.dom = createDom(fiber)
}
const elements = fiber.props.children
reconcileChildren(fiber, elements) // 调和 子 虚拟DOM
}
// 函数组件更新方法
function updateFunctionComponent(fiber) {
// 执行组件方法返回Virtual DOM,创建数组作为children
const children = [fiber.type(fiber.props)]
// children 执行 reconcile ,进行diffing计算,与原生组件逻辑相同
reconcileChildren(fiber, children)
}
// 类组件更新方法
function updateClassComponent(fiber) {
...
}
// 调和子 虚拟DOM
function reconcileChildren(wipFiber, elements){
let index = 0
// 获取上一次提交的fiber的第一个子fiber
let oldFiber = wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null
// 遍历 子 虚拟DOM 和 上一次提交的fiber
while (
index < elements.length ||
oldFiber !== null
) {
const element = elements[index]
let newFiber = null
// diffing ...
const sameType =
oldFiber && // 有老fiber
element && // 有新元素
element.type === oldFiber.type // 相同type
// 为 子 虚拟DOM 创建 fiber
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props, // 仅更新props
dom: oldFiber.dom, // 真实DOM保留
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE", // effect类型 更新
}
}
if (element && !sameType) { // 有新元素 不同type
newFiber = {
type: element.type,
props: element.props,
dom: null, // 真实DOM 待创建
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT", // effect类型 创建
}
}
if (oldFiber && !sameType) { // 有老fiber 不同type
oldFiber.effectTag = "DELETION" // effect类型 删除
/*
对于需要移除的节点,
不会在当前进行中的根fiber的fiber tree中创建新fiber,
而老fiber则存在于上一次提交的根fiber的fiber tree中,
即currentRoot中有老fiber,
wipRoot中既无新fiber也无老fiber,
故需要维护一个用来记录将要删除的节点的数组
*/
deletions.push(oldFiber)
}
/*
事实上,React在构建和更新fiber tree时,
非每次都创建新fiber删除老fiber,
而是尽量循环利用原有fiber
*/
/*
注意:React中的diff算法还包含了通过key优化children比对等
*/
// 将 子 虚拟DOM 的 fiber 添加到 fiber tree
if (index === 0) {
wipFiber.child = newFiber // 第一个子fiber
} else {
prevSibling.sibling = newFiber // 第一个以后的子fiber
}
prevSibling = newFiber
index++
}
}
调用示例:
/** @jsx React.createElement */
const container = document.getElementById("root")
function App (props){
return (<div>
<h2>Hello {props.name}</h2>
</div>)
}
const element = <App name="Ronnie"/>
ReactDOM.render(element, container)