学习React之Fiber和diff
为什么会产生Fiber?
- 在React16之前的版本,diff过程是不可中断的,意味着如果执行diff时间过长,会阻塞了渲染,造成卡顿,为了避免这种情况,让diff过程可以中断,并且不阻塞渲染。
- 如何将diff可以中断呢?首先将diff过程拆分成一个个小单元,每完成一个后如果有其他事情要做则可以中断,恢复的时候接着执行下一个单元即可。为了组织这些小单元,产生了Fiber Tree。
- 如何判断需要中断呢?借鉴
requestIdleCallback
方法,会传递一个timeRemaining
方法,该方法可以实时拿到剩余时间。 - react没有使用
requestIdleCallback
,原因是效率不够
看看Fiber的结构
- 每个 Fiber 的数据都来自于元素树中的一个元素,元素与 Fiber 是一一对应的。与元素树不同的是,元素树每次渲染都会被重建,而 Fiber 会被复用,Fiber 的属性会被更新。
type Fiber = {
// ---- Fiber类型 ----
/** 工作类型,枚举值包括:函数组件、类组件、HTML元素、Fragment等 */
tag: WorkTag,
/** 就是那个子元素列表用的key属性 */
key: null | string,
/** 对应React元素ReactElmement.type属性 */
elementType: any,
/** 函数组件对应的函数或类组件对应的类 */
type: any,
// ---- Fiber Tree树形结构 ----
/** 指向父FiberNode的指针 */
return: Fiber | null,
/** 指向子FiberNode的指针 */
child: Fiber | null,
/** 指向平级FiberNode的指针 */
sibling: Fiber | null,
// ---- Fiber数据 ----
/** 经本次渲染更新的props值 */
pendingProps: any,
/** 上一次渲染的props值 */
memoizedProps: any,
/** 上一次渲染的state值,或是本次更新中的state值 */
memoizedState: any,
/** 各种state更新、回调、副作用回调和DOM更新的队列 */
updateQueue: mixed,
/** 为类组件保存对实例对象的引用,或为HTML元素保存对真实DOM的引用 */
stateNode: any,
// ---- Effect副作用 ----
/** 副作用种类的位域,可同时标记多种副作用,如Placement、Update、Callback等 */
flags: Flags,
/** 指向下一个具有副作用的Fiber的引用,在React 18中貌似已被弃用 */
nextEffect: Fiber | null,
// ---- 异步性/并发性 ----
/** 当前Fiber与成对的进行中Fiber的双向引用 */
alternate: Fiber | null,
/** 标记Lane车道模型中车道的位域,表示调度的优先级 */
lanes: Lanes
};
- Fiber 与 Fiber 之间,并没有按照传统的 parent-children 方式建立树形结构。而是在父节点和它的第一个子节点间,利用child 和 return 属性建立了双向链表。节点与它的平级节点间,利用 sibling 属性建立了单向链表,同时平级节点的 return 属性,也都被设置成和单向链表起点的节点 return 一样的值引用。
创建虚拟DOM
function createElement(type, config, children) {
let propName
const props = {}
let key = null
let ref = null
if (config) {
if (config.key) {
key = '' + config.key;
}
if (config.ref) {
ref = config.ref
}
for (propName in config) {
if (!RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName]
}
}
}
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
const element = ReactElement(
type,
key,
ref,
undefined,
undefined,
ReactSharedInternals.owner,
props,
);
return element
}
- 虚拟DOM其实是一个JS对象
优缺点
- 有跨平台的兼容性,都可以使用同一套虚拟DOM来描述视图内容。
- 减少了操作真实DOM的实际次数。
- 渲染大量虚拟DOM的时候,由于多了一层虚拟DOM的计算,速度会比正常的要慢。
渲染
let workInProgress = null
let currentRootFiber = null
let nextUnitOfWork = null
let deletions = null // 记录需要删除的fiber
function render(element, container) { // 构建当前渲染的fiber树
workInProgress = {
dom: container,
props: {
child: [element]
},
alternate: currentRootFiber // 后续需要进行新旧对比
}
deletions = []
nextUnitOfWork = workInProgress
}
ReactDOM.render(element, container)
可中断过程
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
// timeRemaining方法返回实时剩余时间
// didTimeout属性用于指示回调是否因为超过了设定的超时时间而被触发
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop) // 留到下一个空闲时间执行
}
requestIdleCallback(workLoop)
function performUnitOfWork(fiber) {
// 1. 给新fiber添加dom
// 2. 创建新的fiber
const isFunc = fiber.type instanceof Function
if (isFunc) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
// 3. 返回下一个工作单元
if (fiber.child) { // 先递归子节点
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) { // 没有则寻找兄弟节点
return nextFiber.sibling
}
nextFiber = nextFiber.return // 否则返回到父节点,父节点的child必然递归过了,就接着寻找其兄弟节点即可
}
}
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
// 此时还不能向fiber.return.dom添加当前dom,由于是可中断过程,如果添加了容易导致ui新旧dom同时存在
reconcileChildren(fiber, fiber.props.children)
}
协调阶段
function reconcileChildren(fiber, elements) {
let index = 0
let oldFiber = fiber.alternate && fiber.alternate.child
let prevSibling = null
while (index < elements.length || oldFiber !== null) {
const element = elements[index]
let newFiber = null
// 对比oldFiber和新DOM
const sameType = oldFiber && element && oldFiber.type === element.type
if (sameType) {
// update
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
return: fiber,
alternate: oldFiber,
flags: "Update"
}
}
if (element && !sameType) {
// 新增当前元素
newFiber = {
type: oldFiber.type,
props: element.props,
dom: null,
return: fiber,
alternate: null,
flags: "Placement"
}
}
if (oldFiber && !sameType) {
// 删除当前元素
oldFiber.flags = "Deletion"
deletions.push(oldFiber)
}
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
++index
}
}
初始化更新队列
function initializeUpdateQueue(fiber) { // 初始化更新队列
const updateQueue = {
shared: {
pending: null
}
}
fiber.updateQueue = updateQueue
}
function createUpdate() {
return {}
}
function enqueueUpdate(fiber, update) { // 向当前的fiber更新队列添加更新
const updateQueue = fiber.updateQueue
const sharedQueue = updateQueue.shared
const pending = sharedQueue.pending
// pending 永远指向最新的更新
if (!pending) {
update.next = update
} else {
update.next = pending.next
pending.next = update
}
sharedQueue.pending = update
}
收集副作用链
- 为了避免fiber树寻找有副作用的fiber节点
- fiber树的构建是深度有限的,故EffectList中总是子级Fiber在前面
let rootFiber = {}
function collectEffectList(returnFiber, completedWork) {
if (returnFiber) {
// 把自己的effectList传递给父fiber
if (!returnFiber.firstEffect) { // 如果父fiber没有链表,则指向子fiber的firstEffect
returnFiber.firstEffect = completedWork.firstEffect
}
if (completedWork.lastEffect) {
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect
}
returnFiber.lastEffect = completedWork.lastEffect
}
}
// 将自己传递给父fiber的effectList
const flags = completedWork.flags
if (flags) { // 只要flags不为noFlags(0)就说明有副作用
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = completedWork
} else {
returnFiber.firstEffect = completedWork
}
returnFiber.lastEffect = completedWork
}
}
提交阶段
function commitRoot() {
deletions.forEach(commitWork) // 确保没有旧节点中需要删除的
commitWork(workInProgress.child)
currentRootFiber = workInProgress
workInProgress = null
}
function commitWork(fiber) {
if (!fiber) return
let domParentFiber = fiber.parent
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.return
}
const domParent = fiber.parent.dom
if (fiber.flags === 'Placement' && fiber.dom !== null) {
domParent.appendChild(fiber.dom)
} else if (fiber.flags === 'Deletion') {
commitDeletion(fiber, domParent)
} else if (fiber.flags === 'Update' && fiber.dom !== null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
const isProperty = key => key !== "children"
const isNew = (prev, next) => key =>
prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)
function updateDom(dom, prevProps, nextProps) {
// 移除新属性不存在而旧属性存在的属性
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// 新增旧属性不存在的属性或修改属性新旧属性不一致的属性
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
}
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}
总结执行过程
- 当组件内更新 state 或有 context 更新时,React 会进入渲染阶段(Render Phase)。这一阶段是异步的,Fiber 协调引擎会启动workLoop ,从 Fiber 树的根部开始遍历,快速跳过已处理的节点;对有变化的节点,引擎会为 Current(当前)节点克隆一个 WorkInProgress(进行中)节点,将这两个 Fiber 的 alternate 属性分别指向对方,并把更新都记录在WorkInProgress 节点上。
- 该workLoop可以随时跑、随时停。
- 当 Fiber 树所有节点都完成工作后,WorkInProgress 节点会被改称为 FinishedWork(已完成)节点,WorkInProgress 树也会被改称为 FinishedWork树。这时 React 会进入提交阶段(Commit Phase),这一阶段主要是同步执行的。Fiber 协调引擎会把FinishedWork 节点上记录的所有修改,按一定顺序提交并体现在页面上。
提交阶段
- 分为三个同步执行子阶段:
-
变更前子阶段:调用类组件的
getSnapshotBeforeUpdate
方法 -
变更子阶段:更新真实DOM
- 递归提交与删除相关的副作用(移除ref、真实DOM、执行类组件
componentWillUnmount
) - 递归提交添加、重新排序真实DOM副作用
- 依次执行FiberNode中的
useLayoutEffect
的清除函数 - 引擎用FinishedWork树替换Current树
- 递归提交与删除相关的副作用(移除ref、真实DOM、执行类组件
-
布局子阶段:这个阶段真实DOM树已经完成了变更,会调用
useLayoutEffect
的副作用回调函数和类组件的componentDidMount
方法
- 在上述提交阶段的三个同步子阶段之后,下一次渲染阶段之前,引擎会异步或同步调用
flushPassiveEffects
方法,这个函数会先后两轮按深度优先遍历 Fiber 树上每个节点:
- 第一轮:如果节点的 updateQueue 链表中有待执行的、由 useEffect 定义的副作用,则顺序执行它们的清除函数;
- 第二轮:如果节点的 updateQueue 链表中有待执行的、由 useEffect 定义的副作用,则顺序执行它们的副作用回调函数,并保存清除函数,供下一轮提交阶段执行。
来源
转载自:https://juejin.cn/post/7361261846089302066