react17.x源码解析(1)——源码目录及react架构
react的源码目录如下,主要有三个文件夹:
- fixtures:一些测试demo,方便react编码时的测试
- packages: react的主要源码内容
- script: 和react打包、编译、本地开发相关的命令
我们要探究的源码内容,都存放在packages文件夹下:
根据packages下面各个部分的功能,我将其划分为了几个模块:
核心 api
react的核心api都位于packages/react文件夹下,包括createElement、memo、context以及hooks等,凡是通过react包引入的api,都位于此文件夹下。
调度和协调
调度和协调是 react16 fiber出现后的核心功能,和他们相关的包如下:
- scheduler:对任务进行调度,根据优先级排序
- react-conciler:diff算法相关,对fiber进行副作用标记
渲染
和渲染相关的内容包括以下几个目录:
- react-art:canvas、svg等内容的渲染
- react-dom:浏览器环境下的渲染,也是我们本系列中主要涉及讲解的渲染的包
- react-native-renderer: 用于原生环境渲染相关
- react-noop-renderer: 用于调试环境的渲染
辅助包
- shared:定义了react的公共方法和变量
- react-is:react中的类型判断
其他
其他的包和本次react源码探究的关联不是很多,不过多介绍。
react架构
react为了保证页面能够流畅渲染,react16之后的更新过程分为render和commit两个阶段。render阶段包括Scheduler(调度器)和Reconciler(协调器),commit阶段包括Renderer(渲染器):
触发更新
触发更新的方式主要有以下几种:ReactDOM.render(包括首次渲染)、setState、forUpdate、hooks中的useState以及ref的改变等引起的。
scheduler
当首次渲染或组件状态发生更新等情况时,此时页面就要发生渲染了。scheduler过程会对诸多的任务进行优先级排序,让浏览器的每一帧优先执行高优先级的任务(例如动画、用户点击输入事件等),从而防止react的更新任务太大影响到用户交互,保证了页面的流畅性。
reconciler
reconciler过程中,会开始根据优先级执行更新任务。这一过程主要是根据最新状态构建新的fiber树,与之前的fiber树进行diff对比,对fiber节点标记不同的副作用,对渲染过程中真实dom的增删改。
commit
在render阶段中,最终会生成一个effectList数组,记录了页面真实dom的新增、删除和替换等以及一些事件响应,commit会根据effectList对真实的页面进行更新,从而实现页面的改变。
jsx的转换
在React16版本及之前,应用程序通过@babel/preset-react将jsx语法转换为React.createElement的js代码,因此需要显示将React引入,才能正常调用createElement。
React17版本之后,官方与babel进行了合作,直接通过将react/jsx-runtime对jsx语法进行了新的转换而不依赖React.createElement,转换的结果便是可直接供ReactDOM.render使用的ReactElement对象。因此如果在React17版本后只是用jsx语法不使用其它的react提供的api,可以不引入React,应用程序依然能够正常运行。
React.createElement源码
虽然现在react17之后我们可以不再依赖React.createElement这个api了,但是实际场景中以及很多开源包中可能会有很多通过React.createElement手动创建元素的场景,所以推荐学习一下Reat.createElement源码。
React.createElement其接收三个或以上参数:
- type:要创建的React元素类型,可以是标签名称字符串,如'div'或者'span'等;也可以说是React组件类型(class组件或者函数组件);或者是React fragment类型。
- config:写在标签上的属性的集合,js对象格式,若标签上未添加任何属性则为null。
- children:从第三个参数开始后的参数为当前创建的React元素的子节点,每个参数的类型,若是当前元素节点的textContent则为字符串类型;否则为新的React.createElement创建的元素。
函数中会对参数进行一系列的解析,源码如下,对源码相关的理解都用注释进行了标记:
export function createElement(type,config,children){
let propName;
//记录标签上的属性集合
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
//config不为null时,说明标签上有属性,将属性添加到props中
//其中,key和ref为react提供的特殊属性,不加入到props中,而是用key和ref单独记录
if(config !=null){
if(hasValidRef(config)){
//有合法的ref时,则给ref赋值
ref = config.ref;
if(__DEV__){
warnIfStringRefCannotBeAutoConverted(config);
}
}
if(hasValidKey(config)){
//有合法的key时,则给key赋值
key = '' + config.key;
}
//self和source是开发环境下对代码在编译器中位置信息进行记录,用于开发环境下调试
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 将config中除key、ref、__self、__source之外的属性添加到props中
for(propName in config){
if(
hasOwnProperty.call(config,propName)&&
!RESERVED_PROPS.hasOwnProperty(propName)
){
props[propName] = config[propName];
}
}
}
//将子节点添加到props的children属性上
const childrenLength = arguments.length -2;
if(childrenLength===1){
//共3个参数时表示只有一个子节点,直接将子节点赋值给props的children属性
props.children = children;
}else if (childrenLength > 1){
//3个以上参数时表示有多个子节点,将子节点push到一个数组中然后将数组赋值给props的children
const childArray = Array(childrenLength);
for(let i=0;i<childrenLength;i++){
childArray[i] = arguments[i+2];
}
//开发环境下冻结 childArray,防止被随意修改
if(__DEV__){
if(Object.freeze){
Object.freeze(childArray);
}
}
props.children = childArray;
}
//如果有defaultProps,对其遍历并且将用户在标签上未对其手动设置属性添加props中
//此处针对class组件类型
if(type && type.defaultProps){
const defaultProps = type.defaultProps;
for(propName in defaultProps){
if(props[propName]===undefined){
props[porpName] = defaultProps[propName];
}
}
}
// key 和 ref不挂载到prps上
// 开发环境若想通过props.key 或者props.ref获取warning
if(__DEV__){
if(key || ref){
const displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type;
if(key){
defineKeyPropWarningGetter(props,displayName);
}
if (ref){
defineRefPropWarningGetter(props,displayName);
}
}
}
// 调用 ReactElement并返回
return ReactElement(
type,
key,
self,
source,
ReactCurrentOwner.current,
props,
)
}
由此可知,React.createElement 做的事情主要有:
- 解析config参数中是否有合法的key、ref、__source和__self属性,若存在分别赋值给key、ref、source和self;将剩余的属性解析挂载到props上
- 除type和config外后面的参数,挂载到props.children上
- 针对类组件,如果type.defaultProps存在,遍历type.defaultProps的属性,如果props不存在该属性,则添加到props上
- 将type、key、ref、self、props等信息,调用ReactElement等函数创建虚拟dom,ReactElement主要是在开发环境下通过Object.defineProperty将_store、_self、_source设置为不可枚举,提高element比较时的性能:
const ReactElement = function(type,key,ref,self,source,owner,props){
const element = {
//用于表示是否为ReactElement
&&typeof:REACT_ELEMENT_TYPE,
// 用于创建真实 dom 的相关信息
type:type,
key:key,
ref:ref,
props:props,
_owner:owner,
};
if(__DEV__){
element._store = {};
//开发环境下将_store、_self、_source设置为不可枚举,提高element的比较性能
Object.defineProperty(element._store,'validated',{
configurable:false,
enumerable:false,
writable:true,
value:false,
})
Object.defineProperty(element, '_self', {
configurable: false,
enumerable: false,
writable: false,
value: self,
});
Object.defineProperty(element, '_source', {
configurable: false,
enumerable: false,
writable: false,
value: source,
});
// 冻结 element 和 props,防止被手动修改
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}
return element;
};
所以通过流程图总结一下createElement所做的事情如下:
React.Component源码
function Component(props,context,updater){
//接收props,context,updater进行初始化,挂载到this上
this.props = props;
this.context = context;
this.refs = emptyObject;
//updater 上挂载了isMounted、enqueueForceUpdate、enqueueSetState等触发器方法
this.updater = updater || ReactNoopUpdateQueue;
}
//原型链上挂载 isReactComponent,在ReactDOM.render时用于和函数组件作区分
Component.prototype.isReactComponent = {};
//给类组件添加`this.setState`方法
Component.prototype.setState = function(partialState,callback){
//验证参数是否合法
invariant(
typeof partialState === 'object' || typeof partialState === 'function' || partialState == null
);
//添加至 enqueueSetState队列
this.updater.enqueueSetState(this,partialState,callback,'setState');
};
// 给类组件添加 `this.forceUpdate`方法
Component.prototype.forceUpdate = function(callback){
//添加至 enqueueForceUpdate队列
this.updater.enqueueForceUpdate(this,callback,'forceUpdate');
}
从源码上可以得知,React.Component主要做了以下几件事情:
- 将props,context,updater挂载到this上
- 在Component原型链上添加isReactComponent对象,用于标记类组件
- 在Component原型链上添加setState方法
- 在Component原型链上添加forceUpdate方法
这样我们就理解了react类组件的super()作用,以及this.setState和this.forceUpdate的由来
总结
react17之后babel对jsx的转换就是比之前多了一步 React.createElement的动作:
通过babel及React.createElement,将jsx转换为了浏览器能识别的原生js语法,为react后续对状态改变、事件响应以及页面更新奠定了基础。
fiber节点结构
fiber是一种数据结构,每个fiber节点的内部,都保存了dom相关信息、fiber树相关的引用、要更新时的副作用等,我们可以看下源码中的fiber结构:
export type Fiber = {|
//作为静态数据结构,存储节点dom相关信息
tag:WorkTag,//组件的类型,取决于react的元素类型
key:null | string,
elementType:any,//元素类型
type:any,//定义与此fiber关联的功能或类。对于组件,他指向构造函数;对于DOM元素,他指定HTML tag
stateNode:any,//真实dom节点
// fiber链表树相关
return:Fiber|null,//父 fiber
child:Fiber|null,//第一个子fiber
sibling: Fiber | null,//下一个兄弟fiber
index:number,//在父fiber下面的子fiber中的下标
ref:
|null
|(((handle:mixed)=>void)&{_stringRef:?string,...})
|RefObject,
//工作单元,用于计算 state和props渲染
pendingProps:any,//本次渲染需要使用props
memoizedProps:any,//上次渲染使用的props
updateQueue:mixed,//用于状态更新、回调函数、DOM更新的队列
memoizedState:any,//上次渲染后的state状态
dependencies:Dependencies | null, //contexts、events等依赖
mode:TypeOfMode,
//副作用相关
flags:Flags,//记录更新时当前fiber的副作用(删除、更新、替换等)状态
subtreeFlags:Flags, //当前子树的副作用状态
deletions:Array<Fiber> | null, //要删除的子fiber
nextEffet:Fiber | null,//下一个有副作用的fiber
firstEffect:Fiber | null,//指向第一个有副作用的fiber
lastEffectZ: Fiber | null,//指向最后一个有副作用的fiber
//优先级相关
lanes:Lanes,
childLanes:Lanes,
alternate:Fiber | null,//指向workInProgress fiber树中对应的节点
actualDuration?:number,
actualStartTime?:number,
selfBaseDuration?:number,
treeBaseDuration?:number,
_debugID?:number,
_debugSource?:Source | null,
_debugOwner?:Fiber | null,
_debugIsCurrentlyTiming?:boolean,
_debugNeedsRemount?:bollean,
_debugHookTypes?:Array<HooKType> | null,
|};
dom相关属性
fiber中和dom节点相关的信息主要关注tag、key、type、和stateNode。
tag
fiber中tag属性的ts类型为workType,用于标记不同的react组件类型,我们可以看一下源码中workType的枚举值;
//packages/react-reconciler/src/ReactWorkTags.js
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent =2;
export const HostRoot = 3;
export const HostPortal = 4;
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const FundamentalComponent = 20;
export const ScopeComponent = 21;
export const Block = 22;
export const OffscreenComponent = 23;
export const LegacyHiddenComponent = 24;
在react协调时,beginWork和completeWork等流程时,都会根据tag类型的不同,去执行不同的函数处理fiber节点。
key和type
key和type两项用于react diff过程中确定fiber是否可以复用。
key为用户定义的唯一值。type定义与此fiber关联的功能或类。对于组件,他指向函数或者类本身;对于DOM元素,他指定HTML tag。
stateNode
stateNode用于记录当前fiber所对应的真实dom节点或者当前虚拟组件的实例,这么做的原因第一是为了实现Ref,第二是为了实现真实dom的跟踪。
链表树相关属性
我们看一下和fiber链表树构建相关的return、child和sibling几个字段:
- return:指定父fiber,若没有父fiber则为null
- child: 指向第一个子fiber,若没有任何子fiber则为null
- sibling:指向下一个兄弟fiber,若没有下一个兄弟fiber则为null
通过这几个字段,各个fiber节点构成了fiber链表树结构:
副作用相关属性
首先理解一下react中的副作用,举一个生活中比较通俗的例子:我们感冒了本来吃点药就没事了,但是吃了药身体过敏了,而这个过敏就是副作用。react中,我们修改了state、props、ref等数据,除了数据改变之外,还引起dom的变化,这种render阶段不能完成的工作,我们称之为副作用。
flags
react中通过flags记录每个节点diff后需要变更的状态,例如dom的添加、替换、删除等。
Effect List
在render阶段,react会采用深度优化先遍历,对fiber数进行遍历,把每一个副作用的fiber筛选出来,最后构建生成一个只带副作用的Effect list 链表。和该链表相关的字段有firstEffect、nextEffect和lastEffect:
firstEffect指向第一个有副作用的fiber节点,lastEffect指向最后一个有副作用的节点,中间的节点全部通过nextEffect链接,最终形成Effect链表。
在commit阶段,React拿到Effect list 链表中的数据后,根据每一个fiber节点的flags类型,对相应的DOM进行更改。
其它
其它需要重点关注一下的属性还有lane和alternate。
lane
lane代表react要执行的fiber任务的优先级,通过这个字段,render阶段react确定应该优先将哪些任务提交到commit阶段去执行。
我们看一下源码中lane的枚举值:
同Flags的枚举值一样,Lanes也是用31位的二进制数表示,表示了31条赛道,位数越小的赛道,代表的优先级越高。
例如 InputDiscreteHydrationLane、InputDiscreteLanes、InputContinuousHydrationLane等用户交互引起的更新的优先级较高,DefaultLanes这种请求数据引起更新的优先级中等,而OffscreenLane、IdleLanes这种优先级较低。
优先级越低的任务,在render阶段越容易被打断,commit执行的时机越靠后。
alternate
当react的状态发生更新时,当前页面所对应的fiber树称为current Fiber,同时react会根据新的状态构建一颗新的fiber树,称为 workInProgress Fiber。current Fiber中每个fiber节点通过alternate字段,指向workInProgress Fiber中对应的fiber节点。同样workInProgress Fiber中的fiber节点的alternate字段也会指向current Fiber中对应的fiber节点。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南