学过微机的同学都应该很熟悉「中断」这个概念:

  • CPU 正常运行程序时,内部事件或外设提出中断请求;
  • CPU 予以响应,同时保护好 CPU 执行主程序的现场,转入调用中断服务程序;
  • 调用完毕后恢复现场。

本文学习的 React 源码版本:16.9.0

Fiber 调度/渲染

Stack Reconciler

React 16 之前的组件渲染方式是递归渲染:渲染父节点 -> 渲染子节点

递归渲染看起来十分简单,但是如果想在子节点的渲染过程中执行优先级更高的操作,只能保留调用栈中子节点的渲染及子节点之前节点的渲染,这样是很复杂的,这种调和/渲染也叫做 Stack Reconciler。

Fiber Reconciler

Fiber 使用链表的结构去渲染节点,每一个节点都称之为 Fiber Node,每个节点会有三个属性:

  • child 指向第一个子节点
  • sibling 指向兄弟节点
  • return 指向父节点

Fiber 的渲染方式:从父节点开始,向下依次遍历子节点,深度优先渲染完子节点后,再回到其父节点去检查是否有兄弟节点,如果有兄弟节点,则从该兄弟节点开始继续深度优先的渲染,直到回退到根节点结束。

重复遍历的节点并不会重复渲染,而是为了取到下一个可能需要渲染的节点。

此时每一个节点都是一个渲染任务, 从而将整个界面渲染任务拆分成更小的模块,渲染可拆分就意味着每次任务执行前都可以检查是否去执行优先级更高的操作。


Fiber Node Tree

实际上,真实的 DOM 渲染过程是 diff 两棵 Fiber 节点树得到 effect list,在 commit 阶段执行。在 React16 中,两棵树分别是:

  • current tree (在源码中即 HostRoot.current
  • workInProgress tree(在源码中即 HostRoot.current.alternate

不过 React 并没有实现两棵 Fiber Node Tree,实际情况是两棵树上对应的 Fiber Node 通过 alternate 属性互相引用。

// packages/react-reconiler/src/ReactFiber.js
// This is used to create an alternate fiber to do work on.
export function createWorkInProgress() {// ...workInProgress.pendingProps = pendingProps;workInProgress.child = current.child;workInProgress.sibling = current.sibling;// ...
}

React 渲染流程

可以分为 Scheduler、Reconciliation、Commit 这三个阶段

Scheduler 阶段

Scheduer 流程主要是创建更新,创建更新的方式:

  • ReactDOM.render
  • setState

可以发现 React 将首次渲染和更新渲染统一了起来。

ReactDOM.render

调用 legacyRenderSubtreeIntoContainer

// packages/react-dom/src/client/ReactDOM.js
const ReactDOM = {render() {// ...return legacyRenderSubtreeIntoContainer(null,element,container,false,callback,);}
}

legacyRenderSubtreeIntoContainer

调用 root.render,root 来自调用 legacyCreateRootFromDOMContainer

// packages/react-dom/src/client/ReactDOM.js
function legacyRenderSubtreeIntoContainer() {let root: _ReactSyncRoot = (container._reactRootContainer: any);if (!root) {// Initial mountroot = container._reactRootContainer = legacyCreateRootFromDOMContainer(container,forceHydrate,);} else {// UpdateupdateContainer(children, fiberRoot, parentComponent, callback);}
}

legacyCreateRootFromDOMContainer

  • 清除根节点下的所有子元素
  • 创建 ReactRoot
// packages/react-dom/src/client/ReactDOM.js
function legacyCreateRootFromDOMContainer() {const shouldHydrate =forceHydrate || shouldHydrateDueToLegacyHeuristic(container);if (!shouldHydrate) {let rootSibling;while ((rootSibling = container.lastChild)) {container.removeChild(rootSibling);}}return new ReactSyncRoot(container, LegacyRoot, shouldHydrate);
}

ReactSyncRoot

后面就是创建 FiberRoot 了,不放源码上来了。


setState

enqueueUpdate

将当前的更新压入更新队列

// packages/src/react-reconciler/src/ReactFiberClassComponent.js
const updater = {enqueueUpdate(inst, payload, callback) {const fiber = getInstance(inst);const currentTime = requestCurrentTime();const suspenseConfig = requestCurrentSuspenseConfig();// 到期时间(Fiber 中的优先级)const expirationTime = computeExpirationForFiber(currentTime,fiber,suspenseConfig,);const update = createUpdate(expirationTime, suspenseConfig);update.payload = payload; // payload 为 setState 中的更新对象if (callback !== undefined && callback !== null) {if (__DEV__) {warnOnInvalidCallback(callback, 'setState');}update.callback = callback;}enqueueUpdate(fiber, update); // 更新信息放入 updateQueuescheduleWork(fiber, expirationTime); // 并行渲染的核心:Scheduler}
}

scheduleWork

// packages/react-reconciler/src/ReactFiberWOrkLoop.js
function scheduleUpdateOnFiber() {// ... next
}

scheduler 的具体过程会在之后的并行渲染中展开。


Reconciliation 阶段

workLoop

循环更新,对整棵 Fiber 树都遍历一遍。

循环每渲染完成一个 Fiber Node 就利用 shouldYield 来判断是否有优先级更高的任务存在,是则跳出循环先执行优先级更高的任务,否则继续渲染下一个 Fiber Node。

还记得文章一开始介绍了微机中的中断概念么,可以看到在 workLoop 过程中体现出来了。

简单来说就是判断当前帧是否还有时间更新,如果没有时间更新就将剩余时间去进行其他操作。

// packages/react-reconciler/src/ReactFiberWorkLoop.js
function renderRoot() {do {try {if (isSync) {workLoopSync();} else {workLoop();     }break;}// ...
}function workLoop() {while (workInProgress !== null && !shouldYield()) {workInProgress = performUnitOfWork(workInProgress);}
}

performUnitOfWork

调用 beginWork 更新当前任务节点,如果 Fiber 树已经更新到叶子节点,则调用 completeUnitOfWork 更新。

// packages/react-reconciler/src/ReactFiberWorkLoop.js
function performUnitOfWork(unitOfWork: Fiber): Fiber | null {// 调和阶段都是在 alternate 上完成const current = unitOfWork.alternate;// ...next = beginWork(current, unitOfWork, renderExpirationTime);unitOfWork.memoizedProps = unitOfWork.pendingProps;// Fiber 树已经更新到叶子节点if (next === null) {next = completeUnitOfWork(unitOfWork);}return next;
}

beginWork

根据 workInProgresstag ,把对应的 FiberNode 上下文压入栈,然后更新节点,对应 render 阶段。

// packages/react-reconciler/src/ReactFiberBeginWork.js
function beginWork() {...switch (workInProgress.tag) {case ClassComponent: {const Component = workInProgress.type;if (isLegacyContextProvider(Component)) {pushLegacyContextProvider(workInProgress); // 入栈}break;}}
}

beginWork 会返回当前节点的子节点,如果有子节点,继续 workLoop;如果没有子节点,进入 completeUnitOfWork

子节点的 alternate 改变是在 cloneChildFibers 函数中

completeWork

改变 effectList(firstEffect、lastEffect、nextEffect)

作用是将 Fiber Node 上下文出栈,对应 commit 阶段

// packages/react-reconciler/ReactFiberCompleteWork.js
function completeWork() {const newProps = workInProgress.pendingProps;switch (workInProgress.tag) {case ClassComponent: {const Component = workInProgress.type;if (isLegacyContextProvider(Component)) {popLegacyContext(workInProgress); // 出栈}break;}}
}


commit 阶段

从字面意思来就可以知道, commit 阶段是将调和阶段的更新进行提交,即把更新操作反映到真实的 DOM 上。

同时,commit 阶段是同步执行,不可被中断。

Effect

函数式编程中经常会看见 Effect 这个概念,表示副作用。在 Fiber 架构中,Effect 定义了 Fiber Node 在 commit 阶段要做的事情,在源码中也就是 EffectTag 这个属性。

  • 对于组件:更新 refs、调用 componentDidUpdate...
  • 对于 DOM:增加、更新、删除 DOM...

Effect 组成的链表成为 effects list

  • firstEffect:指向第一个更新的节点
  • nextEffect:指向下一个更新的节点

commitRoot

使 effects list 生效:

  • 第一次遍历 effects list(commitBeforeMutationEffects):在更改前读取 DOM 上的 state,这里是 getSnapshotBeforeUpdate 生命周期调用的地方;
  • 第二次遍历 effects list(commitMutationEffects):此阶段是真正更改 DOM 的阶段;
  • 第三次遍历 effects list(commitLayoutEffects):执行生命周期函数 componentDidMount、componentDidUpdate...
// packages/react-reconciler/src/ReactFiberWorkLoop.js
function commitRootImpl() {// ...// 三次遍历 effects listdo {commitBeforeMutationEffects();} while (nextEffect !== null);do {commitMutationEffects(renderPriorityLevel);} while (nextEffect !== null);do {commitLayoutEffects(root, expirationTime);} while (nextEffect !== null);
}

commitBeforeMutationEffects

  • 通过 prevProps、prevState 以获取 Snapshot
  • 调用组件实例的 getSnapshotBeforeUpdate,返回值用于 componentDidUpdate 的第三个参数。
// packages/react-reconciler/src/ReactFiberCommitWork.js
function commitBeforeMutationLifeCycles() {// ...switch (finishedWork.tag) {case ClassComponent: {if (current !== null) {// pervProps、pervStateconst prevProps = current.memoizedProps;const prevState = current.memoizedState;startPhaseTimer(finishedWork, 'getSnapshotBeforeUpdate');// getSnapshotBeforeUpdateconst snapshot = instance.getSnapshotBeforeUpdate(finishedWork.elementType === finishedWork.type? prevProps: resolveDefaultProps(finishedWork.type, prevProps),prevState,);}}}
}

commitMutationEffects

根据不同的 effectTag 执行不同的操作:

  • 插入节点:commitPlacement
  • 更新节点:commitWork
  • 删除节点:commitDeletion
// packages/react-reconciler/src/ReactFiberWorkLoop.js
function commitMutationEffects() {// ...switch (primaryEffectTag) {case Placement: {commitPlacement(nextEffect);break;}case PlacementAndUpdate: {commitPlacement(nextEffect);// Updateconst current = nextEffect.alternate;commitWork(current, nextEffect);break;}case Update: {const current = nextEffect.alternate;commitWork(current, nextEffect);break;}case Deletion: {commitDeletion(nextEffect, renderPriorityLevel);break;}      }
}

commitPlacement -- 插入节点

  • 找到 finishedWork 的父节点 parentFiber。寻找的是原生的 DOM 节点对应的 Fiber Node,如果父级不是原生 DOM,则继续往上寻找。
  • 找到待插入节点的后一个节点
  • 使用 insertBefore 或 appendChild 或深度优先遍历 class 组件的子节点插入
// packages/react-reconciler/src/ReactFiberCommitWork.js
function commitPlacement {// 递归插入所有节点到父节点const parentFiber = getHostParentFiber(finishedWork);// 找待插入节点的后一个节点const before = getHostSibling(finishedWork);insertBefore(parent, stateNode, before);// or...appendChild(parent, stateNode);// or...while (node.sibling === null) {if (node.return === null || node.return === finishedWork) {return;}node = node.return;}
}

commitWork -- 更新节点

commitWork 只会对 HostComponent 和 HostText 进行更新,也就是 DOM 节点和文本节点。

  • HostComponent 调用 commitUpdate
  • HostText 调用 commitTextUpdate
// packages/react-reconciler/src/ReactFiberCommitWork.js
function commitWork() {switch (finishedWork.tag) {case HostComponent: {commitUpdate(instance,updatePayload,type,oldProps,newProps,finishedWork,);}case HostText: {commitTextUpdate(textInstance, oldText, newText);}}
}

commitUpdate

updatePayload 应用到真实 DOM 上;对一些属性做特殊处理

// packages/react-dom/src/client/ReactDOMHostConfig.js
function commitUpdate() {updateFiberProps(domElement, newProps);updateProperties(domElement, updatePayload, type, oldProps, newProps);
}// packages/react-dom/src/client/ReactDOMComponent.js
function updateDOMProperties(domElement: Element,updatePayload: Array<any>,wasCustomComponentTag: boolean,isCustomComponentTag: boolean,
): void {for (let i = 0; i < updatePayload.length; i += 2) {const propKey = updatePayload[i];const propValue = updatePayload[i + 1];if (propKey === STYLE) {setValueForStyles(domElement, propValue);} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {setInnerHTML(domElement, propValue);} else if (propKey === CHILDREN) {setTextContent(domElement, propValue);} else {setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);}}
}

commitTextUpdate

文本更新很简单,直接替换 value

// packages/react-dom/src/client/ReactDOMHostConfig.js
function commitTextUpdate() {textInstance.nodeValue = newText;
}

commitDeletion -- 删除节点

删除节点也需要考虑到子节点不一定是原生 DOM 的情况,比如如果是 class Component,需要调用 componentWillUnmount,所以还是需要深度遍历整个子树

// packages/react-reconciler/src/ReactFiberCommitWork.jsfunction commitDeletion(current: Fiber,renderPriorityLevel: ReactPriorityLevel,
): void {if (supportsMutation) {unmountHostComponents(current, renderPriorityLevel);} else {// Detach refs and call componentWillUnmount() on the whole subtree.commitNestedUnmounts(current, renderPriorityLevel);}detachFiber(current);
}


commitLayoutEffects

  • 执行 componentDidMountcomponentDidUpdate
// packges/react-reconciler/src/ReactFiberWorkLoop.js
function commitLayoutEffects() {while (nextEffect !== null) {if (effectTag & (Update | Callback)) {recordEffect();const current = nextEffect.alternate;commitLifeCycles(root,current,nextEffect,committedExpirationTime,);}}
}
// packages/react-reconciler/src/ReactFiberCommitWork.js
function commitLifeCycles() {switch (finishedWork.tag) {// ...case ClassComponent: {const instance = finishedWork.stateNode;if (finishedWork.effectTag & Update) {if (current === null) {startPhaseTimer(finishedWork, 'componentDidMount');} else {const prevProps =finishedWork.elementType === finishedWork.type? current.memoizedProps: resolveDefaultProps(finishedWork.type, current.memoizedProps);const prevState = current.memoizedState;startPhaseTimer(finishedWork, 'componentDidUpdate');}}}}
}

参考

  • 探究 React Work Loop 原理

5渲染判断if_React 16 渲染流程相关推荐

  1. 5渲染判断_云渲染怎么收费,5大云渲染平台实测,您选对了吗?

    3DMAX云渲染平台都怎么收费?5大云渲染平台测试后才知道差距这么大,您用对了吗? 本次共计测试了3个镜头,为了让大家很直观地感受各个平台渲染时间和费用上的对比,我整理了个表格,供大家自行参考和判断. ...

  2. 5渲染判断_Vue页面渲染中key的应用实例教程

    引言 在前端项目开发过程中,el-table展示的结果列使用组件形式引入,其中某些字段通过:formatter方法转码,结果栏位的字段显示/隐藏控制也使用组件形式引入,前端在控制字段显示属性时,发现码 ...

  3. 5渲染判断_先渲染再对焦,KeyShot 深度通道在 Photoshop 中的对接

    事情的起因,是在我用华为 P40 Pro 手机的时候,发现华为拍照系统当中的先拍照后对焦功能实在强大,那我会想到这个东西能不能用在我们产品渲染当中呢? 仔细想一想这个东西,无非就是通过距离判断相机对焦 ...

  4. 【Android 应用开发】Paint 渲染 之 BitmapShader 位图渲染 ( 渲染流程 | CLAMP 拉伸最后像素 | REPEAT 重复绘制图片 | MIRROR 绘制反向图片 )

    文章目录 1. 位图渲染 BitmapShader 简介 ( 1 ) 位图渲染综述 ( ① 三种方式 : Shader.TileMode.CLAMP | Shader.TileMode.REPEAT ...

  5. div置于页面底部_浏览器渲染页面的原理及流程

    浏览器渲染页面的原理及流程 浏览器将域名通过网络通信从服务器拿到html文件后,如何渲染页面呢? 1.根据html文件构建DOM树和CSSOM树.构建DOM树期间,如果遇到JS,阻塞DOM树及CSSO ...

  6. 浏览器渲染页面的原理及流程---------重绘与重排(回流)--优化

    浏览器渲染页面的原理及流程 浏览器将域名通过网络通信从服务器拿到html文件后,如何渲染页面呢? 1.根据html文件构建DOM树和CSSOM树.构建DOM树期间,如果遇到JS,阻塞DOM树及CSSO ...

  7. PHP加载lod,面向大场景模型web端动态渲染LOD处理方法与流程

    本发明属于计算机虚拟现实技术领域,具体而言是基于无人机采集的大场景图片进行建模,针对建模后的网格数据处理的方法,处理过的数据能够在web端无延迟加载并能够实时渲染. 背景技术: 近年来随着虚拟现实技术 ...

  8. kotlin设置按钮不可点击_3dmax渲染720全景效果图动画流程,学习VR动画不可错过的必备知识...

    3dmax渲染720全景效果图动画流程,学习VR动画不可错过的必备知识 7 20全景效果图是这几年全国兴起的一门小技术,是适应快速.立体.多方位看图的一个重要方法.今天我就来说一下,制作720全景动画 ...

  9. MutationObserver -- 判断vue Dom渲染完成

    MutationObserver MutationObserver -- 判断vue Dom渲染完成 MutationObserver 作用 方法 应用实例 MutationObserver – 判断 ...

最新文章

  1. FPGA的设计艺术(4)STA实战之不同时序路径的建立保持时间计算
  2. “造车时代”多方笃定小米造车,网络营销外包专员如何看待这一波营销?
  3. php跳出volist,thinkphpvolist
  4. Python教程分享:Python Cookie HTTP获取cookie并处理
  5. JS获取当前日期及时间
  6. MySql PreparedStatement用法 及 Transaction处理
  7. 如何在SQL Server中的SELECT TOP 中使用变量
  8. 2019春运大幕即将开启 西安动车列车员整装迎春运
  9. java架构实践_Java架构实践-关于IO流
  10. BOS物流项目注册流程图
  11. pycharm+mysql安装步骤
  12. 2.5万字讲解DDD领域驱动设计,从理论到实践掌握DDD分层架构设计,赶紧收藏起来吧
  13. 范式通俗理解:1NF、2NF、3NF和BNCF
  14. 正在通过app store进行鉴定解决方案
  15. echart 水滴图水波颜色设置
  16. 全差分运算放大器ADA4930的分析(1)
  17. 镁光ddr3布线规则_PCB设计要点-DDR3布局布线技巧及注意事项
  18. java版冒险岛_CMS072 冒险岛ONLINE 国服072版本 JAVA 服务端 - 下载 - 搜珍网
  19. Python 网络爬虫实战:爬取《去哪儿》网数千篇旅游攻略数据,再也不愁旅游去哪儿玩了
  20. cocos2d 使用TexturePacker制作plist文件

热门文章

  1. lambada表达式
  2. Android中访问通讯录,数据的增删改查
  3. 表的插入、更新、删除、合并操作_14_ 通过表关联更新多个表多个字段
  4. 【python-numpy 】中的随机打乱数据方法np.random.shuffle
  5. linux内存free低,Linux上的内存使用情况与`free`不匹配
  6. 【pytorch】model.train和model.eval用法及区别详解
  7. Crackme006 - 全新160个CrackMe学习系列(图文|视频|注册机源码)
  8. 图片轮播,纯js+css
  9. 实验六 数组 (2)
  10. HDU 2502 月之数(简单递推)