你真的了解React Hooks吗?
ReactHooks从发布到现在也已经有年头了, 它的发布确实带来了很多革命性的变化, 比如大家更频繁的使用了functional component, 甚至以前的函数签名也从 SFC
变成了 FC
, 因为hooks 让它从 stateless变成了现在的模样.
那我们在使用过程中是否有思考过, 这些巧妙的方案, 到底是如何实现的呢?
以及, 为了实现这些, react团队做了那些巧思?
这篇文章, 我通过自己的方式, 带大家了解一下, react hooks的魔法.
react 是怎么捕获到hooks的执行上下文,是在函数组件内部的?
这里我们需要来展示一下简单版的 renderWithHooks
方法
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
let children = Component(props, secondArg);
// Check if there was a render phase update
if (didScheduleRenderPhaseUpdateDuringThisPass) {
// Keep rendering in a loop for as long as render phase updates continue to
// be scheduled. Use a counter to prevent infinite loops.
let numberOfReRenders: number = 0;
do {
didScheduleRenderPhaseUpdateDuringThisPass = false;
invariant(
numberOfReRenders < RE_RENDER_LIMIT,
'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',
);
children = Component(props, secondArg);
} while (didScheduleRenderPhaseUpdateDuringThisPass);
}
// We can assume the previous dispatcher is always this one, since we set it
// at the beginning of the render phase and there's no re-entrancy.
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
return children;
}
复制代码
先插一句题外话, 我们在写组件的过程中, 有可能会遇到死循环导致的崩溃报错, 很少有人有耐心一次一次的debug完, 知道 重复渲染的限制次数.
在读源码的过程中, 我们发现了这个常量
const RE_RENDER_LIMIT = 25;
invariant(
numberOfReRenders < RE_RENDER_LIMIT,
'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',
)
复制代码
言归正传,
我们会在这里 先执行 currentlyRenderingFiber = workInProgress
将准备渲染的内容放在当前渲染队列当中. 并且选择是 创建 还是 更新 的hook方法, 再使用这一行去执行渲染更新我们的组件 children = Component(props, secondArg)
.
ReactCurrentDispatcher.current = ContextOnlyDispatcher
这一句写在组件渲染之后, 也就是组件之外执行. 是为了让ReactCurrentDispatcher只能调用readContext, 调用其它内容都会报错. 简而言之就是, 在组件外执行hooks就会报错.
export const ContextOnlyDispatcher: Dispatcher = {
readContext,
useState: throwInvalidHookError,
...
};
function throwInvalidHookError() {
invariant(
false,
'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://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
);
}
复制代码
为什么不能条件语句中,声明hooks? 并且需要在return语句之前声明?
React hooks: not magic, just arrays
这篇文章比较久远了, 大概是在hooks即将发布的那段日子里. 里面猜测了react hooks的实现方法, 他的推测是使用数组.会用两个数组存储 一个存state, 一个存setter, 并且按照顺序进行一一对应. 如果在条件语句中使用的话, 会造成你声明的hook值对应不上的问题. 二次渲染的时候就会报错了.
原理大概是这个意思.
这条理论从分析上来讲, 实现是有可能的. 但是react团队最终还是采取了由fiber主导的性能优化方案链表.
也就是说, 使用的是 .next 指向下一个hook, 如果在条件语句进行声明, 会导致mountHook的next和updateHook的next指向不一致, 这样就会出错了. 下文会详细进行解释.
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
复制代码
这里用 useState
来举例, 在 renderWithHooks
方法里, 我们可以看到, 是这样加载hook的方法. 而这两个对象的区别在于, 一个是 mountState
一个是 updateState
.
这里讲 mountState
根据 @flowtypes
的定义可以看出来, 这里是接收了一个初始值, 返回了一个数组, [初始值, dispatch]. 接收的初始值可以是一个方法, 但是返回的初始值一定是一个值.
const [value, setValue] = useState('name')
// value 为 'name'
const [value, setValue] = useState(()=>'name')
// value 为 'name'
复制代码
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
复制代码
接着来说这个函数. 第一行 const hook = mountWorkInProgressHook()
, 这里就可以看到fiber的巧思了, 它是创建了一个新的Hook对象, 如果当前没有其它的hook, 那么就将它直接赋值给了当前的 workInprogressHook
. 如果已经存在了hooks, 就将它添加到了 workInprogressHook
的最后. (利用链表的数据结构, 使用next指向)
currentlyRenderingFiber.memoizedState是fiber的实现, 这里先不讲.
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
复制代码
继续讲 queue
的定义. 这里先将默认 UpdateQueue
对象赋给了hook.queue, 再赋值给了queue.
然后再继续通过 dispatchAction
方法, 创造出了一个dispatch对象. 返回.
function函数组件中的useState,和 class类组件 setState有什么区别?
class组件, 它是一个实例. 实例化了以后, 内部会有它自己的状态. 而对于function来说, 它本身是一个方法, 是无状态的. 所以在class的state, 是可以保存的. 而function的state则依赖其它的方式保存它的状态, 比如hooks.
useEffect,useMemo 中,为什么useRef不需要依赖注入,就能访问到最新的改变值?
function updateRef<T>(initialValue: T): {|current: T|} {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}
复制代码
可以看到, 不论你的值如何更改, 你返回的内容都是 hook.memoizedState, 而它在内存当中都指向的是一个对象 memoizedState
. 对象里的值不论怎么修改, 你都会直接拿到最新的值.
我们经常会在useEffect中调用 useState 返回数组的第二个元素 setter 的时候发现, 因为产生了闭包的关系, 里面的value永远不会更新. 这个时候我们就可以借助ref的方式进行处理了.
useMemo是怎么对值做缓存的?如何应用它优化性能?
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
复制代码
其实我们这里就是记录了两个内容, 直接将当前的 依赖参数 deps记录了下来, 并且执行了 memo的第一个参数, 获取结果 存入 nextValue=nextCreate()
当中. 并且返回.
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
// Assume these are defined. If they're not, areHookInputsEqual will warn.
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
复制代码
在updateMemo里, 我们使用 areHookInputsEqual(nextDeps, prevDeps)
进行比较, 如果两次deps没有变动, 那么我们依旧返回刚才的数据. 这样就进行了一次性能优化了.
如果变动了, 就和mountMemo的操作一致.
这里的 areHookInputsEqual
方法, 也是 useEffect
等比较deps的方法.
里面利用的 [Object.is](http://object.is)
的方式进行比较, 这也解释了为什么只能进行浅diff操作
Object.is() – JavaScript | MDN
整篇文章读完了, 如果你看到这里, 我想提一个问题.
为什么 useState
的返回值是 数组? 而不是一个对象?
如果让你猜猜看, 你觉得这样做是为什么? 好处又是什么呢?