预告
本文将解答一些常见问题,以确保使用Hook的时候达到人码合一的境界。问题如下:
- React 是如何把对 Hook 的调用和组件联系起来的?
- 为什么Hook不能写进if语句里?
- 怎么做到多次调用同一个setState只有最后一个触发渲染的?
- React怎么知道Hook在不在函数组件内执行?
1、React 是如何把对 Hook 的调用和组件联系起来的?
这个问题在React文档里有https://react.docschina.org/docs/hooks-faq.html#how-does-react-associate-hook-calls-with-components
讲得很抽象,我们还是从具体例子看吧
例子
- 一个普通的function Component
export default function () {
const [name, setName] = useState[''];
const [value, setValue] = useState[''];
return (
<div className="upload-image">
<TextBox name={name} value={value} />
</div>
);
}
每一次发生渲染的时候,这个function都会被执行以得到一个更新后的vnode(虚拟dom树的一个节点)
问题
每次函数的执行都会执行useState方法,这里有两个useState,为什么每次都能得到正确的value和setValue呢?
相关源码如下:
1、mount阶段
function mountState(initialState) {
var hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
var queue = hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
- 中心思想就是通过mountWorkInProgressHook这个函数得到一个hook对象,然后给这个对象设置lastRenderedReducer和lastRenderedState(对应value和setValue),然后return这个元组。
function mountWorkInProgressHook() {
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
这里创建了一个hook
对象,然后判断一下这个workInProgressHook
全局变量是否有值,如果有值就赋给workInProgressHook.next
,并且将workInProgressHook
置为这个新的hook
。
- 总结
1、在mount阶段,执行useState就是创建一个新对象,保存value、setValue;
2、workInProgressHook是hook链表的尾节点;
3、然后把这个新的hook对象作为hook链表里的一个节点插入到workInProgressHook的下一个节点里,并更新workInProgressHook。
2、update阶段
function updateState(initialState) {
return updateReducer(basicStateReducer);
}
function updateReducer(reducer, initialArg, init) {
var hook = updateWorkInProgressHook();
var queue = hook.queue;
…… // 一些操作
var dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
update阶段也是一样的,需要通过updateWorkInProgressHook来得到hook,这个hook里保存有对应的数据(value、reducer等)
- updateWorkInProgressHook
function updateWorkInProgressHook() {
// This function is used both for updates and for re-renders triggered by a
// render phase update. It assumes there is either a current hook we can
// clone, or a work-in-progress hook from a previous render pass that we can
// use as a base. When we reach the end of the base list, we must switch to
// the dispatcher used for mounts.
var nextWorkInProgressHook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
……
var newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null
};
if (workInProgressHook === null) {
// This is the first hook in the list.
currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
只看workInProgressHook,关键代码就是三行
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
return workInProgressHook;
这里就是将nextWorkInProgressHook指针next了一下。
指向hook链表的下一个节点
总结
- 在mount阶段,每次执行useXXXhook,其实就是创建了一个新的hook对象,用以保存这个hook的值和其他状态;这个对象会被添加到一个链表上。
- workInProgressHook是一个指针,指向hook链表的尾部。
- 在update阶段,也是通过.next遍历链表,得到当前hook对象来做更新操作
2、为什么Hook不能写进if语句里?
React文档里对于这个问题的描述如下
只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。
这个问题和问题1很像。
回答
- 由问题1得知,因为每一次数组重新渲染是通过遍历hook链表来拿到每一个useXXX对应的那个hook对象的;
- 如何遍历一个链表,就是curNode=curNode.next;
- 所以,如果前一次渲染所遍历的那个hook链表和后一个不同,比如使用
if
之后个数就不一样了,那就不能得到正确的hook对象了。
3、怎么做到多次调用同一个setState只有最后一个触发渲染的?
示例
const [val, setValue] = useState('');
useEffect(() => {
setValue('a');
setValue('b');
setValue('c');
setValue('d');
}, []);
上面这段代码不会让function Component 执行四次,而是只有一次。
源码
调用setValue
的时候,其实在调用dispatchAction(fiber, queue, action)
function dispatchAction<A>(
componentIdentity: Object,
queue: UpdateQueue<A>,
action: A,
) {
invariant(
numberOfReRenders < RE_RENDER_LIMIT,
'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',
);
if (componentIdentity === currentlyRenderingComponent) {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
didScheduleRenderPhaseUpdate = true;
const update: Update<A> = {
action,
next: null,
};
if (renderPhaseUpdates === null) {
renderPhaseUpdates = new Map();
}
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate === undefined) {
renderPhaseUpdates.set(queue, update);
} else {
// Append the update to the end of the list.
let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
while (lastRenderPhaseUpdate.next !== null) {
lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
}
lastRenderPhaseUpdate.next = update;
}
} else {
// This means an update has happened after the function component has
// returned. On the server this is a no-op. In React Fiber, the update
// would be scheduled for a future render.
}
}
queue
- 这里的queue就是上面提到的,每一个hook都有自己的hook对象,然后这个hook对象拥有一个属性叫queue
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
这个queue是一个链表,每一个节点叫update,结构如下。
const update: Update<A> = {
action,
next: null,
};
-
然后这不是调了四次setValue嘛,每一次调的时候都会新生成update,然后添加到这个queue的尾部
- 结束了之后,新的一次对于function Component的调用又来了,这时候照常调用了useState,如下。
const [val, setValue] = useState('');
useEffect(() => {
setValue('a');
setValue('b');
setValue('c');
setValue('d');
}, []);
结合我们之前说过的,这个useState会得到val。这个得到val的过程其实就是如果有更新,就更新这个val值,并返回,以供下面用到它的地方使用,源码如下
updateState其实就是调用updateReducer
function updateReducer(reducer, initialArg, init) {
……
do {
……
var action = update.action;
newState = reducer(newState, action);
……
update = update.next;
} while (update !== null && update !== first);
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
……
var dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
这里使用do while一直去遍历这个queue链表,然后计算newState,直到遍历到queue结束为止。
此时得到的newState就是最终的value了。
回答
- 在每次setState的时候,会创建一个update对象用以储存value,然后把这个update对象塞进这个hook持有的queue链表末尾;
- 在发生渲染时,调用useState会拿出queue链表遍历来依次调用reducer得到新的value,而这个新的value最终的值是这个链表末尾的那个update节点的值。
4、React怎么知道Hook在不在函数组件内执行?
报错如下
Invalid hook call. Hooks can only be called inside of the body of a function component.
去源码里搜一下这个报错,找到代码如下
function throwInvalidHookError() {
{
{
throw Error( "Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem." );
}
}
}
……
var ContextOnlyDispatcher = {
readContext: readContext,
useCallback: throwInvalidHookError,
useContext: throwInvalidHookError,
useEffect: throwInvalidHookError,
useImperativeHandle: throwInvalidHookError,
useLayoutEffect: throwInvalidHookError,
useMemo: throwInvalidHookError,
useReducer: throwInvalidHookError,
useRef: throwInvalidHookError,
useState: throwInvalidHookError,
useDebugValue: throwInvalidHookError,
useResponder: throwInvalidHookError,
useDeferredValue: throwInvalidHookError,
useTransition: throwInvalidHookError
};
……
function pushDispatcher(root) {
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = ContextOnlyDispatcher;
……
}
dispatcher
调试的时候发现的
所有的useXXX在执行的时候,都会执行一次这个方法,例如
- 这个方法就是获取dispatcher,如果
dispatcher=ReactCurrentDispatcher.current
没有值就会报错。
这个ReactCurrentDispatcher
对应的源码里的ReactCurrentDispatcher$1
。 - 为什么会有这个
dispatcher
是因为react
需要在不同环境下运行(这是一个设计模式,忘了),比如浏览器、RN、服务器等。
因为React帮我们判断好了当前环境,我们代码只有一份,但是可以跑在不同的环境中。
React是怎么做到的呢?
dispatcher.useState(initialState);
他调用useState的时候是调的dispatcher上的,这个dispatcher会根据环境不同被有差异地处理过。
ReactCurrentDispatcher$1
说回ReactCurrentDispatcher$1.current
涉及到的代码如下
function pushDispatcher(root) {
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = ContextOnlyDispatcher;
if (prevDispatcher === null) {
// The React isomorphic package does not include a default dispatcher.
// Instead the first renderer will lazily attach one, in order to give
// nicer error messages.
return ContextOnlyDispatcher;
} else {
return prevDispatcher;
}
}
function popDispatcher(prevDispatcher) {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
- 也就是说,只有在pushDispatcher和popDispatcher里,ReactCurrentDispatcher$1.current才会被赋值
-
而调用这两个函数只有performConcurrentWorkOnRoot和performSyncWorkOnRoot这两个地方;这已经是组件初始化的地方了。
回答
- 每一个
useXXX
都是React通过dispatcher.useXXX
这种方式来调用的,为什么是这种方式,是因为React希望用统一的写法来运行在不同的环境中; - 如果
dispatcher
是空,则会报这个错; - 而
dispatcher
的赋值是在组件初始化的时候赋值的 - 所以当
useState
被执行的时候,如果dispatcher
没有值,就代表它不在组件内部被调用。
参考:
1、https://mp.weixin.qq.com/s/J0_PLrbVZMRAiwjWK2WDrw
2、https://react.docschina.org/docs/hooks-faq.html#how-does-react-associate-hook-calls-with-components
3、https://react.docschina.org/docs/hooks-rules.html
网友评论