React进阶之React核心源码解析(一)
React核心源码解析
- react 特点
- CPU卡顿
- IO 卡顿
- 新老 react 架构对比
- v15
- v16.8
- Scheduler 调度器
- Reconciler 协调器
- React fiber
- 原理
- 更新dom
- mount 构建过程
- render阶段 — scheduler reconciler
- react源码解析
- react-dom
- react-dom/src/client/ReactDOMRoot.js
- react-reconciler
- react-reconciler/src/ReactFiberReconciler.js
- react-reconciler/src/ReactFiberWorkLoop.js
- react-reconciler/src/ReactFiberBeginWork.js
- react-reconciler/src/ReactChildFiber.js
- react-reconciler/src/ReactFiber.js
- react-reconciler/src/ReactFiberWorkLoop.js
- commit阶段 同步阶段
cline + deeseek => AI工具,上手成本非常低,非常便宜
cursor 编辑器
带着问题学源码
- 为什么 react 会引入 fiber 架构
- 简述 fiber 节点的结构和作用
- fiber 架构 => 架构+流程
- diff 算法
- hooks 原理
学习方法论:由大到小 对比回答问题
react 特点
- 单向数据流
能够快速响应用户操作
公式:ui = render(data)
什么原因导致响应慢?
cpu卡顿,js执行导致画面卡顿
IO卡顿,网络问题等 延迟
CPU卡顿
浏览器一秒60hz,16.6ms刷新一次,超过就会有掉帧现象
就比如这个例子
// index.js
import ReactDOM from "react-dom";import App from "./App";const rootElement = document.getElementById("root");// ReactDOM.render(<App />, rootElement);
ReactDOM.createRoot(rootElement).render(<App />);// APP.js
import "./styles.css";export default function App() {const len = 3000;return (<ul>{Array(len).fill(0).map((_, i) => (<li>{i}</li>))}</ul>);
}
React是怎么解决的?
时间分片:把更新过程碎片化 优先级 应用更新 后台预渲染
- 同步 阻塞 渲染
- 异步 非阻塞 用户优先级渲染
16.8后提出的 concurrent mode
ReactDOM.createRoot(rootEl).render(<App />)
IO 卡顿
接口响应时间长,解决:
- loading
- suspence 兜底 fallback
- error-boundary 错误兜底
新老 react 架构对比
v15 同步的,不可中断的
v16.8 异步,可中断
v18 ssr升级 流式 ssr stream,针对页面所有组件是分块,分局部输出,而不是等所有页面拼接组装好后一次性传输给客户端
页面上的不同接口,包裹成流式,然后传输给客户端,提升TTFB(首字节响应时间)
性能优化有需求,使用nextjs做提升
v15
- Reconciler 协调器,diff 负责找出变化的组件
- update 更新
- component render jsx 渲染 => vdom
- vdom diff
- 找出变化的元素
- 通知 renderer 渲染
通过递归的方式找出变化的组件
mount 阶段 => 调用 mountComponent
update 阶段 => 调用 updateComponent
递归更新子组件
=> 缺点:层级深,递归时间超过16ms
- Renderer 渲染器,负责将变化的组件渲染到页面上
- ReactDom.render
- ReactNative.render
不可中断,中断则后续内容不执行
v16.8
多了一个 Scheduler 调度器
- Scheduler 调度器 调度任务的优先级
- Reconciler 协调器 负责找出变化的组件 递归=>可中断
- Renderer 渲染器 是拿着Reconciler提供的标识 同步渲染
Scheduler 调度器
将大型任务分割成小任务,每一帧分配一定的时间执行小任务
- 时间切片
- 优先级调度
- 每个工作单元,对应一个fiber节点
- 时间分配
- 调度循环 维护任务队列
- 时间检查
- 暂停与恢复
- 利用浏览器API
- requestAnimationFrame 用于在下一帧开始的时候执行回调函数
- requestIdleCallBack 浏览器空闲的时候执行回调函数 setTimeout 模拟了
Reconciler 协调器
// 更新工作从递归变成了可以中断的循环过程。每次循环都会调用shouldYield判断当前是否有剩余时间。/** @noinline */
function workLoopConcurrent() {// Perform work until Scheduler asks us to yieldwhile (workInProgress !== null && !shouldYield()) {workInProgress = performUnitOfWork(workInProgress);}
}
React fiber
原理
react 内部实现的数据结构,支持状态更新,可中断可恢复,恢复后可以复用之前的中间状态
- 架构:v15 stack reconciler,v16 fiber reconciler
- 数据结构:每个 fiber 节点对应 react element,多个组件类型,dom节点各种属性数据
- 动态的工作单元,改变的状态,要执行的工作
function FiberNode(tag: WorkTag,pendingProps: mixed,key: null | string,mode: TypeOfMode,
) {// Instance,静态节点的数据结构属性this.tag = tag;this.key = key;this.elementType = null;this.type = null;this.stateNode = null;// Fiber,用来链接其他fiber节点形成的fiber树this.return = null;this.child = null;this.sibling = null;this.index = 0;this.ref = null;// 作为动态的工作单元的属性this.pendingProps = pendingProps; //即将应用到组件上新属性 相当于是新值newValue this.memoizedProps = null; // 上次渲染时使用的属性值 相当于是旧值oldValuethis.updateQueue = null;this.memoizedState = null;this.dependencies = null;this.mode = mode;// 记录变化的节点 effectlist=>renderthis.effectTag = NoEffect;this.subtreeTag = NoSubtreeEffect;this.deletions = null;this.nextEffect = null;// effectslist链 = firstEffect -> nextEffect -> nextEffect -> lastEffect => 交给render => 渲染this.firstEffect = null;this.lastEffect = null;// 作为调度优先级的属性this.lanes = NoLanes;this.childLanes = NoLanes;// 指向该fiber在另一次更新时对应的fiberthis.alternate = null;if (enableProfilerTimer) {// Note: The following is done to avoid a v8 performance cliff.//// Initializing the fields below to smis and later updating them with// double values will cause Fibers to end up having separate shapes.// This behavior/bug has something to do with Object.preventExtension().// Fortunately this only impacts DEV builds.// Unfortunately it makes React unusably slow for some applications.// To work around this, initialize the fields below with doubles.//// Learn more about this here:// https://github.com/facebook/react/issues/14365// https://bugs.chromium.org/p/v8/issues/detail?id=8538this.actualDuration = Number.NaN;this.actualStartTime = Number.NaN;this.selfBaseDuration = Number.NaN;this.treeBaseDuration = Number.NaN;// It's okay to replace the initial doubles with smis after initialization.// This won't trigger the performance cliff mentioned above,// and it simplifies other profiler code (including DevTools).this.actualDuration = 0;this.actualStartTime = -1;this.selfBaseDuration = 0;this.treeBaseDuration = 0;}if (__DEV__) {// This isn't directly used but is handy for debugging internals:this._debugID = debugCounter++;this._debugSource = null;this._debugOwner = null;this._debugNeedsRemount = false;this._debugHookTypes = null;if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {Object.preventExtensions(this);}}
}
举个例子:
import React, { Component } from 'react';class Header extends Component {render() {return <h1>{this.props.title}</h1>;}
}function Content(props) {return (<div><p>{props.text}</p><Footer /></div>);
}class Footer extends Component {render() {return <footer>Footer Content</footer>;}
}class App extends Component {render() {return (<div><Header title="Welcome to My App" /><Content text="This is some example content." /></div>);}
}export default App;
Fiber结构:
Root Fiber Node
└── Type: “div” (elementType: “div”)
├── Child Fiber Node
│ ├── Type: “Header” (elementType: Header)
│ │ └── State Node: Header instance
│ │ └── Props: { title: “Welcome to My App” }
│ └── Sibling: Content Fiber Node
└── Child Fiber Node
├── Type: “Content” (elementType: Content)
│ └── State Node: null (函数组件没有状态节点)
│ └── Props: { text: “This is some example content.” }
│ └── Child Fiber Node
│ ├── Type: “Footer” (elementType: Footer)
│ │ └── State Node: Footer instance
│ │ └── Props: {}
│ └── Sibling: null
更新dom
双缓存机制
内存中绘制当前的 fiber dom,绘制完后直接替换上一帧的fiber dom,这样省去两帧之间替换的计算时间,就不会存在白屏的情况,因此就有两棵fiber树
- current fiber 屏幕上正在显示的内容
- workingprogress fiber 内存中正在构建的树 简称 wip fiber
alternate 连接
mount 构建过程
- 应用级别的节点 ReactDom.render 创建 fiberRootNode
- rootFiber 组件树的根节点
render阶段 — scheduler reconciler
通过遍历 找到所有的fiber结构 实现可中断的异步递归
- 递 => 生成树
创建节点,形成节点之间的关系
vdom
从 rootfiber 深度优先遍历 fiber 调用 beginwork
- 根据传入的 fiber 节点创建子 fiber 节点,连接两个 fiber 节点
- 遍历叶子节点,进入归的阶段
- 归
调用completework
- 创建真实的dom节点
- 将当前节点下的子节点挂载到当前节点上
- 收集当前节点的effectlist
递 归 交错执行,直到归到rootFiber
react源码解析
react github
react-dom
react-dom/src/client/ReactDOMRoot.js
最终返回ReactDOMRoot这个实例
ReactDOMRoot 和 ReactDOMHydrationRoot 上面都挂载 render 方法
接收children
调用updateContainer方法,传入children,updateContainer来进行递归的这个阶段,创建当前节点
后面还挂载了unmount方法
react-reconciler
react-reconciler/src/ReactFiberReconciler.js
这里着重是 Scheduler
react-reconciler/src/ReactFiberWorkLoop.js
着重介绍循环创建fiber树的方法
核心方法:performUnitOfWork,workLoopConcurrent
workLoopSync 同步方法,不会判断 shouldYield,这是和workLoopConcurrent方法的区别
workLoopConcurrent方法是异步模式,都是调用performUnitOfWork构造fiber树
这里是Scheduler调度器将渲染任务拆分成不同的任务单元去创建对应的fiber,fiber通过performUnitOfWork去完成fiber单元的创建,然后通过shouldYield判断是否执行这样的任务
workInProgress是全局的变量,存储在全局
performUnitOfWork中记录当前“递”和“归”的一个过程,判断当前满足条件,进入beginWork
react-reconciler/src/ReactFiberBeginWork.js
- beginWork
if 阶段:后续进入到diff的过程,非首次渲染
否则为else阶段
后面:
-
updateHostComponent
-
reconcileChildren
mount组件:创建新的子Fiber节点
react-reconciler/src/ReactChildFiber.js
、
通过调用useFiber创建fiber节点
react-reconciler/src/ReactFiber.js
找到workInProgress,为null则创建Fiber节点
不论是哪个方法,最终返回的都是 workInProgress.child下一个节点
react-reconciler/src/ReactFiberWorkLoop.js
performUnitOfWork中
上述 beginWork返回的是workInProgress.child下一个节点,因此next就会发生变化
next为null时候,则叶子节点为空,调用completeUnitOfWork
next不为空,则将next指针赋值给workInProgress,修改workInProgress指向,重新执行beginWork
在 completeUnitOfWork 中,创建对应的dom元素,如果sibling不为null,然后创建对应的指针
commit阶段 同步阶段
effectlist
- before mutation 阶段,执行 dom操作前
- mutation 阶段,执行dom操作阶段,遍历effectlist,执行mutation
- layout 阶段,执行dom操作后,绘制
可以自己写一个实例,然后打断点看操作数据,操作结果