所有的理解都基于React V16.11
给一点take away 吧,说一说对我帮助最大的两个心得:
initialState
的改变不会影响hooks 返回结果的改变,因为mount 前后的hooks 函数根本就是两套。
多个 useEffect
分别set 一下state,dispatch 一下action 啥的其实不一定会引起多次渲染,因为hook 会用 renderPhaseUpdates
来判断race condition,如果优化够好的话多个 useEffect
和单个的效果应该差不多。
两篇很有用的文章
Hooks 有什么好处?
赋予了函数式组件状态,用类组件来管理状态可能有如下缺点:
- 逻辑分散,处理同一个东西的逻辑可能会被分散在
componentDidMount
componentDidUpdate
componentDidUnmount
里面。 特别是useEffect
把一些副作用操作都统一了。 - 范式复杂,class 可能把一些原可以简单的组件弄复杂了。
涉及到的数据结构有哪些?
简单来说,一个fiber 对应一个hooks 链表,一个hook 对应state 和update queue。
这些数据结构的具体定义和交互是怎么样的?
Hook
export type hook = {
memorizedState: any,
baseState: any,
baseUpdate: Update<any, any> | null,
queue: UpdateQueue<<any, any> | null,
next: Hook | null,
};
- hooks 通过链表连接在一起。
- 有
memorizedState
和baseState
的区别,两者的不同之处目前还不得而知,不过每次调用hook 返回的都是memorizedState
。
memorizedState
和baseState
有可能不同吗?
有可能。
memorizedState
代表的是上一次hook 返回给组件的 state,而这个 state 有可能是“非法状态”。
这个“非法状态”会在 updateReducer
里面诞生,当遍历 queue
中的 update
,计算新 state 的时候,有可能发生 update.expirationTime < renderExpirationTime
的情况。此时hook 会选择不去执行 update.action
,非法状态就就诞生了。
当然,为了之后修正当前的这个非法状态,hook 就把 baseState
和 baseUpdate
置成了第一次skip 时的 state 和 update,某个时刻重新从 baseState
和 baseUpdate
计算真正合法的 state。
这是为了在资源不足的情况下优先完成高优先级任务。
不管怎么说,在资源和时间充足的情况下,从 baseState
和 baseUpdate
重新追赶后就能得到正确的state。
为什么hook 的queue
有时是循环链表,有时不是?
参照后面的更新状态callback 具体实现的section。
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,
priority?: ReactPriorityLevel,
};
- updates 也是通过链表连接在一起的。
- update 有
expirationTime
suspenseConfig
priority
各种配置参数。
expirationTime
的具体作用?
不知道。
啥是 suspenseConfig
?
不知道。
为啥要有 eagerReducer
,和传入 updateReducer()
中的 reducer
参数互相区分?
不知道。
UpdateQueue
type UpdateQueue<S, A> = {
last: Update<S, A> | null,
dispatch: (A => mixed) | null,
lastRenderedReducer: ((S, A) => S) | null,
lastRenderedState: S | null,
};
dispatch
被封装过,供外界调用使用。所有的hook 都对同一个公共的dispatch
进行了封装。
queue
里面为什么还要特意封装一下 lastRenderedReducer
和 lastRenderedState
?用hook 自带的一些信息不好吗?
不知道。
HooksDispatcherOnMount & HooksDispatcherOnUpdate
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useResponder: createResponderListener,
};
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useResponder: createResponderListener,
};
第一次mount 组件时用mount 系列的hooks 来执行相关的准备、创建操作;之后update 组件用的是update 系列的hooks ,根据之前的状态和操作来返回新的状态。
renderWithHooks()
里面有一句:
nextCurrentHook = current !== null ? current.memoizedState: null;
ReactCurrentDispatcher.current =
nextCurrentHook === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
其中 current
是传入 renderWithHooks()
的第一个参数,是一个fiber,若 current === null
则目前没有fiber,是首次加载。
首次加载时,hooks 相关的代码都做了什么?
首次加载时调用的是mount 系列hooks,根据hooks 代码的调用创建对应的hooks,并为这些被创建的hooks 分别创建queue,为被创建的queue bind dispatch 函数。
具体创建hook 的代码是什么逻辑?
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
if (workInProgressHook === null) {
firstWorkInProgressHook = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
创建一个hook,并将其设置为全局变量 workInProgressHook
,同时维护一个hooks 链表,每次都把新创建的hook 放在这个链表的最后。
useState
和 useReducer
系列的hooks 可以返回一个更改hooks 状态的callback,这里的状态更新是咋实现的,有没有race condition?
useState
其实就是特殊情况下的 useReducer
,两者返回的callback 其实就是首次加载中bind 到 hook.queue.dispatch
上的dispatch 函数。
- 状态更新就是把传入的action 给添加到queue 的最后,并发出一个请求re-render 的事件。
- 当然可能存在race condition,这个时候就创建一个key 为queue 的map,缓存一下在re-render 期间发起的action,如果race condition 被引发多次,就把actions 串成一个链表。
- 除此之外,根据这个文件中的说明,fiber 级别还会有对race condition 的处理,当进行re-render 时,update 链表会分别在current pointer 和work-in-progress pointer 里面被维护,这两个pointer 我理解被存在了不同的fiber 里面。每当有一个新update 事件被添加时,这个update 会被同时添加进这两个pointer or fiber 里,以防止在某些race condition 下被添加的新update 事件丢失。
把action 添加到queue 的操作是咋样的,这个queue 是单链表对吗?
不是单链表,当 queue.last === null
的时候,被创建出的是一个循环链表。
至于为什么是一个循环链表,是因为 queue.last === null
这个条件只会在mount 的时候发生,mount or commit 之后的re-render 是要从第一个update 开始的,考虑到:
- 循环链表的
last.next
就是链表的头节点。 - 除了第一次re-render 后,之后的更新都是从最后一次update 之后的 update 开始更新了,那么寻求的就是update.next。
我觉得使用循环链表可以达到以下目的:
- 少存一个头节点,只有mount or commit 前才需要头节点,这个时候从
last.next
取就好了,其他情况也不需要头节点,此时queue 就不是循环链表了。 - 范式的统一。
react render 的时候是怎么知道此时是mount 还是update?
根据当前fiber 是否为null 来判断。
在首次加载后,再次调用hooks 发生了什么?
首次加载之后的hooks 就是update 系列的hooks 了,再拿 useState
和 useReducer
来看, useState
会直接调用 updateReducer
,而首次加载后的 useReducer
就是 updateReducer
。
当 updateReducer
被调用时,其会判断当前是否处于re-render 阶段,如果是,按我们上面说的一样,去找key 为queue 的map,取出update 事件链表,计算出新的state。
如果不是re-render 阶段,那么就遍历queue 里面的update ,计算出新的state。
不过这里要注意一下特殊情况,会有资源不足而跳过一些update 的情况,参照上文memorizedState
和baseState
有可能不同吗? 这个问题。
调用hook 后,具体被操作的hook 是怎么被拿到的?
function updateWorkInProgressHook(): Hook {
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
nextCurrentHook = currentHook !== null ? currentHook.next : null;
} else {
// Clone from the current hook.
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
queue: currentHook.queue,
baseUpdate: currentHook.baseUpdate,
next: null,
};
if (workInProgressHook === null) {
workInProgressHook = firstWorkInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
nextCurrentHook = currentHook.next;
}
return workInProgressHook;
}
先看一看有没有workInProgressHook,如果有的话,直接使用,如果没有,从之前留着的hooks 链表里面拷贝需要的hook 出来,这样的结果就是每次re-render 完之后,都多了一个新版本的hooks 链表,我猜这里可能和fiber 涉及到的调解算法有关。
useEffect
的实现?
function commitHookEffectList(
unmountTag: number,
mountTag: number,
finishedWork: Fiber,
) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQuere: any);
let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & unmountTag) !== NoHookEffect) {
// Unmount
const destory = effect.destroy;
effect.destory = undefined;
if (destory !== undefined) {
destory();
}
}
if ((effect.tag & mountTag) !== NoHookEffect) {
// Mount
const create = effect.create;
effect.destory = create();
}
effect = effect.next;
} whlie (effect !== firstEffect);
}
}
这里可以发现,effect 的链表始终是循环链表,因为effect 一旦mount 了之后数量就是恒定的,永远不会受到其他事件的影响,所以每次从 lastEffect.next
走起就好了。
useState
和 useEffect
有何不同?
useState
是在fiber 里面创建初始状态,然后返回一个callback 让用户来schedule 新一轮的re-render。useEffect
是在fiber 里面创建初始状态,然后在每次fiber commit 之后(render 完成之后),依次执行一些副作用操作。
update 的expirationTime
是何时计算的?有何作用?
是在创建update 的时候就决定的,用来帮助fiber 确定update 的优先级,fiber 因为其充分利用device 资源的特性,会根据当前的资源来决定是否进行当前的update,如果资源不足,则跳过,记录被跳过的第一个update 为 baseUpdate
待之后又获得资源后从 baseUpdate
重新更新。
talk about fiber
啥是fiber
React 中的fiber 协调算法的执行单元。
解读一:fiber 相当于是react 中的协程。因为协程之间是主动协调任务的,所以其实react 无法对fiber 进行抢占式调度,全凭使用者的自觉,一般fiber 会主动检查是否超过了浏览器给的运行时间,如果超过就保存现场,把控制权还给浏览器(一般来说这个运行时间是16ms,1000ms / 60)。
解读二:react 中的执行单元,每执行完一个fiber 就检查一下剩余时间。