React源码分析
本文并非按照代码运行流程解析React相关源码,而是按照常用的API去了解源码中的实现,因此章节阅读顺序可以随意切换。
快速浏览react API
参考:React顶层API
为了便于理解源码,我们需要大致了解下面API及其使用
ref提供了访问DOM节点或组件实例的方式
- 可以用于集成第三方库、绕开
props与子节点通信等 - 使用方式:通过
React.createRef创建Refs,将其赋给子节点的ref属性,在挂载之后可以通过ref.current访问 - 如果需要把子组件的ref暴露给父组件,可以通过
React.forwardRef使用refs转发
Fragments允许在render函数或函数组件中返回子组件列表,而非单个子组件
- 无需向DOM中添加额外的子节点
- 使用方式:
<React.Fragment></React.Fragment>或更简单的<></>包裹子组件列表
Portal,提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的方案,
- 可以用来实现全局弹窗组件等需求
- 使用方式:在render方法中返回
ReactDOM.createPortal(child, container)
Hooks可以让开发者在不编写 class 的情况下使用 state 以及其他的 React 特性
- 继高阶组件、render props之后,一种更方便的在组件之间重用状态逻辑的方案
- 使用方式:使用
useState、useEffect等内置Hook,支持自定义Hook
Context提供了一种无须通过props直接在组件树之间进行数据传递的方式
- 可以在多个组件之间使用全局数据,如主题、偏好设置等
- 使用方式:通过
React.createContext创建上下文context,然后使用<context.Provider>组件提供数据,在子组件中通过指定static contextType属性或者<context.Consumer>组件,访问到上下文数据
类组件生命周期
参考:
beginWork阶段
在之前的源码分析中我们了解到, 在beginWork方法中会根据fiber.tag判断对应子节点的类型,如果是ClassComponent,则调用updateClassComponent
// 为了方便理解,下面方法移除了大量代码
function updateClassComponent(){
const instance = workInProgress.stateNode;
if (instance === null) {
// 初始化
constructClassInstance();
mountClassInstance();
} else if (current === null) {
// 当unitOfWork.alternate为null
shouldUpdate = resumeMountClassInstance();
} else {
// 直接更新
shouldUpdate = updateClassInstance();
}
// 如果shouldUpdate为false,则不会重新渲染
return finishClassComponent(shouldUpdate)
}接下来看看初始化时的生命周期函数调用顺序。
function constructClassInstance(){
const instance = new ctor(props, context);
adoptClassInstance(workInProgress, instance); // 将instance挂载到workInProgress.stateNode
}
function mountClassInstance(){
const instance = workInProgress.stateNode;
instance.props = newProps;
instance.state = workInProgress.memoizedState;
instance.refs = emptyRefsObject;
workInProgress.updateQueue !== null && processUpdateQueue()
// 调用static getDerivedStateFromProps 生命周期函数
const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
getDerivedStateFromProps && applyDerivedStateFromProps()
// 当未使用新的生命周期函数时,作为补丁,则会调用旧的生命周期函数componentWillMount
if (
typeof ctor.getDerivedStateFromProps !== 'function' &&
typeof instance.getSnapshotBeforeUpdate !== 'function' &&
(typeof instance.UNSAFE_componentWillMount === 'function' ||
typeof instance.componentWillMount === 'function')
) {
callComponentWillMount(workInProgress, instance);
workInProgress.updateQueue !== null && processUpdateQueue()
}
}
// 开始调用render方法获取子节点,然后构建新的fiber树
function finishClassComponent(){
const instance = workInProgress.stateNode;
let nextChildren = instance.render();
reconcileChildren()
return workInProgress.child;
}getDerivedStateFromProps
从源码可以看出,在getDerivedStateFromProps接收nextProps和当前的state,并返回新的state
function applyDerivedStateFromProps(){
const partialState = getDerivedStateFromProps(nextProps, prevState);
const memoizedState =
partialState === null || partialState === undefined
? prevState
: Object.assign({}, prevState, partialState);
workInProgress.memoizedState = memoizedState;
}可见该方法的作用是:让组件在 props 变化时更新 state。
参考官方提供的这篇文档:什么时候使用派生 state
注意该钩子为静态方法,也就是说不能在其中通过this获得组件实例。
componentWillMount(将废弃)
注意只有当未调用新的生命周期函数时,才会调用componentWillMount
function callComponentWillMount(workInProgress, instance) {
const oldState = instance.state;
instance.componentWillMount && instance.componentWillMount(); // 调用componentWillMount
if (oldState !== instance.state) {
classComponentUpdater.enqueueReplaceState(instance, instance.state, null);
}
}可以看见该方法主要是执行了componentWillMount钩子函数,如果在其中显式修改了this.state的指向,则相当于调用了this.setState方法
componentWillReceiveProps(将废弃)
回到前面的updateClassComponent中,如果不是初始化
当节点未挂载时,则调用
resumeMountClassInstance
function resumeMountClassInstance(){
const instance = workInProgress.stateNode;
// 只有未使用新的生命周期函数时
if (!hasNewLifecycles && instance.componentWillReceiveProps){
// 新旧props不相同
if (oldProps !== newProps || oldContext !== nextContext) {
// 调用组件的componentWillReceiveProps方法
callComponentWillReceiveProps();
}
}
// 调用applyDerivedStateFromProps方法
ctor.getDerivedStateFromProps && applyDerivedStateFromProps();
const shouldUpdate =
checkHasForceUpdateAfterProcessing() || // 判断是否是forceUpdate
checkShouldComponentUpdate(); // 调用组件的 instance.shouldComponentUpdate
if(shouldUpdate){
// 调用componentWillMount
!hasNewLifecycles && instance.componentWillMount();
}
}首先调用了componentWillReceiveProps,然后在非强制更新的情况下调用checkShouldComponentUpdate检测是否需要更新,如果需要,再调用componentWillMount
当节点只需要更新时,调用
updateClassInstance
function updateClassInstance(){
const instance = workInProgress.stateNode;
if (!hasNewLifecycles && instance.componentWillReceiveProps) {
if (oldProps !== newProps || oldContext !== nextContext) {
callComponentWillReceiveProps();
}
}
// 调用applyDerivedStateFromProps方法
ctor.getDerivedStateFromProps && applyDerivedStateFromProps();
const shouldUpdate =
checkHasForceUpdateAfterProcessing() || // 判断是否是forceUpdate
checkShouldComponentUpdate(); // 调用组件的 instance.shouldComponentUpdate
if(shouldUpdate){
// 调用componentWillUpdate
!hasNewLifecycles && instance.componentWillUpdate();
}
}可以看见与上面的resumeMountClassInstance相比,除了shouldUpdate为true时调用的是componentWillUpdate之外,其余流程基本类似。
shouldComponentUpdate
在非强制更新时,上面两个方法都调用了shouldComponentUpdate
function checkShouldComponentUpdate(
const instance = workInProgress.stateNode;
if(instance.shouldComponentUpdate) {
const shouldUpdate = instance.shouldComponentUpdate(
newProps,
newState,
nextContext,
);
return shouldUpdate;
}
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
return true
}可以看见其内部,优先调用组件shouldComponentUpdate方法,如果是PureReactComponent,则进行浅比较,最终返回shouldUpdate。在后续的finishClassComponent方法中,如果传入的shouldUpdate为false,则不会重新渲染组件。
function finishClassComponent(){
if (!shouldUpdate && !didCaptureError) {
return bailoutOnAlreadyFinishedWork();
}
}注意事项
引入了Fiber之后,如果renderRoot方法是传入了异步调用参数,则在上面提到的、在commit之前的所有生命周期函数都可能会被调用多次,程序就可能不会按照开发者预期的流程运行。
commit阶段
当fiber树构建完毕之后,会进入commit阶段,之前分析过commitRoot的大致流程
在commitRootImpl方法中提交更新任务,可以分为如下三个阶段
function commitRoot(){
runWithPriority(ImmediatePriority, commitRootImpl.bind(null, root));
}
function commitRootImpl(){
let firstEffect = finishedWork // 获取需要提交的列表
// before mutation 阶段
nextEffect = firstEffect; // nextEffect是一个全局变量
do {
commitBeforeMutationEffects();
} while (nextEffect !== null);
// mutation阶段
nextEffect = firstEffect;
do {
commitMutationEffects();
} while (nextEffect !== null);
// layout阶段
nextEffect = firstEffect;
do {
commitLayoutEffects(root, expirationTime);
} while (nextEffect !== null);
nextEffect = null;
requestPaint(); // 告诉调度器可以开始绘制下一帧
onCommitRoot(finishedWork.stateNode, expirationTime);
flushSyncCallbackQueue();
}getSnapshotBeforeUpdate
在commitBeforeMutationEffects阶段,会根据nextEffect.effectTag来判断,如果(nextEffect.effectTag & Snapshot) !== NoEffect,则调用commitBeforeMutationEffectOnFiber
// commitBeforeMutationEffectOnFiber,根据`finishedWork.tag`调用对应节点的方法,如果是`ClassComponent`
if (finishedWork.effectTag & Snapshot) {
// 当更新时调用getSnapshotBeforeUpdate
if (current !== null) {
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
}可以看见getSnapshotBeforeUpdate在commit的before mutation阶段调用,它使得组件能在发生更改之前从 DOM 中捕获一些信息。
componentDitMounted、componentDidUpdate
在commitLayoutEffects阶段,会根据nextEffect.effectTag来判断,如果是
effectTag & (Update | Callback),则调用commitLayoutEffectOnFiber,即commitLifeCycleseffectTag & Ref,则调用commitAttachRef
// commitLifeCycles,根据`finishedWork.tag`调用不同类型节点的方法,当其为`ClassComponent`时
const instance = finishedWork.stateNode;
// 如果有更新
if (finishedWork.effectTag & Update) {
if (current === null) {
// 初始化时调用componentDidMount
instance.componentDidMount();
} else {
const prevState = current.memoizedState;
// 更新时调用componentDidUpdate
instance.componentDidUpdate(
prevProps,
prevState,
// 该字段是上面getSnapshotBeforeUpdate钩子的返回值
instance.__reactInternalSnapshotBeforeUpdate
);
}
}
const updateQueue = finishedWork.updateQueue;
commitUpdateQueue(updateQueue)可见初始化时调用的是componentDidMount钩子,后续更新时调用的是componentDidUpdate。
碰巧看见了commitAttachRef,我们顺道看看ref是如何挂载的
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
// 获取节点的实例,DOM节点为实际元素,类组件为组件实例
const instance = finishedWork.stateNode;
let instanceToUse = instance;
if (typeof ref === 'function') {
// ref={(el)=>{this.xxx = el}}形式的ref
ref(instanceToUse);
} else {
// React.createRef形式的ref
ref.current = instanceToUse;
}
}
}初始化时可以通过ref获取子节点的引用,在后续的声明周期如getSnapshotBeforeUpdate中,可以通过ref获取DOM节点,并获取该节点在发生变化之前的信息(如滚动位置等)
componentWillUnmount
在Commit过程的mutation阶段,调用commitMutationEffects,根据effectTag判断实际的改动
Placement,调用commitPlacement插入节点PlacementAndUpdate,调用commitPlacement和commitWorkUpdate,调用commitWork更新容器Deletion,调用commitDeletion删除节点,此处会调用卸载Unmount函数
function commitDeletion(current: Fiber): void {
// 实际上ReactDOM中调用的是unmountHostComponents,将DOM从父节点移除,
// 其内部调用commitNestedUnmounts
commitNestedUnmounts(current);
// 清空fiber的相关引用,准备释放
detachFiber(current);
}
function commitNestedUnmounts(root: Fiber): void {
let node: Fiber = root;
while (true) {
// 依次调用commitUnmount
commitUnmount(node);
// node遍历顺序 root->子节点->兄弟节点->父节点
}
}
function commitUnmount(current: Fiber): void {
if(current.tag === ClassComponent){
// 清除ref
safelyDetachRef(current);
const instance = current.stateNode;
// 调用componentWillUnmount方法
if (typeof instance.componentWillUnmount === 'function') {
safelyCallComponentWillUnmount(current, instance);
}
return;
}
}instance.componentWillUnmount会在组件卸载及销毁之前直接调用,该钩子函数主要用于执行一些清除操作,如计数器、网络请求、事件订阅等。
setState批量更新
参考:
每次setState都会重新渲染子树。如果你想提高性能,就尽量在低层次结构中调用setState或者使用shouldComponentUpdate去阻止渲染很大的子树。
之前在整理ReactDOM.render方法时了解到,在初始化时legacyRenderSubtreeIntoContainer方法内部调用的是
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});这里我们来看看unbatchedUpdates方法的作用,可见其内部只是将全局变量executionContext设置为了
export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
const prevExecutionContext = executionContext;
executionContext &= ~BatchedContext;
executionContext |= LegacyUnbatchedContext;
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
flushSyncCallbackQueue();
}
}
}我们可以回顾一下setState的执行流程
this.setState()
this.updater.enqueueSetState
this.updater = classComponentUpdater搜索executionContext,找到赋值为BatchedContext的地方
小结
在合成事件和钩子函数中,React会通过一个
setState只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout等异步队列中中都是同步的。
React中的事件
参考:
事件注册
在前面的整理中我们知道了DOM节点的初始化流程,主要经过下面步骤
- 调用
renderRoot方法渲染根节点,内部调用performUnitOfWork->completeUnitOfWork->completeWork - 在
completeWork中,根据当前正在运行的workInProgress.tag来选择对应的运行逻辑,如果是HostComponent- 调用
createInstance来创建DOM对象,接着在finalizeInitialChildren方法中调用setInitialProperties来获取需要初始化的属性nextProps,实际上即为JSX在标签上解析的相关属性 - 然后调用在
setInitialDOMProperties,并遍历nextProps上的字段,根据不同的字段类型做对应处理,如dangerouslySetInnerHTML、children和事件名等,这里我们暂时只需要关注事件属性的处理 - 如果节点上包含事件属性,则调用
ensureListeningTo(rootContainerElement, propKey)- 通过
listeningSet集合,保证多个节点的相同事件名只会在document上注册一次 - 事件的注册是在
trapEventForPluginEventSystem方法中完成的,内部会根据事件名确认事件优先级,然后实现对应的事件处理函数如listener = dispatchEvent.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM)
- 通过
- 调用
- 最后将对应事件名和事件处理函数注册在根节点
document上,完成事件委托
事件分发
前面我们知道了所有的事件类型都被注册在document上,现在来整理一下事件触发时的运行流程,以click为例,
- 点击文档时,将触发事件处理函数,此时将获取原始的事件对象
nativeEvent和事件类型,根据nativeEvent.target可以获取对应的targetInst,及该DOM节点对应的Fiber Node - 根据事件类型和原始事件,调用
possiblePlugin.extractEvents初始化一个合成事件event- 调用
EventConstructor.getPooled获取事件对象- 如果事件池有剩余的事件对象,则取出并根据原始事件修改属性
- 如果事件池无可用的事件对象,则初始化一个合成事件对象
- 调用
accumulateTwoPhaseDispatchesSingle获取事件处理函数- 从
targetInst开始,向上遍历父节点,填充事件传递的path,越靠后的节点越顶层 - 从后向前遍历节点,触发
captured,然后获取节点props的on...Capture属性作为事件处理函数,如果存在,则将其保存在event._dispatchListeners - 从前向后遍历节点,触发
bubbled,然后获取节点props的on...属性作为事件处理函数,如果存在,则同样将其保存在event._dispatchListeners - 注意当前
event._dispatchListeners可能保存了多个节点的事件处理函数
- 从
- 调用
- 然后在
executeDispatchesAndRelease方法中执行事件处理函数- 依次调用事件处理函数列表,先捕获后冒泡,并将合成事件
event作为参数传递给事件处理函数 - 事件函数处理完毕,判断合成事件是否可回收,如果可回收,则将其放回事件池,留作下次使用
- 依次调用事件处理函数列表,先捕获后冒泡,并将合成事件
React性能优化
从源码中我们可以发现下面几个优化点
代码分割
React.lazy函数能让你像渲染常规组件一样处理动态引入(的组件),Suspense可以用于等待异步组件加载的展示
合并渲染
在UI变化中,不必立即触发每个更新,比如在极短的时间内页面状态A->B->C,那更新状态B就导致性能浪费。
可以说,setState是对单个组件的合并渲染,batchedUpdates是对多个组件的合并渲染。合并渲染是React最主要的优化手段。
减少渲染次数
React.memo
如果函数组件在给定相同 props 的情况下渲染相同的结果,那么可以通过将其包装在 React.memo 中调用
shouldComponentUpdate
在 shouldComponentUpdate() 中根据当前 state 或 props 判断是否需要调用render方法来重新渲染子节点。
https://zh-hans.reactjs.org/docs/react-component.html#shouldcomponentupdate
PureComponent
React.Component 并未实现 shouldComponentUpdate(),而 React.PureComponent 中以浅层对比 prop 和 state 的方式来实现了该函数。
PureComponent并未实现 shouldComponentUpdate方法,只是对props和state进行浅比较,可以结合使用Immutable.js来创建不可变对象,通过它来简化对象比较,提高性能。
Hooks实现原理
在updateFunctionComponent中,调用的是renderWithHooks返回子节点
全局Context
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。
