Hooks黑盒模拟

本文用于通过对React Hooks使用过程中的行为进行总结和模拟来模仿Hooks,以探究其实现原理和原因。

本文编写仓促,同时由于本人精力、知识有限,因此如有理解错误或知识错误请指出,我将感激不尽

首先我们从React的设计思想函数式编程出发。函数式编程中将特定输入情况下仅且仅能输出某可预期数据的函数被称为纯函数(PureFunction),而PureFunction有一个好处,由于其内部仅进行单纯的数据运算不调用外部API等(会在函数执行过程中引入外部数据(Arbitrary)修改整个系统内部状态)影响系统状态的因素,因此其输出是可以预期的同时也是运算速度较快的(某种程度上)。

从某种程度上来说,整个UI的渲染等变化实际上可以看成一种状态转移,用户通过点击等操作将UI状态由一个状态转移到另一个状态。而React函数组件构成的组件树若不使用hook,则整个组件树的渲染实际上是稳定的。例如从浏览器角度来看,React组件树实际都有一个Leaf节点,而这些叶子节点实际也是单纯的原生DOM元素,将整个React组件树展开之后就是一个单纯的DOM树。

这里我们引入SideEffect副作用的定义,当一段程序单独看其某一次执行过程时,由于其与外部代码(外部作用域)进行交互导致外部数据、状态发生变动,我们称该程序具有SideEffect。因为在不完全掌握上下文的情况下,其执行结果对于用户来说是难以预期的。而具有SideEffect的函数即为纯函数(PureFunciton)的相对面(Impure Function)。

但对于一个UI软件系统来说,用户点击、请求数据等都是外部与程序进行的交互,因此一个UI系统如果内部不存在SideEffect,则其失去了大部分的交互性。

Hooks相当于为React纯函数组件提供了一个合法使用SideEffect的方法,在添加Hooks后,如果将函数组件看做一个黑盒,则其输出(子组件树)在不考虑上下文(程序状态)的情况下无法预期。

我们首先从最为常用的useState开始进行模拟。

useState

首先我们考虑最简单的useState,从程序员使用的视角看,其执行过程分为First Time和Rest Time。

当useState第一次执行时,该hook中保存的状态进行了初始化,而在第二次开始则返回该hook内部保存的状态,而不是其初始值,因此我们可以很容易的得出,useState第一次执行和后续执行内部所执行的逻辑是不一样的,因此需要对该函数执行次数进行区分。

同时这里的第一次执行是指该组件被挂载时的第一次执行,因此在一个React程序生命周期内,一个组件内的useState将在该组件所有实例挂载在组件树上时进行初始化,同时每一个实例保存自身内部数据,即组件状态。

首先简单的把Type写一下:

然后用个简单函数进行下填充:

然后把useState用React接口封装起来:

写一个简单的Counter组件来进行测试:(这里模拟进行两次组件渲染过程中对state的保持)

执行结果:

这里由于我们没有在内部保存状态,因此before和after set都是0。

现在我们要将数据保存起来,而最简单的办法就是在useState作用域建立一个用于存放该state值的变量,而后setState就可以将其更改:

组件代码:

但如果我们将状态值存储在useState函数作用域中,则当程序从组件Counter中退出时,该setCounter在useState作用域中构成的闭包将被销毁,因此我们需要将state值存储到外部某种结构中。这里我们暂时将数据直接存储到React对象上。

同时由于我们仅需要在第一次进入Counter组件(即挂载该组件),因此第二次不需要再次对state进行初始化,很容易可以想到这里肯定是通过某种标志位判定了组件的状态,即挂载和刷新。我们可以考虑以下几种方法达到这种效果:

  • 在useState函数内部进行判断,即判断代码写死到useState内
  • 在进入组件前进行判断,直接将useState替换为同函数签名的不同函数(即根据不同阶段编写同useState函数签名的不同逻辑的函数,函数指针)
  • 在进入组件前进行判断,将函数逻辑封装为另一全局变量,而后在useState内部使用该全局变量

下面我先使用第二种方法构建我们的useState feature:

构建不同阶段的React实例

这里我们采用替换函数执行体的办法来完成这种feature:

上述代码中,mountState对进行挂载的组件进行state初始化,而useSate则对已经挂载的组件所对应的状态进行读取。

可以看到上面首先定义了两种React实例,第一种为Mount阶段的MountReact,其内部使用mountState作为useState类型实例。第二种为UseReact,该实例用于在挂载之后进行状态数据获取与更改。因此在下方执行过程中,首先将MountReact挂载到用户实际使用的React实例上,而后执行Counter组件,而后将React实例上的挂载项更改为UseReact,同时将state传递到UseReact上,这样就可以完成一个简易useState feature。

而对于用户来说,只需要使用useState钩子就就可以完成保持状态这一看似魔法的操作。

一个组件内使用多个Hooks

我们上方代码这种实现可以完成一个简单的Counter组件,但如果我想在该组件中存储另外的状态时就需要在React实例上添加多个值,下面是一种简单的重构:

首先添加一个State类型,该类型通过index标明该组件中useState所处的顺序,而每次退出组件函数后该index将会被归零。

下面是mountState和useState两函数进行的修改,首先在函数最开始获取了当前state的Index,用于对state中对应的值进行存取:

可以看到代码中仅多了关于ticker状态的代码,同时删除了state的传递:

下面我们来看一下React.state中的数据:

到这里我们就完成了对useState的模拟。

注意

从上面的定义我们可以看到useStateSetter实际是通过闭包中index实现对自身state的访问,因此我们可以将该setter传递到任意子组件中,这种特性称为Stable,我理解为可以出自身组件的Scope。

useEffect

useEffect相对于useState来说就没有多大区别了,唯一需要注意的是DependencyList的检查和定义。

首先写一下Type:

由于useEffect相对于state来说需要保存的数据有所不同,useState只需要保存前一次的状态,而useEffect则需要保存前一个依赖项及副作用清理函数,因此对该hook的state接口定义为:

而useEffect内部逻辑代码也比较简单,mountEffect挂载state,同时执行一次effectFn,然后useEffect则比较上一次deps,若发生变化则调用fn:

两个阶段的React实例如下:

由于我们还没有定义unmount阶段的React实例,因此useEffect的state内的eraser在此处暂时没有使用。

下面是组件代码:

可以看到定义了三个钩子,从上往下分别为每次rerender都执行,仅执行一次,当ticker发生变化时执行。因此输出为:

到这里我们的useEffect feature就完成了。

useEffect执行问题

我们上面实现的useEffect实际上还有问题,因为在React中,useEffect应该是在render阶段结束后,对当前软件状态进行变更的Hook,因此实际上组件执行应该首先将useState等执行,而后将计算(render)好的数据送入useEffect中,如果useEffect中对组件状态数据进行了修改,应该首先将该Effect送入SideEffect待处理队列,则将会触发下一次render,直到SideEffect待处理队列为空。

下面我们来首先这个feature。

首先这里就不能直接将组件裸露到全局作用域了,需要一个调度函数判断待处理SideEffect队列是否为空。

==WorkInProcess==

useRef

useRef是相对简单的一个钩子。首先还是定义useRef的type,然后useRef内部就比较简单了。

然后是组件内使用:


v0.3wep 写了部分 2022.08.08

v0.4p 所有图片居中 2022.09.23

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注