【React源码笔记12】- hooks

React | 2020-08-16 23:11:45 505次 2次

经过之前的文章,了解了大体的 react 运行流程,继续对 hooks 进行分析,它可以分为如下三种类型:

State hooks : useState、useReduce

Effect hooks : useEffect、useLayoutEffect

其他 hooks:  useCallback、useMemo、useContext、useRef等


一、流程

在第一篇文章中介绍了 React.js 中部分的方法,同样的这里也暴露了 hooks 相关的 api,同样这里面不会做什么复杂逻辑,在 ReactHooks.js 中可以看到所有方法都是来自 ReactCurrentDispatcher 这个全局变量:

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

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  return dispatcher;
}
...

这个变量会在 react-dom 包中的 ReactFiberHooks.js 文件中被赋值,主要是逻辑依然是依靠 react-dom 处理。

根据堆栈信息可以看到一个函数组件经过调度,最终在 beginWork 中执行了 renderWithHooks,也就是 hooks 执行的起点:

初始加载:
    ... --> beginWork --> mountIndeterminateComponent --> renderWithHooks --> reconcileChildren -->..

更新: 
    ... --> beginWork --> updateFunctionComponent --> renderWithHooks --> reconcileChildren --> ...


二、renderWithHooks

同样的在 renderWithHooks 中会对 ReactCurrentDispatcher.current 赋值,这不过这个赋值是根据各种条件有不同的值。

export function renderWithHooks(...) {
    // 这里注意 将wip赋值给了 currentlyRenderingFiber 变量,后面会全局使用
    currentlyRenderingFiber = workInProgress;
  ...
    // 初始加载 | 更新 
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  ...
  let children = Component(props, secondArg);

  // 检查直接在组件中调用 useState中的更新方法 避免无限loop
  if (workInProgress.expirationTime === renderExpirationTime) {
    let numberOfReRenders: number = 0;
    do {
      workInProgress.expirationTime = NoWork;
      // 证明无限调用  报错提示
      invariant(
        numberOfReRenders < RE_RENDER_LIMIT,
        'Too many re-renders. React limits the number of renders to prevent ' +
          'an infinite loop.',
      );
      // 这个值仅仅是内部计算最大迭代次数  和判断是否循环调用没有关系
      numberOfReRenders += 1;
      ...
    } while (workInProgress.expirationTime === renderExpirationTime);
  }

  // 比如这种嵌套 useEffect(() => { useState(0);}) 抛出异常
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  ...
  return children;
}

这里面涉及到的过程为:

初始加载:HooksDispatcherOnMount

更新 :HooksDispatcherOnUpdate

避免无限调用

export default function Hooks() {
    const [num, updateNum] = useState(0);
    updateNum(1)
    return (
        ...
    )
}

主要是通过 workInProgress.expirationTime 和 renderExpirationTime 来判断避免无限循环,这个值初始为 0,但是因为组件中初始加载时候立即执行了 updateNum,所以会触发 dispatchAction 方法中的一个判断,currentlyRenderingFiber 就是 wip,它有值就证明当前正在处于渲染的过程(最新的源码中直接通过 didScheduleRenderPhaseUpdate  来判断了,不再设置过期时间):

 if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // 标记
    didScheduleRenderPhaseUpdate = true;
    // 将过期时间设置renderExpirationTime 上面比较如果相同则意味循环引用了
    currentlyRenderingFiber.expirationTime = renderExpirationTime;
  }

在初始加载和更新中间还有一个触发更新的状态,当 hooks 方法调用的时候。


三、hooks 数据结构

hooks 的数据结构比较特殊,但总体思路还是链表存储,比如多个 useState 这种 hook 的保存,会存储在当前 fiber(当前组件)的  memoizedState 属性上,类组件数据的则是保存在当前实例。

其中每个 hook 上又有一套如下结构,进行数据的保存和更新状态的记录,一连串的 hooks 中包含各个 hook

const hook: Hook = {
  memoizedState: null,

  baseState: null,
  baseQueue: null,
  queue: null,

  next: null,
};

凭借扎实的美术功底,通过强大的【画板】来描述这个神奇的 hooks 数据结构再适合不过了(查看大图):

微信截图_20200820011152.png

// 对应示例
import React, { useState, useEffect } from 'react'

export default function Hooks() {
    const [num, updateNum] = useState(0);
    const [num1, updateNum1] = useState(100);
    const [num2, updateNum2] = useState(1000);

    let setAdd = () => {
        updateNum(num+1)
        updateNum(num+2)
        updateNum(num+3)
    }

    useEffect(() => {
        console.log('load')
    }, [num1])
    
    return (
        <div onClick = {setAdd}>
            { num }, { num1 }
        </div>
    )
}


四、初始加载

无论初始加载或者是更新时,都会执行这些 hooks 方法,但是执行的处理是不一致的。

通过手动调用某个方法(useState | useReduce dispatch)可以触发更新。

说明:初始加载、更新、调用这三个部分仅仅针对 useState | useReduce  展开描述,其他方法后面再看

const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useResponder: createDeprecatedResponderListener,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
};

useState 方法初始加载时调用 mountState

function mountState(initialState){
  // useState 的链表创建,对应上图的 hook 那一列
  const hook = mountWorkInProgressHook();
  
  // 挂载 memoizedState 值
  hook.memoizedState = hook.baseState = initialState;
  // basicStateReducer 中执行 action 这里暂时看不到目的
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  // 返回的 api 执行调用更新就是触发 dispatch 方法
  return [hook.memoizedState, dispatch];
}

首先通过 mountWorkInProgressHook 方法构建了上图中 hook 列中的数据结构,最终挂载到 currentlyRenderingFiber 变量(对应图最左侧的 fiber 节点),最终返回一个 dispatch 方法。

useReducer 方法初始加载时调用 mountReducer

function mountReducer(reducer, initialArg){
  ...
  const queue = (hook.queue = {
    ...
    lastRenderedReducer: reducer,
    ...
  });
  ...
}

可以看到这两个方法唯一的区别就是 queue lastRenderedReducer 不一致,因为 useReducer 是直接接收一个 reducer 方法的,而 useState 则只是单纯的传入一个值,但是它上面挂载的是 basicStateReducer ,这个暂时忽略,一定是调用那里会进行处理:

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  // $FlowFixMe: Flow doesn't like mixed types
  return typeof action === 'function' ? action(state) : action;
}


、调用

这个过程会触发更新,通过 hook 返回的第二个参数(dispatch),对应的是 dispatchAction

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  ...
  // 对应的 update结构
  const update: Update<S, A> = {
    expirationTime,
    suspenseConfig,
    action,
    eagerReducer: null,
    eagerState: null,
    next: (null: any),
  };

  //对应上图的最右侧 action 环
  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 ) {
    // currentlyRenderingFiber 如果和 fiber 全等  意味着render 阶段触发了更新update
    // renderWithHooks 中 var children = Component(props, secondArg); 
    // 这里等于开始执行具体的函数组件,所以会阻塞
    // 因为 renderWithHooks 中最后会将currentlyRenderingFiber置为null,这边如果产生更新一直占用,所以这么比较
    // 这些变量会用于无限循环的调用
    didScheduleRenderPhaseUpdate = true;
    update.expirationTime = renderExpirationTime;
    // 这个最上面提到过 通过这个时间判断
    currentlyRenderingFiber.expirationTime = renderExpirationTime;
  } else {
    if (
      fiber.expirationTime === NoWork &&
      (alternate === null || alternate.expirationTime === NoWork)
    ) {
      // 更新时,状态发现没有发生变化,is 判断,直接跳过渲染
      // mount传入的 reducer
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        try {
          const currentState: S = (queue.lastRenderedState: any);
          const eagerState = lastRenderedReducer(currentState, action);
          
          // 更新时会用到 判断 update.eagerReducer === reducer
          // 更新时会用到 queue.lastRenderedReducer = reducer;
          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            // 状态没有变化  终止
            return;
          }
        }
        ...
      }
    }
    // 开始调度
    scheduleWork(fiber, expirationTime);
  }
}

其中对于过期时间的判断,来决定是否进行数据预计算以及判断状态是否发生改变,假如执行了三次更新,前两次的值没有发生改变(就是传入的值还是原值的情况),那么这两次都会直接终止,第三次的更新是一个新的值,则会再进入这个逻辑并进行数据的预处理,但是因为状态发生改变,此时才会执行调度,调度之后将过期时间就会发生改变,初始值为 0 === NoWork

    ...
    let setAdd = () => {
        updateNum(num)   ----> 终止
        updateNum(num)   ----> 终止
        updateNum(num+3) ----> 调度,并且数据预处理好了
    }
    ...

再换一种情况:

   ...
    let setAdd = () => {
        updateNum(num+1)   ----> 调度,并且数据预处理好了
        updateNum(num+2)   ----> 调度,没有数据预处理
        updateNum(num+3)   ----> 调度,没有数据预处理
    }
    ...

以上两种情况的示例主要是为了下一步【更新】做准备,因为那里面会通过 .eagerReducer  判断决定是否重新计算值或使用已经计算好的数据。

这个过程,其实做的事情很明了,分为四个步骤:

1) 创建更新链表,对应上图最右侧的 action 环链表

2) 判断是否在渲染阶段触发了更新操作,如果死循环则 wip(currentlyRenderingFiber) 一直无法重置,所以判断条件是 fiber === wip,同时赋值一些变量在 renderWithHooks 中会取检测

3) 优化操作,没有更新,状态没有发生变化直接终止,提供计算好的数据可以给到下一步(更细阶段直接使用)

4) 开始调度,回到之前的流程中


六、更新

更新阶段仍然会调用 useState | useReducer hooks,这个过程中根据更新计算出新的状态。

const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useResponder: createDeprecatedResponderListener,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
};

useState 方法初始加载时调用 updateState

function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  // 直接调用了 updateReducer,
  // 并且传入basicStateReducer 就是相当于帮我们创建了一个reducer
  return updateReducer(basicStateReducer, (initialState: any));
}

useReducer 方法初始加载时调用 updateReducer

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  // 这里和初始加载时候是不一样的
  // 这个方法会从历史的 memoizedState 中(即图中最右侧的 fiber 节点中的属性)拿数据
  // 并创建指向的过程和 mount 时一致
  const hook = updateWorkInProgressHook();
  // hook
  const queue = hook.queue;
  ...

  queue.lastRenderedReducer = reducer;
  
  // 每一次更新都会 currentHook.next 找到下一个 hook
  const current: Hook = (currentHook: any);

  let baseQueue = current.baseQueue;

  // 这个代表 某个hook中触发了更新链表 对应图中最右侧那个部分
  let pendingQueue = queue.pending;
  if (pendingQueue !== null) {
    if (baseQueue !== null) {
       // 假设 baseQueue: b-1(最后一个,baseQueue引用他) -> b0 -> b1 -> b-1
      // 假设 pendingQueue: p-1(最后一个,pendingQueue引用他) -> p0 -> p1 -> p-1
      // 以下2行操作的意思是
      // b-1 -> p0
      // 则操作完成后形成 pendingQueue: p-1 -> b0 -> b1 -> b-1 -> p0 -> p1 -> p-1
      let baseFirst = baseQueue.next;
      let pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }

  if (baseQueue !== null) {
    ...
    do {
      const updateExpirationTime = update.expirationTime;
      if (updateExpirationTime < renderExpirationTime) {
        ...这里和 processUpdateQueue 一样的
        // 该update优先级不够,跳过,新的state是基于baseUpdate计算得出
        // 如果这是第一个跳过的update,则之前的update/state就是新的 base update/state
      } else {
        ...
        // Process this update.
        if (update.eagerReducer === reducer) {
          // 初始加载-->执行-->更新  执行的时候如果已经计算好了值 这里直接使用即可
          newState = ((update.eagerState: any): S);
        } else {
          // 不能使用就直接计算了,比如发生了一次调度,后续的调用都需要重新计算
          const action = update.action;
          newState = reducer(newState, action);
        }
      }
      // 计算完所有的更新
      update = update.next;
    } while (update !== null && update !== first);

    ...
  }
  // 返回最新的值,dispatch方法不变
  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

首先,更新时读取的 hook 是从旧数据中复制的一部分属性,其次是通过 currentHook 全局变量记录的当前 hook,就是取得 currentHook.next

下面就是优先级的一些判断,更新队列做相应的处理,接着来根据上一小节提到的内容决定是数据复用还是重新计算,最后返回是最新数据和 dispatch 方法。

初始加载和更新都是针对 hook 来说,调用执行是针对 hook 返回的第二个参数也就是 dispatch 而言,调用之后触发调度,从而再次进入 renderWithHooks 引起组件的更新,最终进入的流程和之前一样。

下一篇继续看下 hooks 中其他的一些方法原理,useEffectuseMemouseCallback。至于 refcontext 打算和类组件的放在一起分析。

2人赞

分享到: