我只是不能忍受。。。。你一辈子在这里打转。你太聪明,太有趣。。。你只有一次生命,应该尽量活的充实。
前言
基于reactV17.0源码分析
本片文章主要通过react的源码,分析useState、useEffect和两个钩子函数的内在执行原理。
- hook组件没有this指针,没有生命周期,setXxxx函数是如何发起更新,进行调度的?
- hook对象在react内部是以什么结构进行储存运算的?
- useEffect的第一个参数是在调度中是如何执行的,它的返回函数为什么可以在unMount的时候触发?
- useEffect第二个参数又是如何equal,避免重复执行第一个参数的函数?
- 等等。
为了解决包括不限于以上的问题,我们打开react项目研究一下源码。限于个人技术原因,如有问题戳我。
源码入口
领略react-hook之前,我们先回顾一下从reactDom.render(element, dom)开始到hook节点创建中间经历了那些步骤。下面是我根据源码,将初次渲染的关键路径简单的梳理了一下,只涉及关键函数。(打开react项目,点点点)
renderWithHooks
顾名思义,这个函数就是对hook组件的处理函数的入口。下面简单的分析一下这个函数主要做了什么:
- 将全局变量
currentlyRenderFiber指向workInProgress - 根据
memoizedState判断首次挂载/更新,获取hooks方法集的Dispatcher对象,并将此对象赋值给全局变量ReactCurrentDispatcher的current属性 - 最后执行
hook组件的function构造方法
// packages/react-reconciler/src/ReactFiberHooks.old.js
export function renderWithHooks() {
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
...
//赋值全局变量,可以在react中使用相关hook方法
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
let children = Component(props, secondArg);
// 省略一些条件的判断,和置空
return children;
}
ReactCurrentDispatcher
- 这个对象是全局对象,在
react模块中定义,并暴露,所以我们能import { useState, useEffect } from 'react' - 并在
renderWithHooks函数实现各种钩子函数,并将各个钩子函数挂在对应的fiber对象上的hook链表 - 所以最后在执行
hook的构造方法(Component())时,能够使用相关的hook方法 实现并挂载在全局变量ReactCurrentDispatcher之后,在执行hook组件的构造方法时,我们就能拿到相关的hook方法。我们就先拿最常见的useState和useEffect来说明,其他的钩子函数先不考虑。
// packages/react/src/ReactCurrentDispatcher.js
//在react模块中定义,并抛出了这个对象
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;
//packages/react-reconciler/src/ReactFiberHooks.old.js
//在react-reconciler模块中实现
const HooksDispatcherOnMount: Dispatcher = {
//初次挂载
useState: mountState,
useEffect: mountEffect,
};
const HooksDispatcherOnUpdate: Dispatcher = {
//更新
useEffect: updateEffect,
useState: updateState,
};
useState
首先是初次挂载阶段
源码里我们可以看到,最关键的就是利用bind闭包,缓存了currentlyRenderingFiber对象(我也列出了源码中对此对象的解释,就是当前的fiber节点)和queue对象(保存的是更新对象update,在dispatchAction中我们可以看到)
// The work-in-progress fiber. I've named it differently to distinguish it from
// the work-in-progress hook.
let currentlyRenderingFiber: Fiber = (null: any);
...
function mountState(initialState,){
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,
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对象的创建mountWorkInProgress函,在这个函数里主要就是构造了一个hook的单项链表,并将workInProgressHook指针指向当前hook。对于workInProgressHook对象,源码中也有详细的解释,用以保存将要加进当前fiber对象的hook链表
// Hooks are stored as a linked list on the fiber's memoizedState field. The
// current hook list is the list that belongs to the current fiber. The
// work-in-progress hook list is a new list that will be added to the
// work-in-progress fiber.
let currentHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
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 MyName(){
const [name, setName] = useState()
return <div>
<span>{name}</span>
<button onClick={setName('骆家豪')}> 展示名字</button>
</div>
}
看了上面初次挂载的解析后,我们明白setName就是dispatchAction函数,并且利用闭包缓存了当前fiber对象的指引。so,我们来看看最最核心的dispatchAction函数做了什么骚操作。
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 pending = queue.pending;
if (pending === null) {
// 这是第一个更新,创建一个环形链表
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
...
//发起调度
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
我们可以看到其实就是构造了一个update的链式结构,并挂在hook对象上的queue属性上,并在最后发起一个调度。那么调度之后是如何的重新计算fiber节点的,如何处理queue中的update更新链,我们还要看updateState函数。
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
直接返回一个updateReducer函数,顾名思义,就是一个更新合并的函数。将一次或多次setName函数产生的update合并。计算出最新的state。
function updateReducer(){
//1.合并并计算queue队列
let baseQueue = current.baseQueue;
//2.如果存在等待的更新队列,则循环update的单向链表,reducer所有的state
if(baseQueue !== null){
const first = baseQueue.next;
let newState = current.baseState;
let update = first;
do {
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null && update !== first)
}
//3.将最新的state存入当前hook,并返回
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
小结
到此我们将useState这个hook的创建,更新过程过了一遍。下面来技术总计。
hook对象以链表的形式保存在当前fiber对象的memoizedState属性上,形成环形结构。便于之后的便利合并。useState钩子函数返回的第二个参数setXxx,其实就是利用闭包,对应源码dispatchAction.bind(null, currentlyRenderingFiber,queue,)。缓存了当前的fiber对象和queue跟新队列,这也解释了为什么hook函数不需要this指针也能对应上指定的更新对象- 在执行
setXxx时,也就是创建了一个update,并将update对象以单项链表的形式保存在当前hook的queue属性上。并在最后发起一个调度scheduleUpdateOnFiber,所有更新的入口函数。 - 执行上一个步骤的调度时,因为存在
memoizedState,就会执行updateState,也就是执行了updateReducer函数,顾名思义就是合并更新 - 最核心的的就是
updateReducer,在这个函数中将update合并,并do-while遍历update,合并计算update对象中的action属性,就是setXxx的第一个参数,可以是函数。最后返回一个新的state保存在当前hook的memoizedState属性中。
useEffect
看完useState的代码,在看useEffect函数,大部分都是相同的。最大的不同还是触发时机的不同,useEffect在render过程之后触发计算,useState在下次计算当前fiber的时候触发执行计算newState。
初次挂载与更新
我们先一起来看看 useEffect初次挂载,和更新时的代码。
function mountEffect(create,deps) {
return mountEffectImpl(
UpdateEffect | PassiveEffect,
HookPassive,
create,
deps,
);
}
function updateEffect(create,deps) {
return updateEffectImpl(
UpdateEffect | PassiveEffect,
HookPassive,
create,
deps,
);
}
看了代码其实区别就转换成了mountEffectImpl和updateEffectImpl区别。
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}
看了上面的挂载、更新两个阶段的函数对比,我们很容易发现,更新阶段仅仅比挂载阶段多了一段判断依赖数组是否相同的代码,多做了一个减少重复执行的优化(hook第一个参数create)。所有在我们平时开发中一定要注意useEffect第二个参数的使用,当然源码中的比较函数,也只是做了一层浅比较。
hook-effect链表结构
那么接下来我们就打开pushEffect函数看看情况。
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy,
deps,
// 环形链表
next: (null: any),
};
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
其实看完很简单,并没有复杂的合并,计算。只是将生成一个effect对象,并以环形链表的结构存在hook的memoizedState属性上,并将此链表赋值给全局变量componentUpdateQueue。
处理effect链表
最中形成一个effect的环形链表后,放在了componentUpdateQueue中。至于何时触发,找了一会,在commitHookEffectListMount函数中找到了对hook-effect的处理
下面我们就详细的看下
commitHookEffectListMount如何处理effect链表
function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
// 做了判断,过滤了依赖重复时,push进来的effect
if ((effect.tag & tag) === tag) {
// Mount
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
其实很简单,就是遍历effect链表,并利用tag过滤依赖dept没变产生的effect。执行create函数,然后将create函数执行的结果赋值给destory属性。
然后继续在突变阶段-commitWork函数找到了destory执行
function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
总结
useState,还是useEffect都是产生一个链表挂载在当前hook对象的memoizedState属性上。- 不同的事
useState产生的是update更新对象的链表,useEffect产生的是effect副作用链表 - 这两个钩子函数触发时机不同。
useState在下次更新时,合并计算hook对象上的update链表,最中计算出最新的newState,赋值给hook.momoizedState;而useEffect则在commit阶段的突变后才开始执行hook上的effect链表的create函数,在commit阶段的突变时执行destory函数