react hooks推出也有很长一段时间了,我相信很多项目的代码里面都有着 hooks
的身影。那么你在用的时候有没有问过自己,为什么一个函数能记住状态?为什么 hook
写在if else中会有 warning
?下面我们来一点点的扒一扒 hook
的实现原理。
hooks
目前官方提供的 hook
有下面几种:
基础 Hook
- useState
- useEffect
- useContext
额外的 Hook
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- useDebugValue
这些 hook
的作用可以参阅官网文档,他们实现的功能不外乎这几种:
- 在函数中可以记住当前状态
- 实现缓存,能够在整个生命周期内维持变量
- 一些副作用操作可以根据某些条件判断是否执行
- 实现了ref
我们用的最多的可能就是前面两种,那么他们到底是如何实现这些功能的呢?不慌,看看源码就知道了。
为什么必须在函数顶层使用hooks
React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render
我相信很多人都看见过这句话,这是你没有在函数顶层使用 hook
的时候 react
抛出的一个错误,那为什么 react
有这种限制呢?我们来看看 react
第一次创建 hook
时干了什么。
hook的存储
function mountMemo(nextCreate, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
var nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
react
在每个 hook
第一次运行时,总会有一句
var hook = mountWorkInProgressHook();
这个函数是在干嘛呢?
function mountWorkInProgressHook() {
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
//workInProgressHook 是当前最新生成的 hook
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
可以看到,它新声明了一个 hook
对象,里面各个值的含义我们暂且不去关心,在后面的代码中,先是判断了workInprogressHook
是否为空,这个字段其实是指向了一个最新生成的 hook
,如果它为空,证明我们是第一次生成 hook
,我们就把生成的 hook
赋值给 workInProgressHook
和 currentlyRenderingFiber$1.memoizedState
。(currentlyRenderingFiber$1
是一个正在生成的 FiberNode
对象)。而如果已经生成过 hook
了,那么我们就直接让当前 hook
的 next
等于下一个 hook
,再修改 workInprogressHook
为最新生成的 hook
。这是典型的链表结构。我们用一张图来理解一下:
我们知道 react
更新了 fiber
架构,现在 react
渲染的时候会生成一颗fiber
树,这颗树由很多个FiberNode
结点组成。FiberNode
中有一个属性就叫做 memoizedState
。当然还有很多其他的属性,为了排除干扰项我们就不列出来了。
注意: hook
的数据结构中也有一个 memoizedState
,这两个不是同一个东西,大家不要搞混了。
每个组件都会生成一个 FiberNode
。每个组件内使用的 hook
会以链表的形式挂在 FiberNode
的 memoizedState
上面。而每个 FiberNode
汇聚起来会变成一颗 Fiber
树, React
每次会以固定的顺序遍历这棵树,这样就把整个页面的 hook
都串联起来了。
所以,mountWorkInProgressHook
其实就是在做一个初始化的过程,把 hook
挂载到结点上去,再返回这个 hook
。
ps: FiberNode
也不只是单纯用这种单向的方式连接,他们其实会有指向父结点和兄弟结点的指针,同样为了减少干扰在此处没有表现出来。
hook的使用
那么我们初始化 hook
之后,再次 render
的时候会发生什么呢?
function updateMemo(nextCreate, deps) {
var hook = updateWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
var prevState = hook.memoizedState;
...省略
}
我们会发现,每次开头都有一句
var hook = updateWorkInProgressHook();
那这个函数又是在干什么
function updateWorkInProgressHook() {
// This function is used both for updates and for re-renders triggered by a
// render phase update. It assumes there is either a current hook we can
// clone, or a work-in-progress hook from a previous render pass that we can
// use as a base. When we reach the end of the base list, we must switch to
// the dispatcher used for mounts.
var nextCurrentHook;
// currentHook: 已经生成的 fiber 树上的 hook,第一次是空
if (currentHook === null) {
// currentlyRenderingFiber$1: 正在生成的 FiberNode 结点, alternate 上挂载的是上一次已经生成完的 fiber 结点
// 所以 current 就是上次生成的 FiberNode
var current = currentlyRenderingFiber$1.alternate;
if (current !== null) {
// 我们之前说过 hooks 挂在 FiberNode 的 memoizedState 上,这里拿到第一个 hook
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// 不是第一次,则证明已经拿到了 hook,我们只需要用 next 就能找到下一个 hook
nextCurrentHook = currentHook.next;
}
var nextWorkInProgressHook;
// workInProgressHook: 正在生成的 FiberNode 结点上的 hook,第一次为空
if (workInProgressHook === null) {
// currentlyRenderingFiber$1 是当前正在生成的 FiberNode
// 所以这里 nextWorkInProgressHook 的值就是当前正在遍历的 hook,第一次让它等于 memoizedState
nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState;
} else {
// 不是第一次,始终让它指向下一个 hook,如果这是最后一个,那么 nextWorkInProgressHook 就会是 null
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// 不存在的话会根据上一次的 hook 克隆一个新的 hook,挂在新的链表、FiberNode上。
if (!(nextCurrentHook !== null)) {
{
throw Error( "Rendered more hooks than during the previous render." );
}
}
currentHook = nextCurrentHook;
var newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null
};
if (workInProgressHook === null) {
// This is the first hook in the list.
currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
我在代码中加了注释,感兴趣的同学可以看看具体的代码,下面我们大体解释一下这个函数干了什么。
当 react
重新渲染时,会生成一个新的 fiber
树,而这里会根据之前已经生成的 FiberNode
,拿到之前的 hook
,再复制一份到新的 FiberNode
上,生成一个新的 hooks
链表。
而这个 hook
是怎么拿的?是去遍历 hooks
链表拿的,所以每次都会按顺序拿下一个 hook
,然后复制到新的 FiberNode
上。可以理解为这个 updateWorkInProgressHook
每次都会按顺序返回下一个 hook
。
拿到这个 hook
之后再根据我们 setState
的值或者其他的一些东西去更新 hook
对象上的属性。这一步也就是 updateMemo
干的事情。
hooks只能在顶层使用的原因
其实看到这里你就应该明白为什么 hooks
只能在顶层使用了,因为它会按顺序去拿hook
,react
也是按顺序来区分不同的 hook
的,它默认你不会修改这个顺序。如果你没有在顶层使用 hook
,打乱了每次 hook
调用的顺序,就会导致 react
无法区分出对应的 hook
,进而导致错误。那你说,如果我不在顶层使用 hooks
,但是我保证它每次都会被调用,这样行不行?行,但是为什么要给自己徒增烦恼去保证它每次都会被调用,老老实实写在顶层不好吗?
hooks 如何实现一个函数组件能够记住之前的状态
我们知道,一个函数重复运行的时候它的变量都会被销毁,那 react
为什么可以记住上次的变量?因为 react
帮我们把这些变量存了下来。我们之前说到, hook
会以链表的形式被挂在 FiberNode
的 memoizedState
上,你可以把 FiberNode
理解为一个全局变量,它并不会被销毁。所以我们下次 render
的时候就能从这上面拿到上次的 hook
,自然也能拿到 hook
上携带的一些信息,再根据这些信息去 render
新的组件,就能实现函数组件也能有自己的状态了。而 useState
, useMemo
, useRef
这种带缓存效果的 hooks
的实现原理也显而易见了,我们看一个简单的 useMemo
function mountMemo(nextCreate, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
var nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
在生成的时候,就是简单的调用了一下 create
函数生成了初始值并返回。
而在更新的时候
function updateMemo(nextCreate, deps) {
var hook = updateWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
var prevState = hook.memoizedState;
if (prevState !== null) {
// Assume these are defined. If they're not, areHookInputsEqual will warn.
if (nextDeps !== null) {
var prevDeps = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
var nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
会判断一下我们的 deps
依赖是否改变(这里最底层会使用 Object.is
来判断是否相等),如果改变了,那么再调用一下我们传入的 create
来返回最新的值,如果没有改变,那么就直接返回我们上次的值,进而实现缓存的效果。
怎么拿到上次的 hook
?就是通过我们之前说的updateWorkInProgressHook
,那怎么保证两次拿的 hook
是同一个?这就是靠顺序保证了。
结语
本文只是简单叙述了 hooks
背后的实现方式,并没有对每个 hook
的具体实现方式做过多的阐述,我相信大家在了解了基本原理之后再去看各个 hook
的实现方式就会简单很多了。同时我后面也会再出一些关于具体 hook
的实现方式解析,和大家一起共同交流学习。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!