点击上方蓝字,发现更多精彩
导语
ReactHooks从发布到现在也已经有年头了, 它的发布确实带来了很多革命性的变化, 比如大家更频繁的使用了functional component, 甚至以前的函数签名也从 SFC
变成了 FC
, 因为hooks 让它从 stateless变成了现在的模样.
那我们在使用过程中是否有思考过, 这些巧妙的方案, 到底是如何实现的呢?
以及, 为了实现这些, react团队做了那些巧思?
这篇文章, 我通过自己的方式, 带大家了解一下, 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.', );}
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];}
接着来说这个函数. 第一行 consthook=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对象. 返回.
class组件, 它是一个实例. 实例化了以后, 内部会有它自己的状态. 而对于function来说, 它本身是一个方法, 是无状态的. 所以在class的state, 是可以保存的. 而function的state则依赖其它的方式保存它的状态, 比如hooks.
function updateRef<T>(initialValue: T): {|current: T|} { const hook = updateWorkInProgressHook(); return hook.memoizedState;}
可以看到, 不论你的值如何更改, 你返回的内容都是 hook.memoizedState, 而它在内存当中都指向的是一个对象 memoizedState
. 对象里的值不论怎么修改, 你都会直接拿到最新的值.
我们经常会在useEffect中调用 useState 返回数组的第二个元素 setter 的时候发现, 因为产生了闭包的关系, 里面的value永远不会更新. 这个时候我们就可以借助ref的方式进行处理了.
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
的方式进行比较, 这也解释了为什么只能进行浅diff操作
《Object.is() - JavaScript | MDN》:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is)
整篇文章读完了, 如果你看到这里, 我想提一个问题.
为什么 useState
的返回值是 数组? 而不是一个对象?
如果让你猜猜看, 你觉得这样做是为什么? 好处又是什么呢?
END
▼
更多精彩推荐,请关注我们
▼
你的每个赞和在看,我都喜欢!