React 17 hooks 原理(详细解析useEffect源码)

简介

版本为17.0.3,这篇文章之前需要对react 更新的机制有基本了解。
react遵循函数式编程的理念,但实际业务中使用的时候会因为state,接口调用等无法成为一个函数组件

参考hooks的官方文档,hooks的优点有:
  • class在组件之间复用状态逻辑很难,hooks为共享状态逻辑提供更好的原生途径
  • class复杂组件因为生命周期的逻辑变得难以理解,hooks将逻辑拆成更小单元,并提供useEffect之类托管副作用
  • class学习成本高,Hook 使你在非 class 的情况下可以使用更多的 React 特性,更符合函数是编程的理念

Hooks如何挂载

对于函数组件(FunctionComponent)类型,在beginwork的时候会调用renderWithHooks注册,根据mount还是update调用一个函数生成hooks链表挂在Fiber上


function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
  renderLanes = nextRenderLanes;
  currentlyRenderingFiber$1 = workInProgress;
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes; // The following should have already been reset
  {
    if (current !== null && current.memoizedState !== null) {
      ReactCurrentDispatcher$1.current = HooksDispatcherOnUpdateInDEV;
    } else if (hookTypesDev !== null) {
 
      ReactCurrentDispatcher$1.current = HooksDispatcherOnMountWithHookTypesInDEV;
    } else {
      ReactCurrentDispatcher$1.current = HooksDispatcherOnMountInDEV;
    }
  }
   ...
  return children;
}
复制代码
hook对象属性
hook:{
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null
  };
复制代码
mount

会让current指向HooksDispatcherOnMountInDEV,使调用的时候获取到正确的hooks,对象如下

update时跟mount也近似,hooks中对于链表的处理不太一样

  HooksDispatcherOnMountInDEV = {
    ...
    useRef: function (initialValue) {
      currentHookNameInDev = 'useRef';
      mountHookTypesDev();
      return mountRef(initialValue);
    },
    useState: function (initialState) {
      currentHookNameInDev = 'useState';
      mountHookTypesDev();
      var prevDispatcher = ReactCurrentDispatcher$1.current;
      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;
      try {
        return mountState(initialState);
      } finally {
        ReactCurrentDispatcher$1.current = prevDispatcher;
      }
    },
    ...
    unstable_isNewReconciler: enableNewReconciler
  };
复制代码

在functionComponent中调用hooks,会创建一个hook对象 ,挂在当前fiber的memoizedState上,如果已经有hook挂了,就会用next记录下个hooks位置形成一个链表,并且用workInProgressHook记录当前hook

update

对于更新时 由于fiber双缓存机制,current tree 跟workInProgress tree中都会有hooks链表,如果他为空会复制原先的链表,用current作为指针记录工作hook位置


function updateWorkInProgressHook() {
  var nextCurrentHook;
  if (currentHook === null) {
    var current = currentlyRenderingFiber$1.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }
  var nextWorkInProgressHook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }
  if (nextWorkInProgressHook !== null) {
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
    if (!(nextCurrentHook !== null)) {
      {
        throw Error( "Rendered more hooks than during the previous render." );
      }
    }
    currentHook = nextCurrentHook;
    var newHook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null
    };
    if (workInProgressHook === null) {
      // This is the first hook in the list.
      currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
    } else {
      // Append to the end of the list.
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}
复制代码

一些hooks例子源码解析

准备解析下 useEffect|useLayoutEffectuseState

useEffect | useLayoutEffect

创建effect

调用pushEffect创建effect对象挂在hook的memoizedState上,并且挂在fiber的updateQueue当中,形成环形链表

结合之前创建更新hooks部分,有两个useEffect hooks最终类似于这么一个结构:

fiber.memoizedState = hook1.next=hook2

hook1.memoizedState = effect1.next=effect2

hook2.memoizedState = effect2.next=effect1

fiber.updateQueue = effect1.next=effect2 // 跟class/hostComponent存储的updater对象不同

function pushEffect(tag, create, destroy, deps) {
  var effect = {
    tag: tag, // effect的类型,用于区分同步调用还是调度调用,useEffect useLayoutEffect 
    create: create, // 回调函数,类似class组件的componentDidmount/update 时调用
    destroy: destroy, // 回调函数return的函数 类似class组件的componentWIllUnmount执行
    deps: deps, // 依赖项
    // Circular
    next: null // 环形链表
  };
  var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
  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;
}
复制代码
更新effect

逻辑跟上面差不多,不同的是取了当前树的destroy,因为每次对于组件都要先执行销毁,再更新,还对比了deps,监视数组中的值有没有变化,有才会把调用pushEffect去执行,

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

  if (currentHook !== null) {
    var prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;

    if (nextDeps !== null) {
      var prevDeps = prevEffect.deps;

      if (areHookInputsEqual(nextDeps, prevDeps)) {
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }
  currentlyRenderingFiber$1.flags |= fiberFlags;
  hook.memoizedState = pushEffect(HasEffect | hookFlags, create, destroy, nextDeps);
}
复制代码
调度&使用

commit函数比较复杂,分为三个阶段去看: mutation之前, mutation , layout
useEffect在 mutation之前阶段交给scheduleCallback调度,layout阶段执行。
mutation之前执行flushPassiveEffects,因为NormalPriority优先级较低,每次commit之前需要把之前的effect执行,然后调度执行。
在flushPassiveEffects方法内部,会遍历rootWithPendingPassiveEffects(即effectList)执行effect回调函数。
effectList:遍历Fiber树时,由beginWork为Fiber打上Placement Update Deletion标签(Fiber.flag,以前叫effect),在completeWork阶段收集起来的链表。


function commitRootImpl(root, renderPriorityLevel) {
  do {
      flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);
  ...
// before mutation阶段在scheduleCallback中调度flushPassiveEffects
// layout阶段之后将effectList赋值给rootWithPendingPassiveEffects
// scheduleCallback触发flushPassiveEffects,flushPassiveEffects内部遍历rootWithPendingPassiveEffects

  if ((finishedWork.subtreeFlags & PassiveMask) !== NoFlags || (finishedWork.flags & PassiveMask) !== NoFlags) {
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      scheduleCallback(NormalPriority, function () {
        flushPassiveEffects();
        return null;
      });
    }
  } 
  ...
   // 遍历effectlist 对dom进行增删改查,
   //对于updateEffect funComponent 执行useLayoutEffect销毁函数     
   // 调用componentWillUnmount钩子
   // 对deletionEffect 执行删除操作并执行seEffect 回调。去除ref
   commitMutationEffects(root, finishedWork, lanes);
  ...
  // 执行layoutEffect
  commitLayoutEffects(finishedWork, root, lanes);
  requestPaint();
}
复制代码
useLayoutEffect:

会在上述的commit函数中的mutation时调用commitHookEffectListUnmount,作用为销毁,执行updateQueue上的useLayoutEffect的effect的destroy方法

function commitHookEffectListUnmount(flags, finishedWork, nearestMountedAncestor) {
  var updateQueue = finishedWork.updateQueue;
  var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;

  if (lastEffect !== null) {
    var firstEffect = lastEffect.next;
    var effect = firstEffect;

    do {
      if ((effect.tag & flags) === flags) {
        // Unmount
        var destroy = effect.destroy;
        effect.destroy = undefined;

        if (destroy !== undefined) {
          safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
        }
      }

      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
复制代码

接下来commit函数执行到layout阶段,commitLayoutEffects会调commitLayoutEffects_begin,对于有flags(即effect)的fiber,执行commitLayoutEffectOnFiber,他实际上根据tag执行commitHookEffectListMount方法,并会在tag等于class组件时执行didmount的生命周期,更新ref。
commitHookEffectListMount方法会调用create 新建一个destroy。

function commitHookEffectListMount(tag, finishedWork) {
 var updateQueue = finishedWork.updateQueue;
 var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
 if (lastEffect !== null) {
   var firstEffect = lastEffect.next;
   var effect = firstEffect;

   do {
     if ((effect.tag & tag) === tag) {
       // Mount
       var create = effect.create;
       effect.destroy = create();
       // error处理...
       }
      }
   }
  }
复制代码
useEffect:

flushPassiveEffects函数执行useEffect,他的作用是在处理effect时降低任务优先级项,最后再改回来,然后执行flushPassiveEffectsImpl,由于是调度进行会对已经删除的Fiber做处理,最后也会执行上述的commitHookEffectListUnmount,commitHookEffectListMount

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享