Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Note] hooks运行机制 #11

Open
ddzy opened this issue Jul 16, 2019 · 0 comments
Open

[Note] hooks运行机制 #11

ddzy opened this issue Jul 16, 2019 · 0 comments
Assignees
Labels
note 简单笔记

Comments

@ddzy
Copy link
Owner

ddzy commented Jul 16, 2019

前言

俗话说, 工欲善其事必先利其器.

看完函数组件的更新源码, 自然而然地对于hooks的实现原理产生了浓厚的兴趣, 故记录在此.

从如何使用说起

hooks的出现, 完全颠覆了传统的class至上的原则, 解决了代码冗余可维护性编译性能等多个问题.

看源码之前, 先看一下hooks是如何使用的:

展开源码
import React from 'react';

const TestHooks = () => {
  const [name, setName] = React.useState('ddzy');
  const [age, setAge] = React.useState(21);

  React.useEffect(() => {
    console.log('componentDidMount');
  }, []);

  return (
    <div className="test-hooks-wrapper">
      <p>{state.count}</p>
      <div>
        <button onClick={() => setName('duanzhaoyang')}>{ name }</button>
        <button onClick={() => setAge(22)}>{ age }</button>
      </div>
    </div>
  );
}

在源码TestHook组件中, 按照顺序依次调用了三个hooksAPI, 页面正常更新并渲染. 关于useStateuseEffect的使用, 不记录太多了, 官方文档已经解释的非常清楚:

https://react.docschina.org/docs/hooks-intro.html

品内部实现原理

复习了hooks的基本使用, 着重看下它是如何实现的. 由于目前对于源码的理解还不是很深刻, 故本篇笔记会持续更新.

由于hooks的执行, 分为mountupdate两个阶段. 前者在react应用初次渲染时执行, 后者则在组件全部挂载完成, 用户自定义操作(dispatch)时执行. 由于执行时机不同, 故内部的源码实现逻辑则大相径庭. 所以会分成两个部分来记录.

以下的源码分析都以setState为例

1. mount阶段

useState

以最基本的useState为例, 其源码位于ReactHooks.js, 出乎意料的简单, 只有两行代码:

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

接着看到resolveDispatcher:

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

  return dispatcher;
}

没错, 很熟悉, 依稀记得在更新function组件之时, 通过renderWithHooks方法, 将ReactCurrentDispatcher.current赋值为HooksDispatcherOnMount.

mountState

省去了一大堆重复的步骤, 直接来看useState的最终定义. 由于hooks的执行分为mountupdate阶段, 两者采取的更新策略略有不同, 这里只记录mount的过程.

展开源码
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {

  // 创建新的hook对象, 并追加至`workInProgressHook`单向循环链表尾部
  const hook = mountWorkInProgressHook();

  if (typeof initialState === 'function') {
    initialState = initialState();
  }

  hook.memoizedState = hook.baseState = initialState;

  // 创建当前hook对象的更新队列, 按次序保存当前hook上产生的所有dispatch
  const queue = hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };

  // dispatch对应上述案例中的setName、setAge
  // 而对于当前hook来说, 此时的dispatch = setName
  const dispatch = queue.dispatch = dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  );

  return [hook.memoizedState, dispatch];
}

从源码可以看出, hooks的内部还是比较绕的. 但是不影响理解, 由于目前处于mount阶段, 所以需要初始化相关的数据结构结构(有关hooks是如何存储的, 可参考#10):

  • hook
  • hook.memorizedState
  • hook.queue
  • hook.dispatch

而正常情况下, 会在update阶段产生更新, 最常见的就是用户交互(interactive)时, 用上述例子来说:

  • 点击按钮, 通过调用setNamesetAge来产生一个更新

所以将重点放在update阶段的分析.

FAQ

  1. 如果在mount, 也就是首次渲染阶段, 在当前渲染的函数组件内部产生更新和在其它组件内部产生更新有什么区别?

mount阶段的的dispatch实际上调用了dispatchAction方法, 其内部根据根据产生更新的fiber, 进行不同的处理:

  • 如果当前fiber处于mount阶段, 且在其内部产生了更新
  • 如果当前fiber处于mount阶段, 但不是它内部产生的更新, 新的调度(scheduleWork)

看下dispatchAction的源码:

展开源码
function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  const alternate = fiber.alternate;

  // [情况一]: 如果产生更新的fiber = 当前正在渲染的fiber
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // This is a render phase update. Stash it in a lazily-created map of
    // queue -> linked list of updates. After this render pass, we'll restart
    // and apply the stashed updates on top of the work-in-progress hook.

    // 标识是否在`mount`阶段产生更新
    // 产生的更新会保存到`renderPhaseUpdates`字典上
    // 后续在渲染完当前组件之后, 会根据`didScheduleRenderPhaseUpdate`, 来决定是否处理更新
    didScheduleRenderPhaseUpdate = true;
    const update: Update<S, A> = {
      expirationTime: renderExpirationTime,
      action,
      eagerReducer: null,
      eagerState: null,
      next: null,
    };
    if (renderPhaseUpdates === null) {
      renderPhaseUpdates = new Map();
    }
    const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
    if (firstRenderPhaseUpdate === undefined) {
      renderPhaseUpdates.set(queue, update);
    } else {
      // Append the update to the end of the list.
      let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
      while (lastRenderPhaseUpdate.next !== null) {
        lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
      }
      lastRenderPhaseUpdate.next = update;
    }
  }
  // [情况二]:  反之, 如果产生更新的不是currentRenderingFiber, 那么调用scheduleWork重新调度
  else {
    flushPassiveEffects();

    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update: Update<S, A> = {
      expirationTime,
      action,
      eagerReducer: null,
      eagerState: null,
      next: null,
    };

    // Append the update to the end of the list.
    const last = queue.last;
    if (last === null) {
      // This is the first update. Create a circular list.
      update.next = update;
    } else {
      const first = last.next;
      if (first !== null) {
        // Still circular.
        update.next = first;
      }
      last.next = update;
    }
    queue.last = update;

    scheduleWork(fiber, expirationTime);
  }
}

2. update阶段

update阶段产生的更新与mount稍有不同.

ReactCurrentDispatcher

首先, 将ReactCurrentDispatcher的值由HooksDispatcherOnMount变成了HooksDispatcherOnUpdate:

展开源码
const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

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

updateState

内部实现较简单:

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

updateReducer

遍历整个hook.queue.update, 根据basicStaticReducer计算出新的state,

展开源码
function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  invariant(
    queue !== null,
    'Should have a queue. This is likely a bug in React. Please file an issue.',
  );

  queue.lastRenderedReducer = reducer;

  // ! numberOfReRenders记录组件重新渲染的次数
  if (numberOfReRenders > 0) {
    // This is a re-render. Apply the new render phase updates to the previous
    // work-in-progress hook.
    const dispatch: Dispatch<A> = (queue.dispatch: any);
    if (renderPhaseUpdates !== null) {
      // Render phase updates are stored in a map of queue -> linked list
      const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
      if (firstRenderPhaseUpdate !== undefined) {
        renderPhaseUpdates.delete(queue);
        let newState = hook.memoizedState;
        let update = firstRenderPhaseUpdate;
        do {
          // Process this render phase update. We don't have to check the
          // priority because it will always be the same as the current
          // render's.
          const action = update.action;
          newState = reducer(newState, action);
          update = update.next;
        } while (update !== null);

        // Mark that the fiber performed work, but only if the new state is
        // different from the current state.
        if (!is(newState, hook.memoizedState)) {
          markWorkInProgressReceivedUpdate();
        }

        hook.memoizedState = newState;
        // Don't persist the state accumlated from the render phase updates to
        // the base state unless the queue is empty.
        // TODO: Not sure if this is the desired semantics, but it's what we
        // do for gDSFP. I can't remember why.
        if (hook.baseUpdate === queue.last) {
          hook.baseState = newState;
        }

        queue.lastRenderedState = newState;

        return [newState, dispatch];
      }
    }
    return [hook.memoizedState, dispatch];
  }

  // ! hook.queue为单向循环链表结构

  // The last update in the entire queue
  const last = queue.last;

  // The last update that is part of the base state.
  // ? baseUpdate记录当前被中断(优先级不够)的更新的上一个更新
  // ? baseState记录被中断前计算出来的state
  // ? 这是由于当前queue.update优先级不足, 所以需要记录一个位置,
  // ? 等到下一次react应用整体更新时重新处理更新
  const baseUpdate = hook.baseUpdate;
  const baseState = hook.baseState;

  // Find the first unprocessed update.
  // ! 找到第一个未处理的更新
  let first;
  if (baseUpdate !== null) {
    if (last !== null) {
      // For the first update, the queue is a circular linked list where
      // `queue.last.next = queue.first`. Once the first update commits, and
      // the `baseUpdate` is no longer empty, we can unravel the list.
      last.next = null;
    }
    first = baseUpdate.next;
  } else {
    first = last !== null ? last.next : null;
  }
  if (first !== null) {
    let newState = baseState;
    let newBaseState = null;
    let newBaseUpdate = null;
    let prevUpdate = baseUpdate;
    let update = first;
    let didSkip = false;

    // ! 从queue中最后一个update开始, 遍历整个循环链表
    do {
      const updateExpirationTime = update.expirationTime;
      // ! 如果queue.update优先级 < 当前渲染进程的优先级
      if (updateExpirationTime < renderExpirationTime) {
        // Priority is insufficient. Skip this update. If this is the first
        // skipped update, the previous update/state is the new base
        // update/state.
        if (!didSkip) {
          didSkip = true;
          newBaseUpdate = prevUpdate;
          newBaseState = newState;
        }
        // Update the remaining priority in the queue.
        if (updateExpirationTime > remainingExpirationTime) {
          remainingExpirationTime = updateExpirationTime;
        }
      } else {
        // Process this update.
        if (update.eagerReducer === reducer) {
          // If this update was processed eagerly, and its reducer matches the
          // current reducer, we can use the eagerly computed state.
          newState = ((update.eagerState: any): S);
        } else {
          const action = update.action;
          newState = reducer(newState, action);
        }
      }
      prevUpdate = update;
      update = update.next;
    } while (update !== null && update !== first);

    if (!didSkip) {
      newBaseUpdate = prevUpdate;
      newBaseState = newState;
    }

    // Mark that the fiber performed work, but only if the new state is
    // different from the current state.
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }

    hook.memoizedState = newState;
    hook.baseUpdate = newBaseUpdate;
    hook.baseState = newBaseState;

    queue.lastRenderedState = newState;
  }

  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

dispatchAction

重点来了, 回想一下, 在初次渲染的时候, 也就是在mountState时, 会初始化hook.dispatch:

const dispatch = hook.dispatch = dispatchAction.bind(
  null,
  currentlyRenderingFiber,
  currentlyRenderingFiber.queue,
);

后续所有在当前hooks上的dispatch, 都会调用dispatchAction函数, 再来看一下其内部逻辑:

展开源码
function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  const alternate = fiber.alternate;

  // [情况一]: 如果产生更新的fiber = 当前正在渲染的fiber
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // This is a render phase update. Stash it in a lazily-created map of
    // queue -> linked list of updates. After this render pass, we'll restart
    // and apply the stashed updates on top of the work-in-progress hook.

    // 标识是否在`mount`阶段产生更新
    // 产生的更新会保存到`renderPhaseUpdates`字典上
    // 后续在渲染完当前组件之后, 会根据`didScheduleRenderPhaseUpdate`, 来决定是否处理更新
    didScheduleRenderPhaseUpdate = true;
    const update: Update<S, A> = {
      expirationTime: renderExpirationTime,
      action,
      eagerReducer: null,
      eagerState: null,
      next: null,
    };
    if (renderPhaseUpdates === null) {
      renderPhaseUpdates = new Map();
    }
    const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
    if (firstRenderPhaseUpdate === undefined) {
      renderPhaseUpdates.set(queue, update);
    } else {
      // Append the update to the end of the list.
      let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
      while (lastRenderPhaseUpdate.next !== null) {
        lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
      }
      lastRenderPhaseUpdate.next = update;
    }
  }

  // [情况二]: 反之, 如果产生更新的不是currentRenderingFiber, 那么调用scheduleWork重新调度
  // 创建更新, 加入到updateQueue, 重新调度
  // react应用每产生一次更新, 没有特殊情况下, 都会调用scheduleWork
  else {
    flushPassiveEffects();

    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update: Update<S, A> = {
      expirationTime,
      action,
      eagerReducer: null,
      eagerState: null,
      next: null,
    };

    // Append the update to the end of the list.
    const last = queue.last;
    if (last === null) {
      // This is the first update. Create a circular list.
      update.next = update;
    } else {
      const first = last.next;
      if (first !== null) {
        // Still circular.
        update.next = first;
      }
      last.next = update;
    }
    queue.last = update;

    scheduleWork(fiber, expirationTime);
  }
}

hook

hookshook不一样. 前者指的是各种API(useStateuseEfffect); 后者则特指单个hooks的存储结构. 在这里被坑了好久😂...

在react的函数组件内部, 每定义一个hooksAPI, 会产生新的hook对象, 追加到fiber.memorizedState链表中, 关于更多的有关于hook的存储结构的分析, 我抽离出了新的issue:

解以前心头之惑

redux

看了React.useState的源码, 才发现其设计思想与redux极其类似, 或者说它完全借鉴了redux的策略:

  • dispatch一个action
  • 内部经过reducer处理得到新的state
  • 返回新的state

didScheduleRenderPhaseUpdate

这是我比较困惑的全局变量之一.

起什么作用

渲染完当前fiber之后, 根据其值是否为true, 来清理掉在渲染阶段产生的更新

在何处使用

renderWithHooks开始, 根据其值决定是否重新执行该组件

在何处改变

当调用dispatch之时, 在dispatchAction内部, 如果产生更新的fiber和当前正在渲染的fiber相同, 那么表明在渲染阶段产生了更新, 将didScheduleRenderPhaseUpdate设置为true.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
note 简单笔记
Projects
None yet
Development

No branches or pull requests

1 participant