关于前端:reacthooks原理

51次阅读

共计 20740 个字符,预计需要花费 52 分钟才能阅读完成。

一 前言

本篇文章次要从 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 中 保留的是缓存的值和 deps | useRef 中保留的是 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

正文完
 0