当前位置: 首页 > news >正文

启动 React APP 后经历了哪些过程

本文作者为 360 奇舞团前端开发工程师

前言

本文中使用的React版本为18,在摘取代码的过程中删减了部分代码,具体以源代码为准。

React 18里,通过ReactDOM.createRoot创建根节点。并且通过调用原型链上的render来渲染。 本文主要是从以下两个方法来介绍展开。

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';ReactDOM.createRoot(document.getElementById('root')).render(<React.StrictMode><App /></React.StrictMode>
);

一、createRoot()

createRoot这个方法主要是用来创建FiberRoot(全局唯一,保存全局状态)和RootFiber(是应用里的第一个fiber对象),并将其关系关联起来。主要有以下过程:

  1. 校验container有效性,以及处理options参数

  2. 创建FiberRootrootFiber,并关联起来

  3. 事件代理

  4. 返回ReactDOMRoot实例

function createRoot(container: Element | Document | DocumentFragment,options?: CreateRootOptions,
): RootType {// 校验合法性,以及处理options参数,此处省略if (!isValidContainer(container)) {//...}// 调用 createFiberRoot,创建FiberRoot和rootFiber,并关联关系,最终返回FiberRoot。FiberRoot.current = rootFiber; rootFiber.stateNode = FiberRoot;const root = createContainer(container,ConcurrentRoot,null,isStrictMode,concurrentUpdatesByDefaultOverride,identifierPrefix,onRecoverableError,transitionCallbacks,);// 标记container和rootFiber关系  container['__reactContainer$' + randomKey] = root.currentmarkContainerAsRoot(root.current, container); const rootContainerElement: Document | Element | DocumentFragment =container.nodeType === COMMENT_NODE? (container.parentNode: any): container;listenToAllSupportedEvents(rootContainerElement); // 事件代理// 实例化,挂载render,unmount方法return new ReactDOMRoot(root); // this._internalRoot = root;
}

关系结构示意图

a1c45080a08ccad53d32b5d98262e03b.png
image.png

二、render()

render主要是通过调用updateContainer,将组件渲染在页面上。

ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function(children: ReactNodeList,
): void {const root = this._internalRoot;if (root === null) {throw new Error('Cannot update an unmounted root.');}updateContainer(children, root, null, null);
};

updateContainer

updateContainer主要执行了以下步骤:

  1. 获取当前时间eventTime和任务优先级lane,调用createUpdate生成update;

  2. 执行enqueueUpdate将更新添加到更新队列里,并返回FiberRoot;

  3. scheduleUpdateOnFiber 调度更新;

function updateContainer(element: ReactNodeList,container: OpaqueRoot,parentComponent: ?React$Component<any, any>,callback: ?Function,
): Lane {const current = container.current; // rootFiberconst eventTime = requestEventTime(); // 更新触发时间const lane = requestUpdateLane(current); // 获取任务优先级// ... context 处理 // 创建update:{eventTime, lane, tag: UpdateState // 更新类型, payload: null, callback: null, next: null, // 下一个更新}const update = createUpdate(eventTime, lane); update.payload = {element}; // element首次渲染时为Appcallback = callback === undefined ? null : callback;if (callback !== null) {update.callback = callback;}const root = enqueueUpdate(current, update, lane); // 将update添加到concurrentQueues队列里,返回 FiberRootif (root !== null) {scheduleUpdateOnFiber(root, current, lane, eventTime); // 调度entangleTransitions(root, current, lane);}return lane;
}

调度阶段

调度入口:scheduleUpdateOnFiber

主要有以下过程:

  1. root上标记更新

  2. 通过执行ensureRootIsScheduled来调度任务

function scheduleUpdateOnFiber(root: FiberRoot,fiber: Fiber,lane: Lane,eventTime: number,
) {markRootUpdated(root, lane, eventTime); // 在root上标记更新 // root.pendingLanes |= lane; 将update的lane放到root.pendingLanes// 设置lane对应事件时间 root.eventTimes[laneToIndex(lane)] = eventTime;if ((executionContext & RenderContext) !== NoLanes &&root === workInProgressRoot) { // 更新是在渲染阶段调度提示错误 ...} else { // 正常更新// ...ensureRootIsScheduled(root, eventTime); // 调度任务// ...}
}
调度优先级:ensureRootIsScheduled

该函数用于调度任务,一个root只能有一个任务在执行

  1. 设置任务的过期时间,有过期任务加入到expiredLanes

  2. 获取下一个要处理的优先级,没有要执行的则退出

  3. 判断优先级相等则复用,否则取消当前执行的任务,重新调度。

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {const existingCallbackNode = root.callbackNode; // 正在执行的任务// 遍历root.pendingLanes,没有过期时间设置root.expirationTimes,有过期时间判断是否过期,是则加入到root.expiredLanes中markStarvedLanesAsExpired(root, currentTime);// 过期时间设置 root.expirationTimes = currentTime+t(普通任务5000ms,用户输入250ms);// 获取要处理的下一个lanesconst nextLanes = getNextLanes(root,root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,);// 没有要执行的lanesif (nextLanes === NoLanes) {if (existingCallbackNode !== null) {// 取消正在执行的任务cancelCallback(existingCallbackNode);}root.callbackNode = null;root.callbackPriority = NoLane;return;}const newCallbackPriority = getHighestPriorityLane(nextLanes); // 获取最高优先级的laneconst existingCallbackPriority = root.callbackPriority;// 优先级相等复用已有的任务if (existingCallbackPriority === newCallbackPriority &&!(__DEV__ &&ReactCurrentActQueue.current !== null &&existingCallbackNode !== fakeActCallbackNode)) {return;}// 优先级变化,取消正在执行的任务,重新调度if (existingCallbackNode != null) {cancelCallback(existingCallbackNode);}let newCallbackNode; // 注册调度任务// 同步任务,不可中断// 1. 调用scheduleSyncCallback将任务添加到队列syncQueue里;// 2. 创建微任务,调用flushSyncCallbacks,遍历syncQueue队列执行performSyncWorkOnRoot,清空队列;if (newCallbackPriority === SyncLane) {if (root.tag === LegacyRoot) {scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));} else {scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));}if (supportsMicrotasks) {// 支持微任务scheduleMicrotask(() => {if ((executionContext & (RenderContext | CommitContext)) ===NoContext) {flushSyncCallbacks();}});} else {scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);}newCallbackNode = null;} else {let schedulerPriorityLevel;switch (lanesToEventPriority(nextLanes)) {// ...case DefaultEventPriority:schedulerPriorityLevel = NormalSchedulerPriority;break;default:schedulerPriorityLevel = NormalSchedulerPriority;break;}// 非同步任务,可中断// 1. 维护了两个队列 timerQueue taskQueue// 2. 通过requestHostCallback开启宏任务执行任务newCallbackNode = scheduleCallback(schedulerPriorityLevel,performConcurrentWorkOnRoot.bind(null, root),);}root.callbackPriority = newCallbackPriority;root.callbackNode = newCallbackNode;
}
调度任务 scheduleSyncCallback or scheduleCallback
  • scheduleSyncCallback 只有一个队列,将任务添加到队列里。按照顺序同步执行,不能中断。

function scheduleSyncCallback(callback: SchedulerCallback) { // callback =》performSyncWorkOnRootif (syncQueue === null) {syncQueue = [callback];} else {syncQueue.push(callback);}
}
  • scheduleCallback 有两个队列(小顶堆),timerQueue存放未就绪的任务,taskQueue存放已就绪任务。每次循环,判断timerQueue里是否有可执行任务,并将其移动到taskQueue中,然后从taskQueue中取出任务执行。

function unstable_scheduleCallback(priorityLevel, callback, options) {// ... startTime timeout expirationTime 等初始化var newTask = { // 新的调度任务id: taskIdCounter++,callback, // render时为performConcurrentWorkOnRoot.bind(null, root),priorityLevel,startTime, // getCurrentTime()expirationTime, // startTime + timeout(根据priorityLevel,-1、250、1073741823、10000、5000、)sortIndex: -1, // startTime > currentTime ? startTime: expirationTime,};// 按照是否过期将任务推到队列timerQueue或者taskQueue里if (startTime > currentTime) {newTask.sortIndex = startTime;push(timerQueue, newTask);if (peek(taskQueue) === null && newTask === peek(timerQueue)) {if (isHostTimeoutScheduled) {cancelHostTimeout(); // 取消当前的timeout} else {isHostTimeoutScheduled = true;}// 本质上是从timerQueue去取可以执行的任务放到taskQueue里,然后执行requestHostCallbackrequestHostTimeout(handleTimeout, startTime - currentTime);}} else {newTask.sortIndex = expirationTime;push(taskQueue, newTask);// 调度任务if (!isHostCallbackScheduled && !isPerformingWork) {isHostCallbackScheduled = true;requestHostCallback(flushWork); // 设置isMessageLoopRunning,开启宏任务【schedulePerformWorkUntilDeadline】(优先级:setImmediate > MessageChannel > setTimeout)执行 performWorkUntilDeadline()}}return newTask;
}

这里要注意下,一直以来都认为是MessageChannel优先级大于setTimeout,但在浏览器打印之后发现事实相反。这个原因是chrome在某次更新里修改了二者的优先级顺序。想了解更多可以查看这篇文章:聊聊浏览器宏任务的优先级 - 掘金

执行任务 performWorkUntilDeadline

当监听到MessageChannel message的时候,执行该方法。通过调用scheduledHostCallback(即flushWork->workLoop返回的)结果,判断是否还有任务,若有则开启下一个宏任务。

const performWorkUntilDeadline = () => {if (scheduledHostCallback !== null) {const currentTime = getCurrentTime();startTime = currentTime;const hasTimeRemaining = true;let hasMoreWork = true;try {hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime); // scheduledHostCallback = flushWork ->workLoop} finally {if (hasMoreWork) {schedulePerformWorkUntilDeadline(); // 开启下一个宏任务MessageChannel,执行 performWorkUntilDeadline()} else {isMessageLoopRunning = false;scheduledHostCallback = null;}}} else {isMessageLoopRunning = false;}needsPaint = false;
};
workLoop

taskQueue取出任务执行task.callback即(performConcurrentWorkOnRoot)。如果callback返回的是函数,则表示任务被中断。否则任务执行完毕,则弹出该任务。

function workLoop(hasTimeRemaining, initialTime) {let currentTime = initialTime;advanceTimers(currentTime); // 将 timerQueue里到时间执行的定时任务移动到 taskQueue 里currentTask = peek(taskQueue); // 从 taskQueue 取任务while (currentTask !== null &&!(enableSchedulerDebugging && isSchedulerPaused)) {// 任务未过期并且任务被中断if (currentTask.expirationTime > currentTime &&(!hasTimeRemaining || shouldYieldToHost())) {break;}const callback = currentTask.callback; // 在scheduleCallback接受的第二个参数:performConcurrentWorkOnRootif (typeof callback === 'function') {currentTask.callback = null;currentPriorityLevel = currentTask.priorityLevel;const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;// 如果返回是函数,则代表要重新执行;const continuationCallback = callback(didUserCallbackTimeout);currentTime = getCurrentTime();if (typeof continuationCallback === 'function') {// 任务暂停重新赋值callbackcurrentTask.callback = continuationCallback;} else {// 任务完成弹出if (currentTask === peek(taskQueue)) {pop(taskQueue);}}advanceTimers(currentTime); // 每次执行完,去timerQueue查看有没有到时间的任务} else {pop(taskQueue); // 弹出该任务}currentTask = peek(taskQueue);}// 返回给外部判断是否还有任务需要执行,即performWorkUntilDeadline里面的hasMoreWorkif (currentTask !== null) {return true;} else {// taskQueue里面没有任务了,从timerQueue取任务const firstTimer = peek(timerQueue);if (firstTimer !== null) {// 目的将timerQueue里的任务,移动到taskQueue里执行requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);}return false;}
}

Render 阶段

这里render不是实际的dom render,而是fiber树的构建阶段。

Render入口
  • performSyncWorkOnRoot: 同步更新 =》 renderRootSync =》 workLoopSync

  • performConcurrentWorkOnRoot: 异步更新 =》 renderRootConcurrent =》 workLoopConcurrent

二者的区别主要是是否调用shouldYield,判断是否中断循环。

render之后就进入了commit阶段。

function performConcurrentWorkOnRoot(root, didTimeout) {currentEventTime = NoTimestamp;currentEventTransitionLane = NoLanes;const originalCallbackNode = root.callbackNode;const didFlushPassiveEffects = flushPassiveEffects();if (didFlushPassiveEffects) {// ...}let lanes = getNextLanes(root,root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,);if (lanes === NoLanes) {return null;}// 判断是否有用户输入、过期任务打断,需要同步渲染const shouldTimeSlice =!includesBlockingLane(root, lanes) &&!includesExpiredLane(root, lanes) &&(disableSchedulerTimeoutInWorkLoop || !didTimeout); // renderRootConcurrent|renderRootSync里都会调用prepareFreshStack:构建新的workInProgress树let exitStatus = shouldTimeSlice? renderRootConcurrent(root, lanes): renderRootSync(root, lanes);// render执行完成或抛出异常if (exitStatus !== RootInProgress) {if (exitStatus === RootErrored) {}if (exitStatus === RootFatalErrored) {}if (exitStatus === RootDidNotComplete) {markRootSuspended(root, lanes);} else {// render完成const renderWasConcurrent = !includesBlockingLane(root, lanes);const finishedWork: Fiber = (root.current.alternate: any);if (renderWasConcurrent &&!isRenderConsistentWithExternalStores(finishedWork)) {exitStatus = renderRootSync(root, lanes);if (exitStatus === RootErrored) {}if (exitStatus === RootFatalErrored) {}}// 将新的fiber树赋值给root.finishedWorkroot.finishedWork = finishedWork;root.finishedLanes = lanes;// 进入commit阶段->调用 commitRoot-> commitRootImpl;// commitRootImpl 执行完成之后会清空重置root.callbackNode和root.callbackPriority;以及重置workInProgressRoot、workInProgress、workInProgressRootRenderLanes。finishConcurrentRender(root, exitStatus, lanes); }}ensureRootIsScheduled(root, now()); // 退出前检测,是否有其他更新,需要发起调度if (root.callbackNode === originalCallbackNode) { // 没有改变,说明任务被中断,返回function,等待调用return performConcurrentWorkOnRoot.bind(null, root);}return null;
}
是否可中断循环

workLoopSync 和 workLoopConcurrent

  • 共同点:用于构建fiber树,workInProgress从根开始,遍历创建fiber节点。

  • 区别是:workLoopConcurrent里面增加了shouldYield判断。

function workLoopSync() {while (workInProgress !== null) {performUnitOfWork(workInProgress);}
}function workLoopConcurrent() {while (workInProgress !== null && !shouldYield()) {performUnitOfWork(workInProgress);}
}
递归阶段 performUnitOfWork

遍历过程:从rootFiber向下采用深度优先遍历,当遍历到叶子节点时(递),然后会进入到归阶段,即遍历该节点的兄弟节点,如果没有兄弟节点则返回父节点。然后进行递归的交错执行。

  • 递阶段 beginWork: 创建或复用fiber节点。diff过程在此发生;

  • 归阶段 completeWork: 由下至上根据fiber创建或复用真实节点,并赋值给fiber.stateNode

function performUnitOfWork(unitOfWork: Fiber): void { // unitOfWork即workInProgress,指向下一个节点const current = unitOfWork.alternate;let next;next = beginWork(current, unitOfWork, renderLanes); unitOfWork.memoizedProps = unitOfWork.pendingProps;if (next === null) {// 遍历到叶子节点后,开始归阶段,并创建dom节点completeUnitOfWork(unitOfWork);} else {workInProgress = next; // workInProgress指向next}ReactCurrentOwner.current = null;
}
递归后的新的fiber树
b79678cf80026f17731075bf6a3e5e0b.png
image.png

Commit 阶段

通过commitRoot进入commit阶段。此阶段是同步执行的,不可中断。接下来经历了三个过程:

  1. before mutation阶段(执行DOM操作前):处理DOM节点渲染/删除后的focus、blur逻辑;调用getSnapshotBeforeUpdate生命周期钩子;调度useEffect。

  2. mutation阶段(执行DOM操作):DOM 插入、更新、删除

  3. layout阶段(执行DOM操作后):调用类组件的 componentDidMount、componentDidUpdate、setState 的回调函数;或函数组件的useLayoutEffectcreate函数;更新ref

页面渲染结果

import { useState } from 'react';export default function Count() {const [num, setNum] = useState(1);const onClick = () => {setNum(num + 1);};return (<div>num is {num}<button onClick={onClick}>点击+1</button></div>);
}function List() {const arr = [1, 2, 3];return (<ul>{arr.map((item) => (<li key={item}>{item}</li>))}</ul>);
}function App() {return (<div><Count /><List /></div>);
}export default App;
f013f3f1941d7a7bbf59862930b5de92.png
image.png

参考文章

[1] React https://github.com/facebook/react
[2] React技术揭秘 https://react.iamkasong.com/
[3] 图解React https://7km.top/main/macro-structure/
[4] 聊聊浏览器宏任务的优先级 https://juejin.cn/post/7202211586676064315

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

e3b9d87c35cb0a460856abf00100b60e.png

http://www.lryc.cn/news/172587.html

相关文章:

  • 带自动采集小说网站源码 小说听书网站源码 小说网站源码 带教程
  • MySQL学习笔记2
  • 【python爬虫】—星巴克产品
  • 算法 矩阵最长递增路径-(递归回溯+动态规划)
  • 四、数学建模之图与网络模型
  • php在header增加key,sign,timestamp,实现鉴权
  • Spring实例化源码解析之ConfigurationClassParser(三)
  • 在 Substance Painter中实现Unity Standard Shader
  • 第二证券:个人开证券账户要开户费吗?
  • 大厂面试-16道面试题
  • 搭建GraphQL服务
  • 数据仓库介绍及应用场景
  • 代码随想录算法训练营Day56 | 动态规划(16/17) LeetCode 583. 两个字符串的删除操作 72. 编辑距离
  • HTML+CSS+JavaScript 大学生网页设计制作作业实例代码 200套静态响应式前端网页模板(全网最全,建议收藏)
  • CFimagehost私人图床本地部署结合cpolar内网穿透实现公网访问
  • uniapp瀑布流布局写法
  • 蓝桥杯 题库 简单 每日十题 day8
  • Keepalived 高可用(附带配置实例,联动Nginx和LVS)
  • 第二证券:今年来港股回购金额超700亿港元 9月近200家公司获增持
  • Autosar基础——RTE简介
  • 几个国内可用的强大的GPT工具
  • 《Python等级考试(1~6级)历届真题解析》专栏总目录
  • 在IntelliJ IDEA 中安装阿里P3C以及使用指南
  • Java集成支付宝沙箱支付,详细教程(SpringBoot完整版)
  • 详解Nacos和Eureka的区别
  • 在Vue中实现组件间的通信(父子通信,非父子通信,通用通信)
  • LLaMA参数微调方法
  • NSSCTF之Misc篇刷题记录(17)
  • 红与黑(bfs + dfs 解法)(算法图论基础入门)
  • 为何学linux及用处