---
title: React中Hooks源码简单解析              
date: 2022/4/30 09:00:00
updated: 2022/5/6 15:42:24
tags:                    
- 前端
- JavaScript 
            
author: 方玉新         
---

## 使用Hooks时的疑惑
Hooks的出现让我们对`Function Component`逐步拥有了对标 `Class Component`的特性，比如私有状态以及生命周期函数等。`useState`与`useReducer`这两个`Hooks`让我们可以在`Function Component`里使用到私有状态。而`useState`其实相当于简化版的`useReducer`，下面就将这两个放到一起简单介绍下。

#### 1、useState使用示例
```javascript
function PersionInfo ({initialAge,initialName}) {
  const [age, setAge] = useState(initialAge);
  const [name, setName] = useState(initialName);
  return (
    <>
      Age: {age}, Name: {name}
      <button onClick={() => setAge(age + 1)}>Growing up</button>
    </>
  );
}
```
> `useState`可以初始化一个私有状态，它会返回这个状态的最新值和一个用来更新状态的方法。而`useReducer`则是针对更复杂的状态管理场景：

#### 2、useReducer使用示例
```javascript
const initialState = {age: 0, name: 'Dan'};
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {...state, age: state.age + action.age};
    case 'decrement':
      return {...state, age: state.age - action.age};
    default:
      throw new Error();
  }
}
function PersionInfo() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Age: {state.age}, Name: {state.name}
      <button onClick={() => dispatch({type: 'decrement', age: 1})}>-</button>
      <button onClick={() => dispatch({type: 'increment', age: 1})}>+</button>
    </>
  );
}
```
> `useReducer`同样也是返回当前最新的状态，并返回一个用来更新数据的方法。

在使用这两个方法的时候也许我们会想过这样的问题：
```javascript
const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
```

#### 问题1、React内部如何区分这两个状态
`Function Component`不像`Class Component`那样可以将私有状态挂载到类实例中并通过对应的`key`来指向对应的状态，而且每次的页面刷新或者组件的重新渲染都会使得`Function`重新执行一遍。所以在`React`中必定有一种机制来区分这些`Hooks`。
```javascript
const [age, setAge] = useState(initialAge);
// 或
const [state, dispatch] = useReducer(reducer, initialState);
```

#### 问题2、React是如何在每次重新渲染之后都能返回最新的状态
`Class Component`因为自身的特点可以将私有状态持久化的挂载到类实例上，每时每刻保存的都是最新的值。而 `Function Component` 由于本质就是一个函数，并且每次渲染都会重新执行。所以`React`必定拥有某种机制去记住每一次的更新操作，并最终得出最新的值返回。当然还会有其他的一些问题，比如这些状态究竟存放在哪？为什么只能在函数顶层使用Hooks而不能在条件语句等里面使用Hooks？

### 源码中找出上面问题的答案
先来了解`useState`以及`useReducer`的源码实现，并从中解答我们在使用Hooks时的种种疑惑。首先我们从源头开始：
```javascript
import React, { useState } from 'react';
```
> 在项目中我们通常会以这种方式来引入`useState`方法，被我们引入的这个`useState`方法是什么样子的呢？其实这个方法就在源码 `packages/react/src/ReactHook.js` 中。
```javascript
// packages/react/src/ReactHook.js
import ReactCurrentDispatcher from './ReactCurrentDispatcher';
function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  // ... 
  return dispatcher;
}
// 我们代码中引入的useState方法
export function useState(initialState) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState)
}
```
从源码中可以看到，我们调用的其实是 `ReactCurrentDispatcher.js` 中的`dispatcher.useState()`，那么我们继续前往`ReactCurrentDispatcher.js`文件：
```javascript
import type { Dispacther } from 'react-reconciler/src/ReactFiberHooks';
const ReactCurrentDispatcher = {
  current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;
```
好吧，它继续将我们带向 `react-reconciler/src/ReactFiberHooks.js`这个文件。那么我们继续前往这个文件。
```javascript
// react-reconciler/src/ReactFiberHooks.js
export type Dispatcher = {
  useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
  useReducer<S, I, A>(
    reducer: (S, A) => S,
    initialArg: I,
    init?: (I) => S,
  ): [S, Dispatch<A>],
  useEffect(
    create: () => (() => void) | void,
    deps: Array<mixed> | void | null,
  ): void,
  // 其他hooks类型定义
}
```
兜兜转转我们终于清楚了`React Hooks` 的源码就放 `react-reconciler/src/ReactFiberHooks.js` 目录下面。在这里如上图所示我们可以看到有每个`Hooks`的类型定义。同时我们也可以看到`Hooks`的具体实现，大家可以多看看这个文件。首先我们注意到，我们大部分的`Hooks`都有两个定义：
```javascript
// react-reconciler/src/ReactFiberHooks.js
// Mount 阶段Hooks的定义
const HooksDispatcherOnMount: Dispatcher = {
  useEffect: mountEffect,
  useReducer: mountReducer,
  useState: mountState,
 // 其他Hooks
};
// Update阶段Hooks的定义
const HooksDispatcherOnUpdate: Dispatcher = {
  useEffect: updateEffect,
  useReducer: updateReducer,
  useState: updateState,
  // 其他Hooks
};
```
从这里可以看出，我们的`Hooks`在`Mount`阶段和`Update`阶段的逻辑是不一样的。在`Mount`阶段和`Update`阶段他们是两个不同的定义。我们先来看`Mount`阶段的逻辑。在看之前我们先思考一些问题。`React Hooks`需要在`Mount`阶段做什么呢？就拿我们的`useState`和`useReducer`来说：
* 我们需要初始化状态，并返回修改状态的方法，这是最基本的。
* 我们要区分管理每个Hooks。
* 提供一个数据结构去存放更新逻辑，以便后续每次更新可以拿到最新的值。

先看下mountState的实现。
```javascript
// react-reconciler/src/ReactFiberHooks.js
function mountState (initialState) {
  // 获取当前的Hook节点，同时将当前Hook添加到Hook链表中
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  // 声明一个链表来存放更新
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer,
    lastRenderedState,
  });
  // 返回一个dispatch方法用来修改状态，并将此次更新添加update链表中
  const dispatch = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  )));
  // 返回当前状态和修改状态的方法 
  return [hook.memoizedState, dispatch];
}
```

#### 区分管理Hooks
关于初始化状态并返回状态和更新状态的方法。这个没有问题，源码也很清晰利用`initialState`来初始化状态，并且返回了状态和对应更新方法 `return [hook.memoizedState, dispatch]`。那么我们来看看`React`是如何区分不同的`Hooks`的，这里我们可以从 `mountState` 里的 `mountWorkInProgressHook`方法和`Hook`的类型定义中找到答案。
```javascript
// react-reconciler/src/ReactFiberHooks.js
export type Hook = {
  memoizedState: any,
  baseState: any,
  baseUpdate: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null,  // 指向下一个Hook
};
```
首先从`Hook`的类型定义中就可以看到，`React` 对`Hooks`的定义是链表。也就是说我们组件里使用到的`Hooks`是通过链表来联系的，上一个`Hooks`的`next`指向下一个`Hooks`。这些`Hooks`节点是怎么利用链表数据结构串联在一起的呢？相关逻辑就在每个具体`mount` 阶段 `Hooks`函数调用的 `mountWorkInProgressHook`方法里：

```javascript
// react-reconciler/src/ReactFiberHooks.js
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    queue: null,
    baseUpdate: null,
    next: null,
  };
  if (workInProgressHook === null) {
    // 当前workInProgressHook链表为空的话，
    // 将当前Hook作为第一个Hook
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    // 否则将当前Hook添加到Hook链表的末尾
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
```
在`mount`阶段，每当我们调用`Hooks`方法，比如`useState`，`mountState`就会调用`mountWorkInProgressHook` 来创建一个`Hook`节点，并把它添加到`Hooks`链表上。比如我们的这个例子：
```javascript
const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
useEffect(() => {})
```
那么在mount阶段，就会生产如下图这样的单链表：
![链式图](/yifu-study-front-share/images/hooks1.png)

#### 返回最新的值
`useState`和`useReducer`都是使用了一个`queue`链表来存放每一次的更新。以便后面的`update`阶段可以返回最新的状态。每次我们调用`dispatchAction`方法的时候，就会形成一个新的`updata`对象，添加到`queue`链表上，而且这个是一个循环链表。可以看一下`dispatchAction`方法的实现：
```javascript
// react-reconciler/src/ReactFiberHooks.js
// 去除特殊情况和与fiber相关的逻辑
function dispatchAction(fiber,queue,action) {
  const update = {
    action,
    next: null,
  };
  // 将update对象添加到循环链表中
  const last = queue.last;
  if (last === null) {
    // 链表为空，将当前更新作为第一个，并保持循环
    update.next = update;
  } else {
    const first = last.next;
    if (first !== null) {
      // 在最新的update对象后面插入新的update对象
      update.next = first;
    }
    last.next = update;
  }
  // 将表头保持在最新的update对象上
  queue.last = update;
  // 进行调度工作
  scheduleWork();
}
```
也就是每次执行`dispatchAction`方法，比如`setAge`或`setName`。就会创建一个保存着此次更新信息的`update`对象，添加到更新链表`queue`上。然后每个`Hooks`节点就会有自己的一个`queque`。比如假设我们执行了下面几个语句：
```javascript
setAge(19);
setAge(20);
setAge(21);
```
那么Hooks链表就会变成这样：
![链式图](/yifu-study-front-share/images/hooks2.png)
在`Hooks`节点上面，会如上图那样，通过链表来存放所有的历史更新操作。以便在`update`阶段可以通过这些更新获取到最新的值返回。这就是在第一次调用`useState`或`useReducer`之后，每次更新都能返回最新值的原因。再来看看`mountReducer`，你会发现和`mountState`几乎一摸一样，只是状态的初始化逻辑有那么一点区别。毕竟`useState`其实就是阉割版的`useReducer`。这里不详细介绍`mountReducer`了。
```javascript
// react-reconciler/src/ReactFiberHooks.js
function mountReducer(reducer, initialArg, init,) {
  // 获取当前的Hook节点，同时将当前Hook添加到Hook链表中
  const hook = mountWorkInProgressHook();
  let initialState;
  // 初始化
  if (init !== undefined) {
    initialState = init(initialArg);
  } else {
    initialState = initialArg ;
  }
  hook.memoizedState = hook.baseState = initialState;
  // 存放更新对象的链表
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
  });
  // 返回一个dispatch方法用来修改状态，并将此次更新添加update链表中
  const dispatch = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  )));
 // 返回状态和修改状态的方法
  return [hook.memoizedState, dispatch];
}
```
然后我们来看看`update`阶段，也就是看一下我们的`useState`或`useReducer`是如何利用现有的信息，去返回最新的最正确的值的。先来看一下`useState`在`update`阶段的代码也就是`updateState`：
```javascript
// react-reconciler/src/ReactFiberHooks.js
function updateState(initialState) {
  return updateReducer(basicStateReducer, initialState);
}
```
可以看到，`updateState`底层调用的其实就调用`updateReducer`，因为我们调用`useState`的时候，并不会传入`reducer`，所以这里会默认传递一个`basicStateReducer`进去。我们先看看这个`basicStateReducer`：
```javascript
// react-reconciler/src/ReactFiberHooks.js
function basicStateReducer(state, action){
  return typeof action === 'function' ? action(state) : action;
} 
```
在使用`useState(action)`的时候，`action`通常会是一个值，而不是一个方法。所以b`aseStateReducer`要做的其实就是将这个`action`返回。来继续看一下`updateReducer`的逻辑：
```javascript
// react-reconciler/src/ReactFiberHooks.js
// 去掉与fiber有关的逻辑
function updateReducer(reducer,initialArg,init) {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  // 拿到更新列表的表头
  const last = queue.last;
  // 获取最早的那个update对象
  first = last !== null ? last.next : null;
  if (first !== null) {
    let newState;
    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`阶段，也就是我们组件第二次第三次。。执行到`useState`或`useReducer`的时候，会遍历`update`对象循环链表，执行每一次更新去计算出最新的状态来返回，以保证我们每次刷新组件都能拿到当前最新的状态。`useState`的`reducer`是`baseStateReducer`，因为传入的`update.action`为值，所以会直接返回`update.action`，而`useReducer` 的`reducer`是用户定义的`reducer`，所以会根据传入的`action`和每次循环得到的`newState`逐步计算出最新的状态。
![链式图](/yifu-study-front-share/images/hooks3.png)

### useState/useReducer 小总结
* React 如何管理区分Hooks？
React通过单链表来管理Hooks
按Hooks的执行顺序依次将Hook节点添加到链表中
* useState和useReducer如何在每次渲染时，返回最新的值？
每个Hook节点通过循环链表记住所有的更新操作
在update阶段会依次执行update循环链表中的所有更新操作，最终拿到最新的state返回
* 为什么不能在条件语句等中使用Hooks？
链表！
![链式图](/yifu-study-front-share/images/hooks4.png)
如图所示，我们在`mount`阶段调用了`useState('A')`, `useState('B')`, `useState('C')`，如果我们将`useState('B')` 放在条件语句内执行，并且在`update`阶段中因为不满足条件而没有执行的话，那么没法正确的重回`Hooks`链表中获取信息。React也会报错。

#### Hooks链表位置
现在已经了解了`React` 通过链表来管理 `Hooks`，同时也是通过一个循环链表来存放每一次的更新操作，得以在每次组件更新的时候可以计算出最新的状态返回给我们。那么我们这个`Hooks`链表又存放在那里呢？理所当然的我们需要将它存放到一个跟当前组件相对于的地方。那么很明显这个与组件一一对应的地方就是我们的`FiberNode`。
![链式图](/yifu-study-front-share/images/hooks5.png)
如图所示，组件构建的`Hooks`链表会挂载到`FiberNode`节点的`memoizedState`上面去。
![链式图](/yifu-study-front-share/images/hooks6.png)

#### useEffect
先看下`useEffect`是如何工作的
```javascript
function PersionInfo () {
  const [age, setAge] = useState(18);
  useEffect(() =>{
      console.log(age)
  }, [age])
 const [name, setName] = useState('Dan');
 useEffect(() =>{
      console.log(name)
  }, [name])
  return (
    <>
      ...
    </>
  );
}
```
`PersionInfo`组件第一次渲染的时候会在控制台输出`age`和`name`，在后面组件的每次`update`中，如果`useEffect`中的`deps`依赖的值发生了变化的话，也会在控制台中输出对应的状态，同时在`unmount`的时候就会执行清除函数（如果有）。`React`中是怎么实现的呢？其实很简单，在`FiberNode`中通过一个`updateQueue`来存放所有的`effect`，然后在每次渲染之后依次执行所有需要执行的`effect`。`useEffect` 也分为`mountEffect`和`updateEffect`。
#### mountEffect
```javascript
// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑
function mountEffect( create,deps,) {
  return mountEffectImpl(
    create,
    deps,
  );
}
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
  // 获取当前Hook，并把当前Hook添加到Hook链表
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 将当前effect保存到Hook节点的memoizedState属性上，
  // 以及添加到fiberNode的updateQueue上
  hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    next: (null: any),
  };
  // componentUpdateQueue 会被挂载到fiberNode的updateQueue上
  if (componentUpdateQueue === null) {
    // 如果当前Queue为空，将当前effect作为第一个节点
    componentUpdateQueue = createFunctionComponentUpdateQueue();
   // 保持循环
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    // 否则，添加到当前的Queue链表中
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect; 
}
```
可以看到在`mount`阶段，`useEffect`做的事情就是将自己的`effect`添加到了`componentUpdateQueue`上。这个`componentUpdateQueue`会在`renderWithHooks`方法中赋值到`fiberNode`的`updateQueue`上。
```javascript
// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑
export function renderWithHooks() {
   const renderedWork = currentlyRenderingFiber;
   renderedWork.updateQueue = componentUpdateQueue;
}
```
也就是在`mount`阶段我们所有的`effect`都以链表的形式被挂载到了`fiberNode`上。然后在组件渲染完毕之后，`React`就会执行`updateQueue`中的所有方法。
![链式图](/yifu-study-front-share/images/hooks7.png)

#### updateEffect
```javascript
// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑

function updateEffect(create,deps){
  return updateEffectImpl(
    create,
    deps,
  );
}

function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps){
  // 获取当前Hook节点，并把它添加到Hook链表
  const hook = updateWorkInProgressHook();
  // 依赖 
  const nextDeps = deps === undefined ? null : deps;
 // 清除函数
  let destroy = undefined;

  if (currentHook !== null) {
    // 拿到前一次渲染该Hook节点的effect
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      // 对比deps依赖
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 如果依赖没有变化，就会打上NoHookEffect tag，在commit阶段会跳过此
        // effect的执行
        pushEffect(NoHookEffect, create, destroy, nextDeps);
        return;
      }
    }
  }
  hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}
```
`update`阶段和`mount`阶段类似，只不过这次会考虑`effect` 的依赖`deps`，如果此次更新`effect`的依赖没有变化的话，就会被打上`NoHookEffect`标签，最后会在`commit`阶段跳过改`effect`的执行。

```javascript
function commitHookEffectList(unmountTag,mountTag,finishedWork) {
  const updateQueue = finishedWork.updateQueue;
  let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & unmountTag) !== NoHookEffect) {
        // Unmount 阶段执行tag !== NoHookEffect的effect的清除函数 （如果有的话）
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          destroy();
        }
      }
      if ((effect.tag & mountTag) !== NoHookEffect) {
        // Mount 阶段执行所有tag !== NoHookEffect的effect.create，
        // 我们的清除函数（如果有）会被返回给destroy属性，一遍unmount执行
        const create = effect.create;
        effect.destroy = create();
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
```
### useEffect 小总结
![链式图](/yifu-study-front-share/images/hooks8.png)

useEffect做了什么？
`FiberNdoe`节点中又一个`updateQueue`链表来存放所有的本次渲染需要执行的`effect`。
`mountEffect`阶段和`updateEffect`阶段会把`effect` 挂载到`updateQueue`上。
`updateEffect`阶段，`deps`没有改变的`effect`会被打上`NoHookEffect tag`，`commit`阶段会跳过该`Effect`。
到此为止，`useState/useReducer/useEffect`源码也阅读完毕了，相信有了这些基础，剩下的Hooks的源码阅读不会成问题，最后放上完整图示：
![链式图](/yifu-study-front-share/images/hooks9.png)
