React Hooks 的原理

React 是实现了组件的前端框架,它支持 class 和 function 两种形式的组件。

class 组件是通过继承模版类(Component、PureComponent)的方式开发新组件的,继承是 class 本身的特性,它支持设置 state,会在 state 改变后重新渲染,可以重写一些父类的方法,这些方法会在 React 组件渲染的不同阶段调用,叫做生命周期函数。

function 组件不能做继承,因为 function 本来就没这个特性,所以是提供了一些 api 供函数使用,这些 api 会在内部的一个数据结构上挂载一些函数和值,并执行相应的逻辑,通过这种方式实现了 state 和类似 class 组件的生命周期函数的功能,这种 api 就叫做 hooks。

hooks 挂载数据的数据结构叫做 fiber。

那什么是 fiber 呢?

我们知道,React 是通过 jsx 来描述界面结构的,会把 jsx 编译成 render function,然后执行 render function 产生 vdom:

304a73e41b12f01d9a75030c2667e6f9.png

在 v16 之前的 React 里,是直接递归遍历 vdom,通过 dom api 增删改 dom 的方式来渲染的。但当 vdom 过大,频繁调用 dom api 会比较耗时,而且递归又不能打断,所以有性能问题。

12c214f7aac67d4ce41b32b1942356bd.png

后来就引入了 fiber 架构,先把 vdom 树转成 fiber 链表,然后再渲染 fiber。

6f9740678f502d8d948867219f0241a8.png

vdom 转 fiber 的过程叫做 reconcile,是可打断的,React 加入了 schedule 的机制在空闲时调度 reconcile,reconcile 的过程中会做 diff,打上增删改的标记(effectTag),并把对应的 dom 创建好。然后就可以一次性把 fiber 渲染到 dom,也就是 commit。

这个 schdule、reconcile、commit 的流程就是 fiber 架构。当然,对应的这个数据结构也叫 fiber。

(更多 fiber 的介绍可以看我之前的一篇文章:手写简易版 React 来彻底搞懂 fiber 架构

hooks 就是通过把数据挂载到组件对应的 fiber 节点上来实现的。

fiber 节点是一个对象,hooks 把数据挂载在哪个属性呢?

我们可以 debugger 看下。

准备这样一个函数组件(代码没啥具体含义,就是为了调试 hooks):

function App() {
  const [name, setName] = useState("guang");
  useState('dong');

  const handler = useCallback((evt) => {
      setName('dong');
  },[1]);

  useEffect(() => {
    console.log(1);
  });
  
  useRef(1);

  useMemo(() => {
    return 'guang and dong';
  })

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p onClick={handler}>
          {name}
        </p>
      </header>
    </div>
  );
}

在函数打个断点,运行到这个组件就会断住。

我们看下调用栈:

9d0dcf289ac9fce6c04aec8ad3165e96.png

上一个函数是 renderWithHooks,里面有个 workingInProgress 的对象就是当前的 fiber 节点:

5c203a322d0d8124505e7e5d0c104134.png

fiber 节点的 memorizedState 就是保存 hooks 数据的地方。

它是一个通过 next 串联的链表,展开看一下:

c7909de8c1f15d931619900bc4bc147b.png

链表一共六个元素,这和我们在 function 组件写的 hooks 不就对上了么:

a6fa950d0e411b911dfd4cff381ddd32.png

这就是 hooks 存取数据的地方,执行的时候各自在自己的那个 memorizedState 上存取数据,完成各种逻辑,这就是 hooks 的原理。

这个 memorizedState 链表是什么时候创建的呢?

好问题,确实有个链表创建的过程,也就是 mountXxx。链表只需要创建一次,后面只需要 update。

所以第一次调用 useState 会执行 mountState,后面再调用 useState 会执行 updateState。

546f607a9acfee268824c7e85d1de5bb.png 64ba9eeb4e37f0179e4f8be885ccd93c.png

我们先集中精力把 mount 搞明白。

mountXxx 是创建 memorizedState 链表的过程,每个 hooks api 都是这样的:

e025c2b9cc5d59bb3d6bf89299a38374.png 1ac1067d08f513eb56d476a5dfc13028.png af0ee54925a3103161b58864af513ad3.png

它的实现也很容易想到,就是创建对应的 memorizedState 对象,然后用 next 串联起来,也就是这段代码:

31b4a688bd85747cc8a0700410e74d0b.png

当然,创建这样的数据结构还是为了使用的,每种 hooks api 都有不同的使用这些 memorizedState 数据的逻辑,有的比较简单,比如 useRef、useCallback、useMemo,有的没那么简单,比如 useState、useEffect。

为什么这么说呢?我们看下它们的实现再说吧。

先看这几个简单的:

useRef

每个 useXxx 的 hooks 都有 mountXxx 和 updateXxx 两个阶段,比如 ref 就是 mountRef 和 updateRef。

它的代码是最简单的,只有这么几行:

6ae8c2517ee8271c1dc02a9b2fb877d7.png

mountWorkInProgressHook 刚才我们看过,就是创建并返回 memorizedState 链表的,同理,下面那个 updateWorkInProgressHook 是更新的。

这些不用管,只要知道修改的是对应的 memorizedState 链表中的元素就行了。

那 ref 在 memorizedState 上挂了什么呢?

可以看到是把传进来的 value 包装了一个有 current 属性的对象,冻结了一下,然后放在 memorizedState 属性上。

后面 update 的时候,没有做任何处理,直接返回这个对象。

所以,useRef 的功能就很容易猜到了:useRef 可以保存一个数据的引用,这个引用不可变。

这个 hooks 是最简单的 hooks 了,给我们一个地方存数据,我们也能轻易的实现 useRef 这个 hooks。

再来看个稍难点的:

useCallback

useCallback 在 memorizedState 上放了一个数组,第一个元素是传入的回调函数,第二个是传入的 deps(对 deps 做了下 undefined 的处理)。

10c9e96cb07cc09695bd363cd73d93b8.png

更新的时候把之前的那个 memorizedState 取出来,和新传入的 deps 做下对比,如果没变,那就返回之前的回调函数,也就是 prevState[0]。

如果变了,那就创建一个新的数组,第一个元素是传入的回调函数,第二个是传入的 deps。

所以,useCallback 的功能也就呼之欲出了:useCallback 可以实现函数的缓存,如果 deps 没变就不会创建新的,否则才会返回新传入的函数。

这段逻辑其实也不难,就是多了个判断逻辑。

再来看个和它差不多的:

useMemo

useMemo 也在 memorizedState 上放了个数组,第一个元素是传入函数的执行结果,第二个元素是 deps(对 deps 为 undefined 的情况做了下处理)。

2a2457c8f6dc009ab65cc3c7077c5fb2.png

更新的时候也是取出之前的 memorizedState,和新传入的 deps 做下对比,如果没变,就返回之前的值,也就是 prevState[0]。

如果变了,创建一个新的数组放在 memorizedState,第一个元素是新传入函数的执行结果,第二个元素是 deps。

所以,useMemo 的功能大家也能猜出来:useMemo 可以实现函数执行结果的缓存,如果 deps 没变,就直接拿之前的,否则才会执行函数拿到最新结果返回。

实现逻辑和 useCallback 大同小异。

这三个 hooks 难么?给大家一个对象来存储数据,大家都能写出来,并不难。

因为它们是没有别的依赖的,只是单纯的缓存了下值而已。而像 useState、useEffect 这些就复杂一些了,主要是因为需要调度。

useState

state 改了之后是要触发更新的调度的,React 有自己的调度逻辑,就是我们前面提到的 fiber 的 schedule,所以需要 dispatch 一个 action。

(不展开讲,简单看一下)

1b4dff857b8d3c895c99836391bac0a0.png

这里详细讲要涉及到调度,就先不展开了。

useEffect

同样的,effect 传入的函数也是被 React 所调度的,当然,这里的调度不是 fiber 那个调度,而是单独的 effect 调度:

(不展开讲,简单看一下)

1239463ad6b7045261059d8380edddd4.png

hooks 负责把这些 effect 串联成一个 updateQueue 的链表,然后让 React 去调度执行。

4ffd9d33ca6307ea844027bd6372db7b.png

所以说,useState、useEffect 这种 hooks 的实现是和 fiber 的空闲调度,effect 的调度结合比较紧密的,实现上更复杂了一些。

这里没有展开讲,因为这篇文章的目的是把 hooks 的主要原理理清楚,不会太深入细节。

大家可能还听过自定义 hooks 的概念,那个是啥呢?

其实就是个函数调用,没啥神奇的,我们可以把上面的 hooks 放到 xxx 函数里,然后在 function 组件里调用,对应的 hook 链表是一样的。

ba692f78697ae6a2ba17b8f285cf4062.png

只不过一般我们会使用 React 提供的 eslint 插件,lint 了这些函数必须以 use 开头,但其实不用也没事,它们和普通的函数封装没有任何区别。

总结

React 支持 class 和 function 两种形式的组件,class 支持 state 属性和生命周期方法,而 function 组件也通过 hooks api 实现了类似的功能。

fiber 架构是 React 在 16 以后引入的,之前是 jsx -> render function -> vdom 然后直接递归渲染 vdom,现在则是多了一步 vdom 转 fiber 的 reconcile,在 reconcile 的过程中创建 dom 和做 diff 并打上增删改的 effectTag,然后一次性 commit。这个 reconcile 是可被打断的,可以调度,也就是 fiber 的 schedule。

hooks 的实现就是基于 fiber 的,会在 fiber 节点上放一个链表,每个节点的 memorizedState 属性上存放了对应的数据,然后不同的 hooks api 使用对应的数据来完成不同的功能。

链表自然有个创建阶段,也就是 mountXxx,之后就不需要再 mount 了,只需要 update。所以每个 useXx 的实现其实都是分为了 mountXxx 和 updateXxx 两部分的。

我们看了几个简单的 hooks:useRef、useCallback、useMemo,它们只是对值做了缓存,逻辑比较纯粹,没有依赖 React 的调度。而 useState 会触发 fiber 的 schedule,useEffect 也有自己的调度逻辑。实现上相对复杂一些,我们没有继续深入。

其实给我们一个对象来存取数据,实现 useRef、useCallback、useMemo 等 hooks 还是很简单的。对于需要调度的,则复杂一些。

对于自定义的 hooks,那个就是个函数调用,没有任何区别。(lint 的规则不想遵守可以忽略)

所有 hooks api 都是基于 fiber 节点上的 memorizedState 链表来存取数据并完成各自的逻辑的。

所以,hooks 的原理简单么?只能说有的简单,有的不简单。

  • 7
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
React Hooks 是 React 16.8 版本引入的一种新特性,它是为了使函数组件能够拥有状态和其他 React 特性而设计的。 React Hooks 的原理基于两个核心概念:闭包和钩子函数。 1. 闭包:在函数组件内部,可以通过闭包的方式引用外部作用域的变量。React Hooks 利用了闭包的特性,使得可以在函数组件内部存储和更新状态。 2. 钩子函数:React Hooks 提供了一系列的钩子函数,如 useState、useEffect、useContext 等。这些钩子函数是预定义的特殊函数,可以在函数组件中使用,通过调用这些钩子函数,可以获取和操作组件的状态、副作用和上下文等。 当一个组件使用了 React Hooks,React 在底层会创建一个与该组件实例相关联的 Fiber 对象,并在组件渲染时执行组件函数。在执行组件函数时,React 跟踪每个组件函数内部所有的钩子函数调用,并将其与该组件实例相关联。 当组件函数执行时,如果遇到 useState 钩子调用,React 会查找该钩子函数对应的状态值,并将其返回给组件函数。组件通过 useState 返回的状态值可以读取和更新组件的状态。 当组件函数执行完毕后,React 会将该组件的状态和副作用存储在 Fiber 对象中,并将 Fiber 对象添加到更新队列中。之后,React 会根据更新队列中的 Fiber 对象,对组件进行批量更新,实现页面的重新渲染。 通过这种方式,React Hooks 实现了函数组件的状态管理和副作用处理,使得开发者可以更方便地编写和维护 React 组件。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值