【react hooks】--初渲染和更新阶段

hook组件初渲染

hooks组件在初次渲染时,

  1. 解析组件类型,判断是Function还是class类型,然后执行对应类型的处理方法
  2. 判断到当前是Function类型组件后,首先在当前组件,也就是fiberNode上进行hook的创建和挂载,将所有的hook api都挂载到全局变量dispatcher
  3. 顺序执行当前组件,每遇到一个hook api都通过next将它连接到当前fiberNodehook链表上

hooks api 挂载

在初始渲染时,currentDispatcher为空,就会先将所有hook api挂载到当前fiberNodedispatcher上。也就是将HooksDispatcherOnMountInDEV赋值给dispatcher

  {
    //  首次执行currentDispatcher = null,所以进入else分支;在更新阶段会进入if分支
    if (currentDispatcher !== null) {
      currentDispatcher = HooksDispatcherOnUpdateInDEV;
    } else {
      currentDispatcher = HooksDispatcherOnMountInDEV;
    }
  }

而在HooksDispatcherOnMountInDEV中,包含各种原生hook,内部调用mountXXX方法。

HooksDispatcherOnMountInDEV = {
    useCallback: function (callback, deps) {
      return mountCallback(callback, deps);
    },
    useEffect: function (create, deps) {
      return mountEffect(create, deps);
    },
    useMemo: function (create, deps) {
        return mountMemo(create, deps);
    },
    useState: function (initialState) {
        return mountState(initialState);
    }
  }

useState -- mountState

  1. 首先创建一个hook节点,其中的memoizedState是最终返回的初始值;queue是更新队列,当我们多次更新某一状态时需要通过queue队列存取和遍历;next用来连接下一个hook;
  2. 将当前的hook连接到当前的fiberNodehook链表上
  3. 绑定状态更新方法dispatchAction,并返回[state, dispatchAction]
function mountState(initailState){
    var hook = {
        moemoizedState: null,
        queue: {
            pending: null,
            dispatch: null,
        },
        next: null
    }
    ...
    var dispatch = queue.dispatch = dispatchAction.bind(null, currentFiber,queue);
    return [hook.memoizedState,dispatch];
}

useEffect -- mountEffectImpl

  1. mountEffect中,首先将当前的hook挂载到当前fiberNodehook链表上

  2. 由于useEffect是异步执行的,会产生专属于useEffecthook。此时会将产生的useEffect产生的hook存入componentUpdateQueue更新队列中。在某一次页面渲染结束后会去遍历这个更新队列,执行传入的effect hook

function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
    // 创建并获取当前hook节点信息
    var hook = mountWorkInProgressHook();

    hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, undefined, nextDeps);
  }
function mountWorkInProgressHook() {
    // 将当前hook连接到我们的hook链表中
    var hook = {
      memoizedState: null,
      queue: null,
      next: null
    };

    if (workInProgressHook === null) {
      currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
    } else {
      workInProgressHook = workInProgressHook.next = hook;
    }

    return workInProgressHook;
  }
function pushEffect(tag, create, destroy, deps) {
    var effect = {
      tag: tag,  // 更新标识
      create: create, // 传入的回调,也就是我们开发时的第一个参数
      destroy: destroy, // return 的函数,组件销毁时执行的函数
      deps: deps, // 依赖项数组
      next: null
    };
    var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
    
    // 这里做的就是把每个useEffect hook单独以链式结构存到了componentUpdateQueue这个全局变量中
    if (componentUpdateQueue === null) {
      componentUpdateQueue = createFunctionComponentUpdateQueue();
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      var lastEffect = componentUpdateQueue.lastEffect;

      var firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
    
    return effect;
  }

初始化渲染至此结束,此时fiberNodehook链式结构是

currentFiber: {
        ...
        memoizedState: {
                memoizedState: xxx,
                ...
                next: {
                        memoizedState: xxx,
                        ...
                        next: {
                                memoizedState: xxx,
                                ...
                                next:hook4
                        }
                }
        }
}

hook组件更新阶段

在组件更新时,于初始化类似,将更新的对应hook进行挂载,根据链表依次执行hook。在此时,需要执行状态更新和useEffect更新,最后更新完成。

useState

在初始化阶段有提到,mountState阶段会绑定dispatchAction并作为参数返回,其实也就是使用useState时返回的setXXX

而在dispatchAction中实际上,做了两件事

  1. 创建update节点,并连接到useStatequeue后面,这样每次调用dispatchAction都会在后面连接一个update节点,从而生成一个更新队列。
  2. 然后根据更新任务的优先级排列任务,最后遍历整个fiber树执行更新操作。

执行setXXX后,发生了什么?

在初始化阶段讲到的,根据是否存在dispatcher,赋值。

  {
    //  更新阶段dispatcher已存在,执行else部分
    if (currentDispatcher !== null) {
      currentDispatcher = HooksDispatcherOnUpdateInDEV;
    } else {
      currentDispatcher = HooksDispatcherOnMountInDEV;
    }
  }

而在HooksDispatcherOnMountInDEV中各种hooks对应的是update方法,useState对应的是updateState方法。

function updateState(initialState){    return updateReducer(basicStateReducer);}

可以看到,updateState实际上是返回了一个reducer

  1. updateReducer中,获取到hook的更新队列,比如执行了三次setCount,则队列中就会存在三项。

  2. 拿到更新队列后,会对其进行循环遍历,计算赋值,最终会将最新的state值复制到hook的memoizedState上并返回。

function updateReducer(reducer, initialArg, init) {  // 获取到当前hook,其实也就是直接.next就可以  var hook = updateWorkInProgressHook();  var queue = hook.queue;  // 取到待更新的队列  var pendingQueue = queue.pending;    // 如果待更新队列不为空,那么遍历处理  if (pendingQueue !== null) {    var first = pendingQueue.next;    var newState = null;    var update = first;    queue.pending = null;        // 循环遍历,是更新阶段的核心和关键,    do {      var action = update.action;      newState = reducer(newState, action);      update = update.next;    } while (update !== null && update !== first);    // 最新的状态值赋值给memoizedState    hook.memoizedState = newState;  }  // 将状态值和更新方法返回,就和初次渲染一样的流程  var dispatch = queue.dispatch;  return [hook.memoizedState, dispatch];}

如何形成更新队列?

而在updateReducer之前,会执行dispatchAction,将每个update加入到更新队列中。

// dispatchAction核心代码var pending = queue.pending;// 这里是链表创建和连接的核心if (pending === null) {  update.next = update;} else {  update.next = pending.next;  pending.next = update;}queue.pending = update;

每次dispatch时,当目前更新队列为空时,将当前update加入,next指向自己

当不为空时,将当前update放到更新队列最后一项的next上。

需要注意的是,更新队列中的first指向第一个执行的updatequeue.pending指向最后一个。

所以在更新队列循环执行时,结束循环的条件为while (update !== null && update !== first);也就是当前update为更新队列中的第一个也就是唯一一个update

useEffect

useState相似,在更新过程中会执行updateEffect,在其中执行updateEffectImpl

updateEffectImpl中,无论依赖项是否更改,都会调用pushEffect方法。

function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {  // 获取到当前hook  var hook = updateWorkInProgressHook();  // 比较依赖项是否发生了变化  if (areHookInputsEqual(nextDeps, prevDeps)) {    // 如果相同则不对当前hook的属性进行更新    pushEffect(hookEffectTag, create, destroy, nextDeps);    return;  }  // 如果依赖项发生了变化,更新当前hook的memoizedState,这里的赋值只是做一个记录,并没有实际意义  currentlyRenderingFiber$1.effectTag |= fiberEffectTag;  hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, destroy, nextDeps);}

而在pushEffect方法中,

  1. 创建一个effect对象,并返回
  2. 创建/更新componentUpdateQueue队列,其中存储了useEffect产生的回调,componentUpdateQueue队列不存在的话会进行创建,如果存在,会和mountState阶段一样创建一个effect的循环链表。effect对象中的tag就是用来判断这个useEffect回调是否需要被执行。

一些可以被解释了的问题

hook必须按照固定顺序调用,不能在条件判断中调用

由于每一个hook都是通过next指针在链表中按顺序连接的,如果在某个条件判断的情况下,某个hook不存在,就会导致整个hook链表中断,没法继续正常遍历hook链表,产生问题。