注:本文使用的版本是React-17.0.0.3,开启enableNewReconciler = true,即会使用new.js后缀的文件;
文章专注于hook原理和react的渲染部分,涉及到classComponent的部分一律略过;
mode = concurrent
本文所有代码都是基于以下react代码
import { React } from "../CONST";
import { useEffect, useState } from "../react/packages/react";
function FunctionComponent({name}) {
const [count, setCount] = useState(0);
const [subtraction, setSubTraction] = useState(1);
useEffect(() => {
setSubTraction(10);
}, []);
return (
<div className="function border">
{name} {count} {count1}
<button onClick={() => setCount(pre => pre + 1)}>click</button>
</div>
);
}
const jsx = (
<div className="box border">
<p>start debugger</p>
<FunctionComponent name="函数组件" />
</div>
);
ReactDOM.createRoot(
document.getElementById('root')
).render(jsx);
debugger源码方法
https://github.com/bubucuo/De...
制定一个小目标
通过这次源码阅读,搞清楚以下几个问题
- hook是怎么和当前组件关联起来的?又是如何起作用的?
- 为什么定义的state只会执行一次
- hook的异步更新怎么做的
- 为什么不能写在if表格块里
- diff算法怎么做的
- 在同一个事件里多次调用hook的set方法,会重渲染多次吗
自顶向下介绍React
关于concurrent模式和Fiber架构等知识请看《React技术揭秘》
fiber的结构
function FiberNode(
tag: WorkTag, // fiberTag
pendingProps: mixed, // 组件参数/属性
key: null | string, // key
mode: TypeOfMode, // 指示是lagecy还是conCurrent模式
) {
// Instance
this.tag = tag; // fiber类型
this.key = key; // 用来做diff算法
this.elementType = null;
this.type = null;
this.stateNode = null; // 该fiber关联的真实dom
// Fiber
this.return = null; // 父fiber
this.child = null; // 第一个孩子fiber
this.sibling = null; // 相邻的第一个兄弟fiber
this.index = 0;
this.ref = null;
this.pendingProps = pendingProps; // 新的组件参数/属性
this.memoizedProps = null; // 已经渲染在页面上的(旧的)组件参数/属性
this.updateQueue = null; // 一个环状链表,存储的是effect副作用形成的update对象
this.memoizedState = null; // 对于functionComponent来说,存储的是hook对象链表
this.dependencies = null;
this.mode = mode;
// Effects
this.flags = NoFlags; // 本fiber effect形成的副作用
this.subtreeFlags = NoFlags; // 子fiber effect的副作用
this.deletions = null;
this.lanes = NoLanes; //本fiber的update lanes
this.childLanes = NoLanes; // 所有子fiber的update lanes
this.alternate = null;
。。。
}
fibertag用来标示该fiber是哪种组件类型的fiber--25种
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
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;
....
fiber树和dom的对比
fiber树的生成过程
我们从render开始看fiber树是如何一个一个生成的
export function createRoot(
container: Container,
options?: RootOptions,
): RootType {
return new ReactDOMRoot(container, options);
}
function ReactDOMRoot(container: Container, options: void | RootOptions) {
this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
}
function createRootImpl(
container: Container,
tag: RootTag,
options: void | RootOptions,
) {
...
// 生成root
const root = createContainer(
container,
tag,
hydrate,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
);
...
return root;
}
ReactDOMRoot.prototype.render = ReactDOMLegacyRoot.prototype.render = function(
children: ReactNodeList,
): void {
const root = this._internalRoot;
...
updateContainer(children, root, null, null);
};
ReactDOM.createRoot(document.getElementById('root'))会创建ReactDomRoot对象,该类会调用createRootImpl初始化fiberRoot对象。随后的render函数是挂在类原型上的,会调用updateContainer
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
const current = container.current;
const eventTime = requestEventTime();
const lane = requestUpdateLane(current);
const update = createUpdate(eventTime, lane);
// Caution: React DevTools currently depends on this property
// being called "element".
update.payload = {element};
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
enqueueUpdate(current, update, lane);
const root = scheduleUpdateOnFiber(current, lane, eventTime);
return lane;
}
显然该函数是为了给fiberRoot建立一个update,并且update.payload是element即jsx;enqueueUpdate是为了把update挂到fiber.updateQueue;最后调用了scheduleUpdateOnFiber
重点来了,scheduleUpdateOnFiber是调度函数的入口函数,从这里开始进行fiber树的构造以及update的处理;
export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
): FiberRoot | null {
// 从当前fiber开始向上冒泡直到找到root节点,同时更新所有父节点的childLanes
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (root === null) {
return null;
}
// 标记root有一个待更新
markRootUpdated(root, lane, eventTime);
if (lane === SyncLane) {
if (
// Check if we're inside unbatchedUpdates
(executionContext & LegacyUnbatchedContext) !== NoContext &&
// Check if we're not already rendering
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root, eventTime);
}
} else {
// Schedule other updates after in case the callback is sync.
ensureRootIsScheduled(root, eventTime);
}
return root;
}
ensureRootIsScheduled函数是为了向调度堆里push一个回调函数,最后还是会调用performSyncWorkOnFiber/performConcurrentWorkOnFiber,这两个函数会调用renderRootConcurrent或者renderRootSync,进而调用workLoopConcurrent或者workLoopSync
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
// workInProgress是个全局变量,表明当前正在处理的fiber节点,在调用后面的函数时会重新赋值
// 这两个函数唯一的不同点在与concurrent会判断shouldYield()即判断浏览器的这一帧里是否还有剩余的时间,如果时间不够的话,会中断当前执行的过程,直接进行页面渲染,防止页面卡顿
performUnitOfWork函数非常重要,主要有两点1、调用beginWork生成fiber(子fiber或者兄弟fiber),2、调用completeUnitOfWork(会调用dom方法)生成真实dom,并且挂在fiber的stateNode属性上
function performUnitOfWork(unitOfWork: Fiber): void {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
// alternate连接的是当前页面已经渲染好的fiber节点
const current = unitOfWork.alternate;
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
// 在这里用新的props覆盖旧的props
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
beginWork会根据当前fiber生成所有的子fiber(不只第一个子fiber),然后把第一个子fiber作为返回值;如果没有子fiber,返回null,进入completeunitOfWork函数
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
debugger
let updateLanes = workInProgress.lanes;
if (current !== null) {
...
} else {
didReceiveUpdate = false;
}
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
// 还不清楚是classComponent还是functionComponent
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
case FunctionComponent: {
debugger
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 HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
...
}
}
beginWork里首先会判断是否可以使用上次渲染的fiber节点,didReceiveUpdate = false表明可以复用;然后会根据fiber.tag调用不同的update方法生成子fiber,我们重点关注三个:HostRoot、HostComponent和FunctionComponent
HostRoot即根节点会使用上面提到的update.payload即<div class="box border">生成子fiber,然后将此fiber返回给上层performUnitOfWork,把该fiber赋值给workInProgress,从而可以开始下次循环;由于div.box border节点有三个子节点,所以会同时生成三个子fiber,并且返回第一个子fiber<p>;<p>fiber节点只有一个文本子节点,为了节省内存,不必为该文本节点创建新的子fiber;所以p节点是没有子节点的,只能返回null,那么在performUnitOfWork函数里会进入completeUnitOfWork,完成fiber到真实dom的映射过程,完成后,如果该节点有兄弟节点,将兄弟节点赋值给workInProgress,回到performUnitOfWork函数里,如果没有兄弟节点,只能循环迭代其(祖)父节点链,将(祖)父节点赋值给workInProgress,调用completeWork,直到某个(祖)父节点有兄弟节点或者到达根节点;
下面来将整个构建流程过一遍:
渲染阶段
完成fiber树的构造或者超出浏览器一帧时间后,调用commitRoot->commitRootImpl完成dom节点挂到root节点上的工作;
hook-useState、useEffect
好了fiber树的构建以及react渲染流程已经简单的过了一遍,那么hook在哪里呢?
还记得在构建fiber树的时候有一个节点是functionComponent吧,在beginWork函数里会命中FunctionComponent或者IndeterminateComponent,这两个函数都会调用renderWithHooks,然后会使用调用Component(fiber.type)方法,对于functionComponent组件来说,fiber.type就是组件函数本身,执行该函数,也会执行组件里使用的hook
function FunctionComponent({
name
}) {
_s();
const [count, setCount] = Object(_react_packages_react__WEBPACK_IMPORTED_MODULE_2__["useState"])(0);
const [subtraction, setSubtraction] = Object(_react_packages_react__WEBPACK_IMPORTED_MODULE_2__["useState"])(1);
Object(_react_packages_react__WEBPACK_IMPORTED_MODULE_2__["useEffect"])(() => {
setCount1(10);
}, []);
return /*#__PURE__*/Object(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__["jsxDEV"])("div", {
className: "function border",
children: [name, " ", count, " ", subtraction, /*#__PURE__*/Object(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__["jsxDEV"])("button", {
onClick: () => setCount(pre => pre + 1),
children: "click"
}, void 0, false, {
fileName: _jsxFileName,
lineNumber: 19,
columnNumber: 7
}, this)]
}, void 0, true, {
fileName: _jsxFileName,
lineNumber: 17,
columnNumber: 5
}, this);
}
以上就是本例中使用到的函数组件的jsx编译后的样子,显然在执行该函数的时候也会执行里面写的useState和useEffect方法;
const HooksDispatcherOnMount: Dispatcher = {
...
useEffect: mountEffect,
useState: mountState,
...
};
const HooksDispatcherOnUpdate: Dispatcher = {
...
useEffect: updateEffect,
useState: updateState,
...
};
可以看到,hook的实现有两套(实际上不止两套),分别对应mount和update阶段;那么是在什么时候切换的呢?
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
if (__DEV__) {
} else {
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
}
...
return children;
}
上文我们讲到functionComponent会调用renderWithHooks,所以就是在这个函数里会根据当前是mount阶段还是update阶段进行切换;
下面我们来看下hook具体是怎么实现的。因为useState和useEffect实现有很大的不同,所以我们分开来讲;
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 生成hook,并且挂在workInProgressHook和当前fiber即urrentlyRenderingFiber的memoizedState上
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
// 挂载阶段会执行该函数
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null, // update链
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
// 生成hook,并且挂到fiber.memoizedState上(functionComponent的memoizedState才会挂hook链表),更新workInProgressHook
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null, // 在页面渲染的值
baseState: null, // state计算的中间值
baseQueue: null, // 上次遗留的queue
queue: null,
next: null, // 下一个hook
};
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
const eventTime = requestEventTime();
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};
const alternate = fiber.alternate;
...
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
...
}
对于useState,如果参数是个函数,就会去执行该函数,如果不是函数,直接使用该值,赋值给hook.memoizedState;新建的queue主要是时用来存储更新链表的,在pending后会接一个环状链表,dispatch就是我们调用的更新函数;看dispatchAction,每次调用该hook,都会生成一个update(在dispatch里),并且将该update插入到环状链表的第一个(即pending直接指向该update);
好了,现在我们有了hook的值以及它的更新函数,如果我点击button,调用了setCount,给该hook.queue.pending上插入了一个update,那么它是什么时候计算呢?在更新阶段时调用的useState
没错,当我们非首次渲染functionComponent需要更新时,会重新执行组件函数,也就会在此调用useState,只不过这一次在源码里调用的是updateState
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
}
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
// The last rebase update that is NOT part of the base state.
let baseQueue = current.baseQueue;
// The last pending update that hasn't been processed yet.
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
// 有新的update待处理,把他们加到base queue里
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
if (baseQueue !== null) {
// We have a queue to process.
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
const updateLane = update.lane;
// 重!!!lane结构的体现
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
// 优先级不够,跳过这个update,如果这是第一个跳过的update,早前的update/state就是最新的基本update/state
const clone: Update<S, A> = {
lane: updateLane,
action: update.action,
eagerReducer: update.eagerReducer,
eagerState: update.eagerState,
next: (null: any),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Update the remaining priority in the queue.
// TODO: Don't need to accumulate this. Instead, we can remove
// renderLanes from the original lanes.
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
// 标记跳过update的lane
markSkippedUpdateLanes(updateLane);
} else {
// Process this update.
if (update.eagerReducer === reducer) {
// If this update was processed eagerly, and its reducer matches the
// current reducer, we can use the eagerly computed state.
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
// 异步更新的奥秘,action是function,会把新算的newState作为参数执行,但是action是值的化,直接返回值,然鹅在同一次render里,memoizedState是同一个值
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
// 又是一个环
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
// Mark that the fiber performed work, but only if the new state is
// different from the current state.
if (!is(newState, hook.memoizedState)) {
// 可以复用以前的update--还没看懂所有链路
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
hook.baseState = newBaseState;
// baseQueue是以前没更新过的updatelist,比如被sikp的
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
可以看到updateState方法是updateReducer的一层封装,reducer参数使用的是一个全局函数;在updateReducer函数里有这一行newState = reducer(newState, action)
,这行代码就是为什么我门调用set函数时只有传入函数参数才能解决其异步的问题的原因。
在同一个事件里多次调用set
我们修改jsx代码
function FunctionComponent({name}) {
const [count, setCount] = useState(0);
const [count1, setCount1] = useState(1);
useEffect(() => {
setCount1(10);
}, []);
return (
<div className="function border">
{name} {count} {count1}
<button onClick={() => {
setCount(pre => pre + 1);
setCount(pre => pre + 1);
setCount(pre => pre + 1)
setCount(pre => pre + 1)
}}>click</button>
</div>
);
}
在点击button时,调用了四次setCount,会触发四次dispatchAction函数,每次函数执行都会向hook的queue.pending上插入一个update。然后调用scheduleUpdateOnFiber函数,触发ensureRootIsScheduled,构造一个回调函数,由于我们的环境时支持微任务的,所以会调用scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root))把performSyncWorkOnRoot放到syncQueue任务队列里,接着调用scheduleMicrotask(flushSyncCallbacks);在微任务执行时机执行flushSyncCallbacks,在这个函数里会从syncQueue里依次取出函数执行(也就是performSyncWorkOnRoot);由于微任务调用时四次调用dispatchAction已经完成,所以该hook上已经挂上了四个update,这样在重新构建fiber树时就会执行这些update,完成更新;
大家可能要问了,一共四次调用,是不是会在syncQueue里推入四个函数,然后微任务执行阶段执行四次performSycnWorkOnRoot呢?
这当然是不可能的,在ensureRootIsScheduled函数里会比较root.callbackPriority和当前更新的newCallbackPriority(具体怎么得来的以后再说),如果是一样的话,就直接return,不再执行下面的内容。由于第一次调用ensureRootIsScheduled的最后会把newCallbackPriority挂到root.callbackPriority上,而这四次调用产生的newCallbackPriority是一样的,所以后面三次调用都没能得到向syncQueue里push的机会。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。