Hooks
对于 React Hooks
相信很多小伙伴都是在熟悉不过了,不管你会不会用,你肯定都已经听过了,我后端的朋友的知道了。官方对 Hooks
的介绍也是非常的简洁
Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.
就是在 React 16.8 之后才有的,可以在不用class的情况下,使用 state 等其他 React 特性
这里就不多说了,不过像我那个后端朋友,写惯了面向对象,他就很不能理解,为啥好好的class 不要了,整个函数编程呢?面向对象他不香吗?em…这个嘛。。。我们也不在这过的的讨论了,如果你有兴趣,你也可以想一想。
相信大家也都知道了 Hooks
是有使用原则的:
- 只在函数组件中调用
- 不要在循环、条件或嵌套函数中调用
Hook
对于第一条也没啥好说的,就是不能混着用嘛。比较值得关注的,应该是第二条。其实,第二条规则中强调的所有“不要”,都是在指向同一个目的,那就是要确保 Hooks
在每次渲染时都保持同样的执行顺序。那么问题就来了,如果我不呢?
Hooks 不按执行顺序,会出现什么问题
啥也别说了,直接给你上两张图,如果你的 Hooks
不按执行顺序, 结果就可能是两张图中的一张,
意思大概就是:组件渲染的 Hooks 比期望中多了(或者少了)
Why?为啥呢?
源码调用流程
首先要说明的是,React-Hooks
的调用链路在首次渲染和更新阶段是不同的,这里我将两个阶段的链路各总结进了两张大图里,我们依次来看。首先是首次渲染的过程,画了个有点丑的图
在这个流程中,useState
触发的一系列操作最后会落到 mountState
里面去,所以我们重点需要关注的就是 mountState
做了什么事情
// 进入 mounState 逻辑
function mountState(initialState) {
// 将新的 hook 对象追加进链表尾部
var hook = mountWorkInProgressHook();
// initialState 可以是一个回调,若是回调,则取回调执行后的值
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
// 创建当前 hook 对象的更新队列,这一步主要是为了能够依序保留 dispatch
const queue = hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
// 将 initialState 作为一个“记忆值”存下来
hook.memoizedState = hook.baseState = initialState;
// dispatch 是由上下文中一个叫 dispatchAction 的方法创建的,这里不必纠结这个方法具体做了什么
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
// 返回目标数组,dispatch 其实就是示例中常常见到的 setXXX 这个函数,想不到吧?哈哈
return [hook.memoizedState, dispatch];
}
从这段源码中我们可以看出,mounState
的主要工作是初始化 Hooks
。在整段源码中,最需要关注的是 mountWorkInProgressHook
方法,它为我们道出了 Hooks
背后的数据结构组织形式。以下是 mountWorkInProgressHook
方法的源码:
function mountWorkInProgressHook() {
// 注意,单个 hook 是以对象的形式存在的
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
if (workInProgressHook === null) {
// 这行代码每个 React 版本不太一样,但做的都是同一件事:将 hook 作为链表的头节点处理
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// 若链表不为空,则将 hook 追加到链表尾部
workInProgressHook = workInProgressHook.next = hook;
}
// 返回当前的 hook
return workInProgressHook;
}
到这里可以看出,Hook
相关的所有信息收敛在一个 Hook
对象里,而 Hook
对象之间以单向链表的形式相互串联。
而更新的流程大致如下:
首次渲染和更新渲染的区别,在于调用的是 mountState
,还是 updateState
。mountState
做了什么,我们已经清楚了;而 updateState
之后的操作链路就是:按顺序去遍历之前构建好的链表,取出对应的数据信息进行渲染。
我们把 mountState
和 updateState
做的事情放在一起来看:mountState
(首次渲染)构建链表并渲染;updateState
依次遍历链表并渲染。
看到这里,你是不是已经大概知道怎么回事儿了?没错,Hooks
的渲染是通过“依次遍历”来定位每个 hooks 内容的。如果前后两次读到的链表在顺序上出现差异,那么渲染的结果自然是不可控的。
这个现象有点像我们构建了一个长度确定的数组,数组中的每个坑位都对应着一块确切的信息,后续每次从数组里取值的时候,只能够通过索引(也就是位置)来定位数据。也正因为如此,在许多文章里,都会直截了当地下这样的定义:Hooks 的本质就是数组 。但实际上Hooks
的本质其实是链表。