梳理react-hook源码

我只是不能忍受。。。。你一生在这里打转。你太聪明,太有趣。。。你只有一次生命,应该尽可能活的充实。react

前言

基于reactV17.0源码分析数组

本片文章主要经过react的源码,分析useState、useEffect和两个钩子函数的内在执行原理。缓存

  1. hook组件没有this指针,没有生命周期,setXxxx函数是如何发起更新,进行调度的?
  2. hook对象在react内部是以什么结构进行储存运算的?
  3. useEffect的第一个参数是在调度中是如何执行的,它的返回函数为何能够在unMount的时候触发?
  4. useEffect第二个参数又是如何equal,避免重复执行第一个参数的函数?
  5. 等等。

为了解决包括不限于以上的问题,咱们打开react项目研究一下源码。限于我的技术缘由,若有问题戳我。markdown

源码入口

领略react-hook以前,咱们先回顾一下从reactDom.render(element, dom)开始到hook节点建立中间经历了那些步骤。下面是我根据源码,将初次渲染的关键路径简单的梳理了一下,只涉及关键函数。(打开react项目,点点点)闭包

截屏2021-05-31 下午10.26.32.png

renderWithHooks

顾名思义,这个函数就是对hook组件的处理函数的入口。下面简单的分析一下这个函数主要作了什么:dom

  1. 将全局变量currentlyRenderFiber指向workInProgress
  2. 根据memoizedState判断首次挂载/更新,获取hooks方法集的Dispatcher对象,并将此对象赋值给全局变量ReactCurrentDispatchercurrent属性
  3. 最后执行hook组件的function构造方法
// packages/react-reconciler/src/ReactFiberHooks.old.js
export function renderWithHooks() {
  
  currentlyRenderingFiber = workInProgress;
  
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;
  ...

  //赋值全局变量,能够在react中使用相关hook方法
  ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
          ? HooksDispatcherOnMount
          
  let children = Component(props, secondArg);

  // 省略一些条件的判断,和置空

  return children;
}

复制代码

ReactCurrentDispatcher

  • 这个对象是全局对象,在react模块中定义,并暴露,因此咱们能import { useState, useEffect } from 'react'
  • 并在renderWithHooks函数实现各类钩子函数,并将各个钩子函数挂在对应的fiber对象上的hook链表
  • 因此最后在执行hook的构造方法(Component())时,可以使用相关的hook方法

实现并挂载在全局变量ReactCurrentDispatcher以后,在执行hook组件的构造方法时,咱们就能拿到相关的hook方法。咱们就先拿最多见的useStateuseEffect来讲明,其余的钩子函数先不考虑。函数

// packages/react/src/ReactCurrentDispatcher.js
//在react模块中定义,并抛出了这个对象
const ReactCurrentDispatcher = { 
  current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;


//packages/react-reconciler/src/ReactFiberHooks.old.js
//在react-reconciler模块中实现
const HooksDispatcherOnMount: Dispatcher = {
  //初次挂载
  useState: mountState,
  useEffect: mountEffect,
  
};
const HooksDispatcherOnUpdate: Dispatcher = {
  //更新
  useEffect: updateEffect,
  useState: updateState,
  
};
复制代码

useState

首先是初次挂载阶段

源码里咱们能够看到,最关键的就是利用bind闭包,缓存了currentlyRenderingFiber对象(我也列出了源码中对此对象的解释,就是当前的fiber节点)和queue对象(保存的是更新对象update,在dispatchAction中咱们能够看到)源码分析

// The work-in-progress fiber. I've named it differently to distinguish it from
// the work-in-progress hook.
let currentlyRenderingFiber: Fiber = (null: any);
...

function mountState(initialState,){
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    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];
}
复制代码

image.png

接下来咱们能够来看看hook对象的建立mountWorkInProgress函,在这个函数里主要就是构造了一个hook的单项链表,并将workInProgressHook指针指向当前hook。对于workInProgressHook对象,源码中也有详细的解释,用以保存将要加进当前fiber对象的hook链表优化

// Hooks are stored as a linked list on the fiber's memoizedState field. The
// current hook list is the list that belongs to the current fiber. The
// work-in-progress hook list is a new list that will be added to the
// work-in-progress fiber.
let currentHook: Hook | null = null;
let workInProgressHook: Hook | null = null;


function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

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

    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
复制代码

image.png

更新阶段

咱们先来看一段实例代码ui

function MyName(){
    const [name, setName] = useState()   
    return <div> <span>{name}</span> <button onClick={setName('骆家豪')}> 展现名字</button> </div>
}
复制代码

看了上面初次挂载的解析后,咱们明白setName就是dispatchAction函数,而且利用闭包缓存了当前fiber对象的指引。so,咱们来看看最最核心的dispatchAction函数作了什么骚操做。

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),
  };
  
  // 将更新加到链表的最后
  const pending = queue.pending;
  if (pending === null) {
    // 这是第一个更新,建立一个环形链表
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }

  queue.pending = update;

  ...

  //发起调度
  scheduleUpdateOnFiber(fiber, lane, eventTime);
 }
复制代码

image.png

咱们能够看到其实就是构造了一个update的链式结构,并挂在hook对象上的queue属性上,并在最后发起一个调度。那么调度以后是如何的从新计算fiber节点的,如何处理queue中的update更新链,咱们还要看updateState函数。

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

直接返回一个updateReducer函数,顾名思义,就是一个更新合并的函数。将一次或屡次setName函数产生的update合并。计算出最新的state

function updateReducer(){

  //1.合并并计算queue队列
  let baseQueue = current.baseQueue;
  
  //2.若是存在等待的更新队列,则循环update的单向链表,reducer全部的state
  if(baseQueue !== null){
    const first = baseQueue.next;
    let newState = current.baseState;
    let update = first;
    do {
        const action = update.action;
        newState = reducer(newState, action);
        update = update.next;
    } while (update !== null && update !== first)
    
  }
  
  //3.将最新的state存入当前hook,并返回
  hook.memoizedState = newState;
  hook.baseState = newBaseState;
  hook.baseQueue = newBaseQueueLast;
  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}
复制代码

小结

到此咱们将useState这个hook的建立,更新过程过了一遍。下面来技术总计。

  1. hook对象以链表的形式保存在当前fiber对象的memoizedState属性上,造成环形结构。便于以后的便利合并。
  2. useState钩子函数返回的第二个参数setXxx,其实就是利用闭包,对应源码dispatchAction.bind(null, currentlyRenderingFiber,queue,)。缓存了当前的fiber对象和queue跟新队列,这也解释了为何hook函数不须要this指针也能对应上指定的更新对象
  3. 在执行setXxx时,也就是建立了一个update,并将update对象以单项链表的形式保存在当前hookqueue属性上。并在最后发起一个调度scheduleUpdateOnFiber,全部更新的入口函数。
  4. 执行上一个步骤的调度时,由于存在memoizedState,就会执行updateState,也就是执行了updateReducer函数,顾名思义就是合并更新
  5. 最核心的的就是updateReducer,在这个函数中将update合并,并do-while遍历update,合并计算update对象中的action属性,就是setXxx的第一个参数,能够是函数。最后返回一个新的state保存在当前hook的memoizedState属性中。

useEffect

看完useState的代码,在看useEffect函数,大部分都是相同的。最大的不一样仍是触发时机的不一样,useEffectrender过程以后触发计算,useState在下次计算当前fiber的时候触发执行计算newState

初次挂载与更新

咱们先一块儿来看看 useEffect初次挂载,和更新时的代码。

function mountEffect(create,deps) {
  return mountEffectImpl(
    UpdateEffect | PassiveEffect,
    HookPassive,
    create,
    deps,
  );
}
function updateEffect(create,deps) {
  return updateEffectImpl(
    UpdateEffect | PassiveEffect,
    HookPassive,
    create,
    deps,
  );
}
复制代码

看了代码其实区别就转换成了mountEffectImplupdateEffectImpl区别。

function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps,
  );
}

function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;

  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }

  currentlyRenderingFiber.flags |= fiberFlags;

  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}
复制代码

看了上面的挂载、更新两个阶段的函数对比,咱们很容易发现,更新阶段仅仅比挂载阶段多了一段判断依赖数组是否相同的代码,多作了一个减小重复执行的优化(hook第一个参数create)。全部在咱们平时开发中必定要注意useEffect第二个参数的使用,固然源码中的比较函数,也只是作了一层浅比较。

hook-effect链表结构

那么接下来咱们就打开pushEffect函数看看状况。

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) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    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;
}
复制代码

其实看完很简单,并无复杂的合并,计算。只是将生成一个effect对象,并以环形链表的结构存在hookmemoizedState属性上,并将此链表赋值给全局变量componentUpdateQueueimage.png

处理effect链表

最中造成一个effect的环形链表后,放在了componentUpdateQueue中。至于什么时候触发,找了一会,在commitHookEffectListMount函数中找到了对hook-effect的处理

image.png 下面咱们就详细的看下commitHookEffectListMount如何处理effect链表

function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
  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 {
      // 作了判断,过滤了依赖重复时,push进来的effect
      if ((effect.tag & tag) === tag) {
        // Mount
        const create = effect.create;
        effect.destroy = create();
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
复制代码

其实很简单,就是遍历effect链表,并利用tag过滤依赖dept没变产生的effect。执行create函数,而后将create函数执行的结果赋值给destory属性。

而后继续在突变阶段-commitWork函数找到了destory执行

function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
  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 & tag) === tag) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          destroy();
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
复制代码

总结

  1. useState,仍是useEffect都是产生一个链表挂载在当前hook对象的memoizedState属性上。
  2. 不一样的事useState产生的是update更新对象的链表,useEffect产生的是effect反作用链表
  3. 这两个钩子函数触发时机不一样。useState在下次更新时,合并计算hook对象上的update链表,最中计算出最新的newState,赋值给hook.momoizedState;而useEffect则在commit阶段的突变后才开始执行hook上的effect链表的create函数,在commit阶段的突变时执行destory函数
相关文章
相关标签/搜索