Hooks的面世让我们的Function Component逐步拥有了对标Class Component的特性,比如私有状态,生命周期函数等。useState与useReducer这两个Hooks让我们可以在 Function Component里使用到私有状态。而useState其实就是阉割版的useReducer,这也是我那它们两个放在一起讲的原因。应用一下官方的例子:
React是内部是怎么区分这两个状态的呢? Function Component 不像 Class Component那样可以将私有状态挂载到类实例中并通过对应的key来指向对应的状态,而且每次的页面的刷新或者说组件的重新渲染都会使得 Function 重新执行一遍。所以React中必定有一种机制来区分这些Hooks。
另一个问题就是React是如何在每次重新渲染之后都能返回最新的状态? Class Component因为自身的特点可以将私有状态持久化的挂载到类实例上,每时每刻保存的都是最新的值。而 Function Component 由于本质就是一个函数,并且每次渲染都会重新执行。所以React必定拥有某种机制去记住每一次的更新操作,并最终得出最新的值返回。当然我们还会有其他的一些问题,比如这些状态究竟存放在哪?为什么只能在函数顶层使用Hooks而不能在条件语句等里面使用Hooks?
前言
从React Hooks发布以来,整个社区都以积极的态度去拥抱它、学习它。期间也涌现了很多关于React Hooks 源码解析的文章。本文(基于v16.8.6)就以笔者自己的角度来写一篇属于自己的文章吧。希望可以深入浅出、图文并茂的帮助大家对React Hooks的实现原理进行学习与理解。本文将以文字、代码、图画的形式来呈现内容。主要对常用Hooks中的 useState、useReducer、useEffect 进行学习,尽可能的揭开Hooks的面纱。
使用Hooks时的疑惑
Hooks的面世让我们的Function Component逐步拥有了对标Class Component的特性,比如私有状态,生命周期函数等。useState与useReducer这两个Hooks让我们可以在 Function Component里使用到私有状态。而useState其实就是阉割版的useReducer,这也是我那它们两个放在一起讲的原因。应用一下官方的例子:
通过 useState 我们可以初始化一个私有状态,它会返回这个状态的最新值和一个用来更新状态的方法。而useReducer则是针对更复杂的状态管理场景:
同样也是返回当前最新的状态,并返回一个用来更新数据的方法。在使用这两个方法的时候也许我们会想过这样的问题:
React是内部是怎么区分这两个状态的呢? Function Component 不像 Class Component那样可以将私有状态挂载到类实例中并通过对应的key来指向对应的状态,而且每次的页面的刷新或者说组件的重新渲染都会使得 Function 重新执行一遍。所以React中必定有一种机制来区分这些Hooks。
另一个问题就是React是如何在每次重新渲染之后都能返回最新的状态? Class Component因为自身的特点可以将私有状态持久化的挂载到类实例上,每时每刻保存的都是最新的值。而 Function Component 由于本质就是一个函数,并且每次渲染都会重新执行。所以React必定拥有某种机制去记住每一次的更新操作,并最终得出最新的值返回。当然我们还会有其他的一些问题,比如这些状态究竟存放在哪?为什么只能在函数顶层使用Hooks而不能在条件语句等里面使用Hooks?
答案尽在源码之中
我们先来了解useState以及useReducer的源码实现,并从中解答我们在使用Hooks时的种种疑惑。首先我们从源头开始:
在项目中我们通常会以这种方式来引入useState方法,被我们引入的这个useState方法是什么样子的呢?其实这个方法就在源码 packages/react/src/ReactHook.js 中。
从源码中可以看到,我们调用的其实是 ReactCurrentDispatcher.js 中的dispatcher.useState(),那么我们继续前往ReactCurrentDispatcher.js文件:
好吧,它继续将我们带向 react-reconciler/src/ReactFiberHooks.js这个文件。那么我们继续前往这个文件。
兜兜转转我们终于清楚了React Hooks 的源码就放 react-reconciler/src/ReactFiberHooks.js 目录下面。 在这里如上图所示我们可以看到有每个Hooks的类型定义。同时我们也可以看到Hooks的具体实现,大家可以多看看这个文件。首先我们注意到,我们所有的Hooks都有两个定义:
从这里可以看出,我们的Hooks在Mount阶段和Update阶段的逻辑是不一样的。在Mount阶段和Update阶段他们是两个不同的定义。我们先来看Mount阶段的逻辑。在看之前我们先思考一些问题。React Hooks需要在Mount阶段做什么呢?就拿我们的useState和useReducer来说:
我们一下React的实现,先来看mountState的实现。
区分管理Hooks
关于第一件事,初始化状态并返回状态和更新状态的方法。这个没有问题,源码也很清晰利用initialState来初始化状态,并且返回了状态和对应更新方法 return [hook.memoizedState, dispatch]。那么我们来看看React是如何区分不同的Hooks的,这里我们可以从 mountState 里的 mountWorkInProgressHook方法和Hook的类型定义中找到答案。
首先从Hook的类型定义中就可以看到,React 对Hooks的定义是链表。也就是说我们组件里使用到的Hooks是通过链表来联系的,上一个Hooks的next指向下一个Hooks。这些Hooks节点是怎么利用链表数据结构串联在一起的呢?相关逻辑就在每个具体mount 阶段 Hooks函数调用的 mountWorkInProgressHook方法里:
在mount阶段,每当我们调用Hooks方法,比如useState,mountState就会调用mountWorkInProgressHook 来创建一个Hook节点,并把它添加到Hooks链表上。比如我们的这个例子:
那么在mount阶段,就会生产如下图这样的单链表:![hookLink1](https://raw.githubusercontent.com/HolyZheng/holyZheng-blog/master/images/hooklink3.jpg)
返回最新的值
而关于第三件事,useState和useReducer都是使用了一个queue链表来存放每一次的更新。以便后面的update阶段可以返回最新的状态。每次我们调用dispatchAction方法的时候,就会形成一个新的updata对象,添加到queue链表上,而且这个是一个循环链表。可以看一下 dispatchAction 方法的实现:
也就是我们每次执行dispatchAction方法,比如setAge或setName。就会创建一个保存着此次更新信息的update对象,添加到更新链表queue上。然后每个Hooks节点就会有自己的一个queque。比如假设我们执行了下面几个语句:
那么我们的Hooks链表就会变成这样:
在Hooks节点上面,会如上图那样,通过链表来存放所有的历史更新操作。以便在update阶段可以通过这些更新获取到最新的值返回给我们。这就是在第一次调用useState或useReducer之后,每次更新都能返回最新值的原因。再来看看mountReducer,你会发现和mountState几乎一摸一样,只是状态的初始化逻辑有那么一点区别。毕竟useState其实就是阉割版的useReducer。这里就不详细介绍mountReducer了。
然后我们来看看update阶段,也就是看一下我们的useState或useReducer是如何利用现有的信息,去给我们返回最新的最正确的值的。先来看一下useState在update阶段的代码也就是updateState:
可以看到,updateState底层调用的其实就会死updateReducer,因为我们调用useState的时候,并不会传入reducer,所以这里会默认传递一个basicStateReducer进去。我们先看看这个basicStateReducer:
在使用useState(action)的时候,action通常会是一个值,而不是一个方法。所以baseStateReducer要做的其实就是将这个action返回。来继续看一下updateReducer的逻辑:
在update阶段,也就是我们组件第二次第三次。。执行到useState或useReducer的时候,会遍历update对象循环链表,执行每一次更新去计算出最新的状态来返回,以保证我们每次刷新组件都能拿到当前最新的状态。useState的reducer是baseStateReducer,因为传入的update.action为值,所以会直接返回update.action,而useReducer 的reducer是用户定义的reducer,所以会根据传入的action和每次循环得到的newState逐步计算出最新的状态。![updateReducer1](https://raw.githubusercontent.com/HolyZheng/holyZheng-blog/master/images/updateReducer1.jpg)
useState/useReducer 小总结
React 如何管理区分Hooks?
useState和useReducer如何在每次渲染时,返回最新的值?
为什么不能在条件语句等中使用Hooks?
Hooks链表放在哪?
好的,现在我们已经了解了React 通过链表来管理 Hooks,同时也是通过一个循环链表来存放每一次的更新操作,得以在每次组件更新的时候可以计算出最新的状态返回给我们。那么我们这个Hooks链表又存放在那里呢?理所当然的我们需要将它存放到一个跟当前组件相对于的地方。那么很明显这个与组件一一对应的地方就是我们的FiberNode。![hooksInFiberNode](https://raw.githubusercontent.com/HolyZheng/holyZheng-blog/master/images/hooksInFiberNode.png)
如图所示,组件构建的Hooks链表会挂载到FiberNode节点的memoizedState上面去。![hooksInFiberNode2](https://raw.githubusercontent.com/HolyZheng/holyZheng-blog/master/images/hooksInFiberNode2.jpg)
useEffect
看到这,相信你已经对Hooks的源码实现模式已经有一定的了解了,所以你尝试去看一下Effect的实现你会一下子就看懂。首先我们先回忆一下useEffect是怎么样工作的?
PersionInfo组件第一次渲染的时候会在控制台输出age和name,在后面组件的每次update中,如果useEffect中的deps依赖的值发生了变化的话,也会在控制台中输出对应的状态,同时在unmount的时候就会执行清除函数(如果有)。React中是怎么实现的呢?其实很简单,在FiberNode中通过一个updateQueue来存放所有需要被执行的effect,然后在每次渲染之后依次执行所有的effect。useEffect 也分为mountEffect和updateEffect
mountEffect
可以看到在mount阶段,useEffect做的事情就是将自己的effect添加到了componentUpdateQueue上。这个componentUpdateQueue会在renderWithHooks方法中赋值到fiberNode的updateQueue上。
也就是在mount阶段我们所有的effect都以链表的形式被挂载到了fiberNode上。然后在组件渲染完毕之后,React就会执行updateQueue中的所有方法。![useEffectInFiberNode1](https://raw.githubusercontent.com/HolyZheng/holyZheng-blog/master/images/useEffectInFiberNode1.png)
updateEffect
update阶段和mount阶段类似,只不过这次会考虑effect 的依赖deps,如果此次更新effect的依赖没有变化的话,就会被打上NoHookEffect标签,最后会在commit阶段跳过该effect的执行。
useEffect 小总结
![https://raw.githubusercontent.com/HolyZheng/holyZheng-blog/master/images/useEffectAll1.jpg) useEffect做了什么?