function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
invariant(
dispatcher !== null,
'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:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
return dispatcher;
}
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;
}
React Hooks 进阶
前言
上一篇简单地介绍了一下 React Hooks 的背景和 API 的使用,这一篇深入探索一下 React Hooks 的实践和原理。
React Hooks 实践
模拟 Class Component 的生命周期
有的时候还是需要根据不同的生命周期来处理一些逻辑,React Hooks 几乎可以模拟出全部的生命周期。
componentDidMount
使用 useEffect 来实现,如下:
useEffect 第二个参数传空数组时,表示只会在执行一次。
componentWillUnMount
同样可以使用 useEffect 来实现,如下:
componentDidUpdate
componentDidUpdate 生命周期在组件每次更新之后执行,除了初始化 render 的时候不执行,所以可以设置一个标志位来判断是否是第一次 render,使用 useEffect + useRef 配合就可以实现:
getDerivedStateFromProps
getDeriverdStateFromProps 是 react 新版本中用来替代 componentWillReceiveProps,它可以感知 props 的变化,从而更新组件内部的 state,用 hooks 模拟这个生命周期,可以这样实现:
shouldComponentUpdate
React 16.6 引入 React.memo,是用来控制 Function Component 的重新渲染的,类似于 Class Component 的 PureComponent,可以跳过 props 没有变化时的更新,为了支持更加灵活的 props 对比,它还提供了第二个函数参数 areEqual(prevProps, nextProps),和 shouldComponentUpdate 相反的是,当该函数返回 true 时表示不更新函数,返回 false 则重新更新,用法如下:
除了上面这种方法可以模拟 shouldComponentUpdate 之外,React Hooks 还提供一个 useMemo 用来控制子组件重新渲染的,举一个例子如下:
在上面的例子中,只有 Parent 组件中的 count state 更新了,Child 才会重新渲染,否则不会。
React Hooks 原理
还记得我们之前讲过的使用 React Hooks 的两条规则吗?
现在我们来一一剖析一下为什么会有这个限制?
只能在 React 函数和自定义 Hooks 中使用
翻到 ReactHooks 对应的源码,贴出 Hooks 的定义如下:
所有的 Hooks 基本都调用了这个 resolveDispatcher(),定位到 resolveDispatcher,代码如下:
如果 ReactCurrentDispatcher.current 是空的,就会得出我们使用 Hooks 的方式不对,只有在 React 环境中才会给 ReactCurrentDispatcher 的 current 赋值,所以就可以解这个问题。
不在循环、条件或者嵌套函数中调用 Hook
为什么不能在循环、条件或者嵌套函数中调用 Hook,我们还是从源码出发寻找原因:
Hooks 的实现源码在 ReactFiberHooks.js。
在这个文件中,定义了 firstWorkInProgressHook 和 workInProgressHook 这两个全局变量,观察所有的 Hooks 实现,发现都执行了 const hook = mountWorkInProgressHook(),首先来看一下这个函数的实现:
我们来模拟一下定义多个 Hooks 时的流程:
这种结构就是一个链表结构,而每一个 Hook 的结构如下:
其中 memoizedState 存储当前 Hook 的结果,next 则连接到下一个 Hook,从而将所有 Hook 进行串联起来。这个链表结果存储在 Fiber 对象的 memoizedState 属性中,在 React 中,每个节点都对应一个 Fiber 对象,而 Fiber 的 memoizedState 用来存储该节点在上次渲染中的 state,这个属性是 Class Component 用来存储节点的 state 的,这也就是为什么 Hook 可以拥有 Class Component 功能的原因。
链表结构用图形显示如下:
在第二次渲染时,也就是 update 的时候,此时调用的是 Hook 对应的 update 方法,而 update 方法又分别执行了 updateWorkInProgressHook(),先来看看这个方法的实现:
在这个方法中,它会获取渲染时生成的 Hooks,并获取当前 update 的是处于链表的哪个节点,然后返回。
假如在条件语句中使用 Hook,如下:
初始渲染时,拿到的是 state1 => hook1,state2 => hook2,state3 => hook3,再次渲染时,condition 条件不满足,那么执行 state3 时拿到的就是 hook2,那整个逻辑就乱套了...
结语
React Hooks 解决了一部分问题,但同时自身也有一定的缺陷,比如要遵守一定规则、组件嵌套层次不明显导致 bug 定位难。所以在实际的开发实践中,还是要评估再选型。