最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 「react进阶」一文吃透react-hooks原理

    正文概述 掘金(我不是外星人)   2021-03-29   1260

    一 前言

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

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

    • 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的值相同,函数组件不更新?
    • ...

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

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

    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,本文的第二部分和第三部分,将重点两者的联系。

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

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

    三 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 对象。

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

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

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

    next: 指向下一个 hooks对象。

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

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

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

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

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

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

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

    因为一旦在条件语句中声明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中,不过这里顺序要主要,越靠后的effect,越在updateQueue前边。

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

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

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

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

    拓展:effectList

    effect list 可以理解为是一个存储 effectTag 副作用列表容器。它是由 fiber 节点和指针 nextEffect 构成的单链表结构,这其中还包括第一个节点 firstEffect ,和最后一个节点 lastEffectReact 采用深度优先搜索算法,在 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。结构如下图:

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

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

    更新state

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

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

    • 问题一:这里不是执行最后一个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不需要依赖注入,就能访问到最新的改变值。

    一次点击事件更新

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

    五 总结

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

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

    react好文汇总

    react-hooks三部曲另外两部

    • 玩转react-hooks,自定义hooks设计模式及其实战 205+ ?赞

    • react-hooks如何使用 120+ 赞?

    react进阶系列

    • 「react进阶」年终送给react开发者的八条优化建议 880+ 赞?

    • 「react进阶」一文吃透React高阶组件(HOC) 300+ 赞?

    react源码系列

    • 「源码解析 」这一次彻底弄懂react-router路由原理 120+ 赞?

    • 「源码解析」一文吃透react-redux源码(useMemo经典源码级案例) 133+ 赞?

    开源项目系列

    • 「react缓存页面」从需求到开源(我是怎么样让产品小姐姐刮目相看的) 300+ 赞?

    • 「前端工程化」从0-1搭建react,ts脚手架(1.2w字超详细教程) 300+ 赞?

    参考文档

    • react源码

    • React Fiber 源码解析


    起源地下载网 » 「react进阶」一文吃透react-hooks原理

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元