有的同学写了很久的react
但是并不知道其内部原理是怎么执行的,比如在页面初始化展示的过程中内部都发生了什么,做了什么事都不是很清楚,今天看完这篇文章就能让你知道react
初始化挂载页面的时候内部都做了些什么事
目标:阅读完之后了解
ReactDOM.render
从0到1的过程都做了什么
准备工作
前期准备工作需要去
react
官网下载一份源码,之后我在下面的篇幅中会具体指出是哪一个文件哪一行代码具体做了什么事情,大家可以根据自己下载的源码对照着文章去解读源码,在解读的过程中可能会有些痛苦,但是结果有收获就是好的,可能react
的源码解读起来也有一些难度不过这也正常毕竟是比较顶尖的程序猿编写的
1.首先去下面这个地方下载一下源码,防止看的不一样所以就指定tag17.0.2
的源码去阅读
2.下面是我们即将要看的相关文件目录都是一些核心的部分
react-dom
核心文件都在这里暴露出来比如render、createPortal、hydrate
等核心apireact-reconciler
看英文也知道这里主要做的是节点之间的协调,包括节点的创建和节点之间的组装、提交和diff
比较更新等操作scheduler
任务之间的调度主要发生在这个文件
开始
下面会按照文件划分模块去依次讲解里面都做了什么,都会以截图的方式和增加注释进行展示,然后里面我觉得不需要讲解的都会折叠起来或者删掉免得影响阅读,如果大家懒得下载源码自己对着阅读,可以多看两遍这个文章心里有个大概就好
render
我们直接来看render
方法,这个就是我们外面调用的ReadtDOM.render
接下来揭开它神秘的面纱
文件目录
react-dom -> src -> client -> ReactDOMLegacy.js
-
render接收三个参数
- 第一个是我们传进来的
JSX
对象也就是常说的Virtal DOM
,这里直接变成了Virtal DOM
是因为在webpack
启动打包中,就编译成这样子了 - 第二个是渲染元素乘载的容器一般是
<div id='root'></div>
- 第三个是一个回调函数当应用挂载完成之后就会执行,类似于
window.onload
,监测应用挂载完成
- 第一个是我们传进来的
render
方法里面调用了legacyRenderSubtreeIntoContainer
,这个函数在hydrate
里面也有调用也就是服务端渲染的时候,接下来看这个方法内部都干了点啥
legacyRenderSubtreeIntoContainer
文件目录
react-dom -> src -> client -> ReactDOMLegacy.js
-
这个方法做的第一件事判断是否有root如果没有说明是首次渲染
// Initial mount should not be batched. unbatchedUpdates(() => { //首次渲染嵌套了unbatchedUpdates 走非批量更新 updateContainer(children, fiberRoot, parentComponent, callback); }); 复制代码
外层通过调用
unbatchedUpdates
方法使当前环境变为非批量更新阶段 -
如果有root证明是更新阶段进行批量更新渲染
// Update //进行批量更新任务 updateContainer(children, fiberRoot, parentComponent, callback); 复制代码
可以看到外层没有嵌套
unbatchedUpdates
方法说明当前是走批量更新的流程 -
接下来我们看
unbatchedUpdates
方法内部是怎么定义的变量,因为今天主要看初始化流程
unbatchedUpdates
文件目录
react-reconciler -> src -> ReactFiberWorkLoop.old.js
-
设置当前环境为非批量更新
const prevExecutionContext = executionContext; //表示当前上下文环境中BatchedContext的位置是非批量更新 executionContext &= ~BatchedContext; //在executionContext上设置为非批量更新 executionContext |= LegacyUnbatchedContext; 复制代码
-
执行fn方法这个其实就是执行
updateContaine
r进行更新try { return fn(a); } finally { } 复制代码
-
接下来继续看
updateContainer
方法内部做了啥
updateContainer
文件目录
react-reconciler -> src -> ReactFiberWorkLoop.old.js
-
updateContainer方法主要是用来执行更新任务接收四个参数
- 第一个element要更新的节点
- 第二个container是容器元素
- 第三个parentComponent是上一次的节点主要是用在更新阶段的
- 第四个callback参数是完成之后的通知方法
-
第一步创建update节点,用于之后挂载到fiber上面方便后续diff更新
//创建更新任务fiber节点 const update = createUpdate(eventTime, lane); //将JSX对象挂载到payload上 update.payload = {element}; 复制代码
-
createUpdate
方法内部生成fiber节点并返回export function createUpdate(eventTime: number, lane: Lane): Update<*> { const update: Update<*> = { eventTime, lane, tag: UpdateState, payload: null, callback: null, next: null, }; return update; } 复制代码
-
第二步将任务挂载到fiber节点上
//将任务挂载到fiber节点上方便后期更新 enqueueUpdate(current, update); 复制代码
-
enqueueUpdate方法内部进行挂载操作
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) { const updateQueue = fiber.updateQueue; //这里是当执行销毁期函数时会发生 if (updateQueue === null) { // Only occurs if the fiber has been unmounted. return; } const sharedQueue: SharedQueue<State> = (updateQueue: any).shared; const pending = sharedQueue.pending; if (pending === null) { // This is the first update. Create a circular list. update.next = update; } else { update.next = pending.next; pending.next = update; } sharedQueue.pending = update; } 复制代码
-
第三步开始更新fiber节点接下来主要看
scheduleUpdateOnFiber
这个文件有点大
scheduleUpdateOnFiber
文件目录
react-reconciler -> src -> ReactFiberWorkLoop.old.js
-
这个方法主要看红色标注的逻辑
if ( // 判断当前存在的环境是否是批量更新的情况 (executionContext & LegacyUnbatchedContext) !== NoContext && // 判断当期的环境是否出是初始化环境 (executionContext & (RenderContext | CommitContext)) === NoContext ) { //初始化并且不是批量更新的情况会走进来 schedulePendingInteractions(root, lane); //执行任务开始挂载节点 performSyncWorkOnRoot(root); } 复制代码
-
performSyncWorkOnRoot
方法里面调用了renderRootSync
核心逻辑都在这个方法里面
文件目录
react-reconciler -> src -> ReactFiberWorkLoop.old.js
renderRootSync
文件目录
react-reconciler -> src -> ReactFiberWorkLoop.old.js
-
这个方法在
do while
这里主要是循环提交任务,提交任务的操作在workLoopSync
do { try { workLoopSync(); break; } catch (thrownValue) { handleError(root, thrownValue); } } while (true); 复制代码
workLoopSync
文件目录 react-reconciler -> src -> ReactFiberWorkLoop.old.js
- workInProgress是全局存的一个变量主要用来执行当前的任务
- 接下来继续看
performUnitOfWork
performUnitOfWork
文件目录
react-reconciler -> src -> ReactFiberWorkLoop.old.js
- 这里开始调用
beginWork
,我们直接看beginWork
内部做了什么
beginWork
文件目录
react-reconciler -> src -> ReactFiberBeginWork.old.js
-
这个文件比较大我会把咱们需要看的单独拆出来
//新旧两个props比较 //前后context进行比较 //如果有一个为真didReceiveUpdate设置为true if ( oldProps !== newProps || hasLegacyContextChanged() || // Force a re-render if the implementation changed due to hot reload: (__DEV__ ? workInProgress.type !== current.type : false) ) { didReceiveUpdate = true; //! 如果需要更新设置为true } else if (!includesSomeLane(renderLanes, updateLanes)) { didReceiveUpdate = false;//! 如果不需要更新设置为false } 复制代码
-
在往下是根据
workInProgress
上面的tag
去判断执行不同的任务switch (workInProgress.tag) { case LazyComponent: { const elementType = workInProgress.elementType; return mountLazyComponent( current, workInProgress, elementType, updateLanes, renderLanes, ); } case FunctionComponent: { const Component = workInProgress.type; const unresolvedProps = workInProgress.pendingProps; const resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps); return updateFunctionComponent( current, workInProgress, Component, resolvedProps, renderLanes, ); } //更新类组件 case ClassComponent: { const Component = workInProgress.type; const unresolvedProps = workInProgress.pendingProps; const resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps); return updateClassComponent( current, workInProgress, Component, resolvedProps, renderLanes, ); } case HostRoot: return updateHostRoot(current, workInProgress, renderLanes); case HostComponent: return updateHostComponent(current, workInProgress, renderLanes); case HostText: return updateHostText(current, workInProgress); case SuspenseComponent: return updateSuspenseComponent(current, workInProgress, renderLanes); case HostPortal: return updatePortalComponent(current, workInProgress, renderLanes); } 复制代码
-
这里的switch方法删除了一部分然后留下了一部分常见的,之后我们今天用
ClassComponent
来举例看看内部是怎么挂载实现的,类组件case里面调用了updateClassComponent
updateClassComponent
文件目录
react-reconciler -> src -> ReactFiberBeginWork.old.js
这里主要看
resumeMountClassInstance
方法和finishClassComponent
方法
resumeMountClassInstance
文件目录
react-reconciler -> src -> ReactFiberClassComponent.old.js
-
1.先看
resumeMountClassInstance
方法内部做了啥,这里会把咱们要看的代码拿出来因为整个方法比较长const getDerivedStateFromProps = ctor.getDerivedStateFromProps; const hasNewLifecycles = typeof getDerivedStateFromProps === 'function' || typeof instance.getSnapshotBeforeUpdate === 'function'; 复制代码
这里判断了是否有这两个生命周期如果有在下面会执行
-
2.看一下下面处理state的逻辑
const oldState = workInProgress.memoizedState; let newState = (instance.state = oldState); processUpdateQueue(workInProgress, newProps, instance, renderLanes); newState = workInProgress.memoizedState; 复制代码
这里processUpdateQueue内部会进行state合并并且返回一个合并好的新的state,之后在下面进行挂载
-
3.这里判断组件上是否有这个生命周期
if ( unresolvedOldProps === unresolvedNewProps && oldState === newState && !hasContextChanged() && !checkHasForceUpdateAfterProcessing() ) { if (typeof instance.componentDidUpdate === 'function') { if ( unresolvedOldProps !== current.memoizedProps || oldState !== current.memoizedState ) { workInProgress.flags |= Update; } } if (typeof instance.getSnapshotBeforeUpdate === 'function') { if ( unresolvedOldProps !== current.memoizedProps || oldState !== current.memoizedState ) { workInProgress.flags |= Snapshot; } } return false; } 复制代码
判断如果组件中有生命周期就会在当前环境标注一下
-
4.了解一下
getDerivedStateFromProps
方法if (typeof getDerivedStateFromProps === 'function') { applyDerivedStateFromProps( workInProgress, ctor, getDerivedStateFromProps, newProps, ); newState = workInProgress.memoizedState; } 复制代码
这个方法是react新增的可以用来给state赋初始值
-
5.神秘的
ShouldComponentUpdate
方法背后的代码const shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate( workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext, ); if (shouldUpdate) { if ( !hasNewLifecycles && (typeof instance.UNSAFE_componentWillMount === 'function' || typeof instance.componentWillMount === 'function') ) { if (typeof instance.componentWillMount === 'function') { instance.componentWillMount(); } if (typeof instance.UNSAFE_componentWillMount === 'function') { instance.UNSAFE_componentWillMount(); } } if (typeof instance.componentDidMount === 'function') { workInProgress.flags |= Update; } } else { if (typeof instance.componentDidMount === 'function') { workInProgress.flags |= Update; } workInProgress.memoizedProps = newProps; workInProgress.memoizedState = newState; } 复制代码
这里判断
shouldComponentUpdate
是否返回true并且内部是否有用了PureReactComponent
然后之后执行不同的生命周期,最后给workInProgres上面的memoizedProps和memoizedState重新赋值,因为在后期渲染的时候会用到
-
6.这个方法的尾部给实例上的属性赋值
props、state、context
,这就是为什么在组件中可以用this调用这些东西instance.props = newProps; instance.state = newState; instance.context = nextContext; 复制代码
finishClassComponent
文件目录
react-reconciler -> src -> ReactFiberBeginWork.old.js
- 这里主要看里面的
reconcileChildren
方法,这个方法内部进行节点调和
reconcileChildren
文件目录
react-reconciler -> src -> ReactFiberBeginWork.old.js
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
//current为null证明是首次渲染走下面这个if, mountChildFibers
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
复制代码
接下来继续看
mountChildFibers
,这个方法调用的是一个工厂函数叫做ChildReconciler
// 可以看到 都是调用的ChildReconciler方法只是入参不一样 true 是进行更新操作 false是进行初始化操作
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
复制代码
ChildReconciler
文件目录
react-reconciler -> src -> ReactChildFiber.old.js
- 这个方法是一个工厂函数里面包含了20几个方法总共要一千多行,里面主要包括了对各种类型的节点比较处理更新操作,如果大家感兴趣可以跟着文件目录去看一下这个方法大概在270多行,下面我会把初始化的逻辑代码拿出来解释
工厂函数返回了这个方法reconcileChildFibers里面主要是协调子节点
- 当是对象的情况会单独处理对象里面的子节点
//根据对应的节点进行更新操作,针对数组对象还有普通文本节点
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_LAZY_TYPE:
if (enableLazyElements) {
const payload = newChild._payload;
const init = newChild._init;
// TODO: This function is supposed to be non-recursive.
return reconcileChildFibers(
returnFiber,
currentFirstChild,
init(payload),
lanes,
);
}
}
}
复制代码
普通文本节点的处理
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
复制代码
这些节点更新方法内部都会调用一个createFiber方法主要用来创建fiber节点
- 文件目录
react-reconciler -> src ->ReactFiber.old.js
const createFiber = function(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
): Fiber {
// $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
return new FiberNode(tag, pendingProps, key, mode);
};
复制代码
FiberNode
文件目录
react-reconciler -> src ->ReactFiber.old.js
在看一下FiberNode
创建节点内部,这个方法是fiber
创建的一个基类
解析一下fiber中的节点
this.tag = tag; //标记组件类型
this.key = key; //节点key值在后期diff的时候会用到
this.type = null;//节点type类型可能是div或者其他标签,是组件的时候就是函数
this.stateNode = null; //真实节点比如就是div或者是其他标签
// Fiber
this.return = null; //指向的父节点
this.child = null; //指向的子节点
this.sibling = null; //指向的兄弟节点
this.memoizedState = null; //存储渲染完成结束后的state
this.pendingProps = pendingProps; //一般是初始化之后的props
this.memoizedProps = null; // 执行完渲染结束之后的props
this.lanes = NoLanes; //当前节点任务的优先级是否要渲染它,还是有更高的优先级任务
this.alternate = null; //存储上一次的节点,在diff的时候会根据这个节点上面的属性和当前节点属性做判断
复制代码
结语
到这里初始化源码分析就结束了,如果大家有时间的话可以对这源码文件看自己梳理一遍,心里有个大概流程,可能要花上半个小时的时间,但是梳理完了自己对初始化流程的理解肯定是有一定提升的