【万字长文】React hooks 源码详解

2,858 阅读35分钟

前言

其实了解react的源码,并不是一件高成本的事,它的内部架构没有大家想象中的那么难,而阅读源码的收获,却是远远大过了成本,能够学习到其中优秀的设计模式和数据架构,并对业务中的熟练度有非常大的提升。因此,这是在前端进阶上非常值得去做的一件事。

推荐的学习路线:

从社区上一些解读文章中了解react的整体架构和运作流程,然后自己再对照源码,对其中的各个环节进行细致的研究和验证。

本文主要解释 hooks 这部分的源码,对于fiber架构和任务调度,只会说一下必须要用到的部分。

通过这一篇文章,你可以懂得:

  • fiber的基础架构
  • hooks的基础架构
  • hooks大部分api的内部实现
  • 异步可中断模型基础

代码版本:17.0.1

Fiber 架构

Fiber 数据结构

正如同react推崇的组件化一样,在源码内部,也是由一个个"组件"去组成整个架构。这个"组件"就是fiber,它是 React 中的一个基本工作单元,React的一切操作都要基于它去实现。

interface Fiber {
  // 1. Instance 类型信息
  // 标记 Fiber 的实例的类型, 例如函数组件、类组件、宿主组件(即dom)
  tag: WorkTag,
  // class、function组件的构造函数,或者dom组件的标签名。
  type: any,
  // class、function组件的构造函数,或者dom组件的标签名。
  elementType: any,
  // DOM节点 | Class实例
  // 函数组件则为空
  stateNode: any,
	
  key: key,
  ref: ref,
	
  // 2. Fiber 结构信息
  // 指向父fiber 节点
  return: Fiber | null,
  // 指向子fiber节点
  child: Fiber | null,
  // 指向兄弟fiber节点
  sibling: Fiber | null,
	// 指向另外一颗树中对应的fiber节点
  alternate: Fiber | null,

  // 3. Fiber节点的状态
  // 本次更新的props
  pendingProps: any,
  // 上一次渲染的props
  memoizedProps: any, 
  // 如果是class组件,会保存上一次渲染的state
  // 如果是hooks组件,会保存所有hooks组成的链表
  memoizedState: any,
  // 如果是class,将保存setState产生的update链表
  // 如果是hooks,这个地方会存放effect链表
  // 如果是dom节点,会存放他所需更新的props
  updateQueue: UpdateQueue<any> | null, 

  // 4. 副作用
  // 用二进制来存储的当前节点的所需执行的操作,如节点更新、删除、移动
  flags: Flags,
  // 副作用链表,会把所有需要执行副作用的fiber串联起来
  nextEffect: Fiber | null,
  firstEffect: Fiber | null, 
  lastEffect: Fiber | null, 
	
  // 5. 调度优先级相关
  lanes = NoLanes;
  childLanes = NoLanes;

}

通俗易懂的说,所有的element都是一个独立的fiberelement的同级元素用sibling链接,子元素用child链接,这样就由上至下形成了一个fiber tree

例如:

function App() {
  return (
    <div className="App">
      <SubTree />
    </div>
  );
}

function SubTree() {
  return (
    <p>
      subTree
    </p>
  )
}

fiberTree.png

工作流程

react的工作流程实际上就是遍历fiber tree,对每个fiber去执行对应的工作。

// 异步可中断任务暂时不提
function workLoopSync() {
  // workInProgress 是一个全局变量,保存当前所要执行的fiber节点,它会从root节点开始
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

performUnitOfWork中会执行当前fiber,然后把这个fiberchild子节点赋值给workInProgress,当子节点不存在时,就把sibling兄弟节点赋值给workInProgress

上层的workLoopSync函数的 while循环会根据下个workInProgress去遍历。这样就能实现一个深度优先遍历,从而把所有的fiber执行完毕。

performUnitOfWork里,又分为两个阶段,一个是beginWork,一个是completeWork

  • beginWork
    • 执行组件render
      • class组件,会执行实例化,处理state,调用挂载前生命周期钩子等等。最后执行render,获取返回的jsx
      • function组件,会执行组件的构造函数,里面包括了hooks的一系列调用,最后获取返回的jsx
    • 对返回的jsx执行reconcile,也就是俗称的diff。
      • 根据diff生成当前fiber的子节点,并标记上对应的flag,比如这个节点是更新、删除、移动。
      • 这个生成的子节点,会返回出去,赋值给workInProgress,然后上层函数workLoopSync进行下一轮遍历,执行这个新生成的fiber节点
  • completeWork,当遍历到叶子节点,会执行它,对fiber tree进行一个回溯,去迭代return,也就是父节点。在发现有sibling兄弟节点时,会退出遍历,并赋值给workInProgress,以便上层workLoopSync函数遍历。
    • 生成dom节点,并把子孙dom节点插入进去。组成一个虚拟dom
    • 处理props
    • 把所有含有副作用的fiber节点用firstEffectlastEffect链接起来,组成一个链表,以便在commit时去遍历执行。

熟悉算法的人不难发现,beginWorkcompleteWork的交替遍历,其实就是一个回溯法。

completeWork执行到root根节点时,证明所有的工作已经完成,就会执行commitRoot,它又分为三个阶段:

  • before mutation(执行dom操作前)
    • 调用挂载前的生命周期钩子,比如getSnapshotBeforeUpdate,调度useEffect
  • mutation(执行dom操作)
    • 执行dom操作,如果有组件被删除,那么还会调用componentWilUnmountuseLayoutEffect的销毁函数
  • layout(执行dom操作后)
    • 切换fiber tree
    • 调用componentDidUpdate | componentDidMount或者useLayoutEffect的回调函数。
    • layout结束后,执行之前调度的useEffect的创建和销毁函数。

总结上文,在performUnitOfWork时,我们称之为协调阶段,主要依靠beginWorkcompleteWork去交替执行每个fiber,在commitRoot时,我们称之为提交阶段。

双缓冲

我们在看数字电视的时候,切换下一台往往要经过一个黑屏,然后画面才显示出来。双缓冲就是在切换的时候,先在内存中进行构建UI,完成后直接渲染在界面上。这样就省掉了中间过渡的时间,从而优化体验。

react中,有两棵fiber tree,一个是current fiber,一个是workInProcess fiber。这两个fiber通过alternate属性来进行联系。

current fiber是已经渲染在界面上的fiberfiber的根节点叫做rootFiberrootFiber.current = current fiber,它的current指向的那个fiber,就是当前已经渲染在界面上的fiber

workInProcess fiber是由此次更新,而正在内存中构建的fiber。构建完成后,rootFiber.current = workInProcess fiber,就切换为了current fiber,从而渲染到界面上。

由于是在内存中构建,所以它可以随时中断和恢复,不阻塞浏览器渲染。根据优先级而选择先后执行的任务,优先级高的先执行,优先级低的后执行。

联系上文「工作流程」,我们知道react的执行是遍历整个fiber tree,在遍历中,会根据current fiber而去clone一个新的workInPorcess fiber,这个操作是在reconcile中执行,如果是mount时,那么没有current fiber,会直接创建。

一直向下遍历和clone,就会创建出一个新的fiber tree,也就是workInPorcess fiber tree。然后根据这个新生成的树去提交,渲染到界面上。

在下文介绍的源码中,有许多是以current为前缀,这一般就是current fiber,老节点,已经渲染在界面上的。另外则是以workInProcess为前缀,一般就是workInProcess fiber,新节点,正在内存中构建的。

current fiber在下文统称cur fiberworkInProcess fiber在下文统称wip fiber

总结

  1. react的组件架构是由一个个fiber组成的树组成,他的工作流程就是遍历fiber tree去执行每一个工作单元。分为协调阶段,主要负责处理更新和reconcile,收集副作用并链接起来。和提交阶段,负责把副作用节点更新到界面上。
  2. fiber有新旧两棵树,一个是current fiber,是已经渲染在界面上的。一个是workInPorcess,由当前的更新触发而在内存中构建的。构建完成,wip fiber就会替换cur fiber,渲染到界面上。

Hooks 架构

数据结构

先简单看一下hooks所要用到的数据结构,心理有个大概的印象就行。

hook

每一个hook方法的声明,都会生成一个对应的hook对象,来存储一些数据。各自生成的hook会以next链接在一起,组成一个链表。然后挂载到fiber节点的memoizedState

export type Hook = {
  // 上次渲染后的state
  memoizedState: any,
  // 通过已处理好的update,来计算出的state,下文再详述。
  baseState: any,
  // 尚需处理的update,通常是上一轮render中遗留下的优先级过低而暂缓执行的update
  baseQueue: Update<any, any> | null,
  // 当前触发的update链表
  queue: UpdateQueue<any, any> | null,
  // 链接下一个hook
  next: Hook | null,
};

例如:

const [count, setCount] = useState(0);
const [price, setPrice] = useState(10);

对应的hook链表为:

const hook = {
  memoizedState: 0,
  baseState: 0,
  baseQueue: null,
  queue: null,
  next: {
    memoizedState: 10,
    baseState: 10,
    baseQueue: null,
    queue: null,
	},
}

其中,不同的hook方法,其memoizedState储存的东西各不相同。

方法memoizedState
useState/useReducerstate
useEffect/useLayoutEffecteffect对象
useMemo/useCallback[callback, deps]
useRef{current: any}

Update

这一部分只有useState | useReducer会用到

type Update<S, A> = {
  lane: Lane,
  action: A,
  // 触发dispatch时的reducer
  eagerReducer: ((S, A) => S) | null,
  // 触发dispatch时计算好的state
  eagerState: S | null,
  next: Update<S, A>,
  priority?: ReactPriorityLevel,
};

每触发一次setState,就会生成一个update对象,并链接到hook对象的更新队列中,也就是下文的pending

type UpdateQueue<S, A> = {
  // 存放当前触发的update
  pending: Update<S, A> | null,
  // 存放dispatchAction.bind()的值
  dispatch: (A => mixed) | null,
  // 上一次render时的Reducer
  lastRenderedReducer: ((S, A) => S) | null,
  // 上一次render时的state
  lastRenderedState: S | null,
};

例子:

const [count, setCount] = useState(0);

setCount(1)

这个hook对应为:

const hook = {
  memoizedState: 0,
  baseState: 0,
  baseQueue: null,
  queue: {
    {
      action: 1,
    }
	},
  next: null,
}

render 时,会遍历queue来执行每个update并计算更新。

Effect

这一部分只有useEffect | useLayoutEffect | useImperativeHandle会用到

export type Effect = {|
  // 标记此effect是否需要执行
  tag: HookFlags,
  // 回调函数
  create: () => (() => void) | void,
  // 销毁函数
  destroy: (() => void) | void,
  // 依赖数组
  deps: Array<mixed> | null,
  next: Effect,
|};

例如:

useEffect(() => {
  console.log('effect')
}, [])

对应的数据结构:

const hook = {
  memoizedState: {
    create: () => {console.log('effect')},
    destroy: undefined,
    deps: [],
  },
  baseState: null,
  baseQueue: null,
  queue: null,
  next: null,
}

dispatcher

事实上,组件mount时,与组件update时,调用的是不同的hook方法,会通过dispatcher.current来指向当前所需的方法。

render执行前,会根据cur fiber是否存在而决定全局的dispatcher.current指向mount方法还是update方法。

FunctionComponentrender执行完毕后,dispatcher.current会指向ContextOnlyDispatcher,不再允许hooks方法的声明。

// mount时的Dispatcher
const HooksDispatcherOnMount: Dispatcher = {
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  // ...省略
};

// update时的Dispatcher
const HooksDispatcherOnUpdate: Dispatcher = {
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  // ...
};

export const ContextOnlyDispatcher: Dispatcher = {
  readContext,

  useCallback: throwInvalidHookError,
  useContext: throwInvalidHookError,
  useEffect: throwInvalidHookError,
  useImperativeHandle: throwInvalidHookError,
  useLayoutEffect: throwInvalidHookError,
  useMemo: throwInvalidHookError,
  useReducer: throwInvalidHookError,
  useRef: throwInvalidHookError,
  useState: throwInvalidHookError,
  // ...
};

执行流程

renderWithHooks

调用顺序是beginWork --> updateFunctionComponent --> renderWithHooks, 这个函数是FunctionComponentrender主函数。

它主要做两件事,一个是配置hooks所需的全局变量,一个是执行FunctionComponentrender

简版:

export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
	// 用这个变量来记录当前所执行的fiber
  currentlyRenderingFiber = workInProgress;
  
  // cur fiber 不存在或者不存在hooks链表都视为未挂载
  // 更新Dispatcher指向对应的hooks方法
  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
    ? HooksDispatcherOnMount
    : HooksDispatcherOnUpdate;
  
  // Component就是组件的构造函数
  // 执行render,其中就包括了组件里面声明的hooks方法,他们都是在这个地方被执行的。
  let children = Component(props, secondArg);
  
  // 当前fiber已执行结束,重置这些全局变量
  currentlyRenderingFiber = (null: any);
  
  // 返回在render后返回的jsx
  return children;
}

完整版:

export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
  renderLanes = nextRenderLanes;
  // 用这个变量来记录当前所执行的fiber
  currentlyRenderingFiber = workInProgress;

  // 重置wip fiber的状态,然后在后续重新创建。
  // memoizedState保存了hook链表,这一步先置空,到了render时再从cur fiber中clone每个hook
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;

  // cur fiber 不存在或者不存在hooks链表都视为未挂载
  // 更新Dispatcher指向对应的hooks方法
  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
    ? HooksDispatcherOnMount
    : HooksDispatcherOnUpdate;

  // Component就是组件的构造函数
  // 执行render,其中就包括了组件里面声明的hooks方法,他们都是在这个地方被执行的。
  let children = Component(props, secondArg);

  // 如果在render阶段发生了更新,会直接re-render,重新执行。
  if (didScheduleRenderPhaseUpdateDuringThisPass) {
    let numberOfReRenders: number = 0;
    do {
      didScheduleRenderPhaseUpdateDuringThisPass = false;

      numberOfReRenders += 1;

      currentHook = null;
      workInProgressHook = null;

      workInProgress.updateQueue = null;

      ReactCurrentDispatcher.current = __DEV__
        ? HooksDispatcherOnRerenderInDEV
        : HooksDispatcherOnRerender;

      children = Component(props, secondArg);
    } while (didScheduleRenderPhaseUpdateDuringThisPass);
  }
  
  // 函数组件的render已结束,关闭hooks调用接口
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  // 当前fiber已执行结束,重置这些全局变量
  renderLanes = NoLanes;
  currentlyRenderingFiber = (null: any);

  currentHook = null;
  workInProgressHook = null;

  didScheduleRenderPhaseUpdate = false;

  // 返回在render后返回的jsx
  return children;
}

注意看这个地方

workInProgress.memoizedState = null;

上文说过fibermemoizedState中保存着hooks链表,在render之前,会先把这个引用置空,然后在render中,会根据cur fibermemoizedStateclone出来一个个hook,这个在下文updateWorkInProgress中会详述。

这个过程跟wip fiber tree的创建是一样的,也是要根据老节点来一个个clone,生成新的节点。

再看这个地方

if (didScheduleRenderPhaseUpdateDuringThisPass) {
  do {
  // ...
  } while (didScheduleRenderPhaseUpdateDuringThisPass);
}

这个遍历,是在render阶段发生了更新,然后就会re-render,重新执行。一直到不产生新的更新为止。

如这个例子:

const [count, setCount] = useState(0);

if (count === 0) {
	setCount(1)
}

另外,使用useMemo来做这个操作的话,也是同样的效果,因为它的回调函数也同样是在render阶段执行的。

const [count, setCount] = useState(0);

useMemo(() => {
	if (count === 0) {
		setCount(1)
	}
}, [count])

流程图:

renderWithHooks.png

(图画得不够细,re-render时,只需要清空fiberupdateQueue,而不是全部清空)

let children = Component(props, secondArg);

hooks的调用都集中在这个步骤,也就是render,接下来详细讲一下。

mountWorkInProgressHook

每个hooks方法,都要创建或者取出一个hook节点,然后对这个节点进行写入数据。

mount阶段,会调用mountWorkInProgressHook这个方法,来创建一个hook节点并与其他hook链接在一起,然后挂载到wip fibermemoizedState属性中,以便下次在update时可以从中取出链表。

function mountWorkInProgressHook(): Hook {
  // 创建一个空的hook节点
  const hook: Hook = {
    memoizedState: null,

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

    next: null,
  };

  // workInProgressHook 是一个全局变量,保存当前执行的最后一个hook节点。
  if (workInProgressHook === null) {
    // 把hooks链表保存到wip fiber的memoizedState中
    // currentlyRenderingFiber 这个全局变量在上文renderWithHooks中,被赋值为了当前的 wip fiber
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  // 返回创建好的hook节点,然后不同的hook方法就会写入各自的数据。
  return workInProgressHook;
}

每执行一次,workInProgressHook的指针就会后移,始终指向链表中最后一个节点。

注意currentlyRenderingFiber这个全局变量,是在上文renderWithHooks中,被赋值为了当前的 wip fiber

updateWorkInProgressHook

update阶段,会调用这个方法。主要功能是取出保存在当前fiberhooks链表中对应的hook节点。

保存在cur fibermemoizedState中的hooks链表,下文统称cur hook

保存在wip fibermemoizedState中的hooks链表,下文统称wip hook

简版:

function updateWorkInProgressHook(): Hook {
  // 迭代cur hooks链表
  const current = currentlyRenderingFiber.alternate;
  nextCurrentHook = current.memoizedState;
  
  currentHook = nextCurrentHook;
  nextCurrentHook = nextCurrentHook.next
  
  // 根据 cur hook 节点,clone 一个新的 wip hook 节点
  const newHook: Hook = {
    memoizedState: currentHook.memoizedState,

    baseState: currentHook.baseState,
    baseQueue: currentHook.baseQueue,
    queue: currentHook.queue,

    next: null,
  };
  
  if (workInProgressHook === null) {
    // 即首次创建,挂载到 wip fiber 的 memoizedState上
    currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
  } else {
    // 链接到链表尾部
    workInProgressHook = workInProgressHook.next = newHook;
  }
}

完整版:

function updateWorkInProgressHook(): Hook {
  // 迭代cur hooks链表
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    // 取出 cur fiber
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  // 迭代wip hooks链表
  let nextWorkInProgressHook: null | Hook;
  // workInProgressHook为null说明这是第一次取出wip hook,即首次创建。
  if (workInProgressHook === null) {
    // 只有re-render的情况下,wip fiber节点中仍然存在hooks链表,因为在之前的render中已经创建过wip hook了
    // 普通情况下,currentlyRenderingFiber.memoizedState会为null
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    // 普通情况下,workInProgressHook.next会为null,需要到下文创建新的hook节点然后连接上去
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    // 走入这个分支,是只有在render阶段setState了,导致re-render,这个时候wip hooks 链表已经创建过了。
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
  } else {
		// 根据 cur hook 节点,clone 一个新的 wip hook 节点
    
    currentHook = nextCurrentHook;

    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,

      next: null,
    };

    if (workInProgressHook === null) {
      // 即首次创建,挂载到 wip fiber 的 memoizedState上
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

上面的源码可能大家会看得有点迷糊,我这里理一下逻辑:

  1. render前,wip fibermemoizedState被置空了。

    // renderWithHooks 
    workInProgress.memoizedState = null;
    

    所以在render中,是获取不到hooks链表的,需要从cur fiber来获取。

    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
    

    然后,再由这个链表来创建wip hook

    currentHook = nextCurrentHook;
    
    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,
    
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
    
      next: null,
    };
    

    再挂载到wip fibermemoizedState

    if (workInProgressHook === null) {
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      workInProgressHook = workInProgressHook.next = newHook;
    }
    

    这样就完成了从cur hookclonewip hook的过程。

  2. 但是有个例外,那就是re-renderwip hook已经创建过了

    if (workInProgressHook === null) {
      // 成功获取到hooks链表
      nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
    } else {
      nextWorkInProgressHook = workInProgressHook.next;
    }
    

    那么,直接迭代就行,不用再重新clone

    if (nextWorkInProgressHook !== null) {
      workInProgressHook = nextWorkInProgressHook;
      nextWorkInProgressHook = workInProgressHook.next;
    
      currentHook = nextCurrentHook;
    }
    

他的主要流程就是:

  • 迭代cur hook
  • 迭代wip hook
    • 普通情况下wip hooknextnullre-render情况下存在next
  • cur hookclone一个新节点,成为wip hook。并链接到链表尾部,或wip fibermemoizedState(首个wip hook需要这样)
    • re-render情况下不用clone,迭代wip hook就完了
  • 返回这个hook

总结

  1. render前,会保存当前执行的fibercurrentlyRenderingFiber这个全局变量中,并清空fiber的状态,然后开启hooks方法的调用接口。

    render中,执行组件内的hook方法,如果在这个过程发生了setState,那就会触发re-render

    render后,重置全局变量,关闭hooks方法的调用接口。

  2. 每一个hook方法的声明,都会生成一个对应的hook对象,来存储一些数据。各自生成的hook会以next链接在一起,组成一个链表。然后挂载到fiber节点的memoizedState

  3. mount时,需要创建hook节点,update时,需要取出hook节点,确切的说,是从cur hookclone一个新的wip hook

题外话:

现在大家知道为什么hooks不能条件渲染了吧?如果前后的hook节点不一致,那么取值的顺序就会错误,cur hook也会迭代到一个null

自此,hooks的通用架构已经讲完,接下来就到了各自的hooks方法的内部实现。也就是对mountWorkInProgressHookupdateWorkInProgressHook中返回的hook节点进行的操作。

return workInProgressHook;

Hooks 方法

useState&useReducer

先看基本用法:

const [state, setState] = useState(0);

setState(1);

const [state, dispatch] = useReducer(
  function reducer(state, action) {
    switch (action.type) {
      case 'increment':
        return {count: state.count + 1};
      case 'decrement':
        return {count: state.count - 1};
      default:
        throw new Error();
    }
  }, 
  {count: 0}
);

dispatch({type: 'increment'})

尽管后者要比前者看似复杂不少,但二者实际上是同一个方法,useState可视为useReducer的一个语法糖和简化版,是程序内部帮你赋予了一个reducer.

// update阶段useState实际上调用的方法
function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}
// 默认的reducer
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
	// 如果你在setState中传入的是一个方法,如setState(state => state + 1),那么,他会先进行调用再返回。
  return typeof action === 'function' ? action(state) : action;
}

所以,这两个方法会合在一起说。

mountState

主要流程就是初始化state和更新队列,然后返回statedispatch(也就是setState)。

// mount阶段useState实际上调用的方法
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  // 如上文所述,创建一个hook节点并link到链表中
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
	// 对于useState, 其memoizedState会保存state值
  hook.memoizedState = hook.baseState = initialState;
	// 创建一个更新队列
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
	// dispatchAction 下文再详述
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    // 把当前的 wip fiber 传入dispatch参数中
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

来看一下这个queue,它就是上文「数据结构」中提到的updateQueue,其中pending属性会存放由一个个update 组成的链表,update是由setState时创建,这个之后再说。

const queue = (hook.queue = {
  pending: null,
  dispatch: null,
  lastRenderedReducer: basicStateReducer,
  lastRenderedState: (initialState: any),
});

如果你熟悉链表数据结构的话,不难发现这就是一个链表的头节点,专门用来存放一些通用的数据。

  • pending: 头指针,指向最后一个updateupdate会组成一个循环链表,所以只要pending.next就能获取到第一个节点。

  • lastRenderedState: 存放每次render时的state

  • dispatch

    注意,这是个由bind保存下来的方法,预先传入了wip fiberqueue,所以,你调用setState时一定能获取到最新的state

    const dispatch: Dispatch<
      BasicStateAction<S>,
      > = (queue.dispatch = (dispatchAction.bind(
        null,
        currentlyRenderingFiber,
        queue,
      ): any));
    

其他的逻辑就比较简单了,具体的一些细节留待下文再详述。

接下来讲一下这个dispatch

dispatchAction

这个就是setState时调用的方法,也是上文的dispatch,主要功能是创建一个更新对象,链接到hook节点的更新队列中。

其中有关调度的部分本文先不讲

简版:

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  // 创建一个更新事件
  const update: Update<S, A> = {
    lane,
    action,
    eagerReducer: null,
    eagerState: null,
    next: (null: any),
  };
  
  // 把update链接到更新队列中,组成单循环链表
  const pending = queue.pending;
  if (pending === null) {
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  // pending指向链表的最后一个
  queue.pending = update;
  
  // 开启调度,触发新的一轮更新,也就是走beginWork,completeWork那一套流程。
  scheduleUpdateOnFiber(fiber, lane, eventTime);
}

完整版:

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  
  // 获取事件所需时间,和优先级
  const eventTime = requestEventTime();
  const lane = requestUpdateLane(fiber);

  const update: Update<S, A> = {
    lane,
    action,
    eagerReducer: null,
    eagerState: null,
    next: (null: any),
  };

  // 把update链接到更新队列中,组成单循环链表
  // pending指向链表的最后一个
  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 ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // currentlyRenderingFiber仍然存在,证明这是在render中发生的更新.
    didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  } else {
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // fiber.lanes === NoLanes,说明此前未发生更新,本次是第一个
      // 我们可以预先根据reducer来计算state值,如果与当前值相同,则跳过更新。
      // 如果值不同,则保存在eagerState,下次render时可以直接使用,而无需再计算。
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        // 上次render时的state
        const currentState: S = (queue.lastRenderedState: any);
        const eagerState = lastRenderedReducer(currentState, action);
        update.eagerReducer = lastRenderedReducer;
        // 保存计算出来的state
        update.eagerState = eagerState;
        if (is(eagerState, currentState)) {
          // 如果计算好的state和当前的state相同,则不进行更新调度
          return;
        }
      }
    }
    // 开启调度,触发新的一轮更新,也就是走beginWork,completeWork那一套流程。
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }

  if (enableSchedulingProfiler) {
    markStateUpdateScheduled(fiber, lane);
  }
}

pending是一个循环链表,并且指向链表中最后一个节点

const pending = queue.pending;
if (pending === null) {
  update.next = update;
} else {
  update.next = pending.next;
  pending.next = update;
}
queue.pending = update;

那么,为什么要用这种形式呢?因为它既有把节点push到尾部的需求,即setState。也有从头开始遍历链表的需求,即下文的updateReducer。所以用循环链表可以很简单的实现这两个需求,pending.next就是第一个节点,而pending本身就是最后一个节点。在react源码内的很多地方都采用了这种方式,以最简单的数据结构来实现获取链表两端的需求。

这个地方,这是为了在render阶段发生的setState,然后触发re-render,而要做的一个标记。

if (
  fiber === currentlyRenderingFiber ||
  (alternate !== null && alternate === currentlyRenderingFiber)
) {
  didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
}

还记得renderWithHooks中,对currentlyRenderingFiber的赋值吗?

// `renderWithHooks`
currentlyRenderingFiber = workInProgress;
let children = Component(props, secondArg);
currentlyRenderingFiber = (null: any);

正常情况下, render结束后, currentlyRenderingFiber就不存在了。所以如果fiber === currentlyRenderingFiber,就证明是在render阶段发生的更新

// `renderWithHooks`

  do {
    didScheduleRenderPhaseUpdateDuringThisPass = false;

    numberOfReRenders += 1;

    // Start over from the beginning of the list
    currentHook = null;
    workInProgressHook = null;

    workInProgress.updateQueue = null;

    ReactCurrentDispatcher.current = __DEV__
      ? HooksDispatcherOnRerenderInDEV
    : HooksDispatcherOnRerender;

    children = Component(props, secondArg);
  } while (didScheduleRenderPhaseUpdateDuringThisPass);

如果被标记为didScheduleRenderPhaseUpdateDuringThisPass,就会重新执行render,直到没有为止。

这个地方,则是一个小优化,如果当前setState是第一个,那么在setState时,就会计算一下他的state值。

if (
  fiber.lanes === NoLanes &&
  (alternate === null || alternate.lanes === NoLanes)
) {
  // ...
}

fiber.lanes === NoLanes证明当前没有发生过更新,接下来,会根据它的reducer计算state

const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
  // 上次render时的state
  const currentState: S = (queue.lastRenderedState: any);
  const eagerState = lastRenderedReducer(currentState, action);
  // 保存计算出来的state
  update.eagerReducer = lastRenderedReducer;
  update.eagerState = eagerState;
  if (is(eagerState, currentState)) {
    // 如果计算好的state和当前的state相同,则不进行更新调度
    return;
  }
}

比如这个例子:

const [count, setCount] = useState(0);

useEffect(() => {
  setCount(1) // 第一个setState,会直接计算state,保存在eagerState中
  setCount(2) // 第二个,就不进行计算了,push到queue链表中,留待下次render时执行
}, [])

dispatch之后,会触发一轮新的react更新调度,scheduleUpdateOnFiber(fiber, lane, eventTime);。也就是走beginWork ---> updateFunction ---> renderWithWork那一套流程。再到render时,重新执行那些hooks方法,也就到了update阶段。

updateReducer

update阶段,会循环执行update链表,计算最新的state

简版:

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
	// 取出对应的hook节点
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  let baseQueue = queue.pending;
  let newState = current.baseState;

  // 遍历链表,计算state
	do {
		const action = update.action;
    newState = reducer(newState, action);
	} while (update !== null && update !== first);

  hook.memoizedState = newState;

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

完整版:

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  // 取出对应的hook节点
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  queue.lastRenderedReducer = reducer;

  const current: Hook = (currentHook: any);

  // 上次render未能处理完的update
  let baseQueue = current.baseQueue;

  // 把pending链接到baseQueue的尾部,以便一起迭代执行
	// 下面的这些操作就是把循环链表剪开,然后拼接在一起
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {
    if (baseQueue !== null) {
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    // 为什么也要赋值给cur hook的baseQueue里呢?
    // 这涉及到异步可中断模型,在当前的更新执行时,发生了另一个高优先级的任务,这会打断前一个任务,先执行后一个。
    // 但是这样会使得前一个任务的update丢失,
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }

  if (baseQueue !== null) {
    const first = baseQueue.next;
    // 最新的state,经由所有被执行过的update计算而来
    let newState = current.baseState;

    // 相当于master分支的state和update链表
    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;
    // 遍历链表,计算state
    do {
      const updateLane = update.lane;
      if (!isSubsetOfLanes(renderLanes, updateLane)) {
        // 当前update优先级过低,跳过执行。
        const clone: Update<S, A> = {
          lane: updateLane,
          action: update.action,
          eagerReducer: update.eagerReducer,
          eagerState: update.eagerState,
          next: (null: any),
        };
        // 把这个update添加到下次的baseQueue中,留待下次render时执行
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          // 更新baseState为之前update计算好的那个值
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
        currentlyRenderingFiber.lanes = mergeLanes(
          currentlyRenderingFiber.lanes,
          updateLane,
        );
        markSkippedUpdateLanes(updateLane);
      } else {

        if (newBaseQueueLast !== null) {
          // newBaseQueueLast 存在证明此前有update被跳过
          // 因为update互相之间可能存在依赖,所以从那个被跳过的update起,所有后面的update都链接到newBaseQueueLast中
          // 在下次render一齐执行
          const clone: Update<S, A> = {
            lane: NoLane,
            action: update.action,
            eagerReducer: update.eagerReducer,
            eagerState: update.eagerState,
            next: (null: any),
          };
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }

        if (update.eagerReducer === reducer) {
          // eagerReducer不为null,证明dispatch的时候,使用过这个reducer来计算过state值
          // 所以eagerReducer和eagerState都被赋值了。
          // 再检查一下上个reducer和当前的reducer有没有改变,没有的话就可以直接使用其计算出来的值
          newState = ((update.eagerState: any): S);
        } else {
          // 通过reducer来计算state值
          const action = update.action;
          newState = reducer(newState, action);
        }
      }
      update = update.next;
    } while (update !== null && update !== first);

    if (newBaseQueueLast === null) {
      // 证明所有的update都已经处理完,此时更新baseState
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = (newBaseQueueFirst: any);
    }

    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }

    // 如果有update被跳过,那么memoizedState与baseState会是不同的值
    // baseState会停留在被跳过的update之前的计算值。
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;

    queue.lastRenderedState = newState;
  }

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

可能大家会觉得这部分代码有点复杂,其实原因在于要实现这两个功能:

  1. 异步可中断模型,前一个协调阶段的任务执行可以被打断,让位给更高优先级的任务。这个时候,要保证低优先级的任务的状态不会丢失。

  2. 更新队列中,优先级低的update会被暂缓执行,只执行高优先级的任务。这个时候,要保证最后所有的任务执行完毕时,update的顺序不被打乱。因为互相的update是有可能相互依赖的

    比如:

    setState(pre => pre + 1)
    setState(pre => pre + 2)
    
异步可中断模型

想象一个例子,有一个网页,每隔五秒会自动变幻颜色,那么,这个任务是不是一个低优先级的?因为我们并不会在意他是否及时的改变颜色。

在这期间,我们在网页中输入文字,这个任务是不是一个高优先级的?因为我们需要文字及时的呈现在网页中。

所以,当网页正在变幻颜色(即改变state时),我们恰好输入了一个文字,那么,后面的任务,就会打断前面的任务,然后等到文字呈现到界面上后,再去变幻颜色。

举个例子:

function App() {
  const [color, setColor] = useState('black');

  const [value, setValue] = useState('app');

  useEffect(() => {
    setTimeout(() => {
      setColor(pre => {
        pre === 'red' ? 'black' : 'red'
      })
    }, 5000)
  }, [color]);

  return (
    <div className="App" style={{backgroundColor: color}}>
      <input value={value} onChange={(e) => setValue(e.target.value)} />
    </div>
  );
}

asyc.png

在实际中,并不只是有这两个任务,在多个任务互相"插队"的情况下,如何保证所有的任务最终都能顺利执行,而不存在丢失掉某个状态更新呢?

可以类比git,会有一个主分支master,有一堆功能分支,当有一个高优先分支需要提交到master时,需要先pull,然后再push。这样,所有的功能分支都可以以master作为一个基准。

useState中,baseQueuebaseState就是那个master分支。

所以你能在这个地方,看到update队列也链接到了cur hookbaseQueue中。

const pendingQueue = queue.pending;
if (pendingQueue !== null) {
  if (baseQueue !== null) {
    const baseFirst = baseQueue.next;
    const pendingFirst = pendingQueue.next;
    baseQueue.next = pendingFirst;
    pendingQueue.next = baseFirst;
  }
  // 同时保存到 cur hook里
  current.baseQueue = baseQueue = pendingQueue;
  queue.pending = null;
}

因为后来的任务如果打断了前一个任务,那么他仍然是从cur fiber中去clone并执行。因此,前一个任务的update如果也保存在了cur hook里,那么就不会丢失,也会在后来被执行。

保证update的执行顺序

再来看一个例子:

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

  const ref = useRef()

  useEffect(() => {
    setTimeout(() => setCount(1), 0) // 任务1
    setTimeout(() => ref.click(), 4) // 任务2
  }, []);

  return (
    <div className="App" >
      <button ref={ref} onClick={() => setCount(pre => pre + 1)} />
    </div>
  );
}
// 任务1触发,进入第一轮render
// 执行updateReducer前,数据结构是这样的
const wipHook = {
  memoizedState: 0,
  queue: {
    pending: {
      action: 1
    }
  }
}

// 执行updateReducer后,数据结构变成了这样:
const wipHook = {
  memoizedState: 1,
  queue: {
    pending: null
  }
}

const curHook = {
  memoizedState: 1,
  queue: null,
  baseQueue: {
    action: 1
  }
}

// 发现了吗?wip hook的pending更新队列执行完毕后,变成了null,但是却赋值给了cur Hook的baseQueue

// 任务2触发,发现任务1处在协调阶段,即打断,执行任务2
// 由于任务1未提交,所以任务2在执行的时候,仍然是从老的那个cur fiber去clone新树
// 进入第二轮render

// 执行updateReducer前的数据结构:
const wipHook = {
  memoizedState: 0,
  queue: {
    pending: {
      action: (pre) => pre + 1
    }
  }
}

const curHook = {
  memoizedState: 0,
	queue: {
    pending: {
      action: (pre) => pre + 1
    }
  }
  baseQueue: {
    action: 1
  }
}
// 注意其中的区别,curHook的baseQueue仍然带着任务1的update

// 执行updateReducer后的数据结构:
const wipHook = {
  // 执行了任务2,(pre) => pre + 1 , 所以state由0,变为了1
  memoizedState: 1,
  queue: {
    pending: null
  },
  // 由于任务1优先级低,所以先执行了任务2
  // 因此,从被跳过的update开始,把他链接到baseUpdate中
  baseQueue: {
    action: 1,
    next: {
      action: 2
    }
  }
  // baseState 也停留在未被跳过update时的值
  baseState: 0,
}

const curHook = {
  memoizedState: 0,
	queue: {
    pending: null
  }
  baseQueue: {
    action: 1,
    next: {
      action: 2
    }
  }
}

// wip fiber提交,变为cur hook,发现有任务未执行完,再次调度
// 进入第三轮render:
const wipHook = {
  memoizedState: 1,
  baseState: 0,
  queue: {
    pending: null
  },
  baseQueue: {
    action: 1,
    next: {
      action: 2
    }
  }
}

// 执行updateReducer,遍历执行baseQueu:
// action1: state = 1, action2: state = 1(action1计算完的state) + 1
const wipHook = {
  memoizedState: 2,
  baseState: 2,
  queue: {
    pending: null
  },
  // 已无被跳过的update
  baseQueue: null
}

// 提交,state最终为2

// 整个流程是state从 0 -->  1 ---> 2 的变化

只要有update被跳过,那么baseState就会停留在被跳过的update之前的计算值。然后baseQueue会保存从那个被跳过的update,及其之后的所有成员。

// updateReducer

// 当前update优先级过低,跳过执行。
if (!isSubsetOfLanes(renderLanes, updateLane)) {
  const clone: Update<S, A> = {
    lane: updateLane,
    action: update.action,
    eagerReducer: update.eagerReducer,
    eagerState: update.eagerState,
    next: (null: any),
  };
  // 把这个update添加到下次的baseQueue中,留待下次render时执行
  if (newBaseQueueLast === null) {
    newBaseQueueFirst = newBaseQueueLast = clone;
    // 更新baseState为之前update计算好的那个值
    newBaseState = newState;
  } else {
    newBaseQueueLast = newBaseQueueLast.next = clone;
  }
  currentlyRenderingFiber.lanes = mergeLanes(
    currentlyRenderingFiber.lanes,
    updateLane,
  );
  markSkippedUpdateLanes(updateLane);
}

// 同时,也保存它之后的所有update
else {

  if (newBaseQueueLast !== null) {
    // newBaseQueueLast 存在证明此前有update被跳过
    // 因为update互相之间可能存在依赖,所以从那个被跳过的update起,所有后面的update都链接到newBaseQueueLast中
    // 在下次render一齐执行
    const clone: Update<S, A> = {
      lane: NoLane,
      action: update.action,
      eagerReducer: update.eagerReducer,
      eagerState: update.eagerState,
      next: (null: any),
    };
    newBaseQueueLast = newBaseQueueLast.next = clone;
  }
  // ...
}

除开这两个逻辑,其他的就不难了,不过是遍历update来计算state而已。

总结

  1. useState通过一个更新队列来记载所触发的更新,其中,pending所记载的是本次任务触发的updatebaseUpdate所记载的是所有异步任务所触发的update,相当于一个全局的仓库。

    render中,会遍历执行update来计算state值,如果某个update优先级过低,就会暂缓执行,先执行其他的updatecommit到界面上,然后在下次render中,再从被跳过的update开始执行任务,保证其中顺序不变。

  2. dispatch可以生成一个update,链接到hook的更新队列中。如果本次dispatch是第一个,那么会直接计算state值,在下次render时就可以直接使用,而无需计算。

那么到这里useState就已经讲完了,有人可能会问疑惑mountReducer怎么没有,因为它的代码和mountState有90%的相似度,所以就略过。

useMemo&useCallback

官网例子:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。

可以看到,他们的区别就是有没有回调函数而已。事实上在源码中他们的实现也是90%的相似

mountMemo&mountCallback

function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 创建一个hook节点并link到链表中
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // 创建一个hook节点并link到链表中
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

这个hookmemoizedState存放的是一个数组

[callback, nextDeps]

第一项是保存的值,第二项是依赖。

updateMemo&updateCallback

function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 取出hook节点
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      // 对比前后依赖项,如果相同,就直接返回缓存值
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  // 如果不同,则依照最新的create来重新计算,并更新依赖项
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // 取出hook节点
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      // 对比前后依赖项,如果相同,就直接返回缓存值
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  // 如果不同,更新回调函数,更新依赖项
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

对比前后依赖项是否相同,都是靠着这个方法:

function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    // object.is()
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

总的来说,这个hooks还是相对简单的。

useEffect

官网例子:

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);

我把他分为两个阶段,一个是调用阶段,是在render中的显式声明。一个是执行阶段,是在effect的回调函数执行的时候。

调用阶段

mountEffectImpl

mount阶段useEffect调用的方法:

function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return mountEffectImpl(
    UpdateEffect | PassiveEffect | PassiveStaticEffect,
    HookPassive,
    create,
    deps,
  );
}

function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  // 创建一个hook节点并link到链表中
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.flags |= fiberFlags;
  // 创建一个effect对象并添加到hook的memoizedState中
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create, 
    undefined,
    nextDeps,
  );
}
pushEffect

这个方法主要做两件事,一个是创建effect对象并返回,一个是把这个effect链接到wip fiberupdateQueue中。

function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,// 回调函数
    destroy, // 依赖项
    deps,
    // 这也是个循环链表
    next: (null: any),
  };
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
  if (componentUpdateQueue === null) {
    // 首个副作用,给当前的fiber创建一个UpdateQueue,并把effect添加进去。
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    // 链接到当前fiber节点的updateQueue的lastEffect中
    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;
}

对于函数组件,其fiberupdateQueue,也同样是一个链表的头节点,lastEffect指向最后一个effect。与上文hook.queue.pending的实现方式相同。

export type FunctionComponentUpdateQueue = {|lastEffect: Effect | null|};

effect放到fiber节点的updateQueue中,以便在commit阶段就可以发起一个调度延迟执行。

updateEffectImpl

update阶段useEffect调用的方法:

function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
    
  return updateEffectImpl(
    UpdateEffect | PassiveEffect,
    HookPassive,
    create,
    deps,
  );
}

function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  // 取出hooks节点
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;

  if (currentHook !== null) {
    // 取出上一轮render中的effect
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 如果上一轮render的依赖项和当前的依赖项未发生变化,就无需更新hook的memoizedState
        // clone一个effect对象链接到updateQueue中,但是tag不添加HookHasEffect
        pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }

  currentlyRenderingFiber.flags |= fiberFlags;

  // 走到这里说明依赖项变更
  hook.memoizedState = pushEffect(
    // 标记此effect对象的tag为HookHasEffect,表示有副作用,需要执行。
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}

这个地方,为什么要用cur hook而非wip hook?明明这两个的memoizedState都是相同的。

const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;

原因仍然是re-render,这种情况发生时,wip hook不会重新从cur hookclone ,所以,两次的依赖项会是相同的,导致effect的回调不会执行。

// clone一个effect对象链接到updateQueue中,但是tag不添加HookHasEffect,commit阶段不会执行它。
pushEffect(hookFlags, create, destroy, nextDeps);
return;

因此,当你需要取到上一轮render中的数据时,一定要用cur hook,尽管大部分情况下,wip hook都是cur hook的克隆体。

上面都是useEffect的调用,接下来就到useEffect的执行了。

执行阶段

fiber 的「工作流程」中说过。effect的执行,会在commitbeformMutation阶段,去发起一个调度,可以暂时理解为一个setTimeout

然后到了layout阶段结束后,就开始执行异步任务,也就是effect的销毁回调和创建回调。

下面放源码,我会适当简化一下,因为commit 阶段对于本文而言属于超纲,有空我再详细说下。

commitBeforeMutationEffects

在这个阶段,发起useEffect的调度。

// 省略代码
function commitBeforeMutationEffects() {
  while (nextEffect !== null) {
    if ((flags & Passive) !== NoFlags) {
      if (!rootDoesHavePassiveEffects) {
        rootDoesHavePassiveEffects = true;
        // 发起调度
        scheduleCallback(NormalSchedulerPriority, () => {
          flushPassiveEffects();
          return null;
        });
      }
    }
    nextEffect = nextEffect.nextEffect;
  }
}
flushPassiveEffectsImpl

先调用effect的卸载,再调用创建

// 省略代码
function flushPassiveEffectsImpl() {
  if (rootWithPendingPassiveEffects === null) {
    return false;
  }

  // 调用 destroy 回调
  commitPassiveUnmountEffects(root.current);
  // 调用 create 回调
  commitPassiveMountEffects(root, root.current);

  flushSyncCallbackQueue();

  return true;
}
commitHookEffectListUnmount

调用effect的卸载

function commitHookEffectListUnmount(flags: HookFlags, finishedWork: Fiber) {
  // 取出fiber的updateQueue,effect的链表都保存在这里面
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & flags) === flags) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          // 调用 destroy
          safelyCallDestroy(finishedWork, destroy);
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
commitHookEffectListMount

调用effect的创建

function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
  // 取出fiber的updateQueue,effect的链表都保存在这里面
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      // 这个tag能控制哪些effect需要执行,一般来说,只有标记了 HookHasEffect 的,才会执行。
      if ((effect.tag & tag) === tag) {
        // Mount
        const create = effect.create;
        effect.destroy = create();
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

总结

  1. useEffect会生成一个effect对象,保存在hook节点的memoizedState中,同时也pushfiber节点的updateQueue里,组成循环链表。

    每次render时,都会对比一下cur hookwip hook里保存的effectdeps有没有改变,如果改变了,那就更新memoizedState为最新的effect,并且把effecttag打上HasHookEffect的标记,然后pushfiberupdateQueue里。

  2. commit阶段,beforeMutation中,对有副作用的fiber,发起一个异步调度。

    等到layout结束后,这个异步调度的回调开始执行,处理effect的创建和销毁回调。

    它会先调用effectdestroy,再调用create

useLayoutEffectuseEffect 大同小异,只是打的tag不同,并且执行阶段在layout中同步执行,和componentDidMountcomponentDidUpdate的相同。

这个hooks我在之后再更新。

useRef

官网例子:

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

源码:

function mountRef<T>(initialValue: T): {|current: T|} {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}

function updateRef<T>(initialValue: T): {|current: T|} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}

可以说是相当简单了,有了上面几个hooks基础的大家想必都能明白。

从源码中可以看出,useRef只是把一个对象储存了起来然后保存到hook里,并且每次render都返回同一个对象而不做任何改变。

这也就是它被称为可变数据的原因,你在修改这个对象里面的属性时,不论是cur hook还是wip hook,还是函数闭包陷阱,他们的ref引用都是一致的,从中访问current,总是能取得相同的值。

但同样的,修改ref不会引起调度,也就是不会触发渲染。只有setState || React.render才会引起渲染,从内存中构建一颗新树然后替换老树。这也就是后者必须是immutable的原因。

ref还涉及到组件更新ref,以及useImperativeHandle,有空再更新。

后记

本文所讲的react hooks的源码到这里就结束了,其中还有一些细节尚待补充,以后我有时间会添上去。

大家有发现什么不对的地方,或者有哪里绝对说得不够明白,欢迎在评论区留言~

另外,求个内推

WechatIMG16.jpeg