react hook 原理

前言:

本篇文章为作者拜读外星人大佬文章所感受,旨在让更多的小白也能看懂理解,特来写文章分享感受,如有不对请多多指正(原文链接「react进阶」一文吃透react-hooks原理之前的两篇文章,分别介绍了react-hooks如何使用,以及自定义 - 掘金)

一、react fiber渲染机制

在了解reacthook之前,我们要先了解一下react fiber更新机制:

React fiber是react 16的渲染机制,他将react的组件树采用链表的方式进行切割,变为更细的fiber树,各个fiber之间支持任务的中断、恢复和优先级调度,防止浏览器渲染的时候阻塞一些更高优先级的点击输入等事件。

React fiber分为render和commit两个状态,rander状态用来计算需要更新的fiber树,计算diff差异,可以被中断,commit提交时来操作实际dom,不可被中断。

二、普通函数和组件函数

React 中的函数大致可以分为普通函数和组件函数。
普通函数就是普通的 JavaScript 函数,一般用于封装计算逻辑或工具方法,调用方式和 JS 函数一样,例如 sum(a, b)。
组件函数本质上也是 JavaScript 函数,但它会被 React 当成组件来渲染。函数组件通常首字母大写,返回 JSX 结构,使用时通过 这种标签形式。React 在第一次渲染函数组件时,会通过 renderWithHooks 执行组件函数,因此函数组件内部可以在顶层调用 useState、useEffect 等 Hook。

三、react Hook初始化流程

(1)renderWithHooks初始化环境

renderWithHooks会在组件函数被渲染的时候启用,它会初始化这个组件函数的如下参数:

1.current Fiber:当前组件内容树,在提交阶段会转化真正的dom树。初始为空

  1. workInProgress Fiber:本次要渲染的组件内容树。初始为当前组件初始组件内容树

  2. Component:组件本身

4.props:传进来的参数值

  1. context/secondArg: 传给组件的第二个参数,普通组件基本不用管

  2. renderLanes:组件组件渲染优先级

然后根据这个组件函数是否是第一次渲染,赋予ReactCurrentDispatcher.current不同的hooks。ReactCurrentDispatcher.current决定了hook的执行规则(也就是 useState、useEffect 等 Hook 最终会执行哪一套具体实现。)

对于第一次渲染组件,那么用的是HooksDispatcherOnMount hooks对象。 对于渲染后,需要更新的函数组件,则是HooksDispatcherOnUpdate对象,那么两个不同就是通过current树上是否memoizedState(hook信息)来判断的。如果current不存在,证明是第一次渲染函数组件。

(2)组件函数执行

调用Component(props, secondArg);执行我们的函数组件,我们的函数组件在这里真正的被执行了,此时,我们写的hooks也被依次执行。执行之后Components把hooks信息依次保存到workInProgress树上。最终函数返回jsx内容

(3)renderWithHooks清扫环境

ReactCurrentDispatcher.current置为ContextOnlyDispatcher ,此时渲染阶段结束,大部分hook函数再执行就会报错,防止回调函数等。最后再置空一些变量比如currentHook等等。

四、mountWorkInProgressHook挂载阶段创建工作树

初次渲染函数组件时启用,每次执行hook函数的时候都会创建一个hook对象,hook对象之间用链表串连在一起。最终mountWIPHook将链表返回,并将其挂载到WIP的memoizedState上。

Hook内的信息如下:

1.memoizedState:当前最新状态
2.baseState:当前优先级重播的起始位置,二者的值都由newState赋予
3.baseQueue:上一次被跳过的更新队列
4.queue:本次的更新队列
5.next:下一个hook地址

这么说大家也可能有点乱,我特意给大家梳理了一张图来便于理解

接下来我们来看看不同的hook函数在挂载阶段都有什么表现吧

(1)useState 状态如何变化

const [num,setNum] = useState(0)

useState在初始化的时候为mountState。首先mountState将初始化的state传给,经过mountWIPSHook初始化的hook里面的memoizedState和baseState,然后创建queue队列取保存更新信息。

useState的更新方法为dispatchAction,也就是我们的setNumber。dispatchAction的第一个和第二个参数已经被bind绑定为currentlyRenderingFiber和 queue。当调用setState的时候,dispatchAction会产生一个update对象来记录本次的修改信息,并把他放到hook的queue中。接下来判断react是否在渲染中,若在渲染中则标记,当前渲染结束后react会重新计算;不在渲染中则提前算出值并进行浅比较,结果相同则不更新,不同则进行更新。注意render是react来操作的,dispatchAction只是检测当前状态和通知react更新渲染。

(2)useEffect 变化之后,我们能做什么

useEffect(()=>{},[])

useEffect在初始化的时候也是mountEffect,接收create函数和deps依赖。mountEffect先用mountWIPHooks()的dispatch创建effect自己的hook对象,将create函数放到hook的memoizedState里面,并将该hook对象挂载到fiber树的memoizedState链表里面。接下来,mountEffect调用pushEffect方法,将useEffect创建一个effect对象,将其挂载到fiber树的updateQueue中。Effect对象中包含
1.tag 是否要重新执行create函数
2.create生成函数,是useEffect的第一个参数--回调函数
3.destroy销毁函数,也就是return里面的
4.deps依赖
5.next:下一个effect对象的位置

所以总体流程如下:

Mount阶段:函数组件先按照组件顺序初始化hook函数。 当外部触发更新事件的时候,react将useState等的hook变化存到queue里面

Render阶段:函数组件先按照顺序运行hook函数,若queue里面有更新变化则更新useState的值。当useState等hook函数执行完成,进入到更新effect阶段(因为effect的依赖项要在effect之前已经声明好)effect根据传入的deps和原来的deps进行浅比较,若变化则打上tag变化。

Commit 阶段:根据WIP fiber和current fiber进行对比,进行dom更新,更新结束后,将WIP fiber赋值给 current fiber

passive effect阶段:react扫描fiber里的updateQueue链表,对里面的tag进行判断进行重新执行

(3)useMemo 缓存计算值

useMemo在初始化采用mountMemo,mountMemo先初始化hooks,然后将函数计算的结果和依赖项以数组的形式存在hook的memoizedState中

(4)useRef 保存变量

useRef 在初始化执行mountRef,mountRef先初始化hook,然后将初始化的值存成一个对象{current:initalValue}(这样才能保证每次渲染拿到的值都有稳定的地址)。最后将这个对象存到hook的memoizedState中。

先写到这里(真的是太难啦,写作不易,多多指正)