深入 React Hooks 原理

概述

React 在 v16.8 提供了 Hook 特性,React Hooks 通过增强函数式组件,为 Function Component 注入一些功能,例如 useState 让原本的 Stateless Function Component 有了状态。

工作原理

接下来我们从 useState 这个 hook 为切入点,打开 React Hooks 源码看看背后的原理。 先看个 Demo:

import React, { useState }  from 'react';

function App() {
  const [count, dispatchCount] = useState(0);

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => dispatchCount(count + 1)}>
            increment
        </button>
    </div>
  )
}

上述 Demo 使用函数组件定义了一个计数器,相对于普通函数组件,该组件提供了 count 的状态,每点击按钮一次 count 就加 1。 接下来我们看下 useState 的源码,它是如何保存 count 状态、更新 count 状态的。

状态保存与更新

去除无关代码之后,可以看到我们调用的 useState 只是一个入口,最终是调用 dispatcher 的一个方法。并且在 React.js 中只负责定义。

// react/src/ReactHooks.js
const ReactCurrentDispatcher = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: (null: null | Dispatcher),
};

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;
}

Hook 只有在 FunctionalComponent 更新的时候才会被调用,在 updateFunctionComponent 的方法中找到了 Hook 更新的入口 renderWithHooks ,在 renderWithHooks 中依照条件对 ReactCurrentDispatcher.current 进行了赋值。

// react-reconciler/src/ReactFiberHooks.js 
function renderWithHooks(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  props: any,
  secondArg: any,
  nextRenderExpirationTime: ExpirationTime,
) {
    // ... 省略无关代码
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        // Mount
        ? HooksDispatcherOnMount
        // Update
        : HooksDispatcherOnUpdate;
    // ...
}

可以看到 Dispatcher 分为 Mount 和 Update。这里我们可以找到对应的 mountState 和 updateState ,其他 hook 也是这么分类的,如果我们要查看其他 hook 代码,均可以在这里找到对应时机的代码。

状态保存

在了解具体代码之前,我们先了解下 Hook 的定义.

Hook = {
    
    
  // 当前 hook 的 state,就是上述 Demo 中的 count 值
  memoizedState: any,
  // 多次调用,保存队列
  queue: UpdateQueue<any, any> | null,
  // 下一个 hook,通过该属性连接成一个 hook 的单向链表
  next: Hook | null,
|};

对于一个组件内的 hook 对象,会被保存在 App 组件对应的 Fiber 对象的 memoizedState 中。保存结构大致如下:

fiber = {
    
    
    memoizedState: {
        memoizedState: initialState,
        queue: {},
        next: {
            memoizedState: initialState,
            queue: {},
            next: null,
      },
    }
}

状态更新

当首次渲染时,调用 mountState 时,返回 [hook.memoizedState, dispatch] 。

// react-reconciler/src/ReactFiberHooks.js 
// 获取 hook 对象
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

  if (workInProgressHook === null) {
    // 将 hook 加到 fiber.memoizedState
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // 插入链表,指定下一个 hook
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    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];
}

调用上述 Demo 中 dispatchCount ,其实就是调用 dispatchAction.bind(null, currentlyRenderingFiber, queue) ,

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
    // 每调用一次 dispatchCount,都会创建一个 update 对象,记录要更新的值 action
    const update: Update<S, A> = {
        action,
        next: null,
        // ...
      };
    // ...
    // 将更新附加到列表的末尾
  const pending = queue.pending;
  if (pending === null) {
    // 这是第一次更新,创建一个循环列表。
    update.next = update;
  } else {
    // 插入新的 update 节点
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;
  // ...
  // 更新渲染调度
  scheduleWork()
}

更新 state

当调用 dispatchCount 时,这时候实际是调用 updateState 对 state 进行合并处理,在 updateReducer 中会遍历 hook 链表,得到最新 memoizedState 并返回。

// react-reconciler/src/ReactFiberHooks.js 
function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
) {
    const hook = updateWorkInProgressHook();
    const queue = hook.queue;
    // ...
    let first = baseQueue.next;
  
    do {
      // 获取传入的 state action
      const action = update.action;
      // 更新 state
      newState = reducer(newState, action);
      // 遍历下一个更新 action
      update = update.next;
    } while (update !== null && update !== first)
      
    hook.memoizedState = newState;
  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

小结

对于 useState 的逻辑,就相当于原有的 class 组件的 state,只是在函数组件中,他的状态存在 Fiber 节点上。通过链表操作遍历更新该 Fiber 节点下的 hook 对象来更新函数组件中的 state。

最后

如果本文对您的有帮助的话可以点赞评论收藏下转发到您的朋友圈!编辑不易非常感谢!

以下资料需要的可以加qq群:954854084或者关注微信公众号:晨曦大前端

以下资料需要的可以加qq群:954854084或者关注微信公众号:晨曦大前端

以下资料需要的可以加qq群:954854084或者关注微信公众号:晨曦大前端

猜你喜欢

转载自blog.csdn.net/CHENXI_0/article/details/108228851