React Hooks

在web应用无所不能的2019年,组成应用的Components也越来越复杂,冗长而难以复用的代码给开发者们造成了很多麻烦。

  • 难以复用 stateful 的代码,render props及HOC虽然解决了问题,但对组件的包裹改变了组件树的层级,存在冗余;
  • 在ComponentDidMount、ComponentDidUpdate、ComponentWillUnmount等生命周期中做获取数据,订阅/取消事件,操作ref等相互之间无关联的操作,而把订阅/取消这种相关联的操作分开,降低了代码的可读性;
  • 与其他语言中的class概念差异较大,需要对事件处理函数做bind操作,令人困扰。另外class也不利于组件的AOT compile(静态编译),minify(压缩)及hot loading(热加载)。

在这种背景下,React在 16.8.0 引入了React Hooks。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

useState

可以在 useState 附近使用 useEffect 处理副作用

import React from 'react'
function Counter() {
  // 数组解构, 创建 `count` 和 `setCount` 两个变量,用来接收返回的第一、二个值。
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

初始 state 参数只有在第一次渲染的会被用到,而入参不一定要是一个对象,甚至可以是函数。(函数为入参时,初始值为函数的返回值)

探究 useState 源码内部实现

接下来我们去源码一探看似神奇的 useState 到底做了什么!带着如下疑问,寻找从源码寻找答案。

  1. React.useState 都接收那些参数?
  2. 纯函数每次更新同样触发 React.useState 这个方法,为何拿到的值是我们需要,而非保持不变呢?
  3. 为何调用 React.useState 时,入参只在第一次被使用?
  4. 申明使用多个 React.useState 它们是如何甄别并通过第二个返回的函数有效更新的?

为了便于理解隐藏部分源码内容。

/**
  * ReactCurrentDispatcher 是一个内部储存状态的状态机.
  * 主要作用还是用于切换不同执行时机的 dispatcher 对象
  * */
const ReactCurrentDispatcher = { current: null }
function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  return dispatcher;
}
function useState<S>(initialState: (() => S) | S) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

初始化阶段

/**
* 初次渲染 ReactCurrentDispatcher.current 会指向 HooksDispatcherOnMountInDEV 对象
*/
const HooksDispatcherOnMountInDEV = {
  // 其它的 useXX 实现
  useState(initialState) {
    return mountState(initialState)
  }
}
function mountState(initialState) {
  // 初始化 hook,用于保存首次的默认值,后续修改也会对它进行操作
  const hook = {
    memoizedState: null,
    baseState: null,
    queue: null,
    baseUpdate: null,
    next: null
  };
  // 此处一系列的复制操作
  // 一些标示变量会存储这个hook,后续更新阶段会寻找并处理该值

  // 此处回答第一问:`React.useState` 接收任意参数,并且如果参数是函数时,默认值为函数的返回值!
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  // hook 缓存初始值
  hook.memoizedState = hook.baseState = initialState;

  // 
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    eagerReducer: basicStateReducer,
    eagerState: initialState,
  });

  const dispatch = (queue.dispatch = (dispatchAction.bind(null, currentlyRenderingFiber, queue)));
  return [hook.memoizedState, dispatch];
}

// 其它的一些函数
function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}

更新阶段

/**
* 后续更新渲染 ReactCurrentDispatcher.current 会指向 HooksDispatcherOnUpdateInDEV 对象
*/
const HooksDispatcherOnUpdateInDEV = {
  // 其它的 useXX 实现
  useState(initialState) {
    return updateState(initialState)
  }
}

function updateState(initialState) {
  // 此处回答第三问:updateReducer 函数负责更新操作,initialState 在此函数内不被使用。
  return updateReducer(basicStateReducer, initialState);
}

function updateReducer(reducer, initialArg, init) {
  // 获取初始化时的 hook,如果是动态生成的,则初始化 hook
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  // 删减大量代码,便于理解。进入渲染更新阶段
  if (numberOfReRenders > 0) {

    const dispatch = queue.dispatch;
    /**
    * 此处回答第二问:首先更新渲染阶段 ReactCurrentDispatcher.current 会指向 HooksDispatcherOnUpdateInDEV 对象
    * 而渲染阶段 useState 的返回值为 updateReducer, 如果更新有效,返回新的。反正还是返回旧的 hook。
    */
    if (renderPhaseUpdates !== null) {
      // 获取该次的 queue,内部存有本次更新的一系列数据
      const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
      if (firstRenderPhaseUpdate !== undefined) {
        renderPhaseUpdates.delete(queue);
        let newState = hook.memoizedState;
        let update = firstRenderPhaseUpdate;

        // 获取dispatch后的值
        do {
          const action = update.action;
          newState = reducer(newState, action);
          update = update.next;
        } while (update !== null);
        // 对 hook 进行更新操作
        hook.memoizedState = newState;

        // 返回新的 state,及更新 hook 的 dispatch 方法
        return [newState, dispatch];
      }
    }
   /**
    * 此处回答第四问:每个dispatch内部对 hook.queue 进行操作,来更新hook的属性来同步数据。
    * 每个hook都对应state,避免了多个 useState 间的值互相影响。
    */
    return [hook.memoizedState, dispatch];
  }
}

// 其它的一些函数
function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}

参考文章