乐趣区

关于前端: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 循环队列的生成等)

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

退出移动版