React Hooks源代码浅析
前段时间研究了一波React的渲染流程,内部机制的源代码,阅读到一半的时候React终于推出了16.8.x的版本,主要带来的更新就是Hooks的新功能。相信已经有很多的使用教程或者源代码阅读文章。那么我也来一个属于自己的阅读有感的文章,做一个记录吧。
其实React文件中也有说明了Hooks的提出主要是为了解决什么问题的。
就以前React为了将一个元件的逻辑抽离复用,不和渲染程式码混用在一个class的做法,比较推介的是用高阶元件,将状态逻辑抽离出来,通过不同的样式元件传入带有状态逻辑的高阶元件中,增强样式元件的功能,从而达到复用逻辑的功能。再早期就会使用mixin去实现。
在使用class元件的时候,我们少不了在生命周期函式中新增一些操作,例如呼叫一些函式,或者去发起请求,在销毁元件的时候为了防止内存溢位,我们可能还需要对一些事件移除。那么这个时候我们就需要在componentDidMount,componentDidUpdate中可能会呼叫相同的函式获取资料,componentWillUnmount中移除事件等。
以前我们使用纯函式元件时都有一个标准,就是这个元件并不具备自身的生命周期使用,以及自己独立的state。只是单纯的返回一个元件。或者是根据传入的props组装元件。但随着Hook的释出,React团队是想将React更加偏向函数语言程式设计的方式编写元件,让本来存函式元件变得可以使用class元件的一些特性。
接下来我们按照文件的教学,一边看文件一边看源代码,了解Hook背后的原理,以及加入一些自己的思考。
下面是一个官方的DEMO:
useState其实是等价于setState的,只是useState需要在函式元件中使用而已。
像程式码中的useState,传入的引数就是值,返回一个数组,第一个为key,第二个为修改该key的值的函式。换句话说,每一个state的宣告和赋值都需要通过呼叫useState函式来完成。而设定程式码中的count变数的值,都必须使用useState返回的setCount。
然后在DEMO中的button点选事件中,我们呼叫了setCount对count+1。
到这里其实就是一个基本的流程。
现在我们来看看源代码了。
首先在render阶段时候,因为首次渲染,会对ReactElement进行解析,成为一个FIber树。在workLoop函式中,我们看到纯函式的Fiber节点是长这个样子的。
如果ReactElement是一个function的情况下,那么Fiber节点的tag标识是为2,在beginWork函式中,会进入IndeterminateComponent函式中对function型别的Fiber节点例项化。
在断点的过程中,发现有一个名为renderWithHooks函式。在函式中我发现了几点:
将function的内容执行完后得出ReactElement物件。
基本上这就是function型别的Fiber的render阶段会做的事情。那么在DEMO中我们有用到useState,那么具体从源代码上是如何呼叫的呢?
首先react-dom中有一个全域性变数ReactCurrentDispatcher$1。在render阶段的最开始,会将ReactCurrentDispatcher$1赋值为一个全域性变数ContextOnlyDispatcher。
而这个时候useState之类的Hook如果呼叫会出现如下报错:
而在renderWithHooks函式中,在呼叫function型别的Fiber节点,将其例项化前,会有一段程式码。
以下是HooksDispatcherOnMountInDEV程式码。这才是真正的Hook程式码。
在我们DEMO的程式码中会呼叫react提供的useState函式。我们看看useState的程式码是怎样的。注意上面提到的HooksDispatcherOnMountInDEV变数中的useState是在react-dom中的程式码,并非react的程式码,但是在DEMO中我们呼叫的是react提供的useState。
在react档案中提供的useState的程式码是这样的。
最终resolveDispatcher函式返回的ReactCurrentDispatcher.current。ReactCurrentDispatcher和ReactCurrentDispatcher$1是互相引用的关系。所以最终是呼叫react-dom的useState。好了说回重点,useState是如何工作的。
其实真正执行useState工作的是mountState函式,而这个函式主要做了几件事。
mountWorkInProgressHook函式内容
其实看到这里就很清晰的知道useState的工作流程是怎样的了。以我们现在看到内容做个简单的总结。
在demo中我们为button元素绑定了一个onClick事件,经过React的合成事件最终触发了之前说到的dispatchAction函式。在useState的时候返回的setCount函式中,会提前系结好了当前的Fiber节点,以及一个queue的引数。
最终会将新传入的state的值储存在queue中的last物件上。
而当前的Fiber节点中的memoizedState物件中的queue是对queue的一个引用。所以现在Fiber节点中已经带有新的state的记录,之后会将Fiber节点传入到scheduleWork函式中。
在执行renderWithHooks函式中时,发现当前的函式元件并非第一次渲染,所以会将使用HooksDispatcherOnUpdateInDEV这个全域性物件执行setState,而并非像第一次那样使用HooksDispatcherOnMountInDEV全域性物件进行渲染。
useState最终呼叫的是updateReducer,而并非mountState函式。
从程式码可以发现当我们在事件中多次触发setCount函式,其实也只会触发一次render,因为在之前的queue(伫列)物件中,会保持一个关系,如果伫列中存在last物件,那么将会将新的state存到last物件的next物件中,以此类推。所以就形成了一个多次state的呼叫链。
然后触发updateReducer的时候有这么一段程式码:
就是会使用最后一次set的值。这个和class的setState原理是一样。
最后会将新的值赋值到Fiber节点中,并返回新的值,值得注意的是,事件将会复用之前的闭包生成的事件。
之后就是正常的渲染流程了,和class元件并没有什么区别了。
在官网中说道useEffect是componentDidMount,componentDidUpdate和componentWillUnmount这三个函式的组合。
在一次渲染的时候会执行useEffect函式,而这个函式最终生成一个effect物件,effect中的create就是保持useEffect传入的回拨函式。最终会暂时储存在componentUpdateQueue全域性物件中,然后对函式元件例项化成ReactElement之后,将会把componentUpdateQueue赋值到函式元件的updateQueue物件中。
如果在一个函式元件中有多个useEffect函式,那么将会是以下的样子。
最后effect物件将会储存在Fiber节点的updateQueue物件中。
在进入commit阶段,将Fiber节点都渲染到页面上后,就会开始执行Fiber节点中的updateQueue中所储存的函数了。
接着在每一次渲染完元件后,如果是class元件将会呼叫class元件本来的生命周期函式,但是如果是函式元件,那么将会找到updateQueue中的注册函式逐个呼叫。
最终会呼叫commitHookEffectList函式,去触发注册的生命周期函式。执行的方式就是执行完每一个注册事件后,会查询是否存在next,如果存在就继续呼叫,所有注册函式都执行完毕。
在官网的demo中有说道一个例子:
这个元件需要在解除安装的时候移除某一些事件系结,那么官网中的说明是在执行useEffect传入的函式中return一个函式,return的函式在元件解除安装的时执行,那么通过程式码很容易就知道它内部是如何实现的。
什么时候执行呢?在元件解除安装的时候还是呼叫commitHookEffectList函式。
会将之前return的函式在元件解除安装的时候进入这个判断中,然后执行它。
在官网中有一个例子,在class元件中,我们非常常用的一个操作,就是在生命周期中需要做一些判断,当满足条件才会执行一些操作。
但是在我们使用useEffect的时候每一次渲染都会触发,如果我们的函式元件中,存在某些操作需要满足特定条件才会在useEffect中触发,那么如何去做呢?
官网告诉了我们,useEffect可以接受第二个引数,第二个引数其实就是代表当传入的引数和当前的同名引数不相等时,执行useEffect。
那么程式码中是如何实现的呢?
首先在例项我们的函式元件的时候,传入的第二个引数会储存在deps中。
在非第一次的渲染中,执行useEffect,最终是执行updateEffectImpl函式,而这个函式中就会对传入的第二个引数中(阵列)的每一项的值和当前存在的每一项值进行对比,如果不相等,则返回false,那么会赋值给全域性中的sideEffectTag,在之后的renderWithHooks函式中,会将sideEffectTag赋值到函式元件中的effectTag中。
最后因为对Fiber节点赋值了effectTag,所以在渲染后会触发commitHookEffectList函式,具体程式码的呼叫栈非常长,就不一一细说。
下面是我总结了一下整个函式元件的渲染过程,以及上面说到了useState和useEffect的执行过程。
总结: