关于前端:react-hooks源码深入浅出二

  • react hooks源码深入浅出(一)
  • react hooks源码深入浅出(二)

在第一篇文章里咱们理解了首次渲染过程react外部的解决流程和执行机制,接下里持续看看在状态更新阶段react是怎么解决的

当初触发demo中onclick事件,也就是执行setCount办法

同样从两个根底hook登程
  • useState
  • useEffect
更新阶段外围流程

useState

在开始之前咱们带着两个问题:

  1. 执行setCount后,外部产生了什么?
  2. 如果屡次执行setCount,它是怎么样取到最新的值的?
首先解答第一个问题

在第一篇文章说到了,在mountState阶段会绑定一个叫dispatchAction的办法而后作为参数返回,这个办法在咱们的demo中就是setCount办法,没有印象的看下上面的代码

function mountState(initialState) {
  // 还记不记得这个相熟的办法
  var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
  return [hook.memoizedState, dispatch];
}

持续深刻看下diapatchAction干了什么

function dispatchAction(fiber, queue, action) {
    var update = {
      expirationTime: expirationTime,
      action: action,
      next: null
    };

    // 解决以后hook的queue队列
    var pending = queue.pending;

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

    queue.pending = update;

    // 进入调度环节
    scheduleWork(fiber, expirationTime);
  }
}

其实干了两件事

  1. 创立update节点,连贯到以后hook(也就是useState)的queue前面(这个queue遗记的搭档能够翻回第一篇文章中看看),这样每次调用dispatchAction都会在前面连贯一个update节点,从而生成一个更新队列(这个更新队列前面会具体讲
  2. 而后开始这一轮的scheduleWork调度(对于调度做了什么详看这篇文章,因为内容十分多,这里不做过多阐明:https://segmentfault.com/a/1190000020737020?utm_source=tag-newest),大略流程就是将所有更新工作依照优先级排列,最初遍历整个fiberTree执行更新操作,更新阶会调用beginWork办法,这就又回到了咱们首次渲染的流程,因为首次渲染时也会调用这个办法,就对应起来咱们第一篇文章的首次渲染流程图

咱们持续走
依照下面的流程会走到这一步,又是相熟的代码,此时咱们会把HooksDispatcherOnUpdateInDEV赋值到dispatcher

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

持续看看HooksDispatcherOnUpdateInDEV是什么

HooksDispatcherOnUpdateInDEV = {
    useCallback: function (callback, deps) {
      return updateCallback(callback, deps);
    },
    useEffect: function (create, deps) {
      return updateEffect(create, deps);
    },
    useMemo: function (create, deps) {
        return updateMemo(create, deps);
    },
    useState: function (initialState) {
        return updateState(initialState);
    }
  }

发现在更新阶段遍历执行到useState时理论执行的是updateState办法,那持续看看updateState做了什么

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

持续看看updateReducer

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];
}

下面是是外围流程代码,源码要比这个更加简单和健全一些,咱们这里不做过多波及,其实次要做了两件事

  1. 获取以后hook的更新队列pendingQueue也就是下面通过queue连接起来的更新队列,举个形象的例子,比方咱们执行了三次setCount办法,这个时候咱们以后useState hook的queue队列中就会有三项
  2. 拿到咱们的更新队列pendingQueue,循环遍历进行计算和赋值操作,最终会将最新的state值复制到hook的memorizedState上并返回

综上就是咱们抛出的第一个问题的答案,接下来答复第二个问题,在屡次setCount后是怎么获取到最新值的?
所以综上咱们晓得了进行状态更新后办法执行程序为dispatchAction->updateReducer, 咱们把dispatchAction办法的外围代码拿进去,如下

// dispatchAction外围代码
var pending = queue.pending;

// 这里是链表创立和连贯的外围
if (pending === null) {
  update.next = update;
} else {
  update.next = pending.next;
  pending.next = update;
}
queue.pending = update;

假如以后共执行了三次setCount,别离是setCount(1),setCount(2),setCount(3),拆开三次来看,当执行第一次setCount(1)时会执行上面的代码

// dispatchAction内    
var pending = queue.pending;

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

// updateReducer内,此时first是update(1)
first = queue.pending.next;


执行实现后queue队列如上图所示,fisrt是用来记录最开始的update的一个节点,此时就是update(1),先不必关怀,继续执行setCount(2),同上

// 这里的pending其实也就是update(1)
var pending = queue.pending;

// 因为此时pending != null,所以代码走到else中
else{
    update(2).next = pending.next;
    pending.next = update(2);
}

queue.pending = update(2);

// 此时first仍是update(1)
first = queue.pendind.next;


执行完setCount(2)后的queue队列如上,继续执行setCount(3)

// 这里的pending其实也就是update(2)
var pending = queue.pending;

// 因为此时pending != null,所以代码走到else中
else{
    // 这一步很要害,联合下面的图,是把update(1)赋值到了update(3)的next上
    update(3).next = pending.next;
    // 因为此时pending是update(2),所以这一步就是把update(3)赋值到update(2)的next上
    pending.next = update(3);
}

queue.pending = update(3);

// 此时first仍是update(1)
first = queue.pendind.next;

综上,执行完三次setCount后的queue队列为上图所示,接下来react外部会遍历queue队列(也就是update环形链表)
下面说过setCount后react外部办法执行程序为dispatchAction -> updateReducer,当初开始执行updateReducer的遍历过程,依据外围代码

// updateReducer外围代码
var pendingQueue = queue.pending;
  
if (pendingQueue !== null) {
    // first是update(1)
    var first = pendingQueue.next;
    var newState = null;
    var update = first;
        
    // 循环遍历,是更新阶段的外围和要害,
    do {
      var action = update.action;
      // reducer其实就是判断咱们传入的值是否为函数如果是的话执行函数放回新值;如果不是间接返回新值
      newState = reducer(newState, action);
      // 而后遍历下一个update
      update = update.next;
    } while (update !== null && update !== first);

    // 最新的状态值赋值给memoizedState
    hook.memoizedState = newState;
  }

一开始将update(1)赋值给update,而后获取newState也就是1,接下来update=update.next,此时update成了update(2),顺次遍历,终止条件为update === first,也就是当update = update(3)时满足了终止条件,此时newState = 3,取到了最新值。
这样能够保障整个update链表都循环了一遍同时取到的是链表中的最初一个节点(也就是最新节点)
综上,解答了咱们一开始抛出的第二个问题。

useEffect

同上,看到HooksDispatcherOnUpdateInDEV外部useEffect具体执行的是updateEffect

HooksDispatcherOnUpdateInDEV = {
    useCallback: function (callback, deps) {
      return updateCallback(callback, deps);
    },
    useEffect: function (create, deps) {
      return updateEffect(create, deps);
    },
    useMemo: function (create, deps) {
        return updateMemo(create, deps);
    },
    useState: function (initialState) {
        return updateState(initialState);
    }
  }

持续看updateEffect

function updateEffect(create, deps) {
  {
    // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
    if ('undefined' !== typeof jest) {
      warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber$1);
    }
  
  
  return updateEffectImpl(Update | Passive, Passive$1, create, deps);
}

持续看updateEffectImpl

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);
}

会发现无论useEffect的依赖项是否变动,都会执行pushEffect办法,那咱们一探到底

function pushEffect(tag, create, destroy, deps) {
  var effect = {
    tag: tag,
    create: create,
    destroy: destroy,
    deps: deps,
    next: null
  };
  var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;

  // 创立/更新componentUpdateQueue队列
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    var lastEffect = componentUpdateQueue.lastEffect;

    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      var firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }

  return effect;
}

次要做了两件事件:
1、创立一个effect对象并返回(和首次渲染的流程雷同)
2、同时创立/更新componentUpdateQueue队列,这个队列是用来专门存取以后组件中所有的useEffect这个hook的队列(因为useEffect的回调其实是异步执行的,这里专门用一个队列存取是为了在调度阶段对所有的回调函数更不便的进行遍历解决),componentUpdateQueue队列不存在的话会进行创立,如果存在,会和mountState阶段一样创立一个effect的循环链表(这里就不画图了,具体参考下面的update更新队列的图片,只是把update替换成了effect),每个effect对象中有一个tag属性(tag的值相似于0和1),方才说到在调度阶段回遍历每一个effect,这个属性就是在遍历过程中用来判断useEffect回调是否须要被执行(这里再举荐一个解说useEffect执行机会的深度好文:https://www.cnblogs.com/iheyunfei/p/13065047.html

到这里更新阶段两个外围hook的执行流程就解说结束


以下是咱们在应用hook过程中遇到的一些问题,也能够一一解答了

1、为什么hook之间肯定要固定程序/不能用条件判断?

因为在某一组件中,每个hook之间是通过next指针顺次按程序连贯的,所以一旦应用条件判断后会导致某个hook在某个状况下不存在,那么整个hook链表就被中断,无奈失常遍历以及hook的获取,从而引发问题

2、屡次state的更新,是如何以最初一次为准的,外部机制是
怎么的?

一句话陈说:通过一个update队列存储屡次state的值顺次遍历获取到最新值
具体参考上文的具体解说

3、useEffect如何实现仅执行单次/依据依赖项变动执行屡次?

首次遍历都会执行,更新阶段每个effect通过tag标识来判断是都须要执行回调

4、hooks外部为什么应用链表构造而不应用其余数据结构实现?

集体的认识是归纳于链式构造和程序构造的区别以及实用场景:
1、链式构造对内存要求不刻薄,能够随便寄存
2、链式构造更适宜数据的增/删操作,在剖析源码过程发现增/删的场景偏多(update、effect循环队列的生成等)

如果本文对你有帮忙,那就请大佬们点个小赞或者珍藏,如若剖析有误也请及时纠正。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理