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

3,278 阅读2分钟

简介

版本为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

useState|useReducer

这两个hook源码逻辑上基本差不多

自定义useMyReducer

用useState简单的实现自定义useReducer的hook

function useMyReducer(reducer, initialState) {
    const [state, setState] = useState(initialState);
    const dispatch = (action) => {
        setState(reducer(state, action))
    };
    return [state, dispatch]
}

首先生成hook挂在fiber对象上,useState的memoizedState跟useEffect不同,保存的是initalState 生成更新对象挂在hook的queue上.返回state跟dispatch

function mountState(initialState) {
  var hook = mountWorkInProgressHook();

  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }

  hook.memoizedState = hook.baseState = initialState;
  var queue = hook.queue = {
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState
  };
  var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
  return [hook.memoizedState, dispatch];
}

跟enqueueSetState方法有点像,dispatchAction会创建update链表挂在queue.pending上,获取优先级,计算state值比较跟前一次的是否相等,有变化则通过scheduleUpdateOnFiber去开启执行逻辑。
对于useReducer来说lastRenderedReducer就是传入的reducer,useState则是记录了state值,

function dispatchAction(fiber, queue, action) {
  ... // 省略异常处理
  var eventTime = requestEventTime();
  var lane = requestUpdateLane(fiber);
  var update = {
    lane: lane,
    action: action,
    eagerReducer: null,
    eagerState: null,
    next: null
  };
  var alternate = fiber.alternate;
  if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
  // update时重置指针形成环形链表
    didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
    var pending = queue.pending;
    if (pending === null) {
      // This is the first update. Create a circular list.
      update.next = update;
    } else {
      update.next = pending.next;
      pending.next = update;
    }
    queue.pending = update;
  } else {
    if (isInterleavedUpdate(fiber)) {
      var interleaved = queue.interleaved;
      if (interleaved === null) {
        update.next = update; 
        pushInterleavedQueue(queue);
      } else {
        update.next = interleaved.next;
        interleaved.next = update;
      }
      queue.interleaved = update;
    } else {
    // 初次创建时链表的挂载
      var _pending = queue.pending;
      if (_pending === null) {
        update.next = update;
      } else {
        update.next = _pending.next;
        _pending.next = update;
      }

      queue.pending = update;
    }

    if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
      var lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        var prevDispatcher;
        {
          prevDispatcher = ReactCurrentDispatcher$1.current;
          ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
        }
        try {
          var currentState = queue.lastRenderedState;
          var eagerState = lastRenderedReducer(currentState, action); // Stash the eagerly computed state, and the reducer used to compute
          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
          if (objectIs(eagerState, currentState)) {
            return;
          }
        } catch (error) {// Suppress the error. It will throw again in the render phase.
        } finally {
          {
            ReactCurrentDispatcher$1.current = prevDispatcher;
          }
        }
      }
    }
    ...
    var root = scheduleUpdateOnFiber(fiber, lane, eventTime);

    if (isTransitionLane(lane) && root !== null) {
      var queueLanes = queue.lanes; 
      queueLanes = intersectLanes(queueLanes, root.pendingLanes); 
      var newQueueLanes = mergeLanes(queueLanes, lane);
      queue.lanes = newQueueLanes;
      markRootEntangled(root, newQueueLanes);
    }
  }
  {
    markStateUpdateScheduled(fiber, lane);
  }
}