上一篇文章简单介绍了一下两种比较常用的Hooks - useState
和 useEffect
最近稍微拜读了一下Fiber的代码,尝试解释一下这两个Hook
的实现。
Fiber reconciler
首先我们知道的是React
维护了一个自己的Virtual DOM tree
,当我们要创建一个新的元素Element
时,是在VDOM tree
上挂载mount
, 更新update
, 渲染render
,最终在真实的DOM tree
上绘制paint
的。
在React里,实际上对一个元素,自顶向下的来说有三层认知层次:
a. DOM
真实的DOM结点,是我们最终看到的HTML上所写的结点。
b. Instance
也就是React所维护的实例,即Virtual DOM结点
c. Element
也就是我们所编写的代码,用来描述一个元素的样式和要展示的数据。我们调用的render方法实际上是告诉React要更新对应的Instance了。
同时JS是在浏览器的主线程上运行的,和样式计算、布局等绘制工作一起运行。
这样就导致一个问题,如果一个Element
的更新时间太长,导致JS占用了很多浏览器资源,使得这次render
以及paint
的时间超过了1/24秒,那么就会出现肉眼可见的页面卡顿。
于是React 16发布了React Fiber reconciler
来解决这个卡顿问题。
主要解决方法是把一次render
任务拆分成一些更小的任务,每做完一段就把时间控制权交回给浏览器,不让JS一次占用太多时间。
为此我们又种了两棵树fiber tree
(取代了原来的VDOM tree
) 和 workInProgress tree
,其中fiber tree
是由fiber nodes
组成的,记录了增量更新时需要的上下文信息,而workInProgress tree
则是一个进度快照,用来进行断点恢复。
一个fiber node
长得像这样:
{
return, //当前结点处理完后,向谁提交结果,即父结点
child,
sibling,
...
}
而workInProgress tree
则是由fiber tree
构造出来的,其维护了一个effect list
,当fiber
结点需要更新时,则给当前这个结点打一个tag
,同时当前结点的effect
(需要实施的更新)会返回给自己的return
,这样当workInProgress tree
构造出来时,其根节点的effect list
就是要做的所有side effect
。此过程中的任何一步都可以中断。
之后执行commit
操作,即渲染DOM Node
,此过程是不可中断的。
值得注意的是,fiber node
和workInProgress node
使用的是同样的数据结构。
实际上当commit
操作完成以后,react
将workInProgress tree
和fiber node
的指针互换,因为此时workInProgress tree
的状态和真实的DOM tree
相同。
Hooks
现在我们对上文所述的fiber node
扩充一下
{
return, //当前结点处理完后,向谁提交结果,即父结点
child,
sibling,
memoizedState,
}
React
在每次结点render
之前会计算出当前的state
并赋值给fiber
实例的memoizedState
,再调用render
方法。所以React
可以根据这条属性拿到当前的state
。
对于一个class
形式的component
来说,我们可以很轻松的将state
与memoizedState
对应起来。
而在一个function
形式的component
里,我们一般是这样使用state
的
const Example = () => {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
}
我们知道fiber node
上只有一个memoizedState
,但在Hooks
中,React并不知道我们调用了几次useState
,我们要怎么把每个Hook
的state
合并到fiber node
上的memoizedState
上呢?
React定义了一个Hook
对象:
{
memoizedState: any,
baseState: any,
baseUpdate: Update<any, any> | null,
queue: UpdateQueue<any, any> | null,
next: Hook | null,
}
这样当我们每调用一次useState
,React
就创建了一个新的Hook
对象,然后连接到当前Hooks
链表的尾部。
也因此,我们在看Hooks
文档的时候会发现有一条规则,不要在条件判断语句里使用Hooks
每次useXXX
在执行的时候,第一个运行的函数是下面这个:
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
如上例中,如果Name
这个Hook
因为某些原因被跳过的话,那么我们的Email
会成为这次函数执行里第一次被调用的Hook
,也就是会拿到当前Hooks list
里的第一个值。
那么setState是怎样实现的呢?上源码
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
// Flow doesn't know this is non-null, but we do.
((currentlyRenderingFiber: any): Fiber),
queue,
): any));
return [hook.memoizedState, dispatch];
}
mountState
是我们实际调用的useState
实现,根据代码我们可以看出来,我们拿到的setState
方法实际上是一个Dispatch
,而当我们调用得到的setState
时,会创建一个Update
对象:
type Update<S, A> = {
expirationTime: ExpirationTime,
suspenseConfig: null | SuspenseConfig,
action: A,
eagerReducer: ((S, A) => S) | null,
eagerState: S | null,
next: Update<S, A> | null,
};
action
是我们传给dispatch
的action
也就是setState
传入的值。
当我们收集到所有的update
之后,就会调用React
的更新,当其执行到我们的这个Functional Component
时,就会执行对应的useState
, 其Hook
对象上的queue
保存了我们要执行的update
,执行完所有update
后拿到最终的state
保存到memoizedState
上,起到setState
的效果。你可能要问为什么queue
是个UpdateQueue
,因为我们可能会调用多次setState
。
当所有的Hook
执行完以后,拿到全部memoizedState
,更新到fiber node
上。
同样的,React
也为Effect
提供了一个对象:
type Effect = {
tag: HookEffectTag,
create: () => (() => void) | void,
destroy: (() => void) | void,
deps: Array<mixed> | null,
next: Effect,
};
useEffct
的调用分了三个阶段:
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
if (__DEV__) {
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
if ('undefined' !== typeof jest) {
warnIfNotCurrentlyActingEffectsInDEV(
((currentlyRenderingFiber: any): Fiber),
);
}
}
return mountEffectImpl(
UpdateEffect | PassiveEffect,
UnmountPassive | MountPassive,
create,
deps,
);
}
在这个阶段给useEffect
打上了一个effectTag
用来标记这个Effect
的类型
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
sideEffectTag |= fiberEffectTag;
hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
这个阶段把当前Effect
的tag更新到了整个component
上
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy,
deps,
// Circular
next: (null: any),
};
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
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
添加到componentUpdateQueue
的尾部。
值得注意的一点是,componentUpdateQueue
最终形成了一个环,因此需要一个lastEffect
标记实际上的最后一个Effect
是谁。
在拿到所有的effect
后,React
将componentUpdateQueue
更新给了currentlyRenderingFiber
的updateQueue
,最终由workInProgress tree
去收集并执行所有fiber node
上的effect
。