引言
面试的时候,经常会问到hook的使用原则
- 只能在React函数中调用Hook
- 不要在循环、条件或嵌套函数中调用hook
关于第二点的原因,是因为要确保Hooks在每次渲染的时候都保持同样的执行顺序。
要是面试官深入问你,为啥要确保同样的调用顺序呢?这时没看过源码的你是不是就抓瞎了,下面跟着我的总结,不用深入源码,只要抓住几个点也能让你对达如流。
场景举例
运行环境:React 16.8 React-dom:16.8
import React, { useState } from 'react'
let isMounted = false //设置一个变量,用于记录是否已挂载
const MytestDemo = () => {
console.log("isMounted is", isMounted);
let name, age, career, setName, setCareer;
if(!isMounted){
// eslint-disable-next-line //注意要添加eslint忽略否则react会编译不通过
[name, setName] = useState('张三');
isMounted = true
}
[age] = useState(16);
[career, setCareer] = useState('前端')
// 打印对应的career值关注前后变化
console.log("career", career);
return (
<>
{name ? <div>姓名:{name}</div> : null}
{age ? <div>年龄:{age}</div> : null}
{career ? <div>职业:{career}</div> : null}
<button onClick={() => { setName('李四') }}>更改姓名</button>
</>
)
}
export default MytestDemo
运行结果
从图上我们可以看到第一次运行的时候,career这个变量里面是能正确读取到‘前端’这个值的,而当我们点击修改按钮的时候,career读取的值确是16了,为什么呢?因为第一次遍历的时候,会构建一个链表记录hook的调用顺序,而第二次调用的时候,会按之前的对应顺序去取值,而这时候hook对应顺序已经改变,所以取值不一样了。
源码解析
下面我们从UseState源码层面来解析这一个过程
对应文件:ReactFiberHooks.new.js
首次渲染过程
这个流程中mountState的主要工作是初始化Hooks,这里面有一个mountWorkInProgressHook方法需要我们关注下,他里面定义了Hooks的结构形式。
mountWorkInProgressHook源码:
function mountWorkInProgressHook(): Hook {
//这里定义了hook的结构,是一个对象,还有一个next指针
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
//把hook当成链条头节点处理
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
//链表不为空,把hook追加到链表尾部
workInProgressHook = workInProgressHook.next = hook;
}
//返回结果
return workInProgressHook;
}
更新过程
这个过程中,updateState函数是返回了updateReducer的调用结果,而upDateReducer的作用,涉及的 代码有点多,可以忽略,总结下来就是:按顺序去遍历之前构建好的链表,并取出对应的值进行渲染,
所以如果前后两次调用的顺序发生了变化,取值就会发生混乱。
updateState源码
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
updateReducer源码
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}
.....
if (baseQueue !== null) {
// We have a queue to process.
......
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
// Interleaved updates are stored on a separate queue. We aren't going to
// process them during this render, but we do need to track which lanes
// are remaining.
const lastInterleaved = queue.interleaved;
if (lastInterleaved !== null) {
let interleaved = lastInterleaved;
do {
const interleavedLane = interleaved.lane;
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
interleavedLane,
);
markSkippedUpdateLanes(interleavedLane);
interleaved = ((interleaved: any).next: Update<S, A>);
} while (interleaved !== lastInterleaved);
} else if (baseQueue === null) {
// `queue.lanes` is used for entangling transitions. We can set it back to
// zero once the queue is empty.
queue.lanes = NoLanes;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
场景回顾
初始化阶段的对应关系如下:
更新阶段的对应关系如下:
当我们点击更改姓名按钮,去进行页面更新时,react还是会遍历初始化阶段构建的hook链表,然后按照顺序去取出对应的值,然而这时候useState(name)的这个由于我们设置了一个if判断所以没有调用,这时取值顺序已经发生了变化。
总结
React初始化阶段会构建一个hook链表,更新阶段会根据useState的执行顺序去遍历链表取值,如果前后执行顺序不一致,就会导致取出的值不对应,所以我们再写hoos的时候要确保Hooks在每次渲染的时候都保持同样的执行顺序