react学习系列——useEffect不一定是异步的!!!
源码
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();
}
可以看出这里会依次进行
- useLayoutEffect的unmount
- useEffect的unmount
- useLayoutEffect的mount
- 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
任务的后面
performance很具有迷惑性,上图其实processRootScheduleInMicrotask
已经在微任务执行了,挂载了render schedule,然后再执行的flushPassiveEffects
等flushPassiveEffects
执行完后,再在下个宏任务执行render
但是performance没有标注出processRootScheduleInMicrotask
的执行,导致很像宏任务优先于微任务执行。卡在这里一天了......一度怀疑人生
初始化的时候是异步处理useEffect,页面不会有卡顿
同步
点击按钮,页面会出现卡顿,卡顿的时间就是两次useEffect回调里面的1s,一共两秒
总结
useEffect是否是异步 完全取决于触发事件是同步还是异步的
但是useLayout一直都是同步的,他需要阻塞主线程推迟浏览器的渲染
转载自:https://juejin.cn/post/7353543714151612455