【react】react hook运行原理解析

声明:本文的研究的源码是react@16.3.1javascript

hook相关术语

hook

react在顶级命名空间上暴露给开发者的API,好比下面的代码片断:html

import React , { useState, useReducer, useEffect } from 'react'
复制代码

咱们会把useState,useReduceruseEffect等等称之为“hook”。确切来讲,hook是一个javascript函数。java

请注意,当咱们在下文中提到“hook”这个术语,咱们已经明确地跟“hook对象”这个术语区分开来了。react

react内置了如下的hook:算法

/* react/packages/react-reconciler/src/ReactFiberHooks.new.js */
export type HookType =
  | 'useState'
  | 'useReducer'
  | 'useContext'
  | 'useRef'
  | 'useEffect'
  | 'useLayoutEffect'
  | 'useCallback'
  | 'useMemo'
  | 'useImperativeHandle'
  | 'useDebugValue'
  | 'useDeferredValue'
  | 'useTransition'
  | 'useMutableSource'
  | 'useOpaqueIdentifier';
复制代码

hook对象

/* react/packages/react-reconciler/src/ReactFiberHooks.new.js */
export type Hook = {
  memoizedState: any, 
  baseState: any, 
  baseQueue: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null
};
复制代码

从数据类型的角度来讲,hook对象是一个“纯javascript对象(plain javascript object)”。从数据结构的角度来看,它是一个单向链表(linked list)(下文简称为“hook链”)。next字段的值能够佐证这一点。redux

下面简单地解释一下各个字段的含义:数组

  • memoizedState。经过遍历完hook.queue循环单向链表所计算出来的最新值。这个值会在commit阶段被渲染到屏幕上。
  • baseState。咱们调用hook时候传入的初始值。它是计算新值的基准。
  • baseQueue。
  • queue。参见下面的queue对象

update对象

// react/packages/react-reconciler/src/ReactFiberHooks.new.js 
type Update<S, A> = {
  // TODO: Temporary field. Will remove this by storing a map of
  // transition -> start time on the root.
  eventTime: number,
  lane: Lane,
  suspenseConfig: null | SuspenseConfig,
  action: A,
  eagerReducer: ((S, A) => S) | null,
  eagerState: S | null,
  next: Update<S, A>,
  priority?: ReactPriorityLevel,
};
复制代码

咱们只须要关注跟hook原理相关的字段便可,因此update对象的类型能够简化为:markdown

// react/packages/react-reconciler/src/ReactFiberHooks.new.js 
type Update<S, A> = {
  action: A,
  next: Update<S, A>
};
复制代码
  • action。专用于useState,useReducer这两个hook的术语。由于这两个hook是借用redux概念的产物,因此,在这两个hook的内部实现源码中,使用了redux的诸多术语:“dispatch”,“reducer”,“state”,“action”等。可是此处的ation跟redux的action不是彻底同样的。假若有一下代码:
const [count,setState] = useState(0);
const [useInfo,dispatch] = useReducer(reducer,{name:'鲨叔',age:0})
复制代码

从源码的角度来讲,咱们调用setState(1)setState(count=> count+1)dispatch({foo:'bar'})传入的参数就是“action”。对于redux的action,咱们约定俗成为{type:string,payload:any}这种类型,可是update对象中的action却能够为任意的数据类型。好比说,上面的1,count=> count+1{foo:'bar'}都是update对象的action。数据结构

一并须要提到的是“dispatch方法”这个术语。从源码的角度来看,useState/useReducer这两个hook调用所返回数组的第二个元素其实都是内部dispatchAction函数实例的一个引用。咱们以React.useState()为例子,不妨看看它的源码:闭包

// React.useState()在mount阶段的实现
  function mountState(initialState) {
    // 这里省略了不少代码
    // ......
    var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
    return [hook.memoizedState, dispatch];
  }
  
  // React.useState()在update阶段的实现
   function updateState(initialState) {
    return updateReducer(basicStateReducer);
  }
  
  function updateReducer(reducer, initialArg, init) {
    var hook = updateWorkInProgressHook();
    var queue = hook.queue;
    // 这里省略了不少代码
    // ......
    var dispatch = queue.dispatch;
    return [hook.memoizedState, dispatch];
  }
复制代码

能够看得出,咱们开发者拿到的只是dispatch方法的一个引用。因此,下文会把useState/useReducer这两个hook调用所返回数组的第二个元素统称为“dispatch方法”。调用dispatch方法会致使function component从新渲染。

  • next。指向下一个update对象的指针。从这里咱们就能够判断update对象是一个单向链表。至于它何时变成【循环】单向链表,后面会讲到。

queue对象

// react/packages/react-reconciler/src/ReactFiberHooks.new.js 
type UpdateQueue<S, A> = {
  pending: Update<S, A> | null,
  dispatch: (A => mixed) | null,
  lastRenderedReducer: ((S, A) => S) | null,
  lastRenderedState: S | null,
};
复制代码
  • pending。咱们调用dispatch方法,主要是作了两件事:1)生成一个由update对象组成的循环单向链表; 2)触发react的调度流程。而pending就是这个循环单向链表的头指针。
  • dispatch。返回给开发者的用于触发组件re-render的函数实例引用。
  • lastRenderedReducer。 上一次update阶段使用的reducer。
  • lastRenderedState。 使用lastRenderedReducer计算出来并已经渲染到屏幕的state。

currentlyRenderingFiber

这是一个全局变量,存在于function component的生命周期里面。顾名思义,这是一个fiber节点。每个react component都有一个与之对应的fiber节点。按照状态划分,fiber节点有两种:“work-in-progress fiber”和“finished-work fiber”。前者表明的是当前render阶段正在更新react component,然后者表明的是当前屏幕显示的react component。这两种fiber节点经过alternate字段来实现【循环引用】对方。有源码注释为证:

// react/packages/react-reconciler/src/ReactInternalTypes.js
export type Fiber = {
  // ......
  // 此前省略了不少代码
  
  // This is a pooled version of a Fiber. Every fiber that gets updated will
  // eventually have a pair. There are cases when we can clean up pairs to save
  // memory if we need to.
  alternate: Fiber | null,
  
  // 此后省略了不少代码
  // ......

};
复制代码

这里的currentlyRenderingFiber是属于“work-in-progress fiber”。可是为了不歧义,内部源码采用了当前这个命名:

// react/packages/react-reconciler/src/ReactFiberHooks.new.js

// The work-in-progress fiber. I've named it differently to distinguish it from
// the work-in-progress hook.
let currentlyRenderingFiber: Fiber = (null: any);
复制代码

对这个全局变量的赋值行为是发生在function component被调用以前。有源码为证:

export function renderWithHooks<Props, SecondArg>( current: Fiber | null, workInProgress: Fiber, Component: (p: Props, arg: SecondArg) => any, props: Props, secondArg: SecondArg, nextRenderLanes: Lanes, ): any {
	currentlyRenderingFiber = workInProgress;
    // 此间省略了不少代码
    // ......
    let children = Component(props, secondArg);
    // 此后省略了不少代码
    // ......
}
复制代码

没错,这里的Component就是咱们日常所说,所写的function component。能够看出,一开始currentlyRenderingFiber是为null的,在function component调用以前,它被赋值为该function component所对应的fiber节点了。

currentHook

这是一个全局变量,对应于旧的hook链(旧的hook链的产生于hook的mount阶段)上已经遍历过的那个hook对象。当前正在遍历的hook对象存放在updateWorkInProgressHook()方法中的局部变量nextCurrentHook上。

workInProgressHook

mount阶段和update阶段都存在。mount阶段,这是一个全新的javascript对象;update阶段,它是经过对旧hook对象进行浅拷贝获得的新的,对应与当前被调用的hook的hook对象。不管是mount阶段仍是update阶段,它都是指向当前hook链中的最后一个被处理过(mount阶段,对应于最后一个被建立的hook对象;update阶段,对应于最后一个被拷贝的hook对象)的hook对象。

hook的mount阶段

等同于组件的首次挂载阶段,更确切来讲是function component的第一次被调用(由于function component本质上是一个函数)。通常而言,是由ReactDOM.render()来触发的。

hook的update阶段

等同于组件的更新阶段,更确切地说是function component的第二次,第三次......第n次的被调用。通常而言,存在两种状况使得hook进入update阶段。第一种是,父组件的更新致使function component的被动更新;第二种是,在function component内部手动调用hook的dispatch方法而致使的更新。

小结

从数据类型的角度来讲,上面所提到的“xxx对象”从数据结构的角度来看,它们又是相应的数据结构。下面,咱们把上面所提到的数据结构串联到一块以后就是mount阶段,hook所涉及的数据结构:

几个事实

1. mount阶段调用的hook与update阶段调用的hook不是同一个hook

import React, { useState } from 'react';

function Counter(){
	const [count,setState] = useState();
    
    return <div>Now the count is {count}<div>
}
复制代码

就那上面的代码,拿useSate这个hook做说明。Counter函数会被反复调用,第一次调用的时候,对useState这个hook来讲,就是它的“mount阶段”。此后的每一次Counter函数调用,就是useState的“update阶段”。

一个比较颠覆咱们认知的事实是,第一次调用的useState居然跟随后调用的useState不是同一个函数。这恐怕是不少人都没有想到的。useState只是一个引用,mount阶段指向mount阶段的“mountState”函数,update阶段指向update阶段的“updateState”函数,这就是这个事实背后的实现细节。具体来看源码。react package暴露给开发者的useState,实际上是对应下面的实现:

// react/packages/react/src/ReactHooks.js

export function useState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}
复制代码

而resolveDispatcher()函数的实现是这样的:

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  invariant(
    dispatcher !== null,
    '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://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
  );
  return dispatcher;
}
复制代码

ReactCurrentDispatcher.current初始值是为null的:

// react/src/ReactCurrentDispatcher.js

/** * Keeps track of the current dispatcher. */
const ReactCurrentDispatcher = {
  /** * @internal * @type {ReactComponent} */
  current: (null: null | Dispatcher),
};
复制代码

那么它是在何时被赋值了呢?赋了什么值呢?答案是在function Component被调用以前。在renderWithHooks()这个函数里面,有这样的代码:

export function renderWithHooks<Props, SecondArg>( current: Fiber | null, workInProgress: Fiber, Component: (p: Props, arg: SecondArg) => any, props: Props, secondArg: SecondArg, nextRenderLanes: Lanes, ): any {
	// 这里省略了不少代码....
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
        
  let children = Component(props, secondArg);
  // 这里省略了不少代码.....
  return children;
}
复制代码

这里,current是一个fiber节点。从这个判断能够看出,function component没有对应的fiber节点或者该fiber节点上没有hook链表的时候,就是hook的mount阶段。mount阶段,Dispatcher.current指向的是HooksDispatcherOnMount;不然,就是updte阶段。update阶段,Dispatcher.current指向的是HooksDispatcherOnUpdate。

最后,咱们分别定位到HooksDispatcherOnMount和HooksDispatcherOnUpdate对象上,真相就一目了然:

// react/packages/react-reconciler/src/ReactFiberHooks.new.js

const HooksDispatcherOnMount: Dispatcher = {
  readContext,
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState, // 目光请聚焦到这一行
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useMutableSource: mountMutableSource,
  useOpaqueIdentifier: mountOpaqueIdentifier,
  unstable_isNewReconciler: enableNewReconciler,
};

const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState, // 目光请聚焦到这一行
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useMutableSource: updateMutableSource,
  useOpaqueIdentifier: updateOpaqueIdentifier,
  unstable_isNewReconciler: enableNewReconciler,
};
复制代码

能够看到,useState在mount阶段对应的是“mountState”这个函数;在update阶段对应的是“updateState”这个函数。再次强调,这里只是拿useState这个hook举例说明,其余hook也是同样的道理,在这里就不赘言了。

2. useState()实际上是简化版的useReducer()

说这句的意思就是,相比于useReducer,useState这个hook只是在API参数上不同而已。在内部实现里面,useState也是走的是useReducer那一套机制。具体来讲,useState也有本身的reducer,在源码中,它叫basicStateReducer。请看,mount阶段的useState的实现:

// react/packages/react-reconciler/src/ReactFiberHooks.new.js

function mountState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] {
  // ......
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  // .....
}
复制代码

能够看到,useState()也是有对应的reducer的,它就挂载在lastRenderedReducer这个字段上。那basicStateReducer长什么样呢?

// react/packages/react-reconciler/src/ReactFiberHooks.new.js

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === 'function' ? action(state) : action;
}
复制代码

能够看到,这个basicStateReducer跟咱们本身写的(redux式的)reducer是具备相同的函数签名的:(state,action) => state,它也是一个真正的reducer。也就是说,在mount阶段,useReducer使用的reducer是开发者传入的reducer,而useState使用的是react帮咱们对action进行封装而造成的basicStateReducer

上面是mount阶段的useState,下面咱们来看看update阶段的useState是怎样跟useReducer产生关联的。上面已经讲过,useState在update阶段引用的是updateState,那咱们来瞧瞧它的源码实现:

// react/packages/react-reconciler/src/ReactFiberHooks.new.js

function updateState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}
复制代码

没错,updateState()调用的就是updateReducer(),而useReducer在update阶段引用的也是updateReducer函数!到这里,对于这个事实算是论证完毕。

3. useReducer()的第一个参数“reducer”是能够变的

废话很少说,咱们来看看updateReducer函数的源码实现(我对源码进行了精简):

// react/packages/react-reconciler/src/ReactFiberHooks.new.js

function updateReducer(reducer,initialArg,init) {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  // 拿到更新列表的表尾
  const last = queue.pending;

  // 获取最先的那个update对象,时刻记住,这是循环链表
  first = last !== null ? last.next : null;

  if (first !== null) {
    let newState = hook.baseState;
    let update = first;
    do {
      // 执行每一次更新,去更新状态
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== null && update !== first);

    hook.memoizedState = newState;
  }
  const dispatch = queue.dispatch;
  // 返回最新的状态和修改状态的方法
  return [hook.memoizedState, dispatch];
}
复制代码

能够看到,update阶段useReducer传进来的reducer是被用于最新值的计算的。也就是说,在update阶段,咱们能够根据必定的条件来切换reducer的。虽然,实际开发中,咱们不会这么干,可是,从源码来看,咱们确实是能够这么干的。

也许,你会问,useState能够一样这么干嘛?答案是:“不能”。由于useState所用到的reducer不是咱们能左右的。在内部源码中,这个reducer固定为basicStateReducer。

hook运做的基本原理

hook的整个生命周期能够划分为三个阶段:

  • mount阶段
  • 触发更新阶段
  • update阶段

经过了解这三个阶段hook都干了些什么,那么咱们就基本上就能够掌握hook的运做基本原理了。

mount阶段

简单来讲,在mount阶段,咱们每调用一次hook(不区分类型。举个例子说,我连续调用了三次useState(),那么我就会说这是调用了三次hook),实际上会发生下面的三件事情:

  1. 建立一个新的hook对象;
  2. 构建hook单链表;
  3. 补充hook对象的信息。

第一步和第二步:【建立一个新的hook对象】和【构建hook单链表】

咱们每调用一次hook,react在内部都会调用mountWorkInProgressHook()方法。而【hook对象的建立】和【链表的构建】就是发生在这个方法里面。由于它们的实现都是放在同一个方法里面,这里就放在一块讲了:

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
复制代码

显而易见,变量hook装的就是初始的hook对象。因此,【建立一个新的hook对象】这一步算是讲完了。

下面,咱们来看看第二步-【构建hook单链表】。它的代码比较简单,就是上面的mountWorkInProgressHook方法里面的最后几行代码:

if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
复制代码

术语章节已经讲过,workInProgressHook是一个全局变量,它指向的是最后一个被生成的hook对象。若是workInProgressHook为null,那就表明着根本就没有生成过hook对象,对应于当前这个hook对象是第一个hook对象,则它会成为表头,被头指针【currentlyRenderingFiber.memoizedState】所指向;不然,当前建立的hook对象被append到链表的尾部。这里,react内部的实现采用了比较巧妙的实现。它新建了一个指针(workInProgressHook),每一轮构建完hook链表后都让它指向表尾。那么,下一次追加hook对象的时候,咱们只须要把新hook对象追加到workInProgressHook对象的后面就行。实际上,上面的代码能够拆解为这样:

if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState  = hook;
    workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook.next = hook; // 为何对workInProgressHook.next的赋值可以起到append链表的做用呢?这里须要用到【引用传递】的知识来理解。
    workInProgressHook = hook;
  }
复制代码

这种实现方法的好处是:不要经过遍历链表来找到最后一个元素,以便其后插入新元素(时间复杂度为O(n))。而是直接插入到workInProgressHook这个元素后面就好(时间复杂度为O(1))。咱们要知道,常规的链表尾部插入是这样的:

if (currentlyRenderingFiber.memoizedState === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState  = hook;
  } else {
   let currrent = currentlyRenderingFiber.memoizedState;
   while(currrent.next !== null){
   	currrent = currrent.next
   }
   currrent.next = hook;
  }
复制代码

从时间复杂度的角度来讲就是把链表插入算法的时间复杂度从O(n)降到O(1)。好,上面稍微展开了一点。到这里,咱们已经看到react源码中,是如何实现了第一步和第二步的。

第三步:补充hook对象的信息

咱们在第一步建立的hook对象有不少字段,它们的值都是初始值null。那么在第三部,咱们就是对这些字段的值进行填充。这些操做的实现代码都是在mountState函数的里面:

function mountState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}
复制代码

对memoizedState和baseState的填充:

if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
复制代码

对queue字段的填充:

const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
复制代码

对next字段的填充实际上是发生在第二步【构建hook单链表】,这里就不赘述了。

以上就是hook对象填充字段信息的过程。不过,值得指出的是,hook对象的queue对象也是在这里初始化并填充内容的。好比dispatch字段,lastRenderedReducer和lastRenderedState字段等。着重须要提到dispatch方法:

const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
复制代码

从上面的代码能够看到,咱们拿到的dispatch方法实质上是一个引用而已。它指向的是dispatchAction这个方法经过函数柯里化所返回的函数实例。函数柯里化的本质是【闭包】。经过对currentlyRenderingFiberqueue变量的闭包,react能确保咱们调用dispatch方法的时候访问到的是与之对应的queue对象和currentlyRenderingFiber。

好的,以上就是hook在mount阶段所发生的事情。

触发更新阶段

当用户调用dispatch方法的时候,那么咱们就会进入【触发更新阶段】。react的源码中并无这个概念,这是我为了帮助理解hook的运行原理而提出的。

要想知道触发更新阶段发生了什么,咱们只须要查看dispatchAction方法的实现就好。可是,dispatchAction方法实现源码中,参杂了不少跟调度和开发环境相关的代码。这里为了方便聚焦于hook相关的原理,我对源码进行了精简:

function dispatchAction<S, A>(fiber: Fiber,queue: UpdateQueue<S, A>,action: A,) {
    const update: Update<S, A> = {
      eventTime,
      lane,
      suspenseConfig,
      action,
      eagerReducer: null,
      eagerState: null,
      next: (null: any),
    };
  
    // Append the update to the end of the list.
    const pending = queue.pending;
    if (pending === null) {
      // This is the first update. Create a circular list.
      update.next = update;
    } else {
      update.next = pending.next;
      pending.next = update;
    }
    queue.pending = update;
  
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }
复制代码

触发更新阶段,主要发生了如下三件事情:

  1. 建立update对象:
const update: Update<S, A> = {
      eventTime,
      lane,
      suspenseConfig,
      action,
      eagerReducer: null,
      eagerState: null,
      next: (null: any),
    };
复制代码

这里,咱们只须要关注action和next字段就好。从这里能够看出,咱们传给dispatch方法的任何参数,都是action。

  1. 构建updateQueue循环单向链表:
// Append the update to the end of the list.
    const pending = queue.pending;
    if (pending === null) {
      // This is the first update. Create a circular list.
      update.next = update;
    } else {
      update.next = pending.next;
      pending.next = update;
    }
    queue.pending = update;
复制代码

从上面的代码中,能够看到:

  • updateQueue是一个循环单向链表;
  • 链表元素插入的顺序等同于dispatch方法调用的顺序。也就是说最后生成的update对象处于链尾。
  • queue.pending这个指针永远指向链尾元素。
  1. 真正地触发更新

不管是以前的class component时代,仍是如今的function component时代,咱们调用相应的setState()方法或者dispatch()方法的时候,其本质都是向react去请求更新当前组件而已。为何这么说呢?由于,从react接收到用户的更新请求到真正的DOM更新,这中间隔着“千山万水”。之前,这个“千山万水”是react的“批量更新策略”,如今,这个“千山万水”是新加入的“调度层”。

无论怎样,对于function component,咱们内心得有个概念就是:假如react决定要更新当前组件的话,那么它的调用栈必定会进入一个叫“renderWithHooks”的函数。就是在这个函数里面,react才会调用咱们的function component(再次强调,function component是一个函数)。调用function component,则必定会调用hook。这就意味着hook会进入update阶段。

那么,hook在update阶段发生了什么呢?下面,咱们来看看。

update阶段

hook在update阶段作了什么,主要是看updateReducer()这个方法的实现。因为updateReducer方法实现中,包含了很多调度相关的代码,如下是我作了精简的版本:

function updateReducer(reducer,initialArg,init) {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  // 拿到更新链表的表尾元素
  const last = queue.pending;

  // 获取最先插入的那个update对象,时刻记住,这是循环链表:最后一个的next指向的是第一个元素
  first = last !== null ? last.next : null;

  if (first !== null) {
    let newState = hook.baseState;
    let update = first;
    do {
      // 执行每一次更新,去更新状态
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== null && update !== first);

    hook.memoizedState = hook.baseState = newState;
  }
  const dispatch = queue.dispatch;
  // 返回最新的状态和修改状态的方法
  return [hook.memoizedState, dispatch];
}
复制代码

hook的update阶段,主要发生了如下两件事情:

  1. 遍历旧的hook链,经过对每个hook对象的浅拷贝来生成新的hook对象,并依次构建新的hook链。
  2. 遍历每一个hook对象上的由update对象组成queue循环单链表,计算出最新值,更新到hook对象上,并返回给开发者。

updateReducer()方法的源码,咱们能够看到,咱们调用了updateWorkInProgressHook()方法来获得了一个hook对象。就是在updateWorkInProgressHook()方法里面,实现了咱们所说的第一件事情。下面,咱们来看看updateWorkInProgressHook()的源码(一样进行了精简):

function updateWorkInProgressHook(): Hook {
  // This function is used both for updates and for re-renders triggered by a
  // render phase update. It assumes there is either a current hook we can
  // clone, or a work-in-progress hook from a previous render pass that we can
  // use as a base. When we reach the end of the base list, we must switch to
  // the dispatcher used for mounts.
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  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) {
    // This is the first hook in the list.
    currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
  } else {
    // Append to the end of the list.
    workInProgressHook = workInProgressHook.next = newHook;
  }

  return workInProgressHook;
}
复制代码

上面在讲currentlyRenderingFiber的时候讲到,当前已经显示在屏幕上的component所对应的fiber节点是保存在currentlyRenderingFiber.alternate字段上的。那么,旧的hook链的头指针无疑就是currentlyRenderingFiber.alternate.memoizedState。而nextCurrentHook变量指向的就是当前准备拷贝的标本对象,currentHook变量指向的是当前旧的hook链上已经被拷贝过的那个标本对象。结合这三个语义,咱们不难理解这段代码:

let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }
复制代码

用一句话来总结就是:若是当前是第一次hook调用,那么拷贝的标本对象就是旧的hook链的第一个元素;不然,拷贝的标本对象就是当前已经拷贝过的那个标本对象的下一个。

下面这一段代码就是hook对象的浅拷贝:

currentHook = nextCurrentHook;

  const newHook: Hook = {
    memoizedState: currentHook.memoizedState,
    baseState: currentHook.baseState,
    baseQueue: currentHook.baseQueue,
    queue: currentHook.queue,
    next: null,
  };
复制代码

从上面的浅拷贝,咱们能够想到,hook的mount阶段和update阶段都是共用同一个queue链表。

再往下走,即便新链表的构建,几乎跟mount阶段hook链表的构建一摸同样,在这里就不赘述了:

if (workInProgressHook === null) {
    // This is the first hook in the list.
    currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
  } else {
    // Append to the end of the list.
    workInProgressHook = workInProgressHook.next = newHook;
  }
复制代码

到这里,咱们经过解析updateWorkInProgressHook()的源码把第一件事情算是讲完了。下面,咱们接着来说第二件事情-【遍历每一个hook对象上的由update对象组成queue循环单链表,计算出最新值,更新到hook对象上,并返回给开发者】。相关源码就在上面给出的精简版的updateReducer()方法的源码中。咱们再次把它抠出来,放大讲讲:

const queue = hook.queue;

  // 拿到queu循环链表的表尾元素
  const last = queue.pending;

  // 获取最先插入的那个update对象,时刻记住,这是循环链表:最后一个元素的next指针指向的是第一个元素
  first = last !== null ? last.next : null;

  if (first !== null) {
    let newState = hook.baseState;
    let update = first;
    do {
      // 执行每一次更新,去更新状态
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== null && update !== first);

    hook.memoizedState = hook.baseState = newState;
  }
  const dispatch = queue.dispatch;
  // 返回最新的状态和修改状态的方法
  return [hook.memoizedState, dispatch];
复制代码

首先,拿到queue循环链表的第一个元素;

其次,从它开始遍历整个链表(结束条件是:回到链表的头元素),从链表元素,也便是update对象上面拿到action,遵循newState = reducer(newState, action);的计算公式,循环结束的时候,也就是最终值被计算出来的时候;

最后,把新值更新到hook对象上,而后返回出去给用户。

从第二件事件里面,咱们能够理解,为何hook在update阶段被调用的时候,咱们传入的initialValue是被忽略的,以及hook的最新值是如何更新获得的。最新值是挂载在hook对象上的,而hook对象又是挂载在fiber节点上。当component进入commit阶段后,最新值会被flush到屏幕上。hook也所以完成了当前的update阶段。

为何hook的顺序如此重要?

在hook的官方文档:Rules of Hooks中,提到了使用hook的两大戒律:

  • Only Call Hooks at the Top Level
  • Only Call Hooks from React Functions(component)

若是单纯去死记硬背,而不去探究其中的原因,那么咱们的记忆就不会牢固。如今,咱们既然深究到源码层级,咱们就去探究一下提出这戒律后面的依据是什么。

首先,咱们先来解读一下这两大戒律究竟是在讲什么。关于第一条,官方文档已经很明确地指出,之因此让咱们在函数做用域的顶部,而不是在循环语句,条件语句和嵌套函数里面去调用hook,目的只有一个:

By following this rule, you ensure that Hooks are called in the same order each time a component renders.

是的,目的就是保证mount阶段,第一次update阶段,第二update阶段......第n次update阶段之间,全部的hook的调用顺序都是一致的。至于为何,咱们稍后解释。

而第二条戒律,说的是只能在React的function component里面去调用react hook。这条戒律是显而易见的啦。你们都知道react hook对标的是class component的相关feature(状态更新,生命周期函数)的,它确定要跟组件的渲染相挂钩的,而普通的javascript函数是没有跟react界面渲染相关联的。其实这条戒律更准确来讲,应该是这样的:要确保react hook【最终】是在react function component的做用域下面去调用。也就是说,你能够像俄罗斯套娃那样,在遵循第一条戒律的前提下去对react hook层层包裹(这些层层嵌套的函数就是custom hook),可是你要确保最外层的那个函数是在react function componnet里面调用的。

上面的说法依据是什么呢?那是由于hook是挂载在dispatcher身上的,而具体的dispatcher是在运行时注入的。dispatcher的注入时机是发生在renderWithHook()这个方法被调用的时候,这一点在上面【几个事实】一节中有提到。从renderWithHook到hook的调用栈是这样的:

renderWithHook() -> dispatcher注入 -> component() -> hook() -> resolveDispatcher()
复制代码

那么,咱们看一眼resolveDispatcher方法的实现源码就能找到咱们要想找的依据:

function resolveDispatcher() {
    var dispatcher = ReactCurrentDispatcher.current;

    if (!(dispatcher !== null)) {
      {
        throw Error( "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:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem." );
      }
    }

    return dispatcher;
  }
复制代码

也就是说,假如你不在react function component里面调用hook的话,那么renderWithHook()这个方法就不会被执行到,renderWithHook()没有被执行到,也就是说跳过了dispatcher的注入。dispatcher没有被注入,你就去调用hook,此时dispatcher为null,所以就会报错。

以上,就是第二条戒律背后的依据分析。至于第一条戒律,它不断地在强调hook的调用顺序要一致。要想搞懂这个缘由,首先咱们得搞懂什么是【hook的调用顺序】?

什么是hook的调用顺序?

答案是:“hook在【mount阶段】的调用顺序就是hook的调用顺序”。也就是说,咱们判断某一次component render的时候,hook的调用顺序是否一致,参照的是hook在mount阶段所肯定下来的调用顺序。举个例子:

// mount阶段
const [count,setCount] = useState(0)
const [name,setName] = useState('sam')
const [age,setAge] = useState(28)

//第一次update阶段
const [count,setCount] = useState(0)
const [name,setName] = useState('sam')
const [age,setAge] = useState(28)

//第二次update阶段
const [age,setAge] = useState(28)
const [count,setCount] = useState(0)
const [name,setName] = useState('sam')
复制代码

参照mount阶段所肯定的顺序:useState(0) -> useState('sam') -> useState(28),第一次update阶段的hook调用顺序是一致的,第二次update阶段的hook调用顺序就不一致了。

总而言之,hook的调用顺序以mount阶段所确立的顺序为准。

关于hook的调用顺序的结论

首先,二话不说,咱们先下两个有关于hook调用顺序的结论:

  1. hook的【调用次数】要一致。多了,少了,都会报错。
  2. 最好保持hook的【调用位置】是一致的。

其实,通过思考的同窗都会知道,调用顺序一致能够拆分了两个方面:hook的数量一致,hook的调用位置要一致(也就是相同的那个hook要在相同的次序被调用)。

官方文档所提出的的hook的调用顺序要一致,这是没问题的。它这么作,既能保证咱们不出错,又能保证咱们不去作那些无心义的事情(改变hook的调用位置意义不大)。

可是,从源码的角度来看,hook的调用位置不一致并不必定会致使程序出错。假如你知道怎么承接调用hook所返回的引用的话,那么你的程序还会照常运行。之因此探讨这一点,是由于我要打惟官方文档论的迷信思想,加深【源码是检验对错的惟一标准】的认知。

首先,咱们来看看,为何能下第一条结论。咱们先看看hook的调用次数多的状况。假如咱们有这样的代码:

// mount阶段
const [count,setCount] = useState(0)
const [name,setName] = useState('sam')
const [age,setAge] = useState(28)

//第一次update阶段
const [count,setCount] = useState(0)
const [name,setName] = useState('sam')
const [age,setAge] = useState(28)
const [sex,setSex] = useState('男')
复制代码

通过mount阶段,咱们会获得这样的一条hook链:

=============           =============             =============
| count Hook |  ---->   | name Hook  |  ---->     | age Hook  | -----> null
=============           =============             =============
复制代码

上面也提到了,update阶段的主要任务之一就是遍历旧的hook链去建立新的hook链。在updateWorkInProgressHook方法里面,在拷贝hook对象以前的动做是要计算出当前要拷贝的hook对象,也就是变量nextCurrentHook的值:

let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }
复制代码

假设,如今咱们来到了update阶段的第四次hook调用,那么代码会执行到nextCurrentHook = currentHook.next;。currentHook是上一次(第三次)被成功拷贝的对象,而且是存于旧链上。由于旧的hook链只有三个hook对象,那么此时currentHook对象已是最后一个hook对象了,currentHook.next的值天然是为null了。也就是说当前准备拷贝的hook对象(nextCurrentHook)是为null的。咱们的断言失败,程序直接报错:

// 假如咱们断言失败,则会抛出错误
  invariant(
    nextCurrentHook !== null,
    'Rendered more hooks than during the previous render.',
  );
复制代码

以上就是hook调用次数多了会报错的状况。下面,咱们来看看hook调用次数少了的状况。咱们直接关注renderWithHooks方法里面的这几行代码:

export function renderWithHooks<Props, SecondArg>( current: Fiber | null, workInProgress: Fiber, Component: (p: Props, arg: SecondArg) => any, props: Props, secondArg: SecondArg, nextRenderLanes: Lanes, ): any {
  // ....
  let children = Component(props, secondArg);
  // ....
  const didRenderTooFewHooks = currentHook !== null && currentHook.next !== null;
  // .....
  invariant(
    !didRenderTooFewHooks,
    'Rendered fewer hooks than expected. This may be caused by an accidental ' +
      'early return statement.',
  );

  return children;
}
复制代码

这里的Component就是咱们hook调用所在的function component。解读上面所给出的代码,咱们能够得知:若是全部的hook都调用完毕,你那个全局变量currentHook的next指针还指向别的东西(非null值)的话,那么证实update阶段,hook的调用次数少了,致使了next指针的移动次数少了。若是hook的调用次数是同样的话,那么此时currentHook是等于旧的hook链上的最后一个元素,咱们的断言就不会失败。

从上面的源码分析,咱们能够得知,hook的调用次数是不能多,也不能少了。由于多了,少了,react都会报错,程序就中止运行。

最后,咱们来看看结论二。

为了证实个人观点是对的,咱们直接来运行下面的示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>react hook</title>
</head>
<body>
    <div id="root"></div>
    <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
    <script></script>
    <script> window.onload = function(){ let _count = 0; const { useState, useEffect, createElement } = React const root = document.getElementById('root') let count,setState let name,setName function Counter(){ if(_count === 0){ // mount阶段 const arr1= useState(0) count = arr1[0] setState = arr1[1] const arr2 = useState('sam') name = arr2[0] setName = arr2[1] }else if(_count >= 1){ // update阶段 const arr1 = useState('little sam') count = arr1[0] setState = arr1[1] const arr2 = useState(0) name = arr2[0] setName = arr2[1] } _count += 1 return createElement('button',{ onClick:()=> { setState(count=> count + 1) } },count) } ReactDOM.render(createElement(Counter),root) } </script>
</body>
</html>
复制代码

直接运行上面的例子,程序是会正常运行的,界面的显示效果也是正确的。从而佐证了个人结论是正确的。那为何呢?那是由于在update阶段,要想正确地承接住hook调用所返回的引用,hook的名字是不重要的,重要的是它的位置。上面update阶段,虽然hook的调用的位置是调换了,可是咱们知道第一个位置的hook对象仍是指向mount阶段的count hook对象,因此,我仍是能正确地用它所对应的变量来承接,因此,后面的render也就不会出错。

以上示例仅仅是为了佐证个人第二个结论。实际的开发中,咱们不会这么干。或者说,目前我没有遇到必须这么作的开发场景。

以上就是从源码角度去探索react hook的调用顺序为何这么重要的缘由。

其余hook

上面只是拿useState和useReducer这两个hook做为本次讲解react hook原理的样例,还有不少hook没有涉及到,好比说十分重要的useEffect就没有讲到。可是,若是你深刻到hook的源码(react/packages/react-reconciler/src/ReactFiberHooks.new.js)中去看的话,几乎全部的都有如下共性:

  • 都有mount阶段和update阶段
  • 在mount阶段,都会调用mountWorkInProgressHook()来生成hook对象;在update阶段,都会调用updateWorkInProgressHook()来拷贝生成新的hook对象。这就意味着,相同阶段,无论你是什么类型的hook,你们都是处在同一个hook链身上
  • 每一个hook都对应一个hook对象,不一样类型的hook的不一样之处主要体如今它们挂载在hook.memoizedState字段上的值是不一样的。好比说,useState和useReducer挂载的是state(state的数据结构由咱们本身决定),useEffect挂载的是effect对象,useCallback挂载的是由callback和依赖数组组成的数组等等。

而在其余的hook中,useEffect过重要了,又相对不同,无疑是最值得咱们深刻探索的hook。假若有时间,下一篇文章不妨探索探索它的运行原理。

总结

参考资料

相关文章
相关标签/搜索