useLayoutEffect为什么能阻塞浏览器的绘制?

前言

在react的官方文档上,useLayoutEffect有这么一段描述,它可以阻塞浏览器的绘制,实际情况下这个api我们可能用的比较少,但是对于一些关乎用户体验的场景,它的作用就变得非常大,它可以使错误绘制的一帧被掩盖,直到我们计算出正确的内容,再显示给用户。

接下来就话不多说,直接步入主题,它究竟是怎么做到的?

useLayoutEffect源码分析

react的仓库在facebook/react,而useLayoutEffect的源码在packages/react-reconciler/src/ReactFiberHooks.js

在函数组件被调用的过程中,useLayoutEffect的hook也会被调用,在组件挂载和组件更新的周期中,useLayoutEffect对应着两个方法,即mountLayoutEffect和updateLayoutEffect

它们则分别调用了mountEffectImpl,updateEffectImpl,这两个函数useEffect也会用到,因此useLayoutEffect在挂载时传了一个fiberFlags用于区分

js 复制代码
function mountLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  // 此处省略, 注意这里传了一个fiberFlags
  //用于区分useEffect和useLayoutEffect
  return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}

function updateLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}

mountEffectImpl 和 updateEffectImpl

mountEffectImpln即挂载Effect,它会创建一个hook对象并添加到当前fiber的hooks链表中,并且创建一个Effect,并记录到hook对象中,记录的东西自然也就是useLayoutEffect的回调函数、依赖数组了

updateEffectImpl则是去浅比较前后的依赖数组中的变量是否有改变,如果有则改变更新它的回调函数,并打上标记,在后续的流程中触发它

js 复制代码
function mountEffectImpl(
  fiberFlags: Flags,
  hookFlags: HookFlags,
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    createEffectInstance(),
    nextDeps,
  );
}

function updateEffectImpl(
  fiberFlags: Flags,
  hookFlags: HookFlags,
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const effect: Effect = hook.memoizedState;
  const inst = effect.inst;

  // currentHook is null on initial mount when rerendering after a render phase
  // state update or for strict mode.
  if (currentHook !== null) {
    if (nextDeps !== null) {
      const prevEffect: Effect = currentHook.memoizedState;
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps);
        return;
      }
    }
  }

  currentlyRenderingFiber.flags |= fiberFlags;

  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    inst,
    nextDeps,
  );
}

看完useLayoutEffect的基本源码,接下来我们看看它的调用时机

useLayoutEffect回调函数的调用时机

react中有一个关键的概念,就是fiber架构,而fiber架构的核心也就是调度器了,16.8以后,react中几乎所有的任务都通过调度器来进行调度,useLayoutEffect的回调也不例外,在分析其回调的调用时机之前,还需先讲一下react的调度器调度任务的本质

react调度任务时会根据优先级分别调用两个api,也就是scheduleImmediateTaskscheduleCallback

scheduleImmediateTask会通过微任务进行调度任务(microtask 或 promise),而scheduleCallback则通过宏任务进行调度(setImediate、messageChannel和setTimeout),这样调度任务正好与浏览器的绘制时机撞上了

浏览器在一帧的绘制中会执行以下的任务,其中js的任务执行,浏览器会保证同步任务和微任务都得到执行完,而宏任务如果时间不够则会放到下一帧进行执行,也因此,react大量的使用宏任务去调度任务,一定程度上避免了阻塞浏览器的绘制(更多的还是依赖时间切片完成的)

那么useLayoutEffect的回调的调度不言而喻也就是微任务调度的了,现在我们来找找它的调度时机

在函数组件一次的更新中,会先通过调度器调度fiber节点上的所有任务,再根据是否要进行时间切片,进入协调阶段,更新fiber树,当所有fiber节点协调阶段的递与归阶段都执行完了以后,就到了react提交阶段,这个也就是useLayoutEffect和useEffect执行时机不同的地方

react的提交阶段也分为三个阶段,分别对应着commitBeforeMutationEffects、commitEffects和commitLayoutEffects这三个方法的调用

它会在commitBeforeMutationEffects方法之前通过宏任务调度flushPassiveEffects方法来useEffect的回调和销毁函数

js 复制代码
function commitRootImpl(
  root: FiberRoot,
  recoverableErrors: null | Array<CapturedValue<mixed>>,
  transitions: Array<Transition> | null,
  renderPriorityLevel: EventPriority,
  spawnedLane: Lane,
) {
  // If there are pending passive effects, schedule a callback to process them.
  // Do this as early as possible, so it is queued before anything else that
  // might get scheduled in the commit phase. (See #16714.)
  // TODO: Delete all other places that schedule the passive effect callback
  // They're redundant.
  if (
    (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
    (finishedWork.flags & PassiveMask) !== NoFlags
  ) {
    if (!rootDoesHavePassiveEffects) {
      scheduleCallback(NormalSchedulerPriority, () => {
        flushPassiveEffects();
        return null;
      });
    }
  }

  if (subtreeHasEffects || rootHasEffect) {
    const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
      root,
      finishedWork,
    );

    // The next phase is the mutation phase, where we mutate the host tree.
    commitMutationEffects(root, finishedWork, lanes);
    commitLayoutEffects(finishedWork, root, lanes);
  }


  // Always call this before exiting `commitRoot`, to ensure that any
  // additional work on this root is scheduled.
  ensureRootIsScheduled(root);
  // If layout work was scheduled, flush it now.
  flushSyncWorkOnAllRoots();

  return null;
}

useLayoutEffect的回调和销毁函数则在commitLayoutEffects方法中被高优先级调度

而这个时候react的dom操作都已经操作完了,已经到了浏览器的layout阶段,但是useLayoutEffect的微任务调度导致它的任务可以被浏览器保证执行,也就阻塞了浏览器的绘制,直到其任务执行完,浏览器才能执行paint阶段

最后

以上内容如有纰漏,敬请指教

相关推荐
zwjapple4 小时前
docker-compose一键部署全栈项目。springboot后端,react前端
前端·spring boot·docker
像风一样自由20206 小时前
HTML与JavaScript:构建动态交互式Web页面的基石
前端·javascript·html
aiprtem7 小时前
基于Flutter的web登录设计
前端·flutter
浪裡遊7 小时前
React Hooks全面解析:从基础到高级的实用指南
开发语言·前端·javascript·react.js·node.js·ecmascript·php
why技术7 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
GISer_Jing7 小时前
0704-0706上海,又聚上了
前端·新浪微博
止观止8 小时前
深入探索 pnpm:高效磁盘利用与灵活的包管理解决方案
前端·pnpm·前端工程化·包管理器
whale fall8 小时前
npm install安装的node_modules是什么
前端·npm·node.js
烛阴8 小时前
简单入门Python装饰器
前端·python
袁煦丞9 小时前
数据库设计神器DrawDB:cpolar内网穿透实验室第595个成功挑战
前端·程序员·远程工作