react-hooks原理

一 前言

本篇文章主要从react-hooks起源,原理,源码角度,开始剖析react-hooks运行机制和内部原理,相信这篇文章事后,对于面试的时候那些hooks问题,也就迎刃而解了。实际react-hooks也并无那么难以理解,听起来很cool,实际就是函数组件解决没有state,生命周期,逻辑不能复用的一种技术方案。react

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>
    }
}

打印结果?闭包

再来看看函数组件中:app

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>
}

打印结果?dom

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

在第一个例子🌰打印结果: 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 ,和最后一个节点 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。结构以下图:

图片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

相关文章
相关标签/搜索