likes
comments
collection
share

react学习系列——useEffect不一定是异步的!!!

作者站长头像
站长
· 阅读数 49

源码

useEffect触发在flushPassiveEffects,其可能是异步也可能是同步触发的

先来看flushPassiveEffects的逻辑


function flushPassiveEffects() {
  // Returns whether passive effects were flushed.
  // TODO: Combine this check with the one in flushPassiveEFfectsImpl. We should
  // probably just combine the two functions. I believe they were only separate
  // in the first place because we used to wrap it with
  // `Scheduler.runWithPriority`, which accepts a function. But now we track the
  // priority within React itself, so we can mutate the variable directly.
  if (rootWithPendingPassiveEffects !== null) {
    // Cache the root since rootWithPendingPassiveEffects is cleared in
    // flushPassiveEffectsImpl
    var root = rootWithPendingPassiveEffects; // Cache and clear the remaining lanes flag; it must be reset since this
    // method can be called from various places, not always from commitRoot
    // where the remaining lanes are known

    var remainingLanes = pendingPassiveEffectsRemainingLanes;
    pendingPassiveEffectsRemainingLanes = NoLanes;
    var renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes);
    var priority = lowerEventPriority(DefaultEventPriority, renderPriority);
    var prevTransition = ReactCurrentBatchConfig$1.transition;
    var previousPriority = getCurrentUpdatePriority();

    try {
      ReactCurrentBatchConfig$1.transition = null;
      setCurrentUpdatePriority(priority);
      return flushPassiveEffectsImpl();
    } finally {
      setCurrentUpdatePriority(previousPriority);
      ReactCurrentBatchConfig$1.transition = prevTransition; // Once passive effects have run for the tree - giving components a
      // chance to retain cache instances they use - release the pooled
      // cache at the root (if there is one)

      releaseRootPooledCache(root, remainingLanes);
    }
  }

  return false;
}

rootWithPendingPassiveEffects只有在commitRootImpl 运行到 commitLayoutEffects 之后才会赋值,进入flushPassiveEffectsImpl

function flushPassiveEffectsImpl() {
  if (rootWithPendingPassiveEffects === null) {
    return false;
  } // Cache and clear the transitions flag


  var transitions = pendingPassiveTransitions;
  pendingPassiveTransitions = null;
  var root = rootWithPendingPassiveEffects;
  var lanes = pendingPassiveEffectsLanes;
  rootWithPendingPassiveEffects = null; // TODO: This is sometimes out of sync with rootWithPendingPassiveEffects.
  // Figure out why and fix it. It's not causing any known issues (probably
  // because it's only used for profiling), but it's a refactor hazard.

  pendingPassiveEffectsLanes = NoLanes;

  if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
    throw new Error('Cannot flush passive effects while already rendering.');
  }

  {
    isFlushingPassiveEffects = true;
    didScheduleUpdateDuringPassiveEffects = false;
  }

  {
    markPassiveEffectsStarted(lanes);
  }

  var prevExecutionContext = executionContext;
  executionContext |= CommitContext;
  commitPassiveUnmountEffects(root.current);
  commitPassiveMountEffects(root, root.current, lanes, transitions); // TODO: Move to commitPassiveMountEffects

  {
    var profilerEffects = pendingPassiveProfilerEffects;
    pendingPassiveProfilerEffects = [];

    for (var i = 0; i < profilerEffects.length; i++) {
      var fiber = profilerEffects[i];
      commitPassiveEffectDurations(root, fiber);
    }
  }

  {
    markPassiveEffectsStopped();
  }

  {
    commitDoubleInvokeEffectsInDEV(root, true);
  }

  executionContext = prevExecutionContext;
  flushSyncWorkOnAllRoots();

  {
    // If additional passive effects were scheduled, increment a counter. If this
    // exceeds the limit, we'll fire a warning.
    if (didScheduleUpdateDuringPassiveEffects) {
      if (root === rootWithPassiveNestedUpdates) {
        nestedPassiveUpdateCount++;
      } else {
        nestedPassiveUpdateCount = 0;
        rootWithPassiveNestedUpdates = root;
      }
    } else {
      nestedPassiveUpdateCount = 0;
    }

    isFlushingPassiveEffects = false;
    didScheduleUpdateDuringPassiveEffects = false;
  } // TODO: Move to commitPassiveMountEffects


  onPostCommitRoot(root);

  {
    var stateNode = root.current.stateNode;
    stateNode.effectDuration = 0;
    stateNode.passiveEffectDuration = 0;
  }

  return true;
}

第一次处理

flushPassiveEffectsImpl 最关键的是这两句

commitPassiveUnmountEffects(root.current);
commitPassiveMountEffects(root, root.current, lanes, transitions); // TODO: Move to commitPassiveMountEffects

首先会触发 useEffect的destory

function commitPassiveUnmountEffects(finishedWork) {
  setCurrentFiber(finishedWork);
  commitPassiveUnmountOnFiber(finishedWork);
  resetCurrentFiber();
} 
function commitPassiveUnmountOnFiber(finishedWork) {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
      {
        recursivelyTraversePassiveUnmountEffects(finishedWork);

        if (finishedWork.flags & Passive$1) {
          commitHookPassiveUnmountEffects(finishedWork, finishedWork.return, Passive | HasEffect);
        }

        break;
      }

    case OffscreenComponent:
      {
        var instance = finishedWork.stateNode;
        var nextState = finishedWork.memoizedState;
        var isHidden = nextState !== null;

        if (isHidden && instance._visibility & OffscreenPassiveEffectsConnected && ( // For backwards compatibility, don't unmount when a tree suspends. In
        // the future we may change this to unmount after a delay.
        finishedWork.return === null || finishedWork.return.tag !== SuspenseComponent)) {
          // The effects are currently connected. Disconnect them.
          // TODO: Add option or heuristic to delay before disconnecting the
          // effects. Then if the tree reappears before the delay has elapsed, we
          // can skip toggling the effects entirely.
          instance._visibility &= ~OffscreenPassiveEffectsConnected;
          recursivelyTraverseDisconnectPassiveEffects(finishedWork);
        } else {
          recursivelyTraversePassiveUnmountEffects(finishedWork);
        }

        break;
      }

    default:
      {
        recursivelyTraversePassiveUnmountEffects(finishedWork);
        break;
      }
  }
}

commitPassiveUnmountOnFiber会调用recursivelyTraversePassiveUnmountEffects

function recursivelyTraversePassiveUnmountEffects(parentFiber) {
  // Deletions effects can be scheduled on any fiber type. They need to happen
  // before the children effects have fired.
  var deletions = parentFiber.deletions;

  if ((parentFiber.flags & ChildDeletion) !== NoFlags$1) {
    if (deletions !== null) {
      for (var i = 0; i < deletions.length; i++) {
        var childToDelete = deletions[i]; // TODO: Convert this to use recursion

        nextEffect = childToDelete;
        commitPassiveUnmountEffectsInsideOfDeletedTree_begin(childToDelete, parentFiber);
      }
    }

    detachAlternateSiblings(parentFiber);
  }

  var prevDebugFiber = getCurrentFiber(); // TODO: Split PassiveMask into separate masks for mount and unmount?

  if (parentFiber.subtreeFlags & PassiveMask) {
    var child = parentFiber.child;

    while (child !== null) {
      setCurrentFiber(child);
      commitPassiveUnmountOnFiber(child);
      child = child.sibling;
    }
  }

  setCurrentFiber(prevDebugFiber);
}

和mount的删除逻辑类似,若是有删除的fiber,并且fiber上有effect hook,会调用它的destory,然后处理它的兄弟节点

如果没有的话,遍历fiber找到挂载的effect hook,然后destory

然后是commitPassiveMountEffects

function commitPassiveMountEffects(root, finishedWork, committedLanes, committedTransitions) {
  setCurrentFiber(finishedWork);
  commitPassiveMountOnFiber(root, finishedWork, committedLanes, committedTransitions);
  resetCurrentFiber();
}
function commitPassiveMountOnFiber(finishedRoot, finishedWork, committedLanes, committedTransitions) {
  // When updating this function, also update reconnectPassiveEffects, which does
  // most of the same things when an offscreen tree goes from hidden -> visible,
  // or when toggling effects inside a hidden tree.
  var flags = finishedWork.flags;

  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
      {
        recursivelyTraversePassiveMountEffects(finishedRoot, finishedWork, committedLanes, committedTransitions);

        if (flags & Passive$1) {
          commitHookPassiveMountEffects(finishedWork, Passive | HasEffect);
        }

        break;
      }

    case HostRoot:
      {
        recursivelyTraversePassiveMountEffects(finishedRoot, finishedWork, committedLanes, committedTransitions);

        if (flags & Passive$1) {
          {
            var previousCache = null;

            if (finishedWork.alternate !== null) {
              previousCache = finishedWork.alternate.memoizedState.cache;
            }

            var nextCache = finishedWork.memoizedState.cache; // Retain/release the root cache.
            // Note that on initial mount, previousCache and nextCache will be the same
            // and this retain won't occur. To counter this, we instead retain the HostRoot's
            // initial cache when creating the root itself (see createFiberRoot() in
            // ReactFiberRoot.js). Subsequent updates that change the cache are reflected
            // here, such that previous/next caches are retained correctly.

            if (nextCache !== previousCache) {
              retainCache(nextCache);

              if (previousCache != null) {
                releaseCache(previousCache);
              }
            }
          }
        }

        break;
      }

    case LegacyHiddenComponent:
      {

        break;
      }

    case OffscreenComponent:
      {
        // TODO: Pass `current` as argument to this function
        var _instance3 = finishedWork.stateNode;
        var nextState = finishedWork.memoizedState;
        var isHidden = nextState !== null;

        if (isHidden) {
          if (_instance3._visibility & OffscreenPassiveEffectsConnected) {
            // The effects are currently connected. Update them.
            recursivelyTraversePassiveMountEffects(finishedRoot, finishedWork, committedLanes, committedTransitions);
          } else {
            if (finishedWork.mode & ConcurrentMode) {
              // The effects are currently disconnected. Since the tree is hidden,
              // don't connect them. This also applies to the initial render.
              {
                // "Atomic" effects are ones that need to fire on every commit,
                // even during pre-rendering. An example is updating the reference
                // count on cache instances.
                recursivelyTraverseAtomicPassiveEffects(finishedRoot, finishedWork);
              }
            } else {
              // Legacy Mode: Fire the effects even if the tree is hidden.
              _instance3._visibility |= OffscreenPassiveEffectsConnected;
              recursivelyTraversePassiveMountEffects(finishedRoot, finishedWork, committedLanes, committedTransitions);
            }
          }
        } else {
          // Tree is visible
          if (_instance3._visibility & OffscreenPassiveEffectsConnected) {
            // The effects are currently connected. Update them.
            recursivelyTraversePassiveMountEffects(finishedRoot, finishedWork, committedLanes, committedTransitions);
          } else {
            // The effects are currently disconnected. Reconnect them, while also
            // firing effects inside newly mounted trees. This also applies to
            // the initial render.
            _instance3._visibility |= OffscreenPassiveEffectsConnected;
            var includeWorkInProgressEffects = (finishedWork.subtreeFlags & PassiveMask) !== NoFlags$1;
            recursivelyTraverseReconnectPassiveEffects(finishedRoot, finishedWork, committedLanes, committedTransitions, includeWorkInProgressEffects);
          }
        }

        if (flags & Passive$1) {
          var _current = finishedWork.alternate;
          commitOffscreenPassiveMountEffects(_current, finishedWork);
        }

        break;
      }

    case CacheComponent:
      {
        recursivelyTraversePassiveMountEffects(finishedRoot, finishedWork, committedLanes, committedTransitions);

        if (flags & Passive$1) {
          // TODO: Pass `current` as argument to this function
          var _current2 = finishedWork.alternate;
          commitCachePassiveMountEffect(_current2, finishedWork);
        }

        break;
      }

    case TracingMarkerComponent:

    default:
      {
        recursivelyTraversePassiveMountEffects(finishedRoot, finishedWork, committedLanes, committedTransitions);
        break;
      }
  }
}
function recursivelyTraversePassiveMountEffects(root, parentFiber, committedLanes, committedTransitions) {
  var prevDebugFiber = getCurrentFiber();

  if (parentFiber.subtreeFlags & PassiveMask) {
    var child = parentFiber.child;

    while (child !== null) {
      setCurrentFiber(child);
      commitPassiveMountOnFiber(root, child, committedLanes, committedTransitions);
      child = child.sibling;
    }
  }

  setCurrentFiber(prevDebugFiber);
}

也会从root递归找到挂载effect hook的fiber

首次mount的时候,effect还没有create,destory也是空的,就会进行一次commitPassiveMountEffects

第二次处理

此时进入legacyCommitDoubleInvokeEffectsInDEV 进行第二次unmount

function legacyCommitDoubleInvokeEffectsInDEV(fiber, hasPassiveEffects) {
  // TODO (StrictEffects) Should we set a marker on the root if it contains strict effects
  // so we don't traverse unnecessarily? similar to subtreeFlags but just at the root level.
  // Maybe not a big deal since this is DEV only behavior.
  setCurrentFiber(fiber);
  invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectUnmountInDEV);

  if (hasPassiveEffects) {
    invokeEffectsInDev(fiber, MountPassiveDev, invokePassiveEffectUnmountInDEV);
  }

  invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectMountInDEV);

  if (hasPassiveEffects) {
    invokeEffectsInDev(fiber, MountPassiveDev, invokePassiveEffectMountInDEV);
  }

  resetCurrentFiber();
}

可以看出这里会依次进行

  1. useLayoutEffect的unmount
  2. useEffect的unmount
  3. useLayoutEffect的mount
  4. useEffect的mount
function invokeEffectsInDev(firstChild, fiberFlags, invokeEffectFn) {
  var current = firstChild;
  var subtreeRoot = null;

  while (current != null) {
    var primarySubtreeFlag = current.subtreeFlags & fiberFlags;

    if (current !== subtreeRoot && current.child != null && primarySubtreeFlag !== NoFlags$1) {
      current = current.child;
    } else {
      if ((current.flags & fiberFlags) !== NoFlags$1) {
        invokeEffectFn(current);
      }

      if (current.sibling !== null) {
        current = current.sibling;
      } else {
        current = subtreeRoot = current.return;
      }
    }
  }
}

invokeEffectsInDev会先进行深度遍历,先处理child,然后再是父级以及兄弟节点

运行时机

function Child() {
    useEffect(()=>{
      console.log('--------Son useEffect-------')
      return () => {
        console.log('effect destory')
      }
    })
    useLayoutEffect(()=>{
        console.log('--------Son useLayoutEffect-------')
    })
    useInsertionEffect(()=>{
        console.log('--------Son useInsertionEffect-------')
    })
    return <div>123</div>
  }
  export default function Index() {
    const [isShow, setIsShow] = useState(true)
  
    useEffect(()=>{
      
      let now = performance.now();
      while (performance.now() - now < 1000) {
        // 不做任何事情...
      }
      console.log('--------Father useEffect-------')
      return () => {
        console.log('effect destory')
      }
    })
    useLayoutEffect(()=>{
        console.log('--------Father useLayoutEffect-------')
    })
    useInsertionEffect(()=>{
        console.log('--------Father useInsertionEffect-------')
    })
  
    return <>
    <button onClick={() => setIsShow(v => !v)}>{isShow.toString()}</button>
    {isShow && <div>hhahh
      <Child />
      </div>}
      {!isShow && <div>add</div>}
    </>
  }

异步

首先很容易陷入误区的一点是:

初始化首次加载采用concurrent

此时schedule的任务队列不为空,并且isHostCallbackScheduled = true,后面再进入schedule,只会增加taskQueue里面的任务

因此,当进入commitRootImpl采用sheduler运行flushPassiveEffects的时候

scheduleCallback(NormalPriority$1, function () {
        flushPassiveEffects(); // This render triggered passive effects: release the root cache pool
        // *after* passive effects fire to avoid freeing a cache pool that may
        // be referenced by a node in the tree (HostRoot, Cache boundary etc)

        return null;
      });

此时只是在taskQueue增加了任务,而当前commit执行完后,schedule的while循环里面继续遍历,但是由于commit + render 超过了5ms,会在下一个宏任务进行flushPassiveEffects

commitRootImpl后面进行了ensureRootIsScheduled(root),会重新进行一次fiber的处理processRootScheduleInMicrotask,这个任务是挂载到微任务上面

所以flushPassiveEffects先于processRootScheduleInMicrotask执行

但是processRootScheduleInMicrotask 对render是通过schedule调度执行的,因此真正的render排在了flushPassiveEffects任务的后面

react学习系列——useEffect不一定是异步的!!!

performance很具有迷惑性,上图其实processRootScheduleInMicrotask已经在微任务执行了,挂载了render schedule,然后再执行的flushPassiveEffects

react学习系列——useEffect不一定是异步的!!!

flushPassiveEffects执行完后,再在下个宏任务执行render

但是performance没有标注出processRootScheduleInMicrotask的执行,导致很像宏任务优先于微任务执行。卡在这里一天了......一度怀疑人生

初始化的时候是异步处理useEffect,页面不会有卡顿

同步

点击按钮,页面会出现卡顿,卡顿的时间就是两次useEffect回调里面的1s,一共两秒

总结

useEffect是否是异步 完全取决于触发事件是同步还是异步的

但是useLayout一直都是同步的,他需要阻塞主线程推迟浏览器的渲染

转载自:https://juejin.cn/post/7353543714151612455
评论
请登录