「react进阶」一文吃透react-hooks原理

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 一文通关 React Hooks

一 前言

之前的两篇文章,分别介绍了react-hooks如何使用,以及自定义hooks设计模式及其实战,本篇文章主要从react-hooks起源,原理,源码角度,开始剖析react-hooks运行机制和内部原理,相信这篇文章过后,对于面试的时候那些hooks问题,也就迎刃而解了。实际react-hooks也并没有那么难以理解,听起来很cool,实际就是函数组件解决没有state,生命周期,逻辑不能复用的一种技术方案。

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

老规矩,🤔️🤔️🤔️我们带着疑问开始今天的探讨(能回答上几个,自己可以尝试一下,掌握程度):

  • 1 在无状态组件每一次函数上下文执行的时候,react用什么方式记录了hooks的状态?
  • 2 多个react-hooks用什么来记录每一个hooks的顺序的 ? 换个问法!为什么不能条件语句中,声明hooks? hooks声明为什么在组件的最顶部?
  • 3 function函数组件中的useState,和 class类组件 setState有什么区别?
  • 4 react 是怎么捕获到hooks的执行上下文,是在函数组件内部的?
  • 5 useEffect,useMemo 中,为什么useRef不需要依赖注入,就能访问到最新的改变值?
  • 6 useMemo是怎么对值做缓存的?如何应用它优化性能?
  • 7 为什么两次传入useState的值相同,函数组件不更新?
  • ...

大纲.jpg

如果你认真读完这篇文章,这些问题全会迎刃而解。

function组件和class组件本质的区别

在解释react-hooks原理的之前,我们要加深理解一下, 函数组件和类组件到底有什么区别,废话不多说,我们先看
两个代码片段。

class Index extends React.Component<any,any>{
   
   
    constructor(props){
   
   
        super(props)
        this.state={
   
   
            number:0
        }
    }
    handerClick=()=>{
   
   
       for(let i = 0 ;i<5;i++){
   
   
           setTimeout(()=>{
   
   
               this.setState({
   
    number:this.state.number+1 })
               console.log(this.state.number)
           },1000)
       }
    }

    render(){
   
   
        return <div>
            <button onClick={
   
    this.handerClick } >num++</button>
        </div>
    }
}

打印结果?

再来看看函数组件中:

function Index(){
   
   
    const [ num ,setNumber ] = React.useState(0)
    const handerClick=()=>{
   
   
        for(let i=0; i<5;i++ ){
   
   
           setTimeout(() => {
   
   
                setNumber(num+1)
                console.log(num)
           }, 1000)
        }
    }
    return <button onClick={
   
    handerClick } >{
   
    num }</button>
}

打印结果?

------------公布答案-------------

在第一个例子🌰打印结果:
1 2 3 4 5

在第二个例子🌰打印结果:
0 0 0 0 0

这个问题实际很蒙人,我们来一起分析一下,第一个类组件中,由于执行上setState没有在react正常的函数执行上下文上执行,而是setTimeout中执行的,批量更新条件被破坏。原理这里我就不讲了,所以可以直接获取到变化后的state

但是在无状态组件中,似乎没有生效。原因很简单,在class状态中,通过一个实例化的class,去维护组件中的各种状态;但是在function组件中,没有一个状态去保存这些信息,每一次函数上下文执行,所有变量,常量都重新声明,执行完毕,再被垃圾机制回收。所以如上,无论setTimeout执行多少次,都是在当前函数上下文执行,此时num = 0不会变,之后setNumber执行,函数组件重新执行之后,num才变化。

所以, 对于class组件,我们只需要实例化一次,实例中保存了组件的state等状态。对于每一次更新只需要调用render方法就可以。但是在function组件中,每一次更新都是一次新的函数执行,为了保存一些状态,执行一些副作用钩子,react-hooks应运而生,去帮助记录组件的状态,处理一些额外的副作用。

一 初识:揭开hooks的面纱

1 当我们引入hooks时候发生了什么?

我们从引入 hooks开始,以useState为例子,当我们从项目中这么写:

import {
   
    useState } from 'react'

于是乎我们去找useState,看看它到底是哪路神仙?

react/src/ReactHooks.js

useState

export function useState(initialState){
   
   
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

useState() 的执行等于 dispatcher.useState(initialState)
这里面引入了一个dispatcher,我们看一下resolveDispatcher做了些什么?

resolveDispatcher

function resolveDispatcher() {
   
   
  const dispatcher = ReactCurrentDispatcher.current
  return dispatcher
}

ReactCurrentDispatcher

react/src/ReactCurrentDispatcher.js

const ReactCurrentDispatcher = {
   
   
  current: null,
};

我们看到ReactCurrentDispatcher.current初始化的时候为null,然后就没任何下文了。我们暂且只能把ReactCurrentDispatcher记下来。看看ReactCurrentDispatcher什么时候用到的 ?

2 开工造物,从无状态组件的函数执行说起

想要彻底弄明白hooks,就要从其根源开始,上述我们在引入hooks的时候,最后以一个ReactCurrentDispatcher草草收尾,线索全部断了,所以接下来我们只能从函数组件执行开始。

renderWithHooks 执行函数

对于function组件是什么时候执行的呢?

react-reconciler/src/ReactFiberBeginWork.js

function组件初始化:

renderWithHooks(
    null,                // current Fiber
    workInProgress,      // workInProgress Fiber
    Component,           // 函数组件本身
    props,               // props
    context,             // 上下文
    renderExpirationTime,// 渲染 ExpirationTime
);

对于初始化是没有current树的,之后完成一次组件更新后,会把当前workInProgress树赋值给current树。

function组件更新:

renderWithHooks(
    current,
    workInProgress,
    render,
    nextProps,
    context,
    renderExpirationTime,
);

我们从上边可以看出来,renderWithHooks函数作用是调用function组件函数的主要函数。我们重点看看renderWithHooks做了些什么?

renderWithHooks
react-reconciler/src/ReactFiberHooks.js

export function renderWithHooks(
  current,
  workInProgress,
  Component,
  props,
  secondArg,
  nextRenderExpirationTime,
) {
   
   
  renderExpirationTime = nextRenderExpirationTime;
  currentlyRenderingFiber = workInProgress;

  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.expirationTime = NoWork;

  ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;

  let children = Component(props, secondArg);

  if (workInProgress.expirationTime === renderExpirationTime) {
   
    
       // ....这里的逻辑我们先放一放
  }

  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  renderExpirationTime = NoWork;
  currentlyRenderingFiber = null;

  currentHook = null
  workInProgressHook = null;

  didScheduleRenderPhaseUpdate = false;

  return children;
}

所有的函数组件执行,都是在这里方法中,首先我们应该明白几个感念,这对于后续我们理解useState是很有帮助的。

current fiber树: 当完成一次渲染之后,会产生一个current树,current会在commit阶段替换成真实的Dom树。

workInProgress fiber树: 即将调和渲染的 fiber 树。再一次新的组件更新过程中,会从current复制一份作为workInProgress,更新完毕后,将当前的workInProgress树赋值给current树。

workInProgress.memoizedState: 在class组件中,memoizedState存放state信息,在function组件中,这里可以提前透漏一下,memoizedState在一次调和渲染过程中,以链表的形式存放hooks信息。

workInProgress.expirationTime: react用不同的expirationTime,来确定更新的优先级。

currentHook : 可以理解 current树上的指向的当前调度的 hooks节点。

workInProgressHook : 可以理解 workInProgress树上指向的当前调度的 hooks节点。

renderWithHooks函数主要作用:

首先先置空即将调和渲染的workInProgress树的memoizedStateupdateQueue,为什么这么做,因为在接下来的函数组件执行过程中,要把新的hooks信息挂载到这两个属性上,然后在组件commit阶段,将workInProgress树替换成current树,替换真实的DOM元素节点。并在current树保存hooks信息。

然后根据当前函数组件是否是第一次渲染,赋予ReactCurrentDispatcher.current不同的hooks,终于和上面讲到的ReactCurrentDispatcher联系到一起。对于第一次渲染组件,那么用的是HooksDispatcherOnMount hooks对象。
对于渲染后,需要更新的函数组件,则是HooksDispatcherOnUpdate对象,那么两个不同就是通过current树上是否memoizedState(hook信息)来判断的。如果current不存在,证明是第一次渲染函数组件。

接下来,调用Component(props, secondArg);执行我们的函数组件,我们的函数组件在这里真正的被执行了,然后,我们写的hooks被依次执行,把hooks信息依次保存到workInProgress树上。 至于它是怎么保存的,我们马上会讲到。

接下来,也很重要,将ContextOnlyDispatcher赋值给ReactCurrentDispatcher.current,由于js是单线程的,也就是说我们没有在函数组件中,调用的hooks,都是ContextOnlyDispatcher对象上hooks,我们看看ContextOnlyDispatcherhooks,到底是什么。

const ContextOnlyDispatcher = {
   
   
    useState:throwInvalidHookError
}
function throwInvalidHookError() {
   
   
  invariant(
    false,
    'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
      ' one of the following reasons:\n' +
      '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
      '2. You might be breaking the Rules of Hooks\n' +
      '3. You might have more than one copy of React in the same app\n' +
      'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
  );
}

原来如此,react-hooks就是通过这种函数组件执行赋值不同的hooks对象方式,判断在hooks执行是否在函数组件内部,捕获并抛出异常的。

最后,重新置空一些变量比如currentHookcurrentlyRenderingFiber,workInProgressHook等。

3 不同的hooks对象

上述讲到在函数第一次渲染组件和更新组件分别调用不同的hooks对象,我们现在就来看看HooksDispatcherOnMountHooksDispatcherOnUpdate

第一次渲染(我这里只展示了常用的hooks):

const HooksDispatcherOnMount = {
   
   
  useCallback: mountCallback,
  useEffect: mountEffect,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
};

更新组件:

const HooksDispatcherOnUpdate = {
   
   
  useCallback: updateCallback,
  useEffect: updateEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState
};

看来对于第一次渲染组件,和更新组件,react-hooks采用了两套Api,本文的第二部分和第三部分,将重点两者的联系。

我们用流程图来描述整个过程:

17AC0A26-745A-4FD8-B91B-7CADB717234C.jpg

三 hooks初始化,我们写的hooks会变成什么样子

本文将重点围绕四个中重点hooks展开,分别是负责组件更新的useState,负责执行副作用useEffect ,负责保存数据的useRef,负责缓存优化的useMemo, 至于useCallback,useReducer,useLayoutEffect原理和那四个重点hooks比较相近,就不一一解释了。

我们先写一个组件,并且用到上述四个主要hooks

请记住如下代码片段,后面讲解将以如下代码段展开

import React , {
   
    useEffect , useState , useRef , useMemo  } from 'react'
function Index(){
   
   
    const [ number , setNumber ] = useState(0)
    const DivDemo = useMemo(() => <div> hello , i am useMemo </div>,[])
    const curRef  = useRef(null)
    useEffect(()=>{
   
   
       console.log(curRef.current)
    },[])
    return <div ref={
   
    curRef } >
        hello,world {
   
    number } 
        {
   
    DivDemo }
        <button onClick={
   
   () => setNumber(number+1) } >number++</button>
     </div>
}

接下来我们一起研究一下我们上述写的四个hooks最终会变成什么?

1 mountWorkInProgressHook

在组件初始化的时候,每一次hooks执行,如useState(),useRef(),都会调用mountWorkInProgressHook,mountWorkInProgressHook到底做了写什么,让我们一起来分析一下:

react-reconciler/src/ReactFiberHooks.js -> mountWorkInProgressHook

function mountWorkInProgressHook() {
   
   
  const hook: Hook = {
   
   
    memoizedState: null,  // useState中 保存 state信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和deps | useRef中保存的是ref 对象
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };
  if (workInProgressHook === null) {
   
    // 例子中的第一个`hooks`-> useState(0) 走的就是这样。
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
   
   
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

mountWorkInProgressHook这个函数做的事情很简单,首先每次执行一个hooks函数,都产生一个hook对象,里面保存了当前hook信息,然后将每个hooks以链表形式串联起来,并赋值给workInProgressmemoizedState。也就证实了上述所说的,函数组件用memoizedState存放hooks链表。

至于hook对象中都保留了那些信息?我这里先分别介绍一下
:

memoizedStateuseState中 保存 state 信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和 depsuseRef 中保存的是 ref 对象。

baseQueue : usestateuseReducer中 保存最新的更新队列。

baseStateusestateuseReducer中,一次更新中 ,产生的最新state值。

queue : 保存待更新队列 pendingQueue ,更新函数 dispatch 等信息。

next: 指向下一个 hooks对象。

那么当我们函数组件执行之后,四个hooksworkInProgress将是如图的关系。

shunxu.jpg

知道每个hooks关系之后,我们应该理解了,为什么不能条件语句中,声明hooks

我们用一幅图表示如果在条件语句中声明会出现什么情况发生。

如果我们将上述demo其中的一个 useRef 放入条件语句中,

 let curRef  = null
 if(isFisrt){
   
   
  curRef = useRef(null)
 }

hoo11.jpg

因为一旦在条件语句中声明hooks,在下一次函数组件更新,hooks链表结构,将会被破坏,current树的memoizedState缓存hooks信息,和当前workInProgress不一致,如果涉及到读取state等操作,就会发生异常。

上述介绍了 hooks通过什么来证明唯一性的,答案 ,通过hooks链表顺序。和为什么不能在条件语句中,声明hooks,接下来我们按照四个方向,分别介绍初始化的时候发生了什么?

2 初始化useState -> mountState

mountState

function mountState(
  initialState
){
   
   
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
   
   
    // 如果 useState 第一个参数为函数,执行函数得到state
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
   
   
    pending: null,  // 带更新的
    dispatch: null, // 负责更新函数
    lastRenderedReducer: basicStateReducer, //用于得到最新的 state ,
    lastRenderedState: initialState, // 最后一次得到的 state
  });

  const dispatch = (queue.dispatch = (dispatchAction.bind( // 负责更新的函数
    null,
    currentlyRenderingFiber,
    queue,
  )))
  return [hook.memoizedState, dispatch];
}

mountState到底做了些什么,首先会得到初始化的state,将它赋值给mountWorkInProgressHook产生的hook对象的
memoizedStatebaseState属性,然后创建一个queue对象,里面保存了负责更新的信息。

这里先说一下,在无状态组件中,useStateuseReducer触发函数更新的方法都是dispatchAction,useState,可以看成一个简化版的useReducer,至于dispatchAction怎么更新state,更新组件的,我们接着往下研究dispatchAction

在研究之前 我们先要弄明白dispatchAction是什么?

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
)
const [ number , setNumber ] = useState(0)

dispatchAction 就是 setNumber , dispatchAction 第一个参数和第二个参数,已经被bind给改成currentlyRenderingFiberqueue,我们传入的参数是第三个参数action

dispatchAction 无状态组件更新机制

作为更新的主要函数,我们一下来研究一下,我把 dispatchAction 精简,精简,再精简,

function dispatchAction(fiber, queue, action) {
   
   

  // 计算 expirationTime 过程略过。
  /* 创建一个update */
  const update= {
   
   
    expirationTime,
    suspenseConfig,
    action,
    eagerReducer: null,
    eagerState: null,
    next: null,
  }
  /* 把创建的update */
  const pending = queue.pending;
  if (pending === null) {
   
     // 证明第一次更新
    update.next = update;
  } else {
   
    // 不是第一次更新
    update.next = pending.next;
    pending.next = update;
  }

  queue.pending = update;
  const alternate = fiber.alternate;
  /* 判断当前是否在渲染阶段 */
  if ( fiber === currentlyRenderingFiber || (alternate !== null && alternate === currentlyRenderingFiber)) {
   
   
    didScheduleRenderPhaseUpdate = true;
    update.expirationTime = renderExpirationTime;
    currentlyRenderingFiber.expirationTime = renderExpirationTime;
  } else {
   
    /* 当前函数组件对应fiber没有处于调和渲染阶段 ,那么获取最新state , 执行更新 */
    if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) {
   
   
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
   
   
        let prevDispatcher;
        try {
   
   
          const currentState = queue.lastRenderedState; /* 上一次的state */
          const eagerState = lastRenderedReducer(currentState, action); /**/
          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
   
    
            return
          }
        } 
      }
    }
    scheduleUpdateOnFiber(fiber, expirationTime);
  }
}

无论是类组件调用setState,还是函数组件的dispatchAction ,都会产生一个 update对象,里面记录了此次更新的信息,然后将此update放入待更新的pending队列中,dispatchAction第二步就是判断当前函数组件的fiber对象是否处于渲染阶段,如果处于渲染阶段,那么不需要我们在更新当前函数组件,只需要更新一下当前updateexpirationTime即可。

如果当前fiber没有处于更新阶段。那么通过调用lastRenderedReducer获取最新的state,和上一次的currentState,进行浅比较,如果相等,那么就退出,这就证实了为什么useState,两次值相等的时候,组件不渲染的原因了,这个机制和Component模式下的setState有一定的区别。

如果两次state不相等,那么调用scheduleUpdateOnFiber调度渲染当前fiberscheduleUpdateOnFiberreact渲染更新的主要函数。

我们把初始化mountState无状态组件更新机制讲明白了,接下来看一下其他的hooks初始化做了些什么操作?

3 初始化useEffect -> mountEffect

上述讲到了无状态组件中fiber对象memoizedState保存当前的hooks形成的链表。那么updateQueue保存了什么信息呢,我们会在接下来探索useEffect过程中找到答案。
当我们调用useEffect的时候,在组件第一次渲染的时候会调用mountEffect方法,这个方法到底做了些什么?

mountEffect

function mountEffect(
  create,
  deps,
) {
   
   
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookEffectTag, 
    create, // useEffect 第一次参数,就是副作用函数
    undefined,
    nextDeps, // useEffect 第二次参数,deps
  );
}

每个hooks初始化都会创建一个hook对象,然后将hook的memoizedState保存当前effect hook信息。

有两个memoizedState大家千万别混淆了,我这里再友情提示一遍

  • workInProgress / current 树上的 memoizedState 保存的是当前函数组件每个hooks形成的链表。

  • 每个hooks上的memoizedState 保存了当前hooks信息,不同种类的hooksmemoizedState内容不同。上述的方法最后执行了一个pushEffect,我们一起看看pushEffect做了些什么?

pushEffect 创建effect对象,挂载updateQueue

function pushEffect(tag, create, destroy, deps) {
   
   
  const effect = {
   
   
    tag,
    create,
    destroy,
    deps,
    next: null,
  };
  let componentUpdateQueue = currentlyRenderingFiber.updateQueue
  if (componentUpdateQueue === null) {
   
    // 如果是第一个 useEffect
    componentUpdateQueue = {
   
     lastEffect: null  }
    currentlyRenderingFiber.updateQueue = componentUpdateQueue
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
   
     // 存在多个effect
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
   
   
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
   
   
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}

这一段实际很简单,首先创建一个 effect ,判断组件如果第一次渲染,那么创建 componentUpdateQueue ,就是workInProgressupdateQueue。然后将effect放入updateQueue中。

假设我们在一个函数组件中这么写:

useEffect(()=>{
   
   
    console.log(1)
},[ props.a ])
useEffect(()=>{
   
   
    console.log(2)
},[])
useEffect(()=>{
   
   
    console.log(3)
},[])

最后workInProgress.updateQueue会以这样的形式保存:

7B8889E7-05B3-4BC4-870A-0D4C1CDF6981.jpg

拓展:effectList

effect list 可以理解为是一个存储 effectTag 副作用列表容器。它是由 fiber 节点和指针 nextEffect 构成的单链表结构,这其中还包括第一个节点 firstEffect ,和最后一个节点 lastEffect
React 采用深度优先搜索算法,在 render 阶段遍历 fiber 树时,把每一个有副作用的 fiber 筛选出来,最后构建生成一个只带副作用的 effect list 链表。
commit 阶段,React 拿到 effect list 数据后,通过遍历 effect list,并根据每一个 effect 节点的 effectTag 类型,执行每个effect,从而对相应的 DOM 树执行更改。

4 初始化useMemo -> mountMemo

不知道大家是否把 useMemo 想象的过于复杂了,实际相比其他 useState , useEffect等,它的逻辑实际简单的很。

function mountMemo(nextCreate,deps){
   
   
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

初始化useMemo,就是创建一个hook,然后执行useMemo的第一个参数,得到需要缓存的值,然后将值和deps记录下来,赋值给当前hookmemoizedState。整体上并没有复杂的逻辑。

5 初始化useRef -> mountRef

对于useRef初始化处理,似乎更是简单,我们一起来看一下:

function mountRef(initialValue) {
   
   
  const hook = mountWorkInProgressHook();
  const ref = {
   
   current: initialValue};
  hook.memoizedState = ref;
  return ref;
}

mountRef初始化很简单, 创建一个ref对象, 对象的current 属性来保存初始化的值,最后用memoizedState保存ref,完成整个操作。

6 mounted 阶段 hooks 总结

我们来总结一下初始化阶段,react-hooks做的事情,在一个函数组件第一次渲染执行上下文过程中,每个react-hooks执行,都会产生一个hook对象,并形成链表结构,绑定在workInProgressmemoizedState属性上,然后react-hooks上的状态,绑定在当前hooks对象的memoizedState属性上。对于effect副作用钩子,会绑定在workInProgress.updateQueue上,等到commit阶段,dom树构建完成,在执行每个 effect 副作用钩子。

四 hooks更新阶段

上述介绍了第一次渲染函数组件,react-hooks初始化都做些什么,接下来,我们分析一下,

对于更新阶段,说明上一次 workInProgress 树已经赋值给了 current 树。存放hooks信息的memoizedState,此时已经存在current树上,react对于hooks的处理逻辑和fiber树逻辑类似。

对于一次函数组件更新,当再次执行hooks函数的时候,比如 useState(0) ,首先要从currenthooks中找到与当前workInProgressHook,对应的currentHooks,然后复制一份currentHooksworkInProgressHook,接下来hooks函数执行的时候,把最新的状态更新到workInProgressHook,保证hooks状态不丢失。

所以函数组件每次更新,每一次react-hooks函数执行,都需要有一个函数去做上面的操作,这个函数就是updateWorkInProgressHook,我们接下来一起看这个updateWorkInProgressHook

1 updateWorkInProgressHook

function updateWorkInProgressHook() {
   
   
  let nextCurrentHook;
  if (currentHook === null) {
   
     /* 如果 currentHook = null 证明它是第一个hooks */
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
   
   
      nextCurrentHook = current.memoizedState;
    } else {
   
   
      nextCurrentHook = null;
    }
  } else {
   
    /* 不是第一个hooks,那么指向下一个 hooks */
    nextCurrentHook = currentHook.next;
  }
  let nextWorkInProgressHook
  if (workInProgressHook === null) {
   
     //第一次执行hooks
    // 这里应该注意一下,当函数组件更新也是调用 renderWithHooks ,memoizedState属性是置空的
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
   
    
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
   
    
      /* 这个情况说明 renderWithHooks 执行 过程发生多次函数组件的执行 ,我们暂时先不考虑 */
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
   
   
    invariant(
      nextCurrentHook !== null,
      'Rendered more hooks than during the previous render.',
    );
    currentHook = nextCurrentHook;
    const newHook = {
   
    //创建一个新的hook
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null,
    };
    if (workInProgressHook === null) {
   
    // 如果是第一个hooks
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
   
    // 重新更新 hook
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

这一段的逻辑大致是这样的:

  • 首先如果是第一次执行hooks函数,那么从current树上取出memoizedState ,也就是旧的hooks
  • 然后声明变量nextWorkInProgressHook,这里应该值得注意,正常情况下,一次renderWithHooks执行,workInProgress上的memoizedState会被置空,hooks函数顺序执行,nextWorkInProgressHook应该一直为null,那么什么情况下nextWorkInProgressHook不为null,也就是当一次renderWithHooks执行过程中,执行了多次函数组件,也就是在renderWithHooks中这段逻辑。
  if (workInProgress.expirationTime === renderExpirationTime) {
   
    
       // ....这里的逻辑我们先放一放
  }

这里面的逻辑,实际就是判定,如果当前函数组件执行后,当前函数组件的还是处于渲染优先级,说明函数组件又有了新的更新任务,那么循坏执行函数组件。这就造成了上述的,nextWorkInProgressHook不为 null 的情况。

  • 最后复制currenthooks,把它赋值给workInProgressHook,用于更新新的一轮hooks状态。

接下来我们看一下四个种类的hooks,在一次组件更新中,分别做了那些操作。

2 updateState

useState

function updateReducer(
  reducer,
  initialArg,
  init,
){
   
   
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  const current = currentHook;
  let baseQueue = current.baseQueue;
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {
   
   
     // 这里省略... 第一步:将 pending  queue 合并到 basequeue
  }
  if (baseQueue !== null) {
   
   
    const first = baseQueue.next;
    let newState = current.baseState;
    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;
    do {
   
   
      const updateExpirationTime = update.expirationTime;
      if (updateExpirationTime < renderExpirationTime) {
   
    //优先级不足
        const clone  = {
   
   
          expirationTime: update.expirationTime,
          ...
        };
        if (newBaseQueueLast === null) {
   
   
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
   
   
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
      } else {
   
     //此更新确实具有足够的优先级。
        if (newBaseQueueLast !== null) {
   
   
          const clone= {
   
   
            expirationTime: Sync, 
             ...
          };
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
        /* 得到新的 state */
        newState = reducer(newState, action);
      }
      update = update.next;
    } while (update !== null && update !== first);
    if (newBaseQueueLast === null) {
   
   
      newBaseState = newState;
    } else {
   
   
      newBaseQueueLast.next = newBaseQueueFirst;
    }
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
    queue.lastRenderedState = newState;
  }
  const dispatch = queue.dispatch
  return [hook.memoizedState, dispatch];
}

这一段看起来很复杂,让我们慢慢吃透,首先将上一次更新的pending queue 合并到 basequeue,为什么要这么做,比如我们再一次点击事件中这么写,

function Index(){
   
   
   const [ number ,setNumber ] = useState(0)
   const handerClick = ()=>{
   
   
    //    setNumber(1)
    //    setNumber(2)
    //    setNumber(3)
       setNumber(state=>state+1)
       // 获取上次 state = 1 
       setNumber(state=>state+1)
       // 获取上次 state = 2
       setNumber(state=>state+1)
   }
   console.log(number) // 3 
   return <div>
       <div>{
   
    number }</div>
       <button onClick={
   
    ()=> handerClick() } >点击</button>
   </div>
}

点击按钮, 打印 3

三次setNumber产生的update会暂且放入pending queue,在下一次函数组件执行时候,三次 update被合并到 baseQueue。结构如下图:

setState.jpg

接下来会把当前useState或是useReduer对应的hooks上的baseStatebaseQueue更新到最新的状态。会循环baseQueueupdate,复制一份update,更新
expirationTime,对于有足够优先级的update(上述三个setNumber产生的update都具有足够的优先级),我们要获取最新的state状态。,会一次执行useState上的每一个action。得到最新的state

更新state

sset1.jpg

这里有会有两个疑问🤔️:

  • 问题一:这里不是执行最后一个action不就可以了嘛?
    答案: 原因很简单,上面说了 useState逻辑和useReducer差不多。如果第一个参数是一个函数,会引用上一次 update产生的 state, 所以需要循环调用,每一个updatereducer,如果setNumber(2)是这种情况,那么只用更新值,如果是setNumber(state=>state+1),那么传入上一次的 state 得到最新state

  • 问题二:什么情况下会有优先级不足的情况(updateExpirationTime < renderExpirationTime)?

答案: 这种情况,一般会发生在,当我们调用setNumber时候,调用scheduleUpdateOnFiber渲染当前组件时,又产生了一次新的更新,所以把最终执行reducer更新state任务交给下一次更新。

3 updateEffect

function updateEffect(create, deps): void {
   
   
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;
  if (currentHook !== null) {
   
   
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
   
   
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
   
   
        pushEffect(hookEffectTag, create, destroy, nextDeps);
        return;
      }
    }
  }
  currentlyRenderingFiber.effectTag |= fiberEffectTag
  hook.memoizedState = pushEffect(
    HookHasEffect | hookEffectTag,
    create,
    destroy,
    nextDeps,
  );
}

useEffect 做的事很简单,判断两次deps 相等,如果相等说明此次更新不需要执行,则直接调用 pushEffect,这里注意 effect的标签,hookEffectTag,如果不相等,那么更新 effect ,并且赋值给hook.memoizedState,这里标签是 HookHasEffect | hookEffectTag,然后在commit阶段,react会通过标签来判断,是否执行当前的 effect 函数。

4 updateMemo

function updateMemo(
  nextCreate,
  deps,
) {
   
   
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps; // 新的 deps 值
  const prevState = hook.memoizedState; 
  if (prevState !== null) {
   
   
    if (nextDeps !== null) {
   
   
      const prevDeps = prevState[1]; // 之前保存的 deps 值
      if (areHookInputsEqual(nextDeps, prevDeps)) {
   
    //判断两次 deps 值
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

在组件更新过程中,我们执行useMemo函数,做的事情实际很简单,就是判断两次 deps是否相等,如果不想等,证明依赖项发生改变,那么执行 useMemo的第一个函数,得到新的值,然后重新赋值给hook.memoizedState,如果相等 证明没有依赖项改变,那么直接获取缓存的值。

不过这里有一点,值得注意,nextCreate()执行,如果里面引用了usestate等信息,变量会被引用,无法被垃圾回收机制回收,就是闭包原理,那么访问的属性有可能不是最新的值,所以需要把引用的值,添加到依赖项 dep 数组中。每一次dep改变,重新执行,就不会出现问题了。

温馨小提示: 有很多同学说 useMemo怎么用,到底什么场景用,用了会不会起到反作用,通过对源码原理解析,我可以明确的说,基本上可以放心使用,说白了就是可以定制化缓存,存值取值而已。

5 updateRef

function updateRef(initialValue){
   
   
  const hook = updateWorkInProgressHook()
  return hook.memoizedState
}

函数组件更新useRef做的事情更简单,就是返回了缓存下来的值,也就是无论函数组件怎么执行,执行多少次,hook.memoizedState内存中都指向了一个对象,所以解释了useEffect,useMemo 中,为什么useRef不需要依赖注入,就能访问到最新的改变值。

一次点击事件更新

91A72028-3A38-4491-9375-0895F420B7CD.jpg

五 总结

上面我们从函数组件初始化,到函数组件更新渲染,两个维度分解讲解了react-hooks原理,掌握了react-hooks原理和内部运行机制,有助于我们在工作中,更好的使用react-hooks

最后, 送人玫瑰,手留余香,觉得有收获的朋友可以给笔者点赞,关注一波 ,陆续更新前端超硬核文章。

react好文汇总

react-hooks三部曲另外两部

react进阶系列

react源码系列

开源项目系列

参考文档

相关文章
|
7月前
|
算法 前端开发 JavaScript
React的diff算法原理
React的diff算法原理
133 0
|
7月前
|
JSON 缓存 前端开发
【React】React原理面试题集锦
本文集合一些React的原理面试题,方便读者以后面试查漏补缺。作者给出自认为可以让面试官满意的简易答案,如果想要了解更深刻,可以点击链接查看对应的详细博文。在此对链接中的博文作者非常感谢🙏。
147 0
|
2月前
|
存储 前端开发 测试技术
React Hooks 的工作原理
【10月更文挑战第1天】
|
4月前
|
前端开发 算法 JavaScript
React原理之Diff算法
【8月更文挑战第24天】
|
4月前
|
前端开发 JavaScript 算法
如何学习react原理
【8月更文挑战第9天】 如何学习react原理
48 6
|
4月前
|
开发者 安全 UED
JSF事件监听器:解锁动态界面的秘密武器,你真的知道如何驾驭它吗?
【8月更文挑战第31天】在构建动态用户界面时,事件监听器是实现组件间通信和响应用户操作的关键机制。JavaServer Faces (JSF) 提供了完整的事件模型,通过自定义事件监听器扩展组件行为。本文详细介绍如何在 JSF 应用中创建和使用事件监听器,提升应用的交互性和响应能力。
41 0
|
4月前
|
前端开发 Java UED
瞬间变身高手!JSF 与 Ajax 强强联手,打造极致用户体验的富客户端应用,让你的应用焕然一新!
【8月更文挑战第31天】JavaServer Faces (JSF) 是 Java EE 标准的一部分,常用于构建企业级 Web 应用。传统 JSF 应用采用全页面刷新方式,可能影响用户体验。通过集成 Ajax 技术,可以显著提升应用的响应速度和交互性。本文详细介绍如何在 JSF 应用中使用 Ajax 构建富客户端应用,并通过具体示例展示 Ajax 在 JSF 中的应用。首先,确保安装 JDK 和支持 Java EE 的应用服务器(如 Apache Tomcat 或 WildFly)。
47 0
|
4月前
|
缓存 JavaScript 前端开发
【React生态进阶】React与Redux完美结合:从原理到实践全面解析构建大规模应用的最佳策略与技巧分享!
【8月更文挑战第31天】React 与 Redux 的结合解决了复杂状态管理的问题,提升了应用性能。本文详细介绍了在 React 应用中引入 Redux 的原因、步骤及最佳实践,包括安装配置、状态管理、性能优化等多方面内容,并提供了代码示例,帮助你构建高性能、易维护的大规模应用。
72 0
|
4月前
|
前端开发 JavaScript 中间件
【前端状态管理之道】React Context与Redux大对决:从原理到实践全面解析状态管理框架的选择与比较,帮你找到最适合的解决方案!
【8月更文挑战第31天】本文通过电子商务网站的具体案例,详细比较了React Context与Redux两种状态管理方案的优缺点。React Context作为轻量级API,适合小规模应用和少量状态共享,实现简单快捷。Redux则适用于大型复杂应用,具备严格的状态管理规则和丰富的社区支持,但配置较为繁琐。文章提供了两种方案的具体实现代码,并从适用场景、维护成本及社区支持三方面进行对比分析,帮助开发者根据项目需求选择最佳方案。
75 0
|
5月前
|
存储 前端开发 JavaScript
react hooks 学习进阶
【7月更文挑战第12天】 React Hooks(自16.8版起)让函数组件能处理状态和副作用。useState用于添加状态管理,useEffect处理副作用,useContext共享数据,useReducer处理复杂状态逻辑,useRef获取引用。进阶技巧涉及性能优化,如useMemo和useCallback,以及遵循规则避免在不适当位置调用Hooks。理解异步更新机制和结合Redux等库提升应用复杂性管理。持续学习新技巧是关键。
60 0