「react进阶」一文吃透react-hooks原理
之前的两篇文章,分别介绍了react-hooks
如何使用,以及自定义hooks
设计模式及其实战,本篇文章主要从react-hooks
起源,原理,源码角度,开始剖析react-hooks
运行机制和内部原理,相信这篇文章过后,对于面试的时候那些hooks
问题,也就迎刃而解了。实际react-hooks
也并没有那么难以理解,听起来很cool
,实际就是函数组件解决没有state
,生命周期,逻辑不能复用的一种技术方案。
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
老规矩,🤔️🤔️🤔️我们带着疑问开始今天的探讨(能回答上几个,自己可以尝试一下,掌握程度):
1 在无状态组件每一次函数上下文执行的时候,
react
用什么方式记录了hooks
的状态?2 多个
react-hooks
用什么来记录每一个hooks
的顺序的 ?换个问法!为什么不能条件语句中,声明hooks
?hooks
声明为什么在组件的最顶部?3
function
函数组件中的useState
,和class
类组件setState
有什么区别?4
react
是怎么捕获到hooks
的执行上下文,是在函数组件内部的?5
useEffect
,useMemo
中,为什么useRef
不需要依赖注入,就能访问到最新的改变值?6
useMemo
是怎么对值做缓存的?如何应用它优化性能?7 为什么两次传入
useState
的值相同,函数组件不更新?...
如果你认真读完这篇文章,这些问题全会迎刃而解。
function组件和class组件本质的区别
在解释react-hooks
原理的之前,我们要加深理解一下, 函数组件和类组件到底有什么区别,废话不多说,我们先看 两个代码片段。
class Index extends React.Component<any,any>{
constructor(props){
super(props)
this.state={
number:0
}
}
handerClick=()=>{
for(let i = 0 ;i<5;i++){
setTimeout(()=>{
this.setState({ number:this.state.number+1 })
console.log(this.state.number)
},1000)
}
}
render(){
return <div>
<button onClick={ this.handerClick } >num++</button>
</div>
}
}
打印结果?
再来看看函数组件中:
function Index(){
const [ num ,setNumber ] = React.useState(0)
const handerClick=()=>{
for(let i=0; i<5;i++ ){
setTimeout(() => {
setNumber(num+1)
console.log(num)
}, 1000)
}
}
return <button onClick={ handerClick } >{ num }</button>
}
打印结果?
------------公布答案-------------
在第一个例子🌰打印结果:1 2 3 4 5
在第二个例子🌰打印结果:0 0 0 0 0
这个问题实际很蒙人,我们来一起分析一下,第一个类组件中,由于执行上setState
没有在react
正常的函数执行上下文上执行,而是setTimeout
中执行的,批量更新条件被破坏。原理这里我就不讲了,所以可以直接获取到变化后的state
。
但是在无状态组件中,似乎没有生效。原因很简单,在class
状态中,通过一个实例化的class
,去维护组件中的各种状态;但是在function
组件中,没有一个状态去保存这些信息,每一次函数上下文执行,所有变量,常量都重新声明,执行完毕,再被垃圾机制回收。所以如上,无论setTimeout
执行多少次,都是在当前函数上下文执行,此时num = 0
不会变,之后setNumber
执行,函数组件重新执行之后,num
才变化。
所以, 对于class
组件,我们只需要实例化一次,实例中保存了组件的state
等状态。对于每一次更新只需要调用render
方法就可以。但是在function
组件中,每一次更新都是一次新的函数执行,为了保存一些状态,执行一些副作用钩子,react-hooks
应运而生,去帮助记录组件的状态,处理一些额外的副作用。
一 初识:揭开hooks的面纱
1 当我们引入hooks时候发生了什么?
我们从引入 hooks
开始,以useState
为例子,当我们从项目中这么写:
import { useState } from 'react'
于是乎我们去找useState
,看看它到底是哪路神仙?
react/src/ReactHooks.js
useState
export function useState(initialState){
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
useState()
的执行等于 dispatcher.useState(initialState)
这里面引入了一个dispatcher
,我们看一下resolveDispatcher
做了些什么?
resolveDispatcher
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current
return dispatcher
}
ReactCurrentDispatcher
react/src/ReactCurrentDispatcher.js
const ReactCurrentDispatcher = {
current: null,
};
我们看到ReactCurrentDispatcher.current
初始化的时候为null
,然后就没任何下文了。我们暂且只能把**ReactCurrentDispatcher
**记下来。看看ReactCurrentDispatcher
什么时候用到的 ?
2 开工造物,从无状态组件的函数执行说起
想要彻底弄明白hooks
,就要从其根源开始,上述我们在引入hooks
的时候,最后以一个ReactCurrentDispatcher
草草收尾,线索全部断了,所以接下来我们只能从函数组件执行开始。
renderWithHooks 执行函数
对于function
组件是什么时候执行的呢?
react-reconciler/src/ReactFiberBeginWork.js
function
组件初始化:
renderWithHooks(
null, // current Fiber
workInProgress, // workInProgress Fiber
Component, // 函数组件本身
props, // props
context, // 上下文
renderExpirationTime,// 渲染 ExpirationTime
);
对于初始化是没有current
树的,之后完成一次组件更新后,会把当前workInProgress
树赋值给current
树。
function
组件更新:
renderWithHooks(
current,
workInProgress,
render,
nextProps,
context,
renderExpirationTime,
);
我们从上边可以看出来,renderWithHooks
函数作用是调用function
组件函数的主要函数。我们重点看看renderWithHooks
做了些什么?
renderWithHooks react-reconciler/src/ReactFiberHooks.js
export function renderWithHooks(
current,
workInProgress,
Component,
props,
secondArg,
nextRenderExpirationTime,
) {
renderExpirationTime = nextRenderExpirationTime;
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.expirationTime = NoWork;
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
let children = Component(props, secondArg);
if (workInProgress.expirationTime === renderExpirationTime) {
// ....这里的逻辑我们先放一放
}
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
renderExpirationTime = NoWork;
currentlyRenderingFiber = null;
currentHook = null
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
return children;
}
所有的函数组件执行,都是在这里方法中,首先我们应该明白几个感念,这对于后续我们理解useState
是很有帮助的。
current fiber树
: 当完成一次渲染之后,会产生一个current
树,current
会在commit
阶段替换成真实的Dom
树。
workInProgress fiber树
: 即将调和渲染的 fiber
树。再一次新的组件更新过程中,会从current
复制一份作为workInProgress
,更新完毕后,将当前的workInProgress
树赋值给current
树。
workInProgress.memoizedState
: 在class
组件中,memoizedState
存放state
信息,在function
组件中,这里可以提前透漏一下,memoizedState
在一次调和渲染过程中,以链表的形式存放hooks
信息。
workInProgress.expirationTime
: react
用不同的expirationTime
,来确定更新的优先级。
currentHook
: 可以理解 current
树上的指向的当前调度的 hooks
节点。
workInProgressHook
: 可以理解 workInProgress
树上指向的当前调度的 hooks
节点。
renderWithHooks
函数主要作用:
首先先置空即将调和渲染的workInProgress
树的memoizedState
和updateQueue
,为什么这么做,因为在接下来的函数组件执行过程中,要把新的hooks
信息挂载到这两个属性上,然后在组件commit
阶段,将workInProgress
树替换成current
树,替换真实的DOM
元素节点。并在current
树保存hooks
信息。
然后根据当前函数组件是否是第一次渲染,赋予ReactCurrentDispatcher.current
不同的hooks
,终于和上面讲到的ReactCurrentDispatcher
联系到一起。对于第一次渲染组件,那么用的是HooksDispatcherOnMount
hooks对象。对于渲染后,需要更新的函数组件,则是HooksDispatcherOnUpdate
对象,那么两个不同就是通过current
树上是否memoizedState
(hook信息)来判断的。如果current
不存在,证明是第一次渲染函数组件。
接下来,调用Component(props, secondArg);
执行我们的函数组件,我们的函数组件在这里真正的被执行了,然后,我们写的hooks
被依次执行,把hooks
信息依次保存到workInProgress
树上。 至于它是怎么保存的,我们马上会讲到。
接下来,也很重要,将ContextOnlyDispatcher
赋值给 ReactCurrentDispatcher.current
,由于js
是单线程的,也就是说我们没有在函数组件中,调用的hooks
,都是ContextOnlyDispatcher
对象上hooks
,我们看看ContextOnlyDispatcher
hooks,到底是什么。
const ContextOnlyDispatcher = {
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://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
}
原来如此,react-hooks
就是通过这种函数组件执行赋值不同的hooks
对象方式,判断在hooks
执行是否在函数组件内部,捕获并抛出异常的。
最后,重新置空一些变量比如currentHook
,currentlyRenderingFiber
,workInProgressHook
等。
3 不同的hooks
对象
上述讲到在函数第一次渲染组件和更新组件分别调用不同的hooks
对象,我们现在就来看看HooksDispatcherOnMount
和 HooksDispatcherOnUpdate
。
第一次渲染(我这里只展示了常用的hooks
):
const HooksDispatcherOnMount = {
useCallback: mountCallback,
useEffect: mountEffect,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
};
更新组件:
const HooksDispatcherOnUpdate = {
useCallback: updateCallback,
useEffect: updateEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState
};
看来对于第一次渲染组件,和更新组件,react-hooks
采用了两套Api
,本文的第二部分和第三部分,将重点两者的联系。
我们用流程图来描述整个过程:
三 hooks初始化,我们写的hooks会变成什么样子
本文将重点围绕四个中重点hooks
展开,分别是负责组件更新的useState
,负责执行副作用useEffect
,负责保存数据的useRef
,负责缓存优化的useMemo
, 至于useCallback
,useReducer
,useLayoutEffect
原理和那四个重点hooks
比较相近,就不一一解释了。
我们先写一个组件,并且用到上述四个主要hooks
:
请记住如下代码片段,后面讲解将以如下代码段展开
import React , { useEffect , useState , useRef , useMemo } from 'react'
function Index(){
const [ number , setNumber ] = useState(0)
const DivDemo = useMemo(() => <div> hello , i am useMemo </div>,[])
const curRef = useRef(null)
useEffect(()=>{
console.log(curRef.current)
},[])
return <div ref={ curRef } >
hello,world { number }
{ DivDemo }
<button onClick={() => setNumber(number+1) } >number++</button>
</div>
}
接下来我们一起研究一下我们上述写的四个hooks
最终会变成什么?
1 mountWorkInProgressHook
在组件初始化的时候,每一次hooks
执行,如useState()
,useRef()
,都会调用mountWorkInProgressHook
,mountWorkInProgressHook
到底做了些什么,让我们一起来分析一下:
react-reconciler/src/ReactFiberHooks.js -> mountWorkInProgressHook
function mountWorkInProgressHook() {
const hook: Hook = {
memoizedState: null, // useState中 保存 state信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和deps | useRef中保存的是ref 对象
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) { // 例子中的第一个`hooks`-> useState(0) 走的就是这样。
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
mountWorkInProgressHook
这个函数做的事情很简单,首先每次执行一个hooks
函数,都产生一个hook
对象,里面保存了当前hook
信息,然后将每个hooks
以链表形式串联起来,并赋值给workInProgress
的memoizedState
。也就证实了上述所说的,函数组件用memoizedState
存放hooks
链表。
至于hook
对象中都保留了哪些信息?我这里先分别介绍一下 :
memoizedState: useState中
保存 state
信息 | useEffect
中 保存着 effect
对象 | useMemo
中 保存的是缓存的值和 deps
| useRef
中保存的是 ref
对象。
baseState : usestate
和useReducer
中 保存最新的更新队列。
baseState : usestate
和useReducer
中,一次更新中 ,产生的最新state
值。
queue :保存待更新队列 pendingQueue
,更新函数 dispatch
等信息。
next: 指向下一个 hooks
对象。
那么当我们函数组件执行之后,四个hooks
和workInProgress
将是如图的关系。
知道每个hooks
关系之后,我们应该理解了,为什么不能条件语句中,声明hooks
。
我们用一幅图表示如果在条件语句中声明会出现什么情况发生。
如果我们将上述demo
其中的一个 useRef
放入条件语句中,
let curRef = null
if(isFisrt){
curRef = useRef(null)
}
因为一旦在条件语句中声明hooks
,在下一次函数组件更新,hooks
链表结构,将会被破坏,current
树的memoizedState
缓存hooks
信息,和当前workInProgress
不一致,如果涉及到读取state
等操作,就会发生异常。
上述介绍了 hooks
通过什么来证明唯一性的,答案 ,通过hooks
链表顺序。和为什么不能在条件语句中,声明hooks
,接下来我们按照四个方向,分别介绍初始化的时候发生了什么?
2 初始化useState -> mountState
mountState
function mountState(
initialState
){
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// 如果 useState 第一个参数为函数,执行函数得到state
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null, // 带更新的
dispatch: null, // 负责更新函数
lastRenderedReducer: basicStateReducer, //用于得到最新的 state ,
lastRenderedState: initialState, // 最后一次得到的 state
});
const dispatch = (queue.dispatch = (dispatchAction.bind( // 负责更新的函数
null,
currentlyRenderingFiber,
queue,
)))
return [hook.memoizedState, dispatch];
}
mountState
到底做了些什么,首先会得到初始化的state
,将它赋值给mountWorkInProgressHook
产生的hook
对象的 memoizedState
和baseState
属性,然后创建一个queue
对象,里面保存了负责更新的信息。
这里先说一下,在无状态组件中,useState
和useReducer
触发函数更新的方法都是dispatchAction
,useState
,可以看成一个简化版的useReducer
,至于dispatchAction
怎么更新state
,更新组件的,我们接着往下研究dispatchAction
。
在研究之前 我们先要弄明白dispatchAction
是什么?
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
)
const [ number , setNumber ] = useState(0)
dispatchAction
就是 setNumber
, dispatchAction
第一个参数和第二个参数,已经被bind
给改成currentlyRenderingFiber
和 queue
,我们传入的参数是第三个参数action
dispatchAction 无状态组件更新机制
作为更新的主要函数,我们一下来研究一下,我把 dispatchAction
精简,精简,再精简:
function dispatchAction(fiber, queue, action) {
// 计算 expirationTime 过程略过。
/* 创建一个update */
const update= {
expirationTime,
suspenseConfig,
action,
eagerReducer: null,
eagerState: null,
next: null,
}
/* 把创建的update */
const pending = queue.pending;
if (pending === null) { // 证明第一次更新
update.next = update;
} else { // 不是第一次更新
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
const alternate = fiber.alternate;
/* 判断当前是否在渲染阶段 */
if ( fiber === currentlyRenderingFiber || (alternate !== null && alternate === currentlyRenderingFiber)) {
didScheduleRenderPhaseUpdate = true;
update.expirationTime = renderExpirationTime;
currentlyRenderingFiber.expirationTime = renderExpirationTime;
} else { /* 当前函数组件对应fiber没有处于调和渲染阶段 ,那么获取最新state , 执行更新 */
if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
const currentState = queue.lastRenderedState; /* 上一次的state */
const eagerState = lastRenderedReducer(currentState, action); /**/
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
return
}
}
}
}
scheduleUpdateOnFiber(fiber, expirationTime);
}
}
无论是类组件调用setState
,还是函数组件的dispatchAction
,都会产生一个 update
对象,里面记录了此次更新的信息,然后将此update
放入待更新的pending
队列中,dispatchAction
第二步就是判断当前函数组件的fiber
对象是否处于渲染阶段,如果处于渲染阶段,那么不需要我们在更新当前函数组件,只需要更新一下当前update
的expirationTime
即可。
如果当前fiber
没有处于更新阶段。那么通过调用lastRenderedReducer
获取最新的state
,和上一次的currentState
,进行浅比较,如果相等,那么就退出,这就证实了为什么useState
,两次值相等的时候,组件不渲染的原因了,这个机制和Component
模式下的setState
有一定的区别。
如果两次state
不相等,那么调用scheduleUpdateOnFiber
调度渲染当前fiber
,scheduleUpdateOnFiber
是react
渲染更新的主要函数。
我们把初始化mountState
和无状态组件更新机制讲明白了,接下来看一下其他的hooks初始化做了些什么操作?
3 初始化useEffect -> mountEffect
上述讲到了无状态组件中fiber
对象memoizedState
保存当前的hooks
形成的链表。那么updateQueue
保存了什么信息呢,我们会在接下来探索useEffect
过程中找到答案。当我们调用useEffect
的时候,在组件第一次渲染的时候会调用mountEffect
方法,这个方法到底做了些什么?
mountEffect
function mountEffect(
create,
deps,
) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = pushEffect(
HookHasEffect | hookEffectTag,
create, // useEffect 第一次参数,就是副作用函数
undefined,
nextDeps, // useEffect 第二次参数,deps
);
}
每个hooks
初始化都会创建一个hook
对象,然后将hook的memoizedState
保存当前effect hook
信息。
有两个memoizedState
大家千万别混淆了,我这里再友情提示一遍
workInProgress / current
树上的memoizedState
保存的是当前函数组件每个hooks
形成的链表。每个
hooks
上的memoizedState
保存了当前hooks
信息,不同种类的hooks
的memoizedState
内容不同。上述的方法最后执行了一个pushEffect
,我们一起看看pushEffect
做了些什么?
pushEffect 创建effect对象,挂载updateQueue
function pushEffect(tag, create, destroy, deps) {
const effect = {
tag,
create,
destroy,
deps,
next: null,
};
let componentUpdateQueue = currentlyRenderingFiber.updateQueue
if (componentUpdateQueue === null) { // 如果是第一个 useEffect
componentUpdateQueue = { lastEffect: null }
currentlyRenderingFiber.updateQueue = componentUpdateQueue
componentUpdateQueue.lastEffect = effect.next = effect;
} else { // 存在多个effect
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
这一段实际很简单,首先创建一个 effect
,判断组件如果第一次渲染,那么创建 componentUpdateQueue
,就是workInProgress
的updateQueue
。然后将effect
放入updateQueue
中,不过这里顺序要主要,越靠后的effect
,越在updateQueue
前边。
假设我们在一个函数组件中这么写:
useEffect(()=>{
console.log(1)
},[ props.a ])
useEffect(()=>{
console.log(2)
},[])
useEffect(()=>{
console.log(3)
},[])
最后workInProgress.updateQueue
会以这样的形式保存:
拓展:effectList
effect list
可以理解为是一个存储 effectTag
副作用列表容器。它是由 fiber
节点和指针 nextEffect
构成的单链表结构,这其中还包括第一个节点 firstEffect
,和最后一个节点 lastEffect
。 React
采用深度优先搜索算法,在 render
阶段遍历 fiber
树时,把每一个有副作用的 fiber
筛选出来,最后构建生成一个只带副作用的 effect list
链表。在 commit
阶段,React
拿到 effect list
数据后,通过遍历 effect list
,并根据每一个 effect
节点的 effectTag
类型,执行每个effect
,从而对相应的 DOM
树执行更改。
4 初始化useMemo -> mountMemo
不知道大家是否把 useMemo
想象的过于复杂了,实际相比其他 useState
, useEffect
等,它的逻辑实际简单的很。
function mountMemo(nextCreate,deps){
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
初始化useMemo
,就是创建一个hook
,然后执行useMemo
的第一个参数,得到需要缓存的值,然后将值和deps
记录下来,赋值给当前hook
的memoizedState
。整体上并没有复杂的逻辑。
5 初始化useRef -> mountRef
对于useRef
初始化处理,似乎更是简单,我们一起来看一下:
function mountRef(initialValue) {
const hook = mountWorkInProgressHook();
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;
}
mountRef
初始化很简单, 创建一个ref对象, 对象的current
属性来保存初始化的值,最后用memoizedState
保存ref
,完成整个操作。
6 mounted 阶段 hooks 总结
我们来总结一下初始化阶段,react-hooks
做的事情,在一个函数组件第一次渲染执行上下文过程中,每个react-hooks
执行,都会产生一个hook
对象,并形成链表结构,绑定在workInProgress
的memoizedState
属性上,然后react-hooks
上的状态,绑定在当前hooks
对象的memoizedState
属性上。对于effect
副作用钩子,会绑定在workInProgress.updateQueue
上,等到commit
阶段,dom
树构建完成,在执行每个 effect
副作用钩子。
四 hooks更新阶段
上述介绍了第一次渲染函数组件,react-hooks
初始化都做些什么,接下来,我们分析一下,
对于更新阶段,说明上一次 workInProgress
树已经赋值给了 current
树。存放hooks
信息的memoizedState
,此时已经存在current
树上,react
对于hooks
的处理逻辑和fiber
树逻辑类似。
对于一次函数组件更新,当再次执行hooks
函数的时候,比如 useState(0)
,首先要从current
的hooks
中找到与当前workInProgressHook
,对应的currentHooks
,然后复制一份currentHooks
给workInProgressHook
,接下来hooks
函数执行的时候,把最新的状态更新到workInProgressHook
,保证hooks
状态不丢失。
所以函数组件每次更新,每一次react-hooks
函数执行,都需要有一个函数去做上面的操作,这个函数就是updateWorkInProgressHook
,我们接下来一起看这个updateWorkInProgressHook
。
1 updateWorkInProgressHook
function updateWorkInProgressHook() {
let nextCurrentHook;
if (currentHook === null) { /* 如果 currentHook = null 证明它是第一个hooks */
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else { /* 不是第一个hooks,那么指向下一个 hooks */
nextCurrentHook = currentHook.next;
}
let nextWorkInProgressHook
if (workInProgressHook === null) { //第一次执行hooks
// 这里应该注意一下,当函数组件更新也是调用 renderWithHooks ,memoizedState属性是置空的
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
/* 这个情况说明 renderWithHooks 执行 过程发生多次函数组件的执行 ,我们暂时先不考虑 */
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
invariant(
nextCurrentHook !== null,
'Rendered more hooks than during the previous render.',
);
currentHook = nextCurrentHook;
const newHook = { //创建一个新的hook
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) { // 如果是第一个hooks
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else { // 重新更新 hook
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
这一段的逻辑大致是这样的:
首先如果是第一次执行
hooks
函数,那么从current
树上取出memoizedState
,也就是旧的hooks
。然后声明变量
nextWorkInProgressHook
,这里应该值得注意,正常情况下,一次renderWithHooks
执行,workInProgress
上的memoizedState
会被置空,hooks
函数顺序执行,nextWorkInProgressHook
应该一直为null
,那么什么情况下nextWorkInProgressHook
不为null
,也就是当一次renderWithHooks
执行过程中,执行了多次函数组件,也就是在renderWithHooks
中这段逻辑。if (workInProgress.expirationTime === renderExpirationTime) {
// ....这里的逻辑我们先放一放
}
这里面的逻辑,实际就是判定,如果当前函数组件执行后,当前函数组件的还是处于渲染优先级,说明函数组件又有了新的更新任务,那么循坏执行函数组件。这就造成了上述的,nextWorkInProgressHook
不为 null
的情况。
最后复制
current
的hooks
,把它赋值给workInProgressHook
,用于更新新的一轮hooks
状态。
接下来我们看一下四个种类的hooks
,在一次组件更新中,分别做了那些操作。
2 updateState
useState
function updateReducer(
reducer,
initialArg,
init,
){
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current = currentHook;
let baseQueue = current.baseQueue;
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// 这里省略... 第一步:将 pending queue 合并到 basequeue
}
if (baseQueue !== null) {
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime < renderExpirationTime) { //优先级不足
const clone = {
expirationTime: update.expirationTime,
...
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
} else { //此更新确实具有足够的优先级。
if (newBaseQueueLast !== null) {
const clone= {
expirationTime: Sync,
...
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
/* 得到新的 state */
newState = reducer(newState, action);
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = newBaseQueueFirst;
}
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
const dispatch = queue.dispatch
return [hook.memoizedState, dispatch];
}
这一段看起来很复杂,让我们慢慢吃透,首先将上一次更新的pending queue
合并到 basequeue
,为什么要这么做,比如我们在一次点击事件中这么写,
function Index(){
const [ number ,setNumber ] = useState(0)
const handerClick = ()=>{
// setNumber(1)
// setNumber(2)
// setNumber(3)
setNumber(state=>state+1)
// 获取上次 state = 1
setNumber(state=>state+1)
// 获取上次 state = 2
setNumber(state=>state+1)
}
console.log(number) // 3
return <div>
<div>{ number }</div>
<button onClick={ ()=> handerClick() } >点击</button>
</div>
}
点击按钮, 打印 3
三次setNumber
产生的update
会暂且放入pending queue
,在下一次函数组件执行时候,三次 update
被合并到 baseQueue
。结构如下图:
接下来会把当前useState
或是useReduer
对应的hooks
上的baseState
和baseQueue
更新到最新的状态。会循环baseQueue
的update
,复制一份update
,更新 expirationTime
,对于有足够优先级的update
(上述三个setNumber
产生的update
都具有足够的优先级),我们要获取最新的state
状态。,会一次执行useState
上的每一个action
。得到最新的state
。
更新state
这里有会有两个疑问🤔️:
问题一:这里不是执行最后一个
action
不就可以了嘛?
答案:原因很简单,上面说了 useState
逻辑和useReducer
差不多。如果第一个参数是一个函数,会引用上一次 update
产生的 state
, 所以需要循环调用,每一个update
的reducer
,如果setNumber(2)
是这种情况,那么只用更新值,如果是setNumber(state=>state+1)
,那么传入上一次的 state
得到最新state
。
问题二:什么情况下会有优先级不足的情况(
updateExpirationTime < renderExpirationTime
)?
答案:这种情况,一般会发生在,当我们调用setNumber
时候,调用scheduleUpdateOnFiber
渲染当前组件时,又产生了一次新的更新,所以把最终执行reducer
更新state
任务交给下一次更新。
3 updateEffect
function updateEffect(create, deps): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
pushEffect(hookEffectTag, create, destroy, nextDeps);
return;
}
}
}
currentlyRenderingFiber.effectTag |= fiberEffectTag
hook.memoizedState = pushEffect(
HookHasEffect | hookEffectTag,
create,
destroy,
nextDeps,
);
}
useEffect
做的事很简单,判断两次deps
相等,如果相等说明此次更新不需要执行,则直接调用 pushEffect
,这里注意 effect
的标签,hookEffectTag
,如果不相等,那么更新 effect
,并且赋值给hook.memoizedState
,这里标签是 HookHasEffect | hookEffectTag
,然后在commit
阶段,react
会通过标签来判断,是否执行当前的 effect
函数。
4 updateMemo
function updateMemo(
nextCreate,
deps,
) {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps; // 新的 deps 值
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps = prevState[1]; // 之前保存的 deps 值
if (areHookInputsEqual(nextDeps, prevDeps)) { //判断两次 deps 值
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
在组件更新过程中,我们执行useMemo
函数,做的事情实际很简单,就是判断两次 deps
是否相等,如果不想等,证明依赖项发生改变,那么执行 useMemo
的第一个函数,得到新的值,然后重新赋值给hook.memoizedState
,如果相等 证明没有依赖项改变,那么直接获取缓存的值。
不过这里有一点,值得注意,nextCreate()
执行,如果里面引用了usestate
等信息,变量会被引用,无法被垃圾回收机制回收,就是闭包原理,那么访问的属性有可能不是最新的值,所以需要把引用的值,添加到依赖项 dep
数组中。每一次dep
改变,重新执行,就不会出现问题了。
温馨小提示:有很多同学说 useMemo
怎么用,到底什么场景用,用了会不会起到反作用,通过对源码原理解析,我可以明确的说,基本上可以放心使用,说白了就是可以定制化缓存,存值取值而已。
5 updateRef
function updateRef(initialValue){
const hook = updateWorkInProgressHook()
return hook.memoizedState
}
函数组件更新useRef做的事情更简单,就是返回了缓存下来的值,也就是无论函数组件怎么执行,执行多少次,hook.memoizedState
内存中都指向了一个对象,所以解释了useEffect
,useMemo
中,为什么useRef
不需要依赖注入,就能访问到最新的改变值。
一次点击事件更新
五 总结
上面我们从函数组件初始化,到函数组件更新渲染,两个维度分解讲解了react-hooks
原理,掌握了react-hooks
原理和内部运行机制,有助于我们在工作中,更好的使用react-hooks
。
最后, 送人玫瑰,手留余香,觉得有收获的朋友可以给笔者点赞,关注一波 ,陆续更新前端超硬核文章。
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-) 欢迎加我微信「Augenstern9728」一起交流学习... 关注公众号「前端时光屋」,持续为你推送精选好文。