一 前言

本篇文章次要从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.jsconst 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 \-> mountWorkInProgressHookfunction 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