文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

面试官:useEffect和useLayoutEffect有什么区别?你能说说吗?

2024-11-30 04:14

关注

而对于副作用(useEffect or useLayoutEffect)来说,对应其hook类型就是Effect。

单个的effect对象包括以下几个属性:

单纯看这些字段,和平时使用层面来联想还是很通俗易懂的,这里还是补充一下hooks链表的概念,有如下的例子:

const Hello = () => {
    const [ text, setText ] = useState('hello')
    useEffect(() => {
        console.log('effect1')
        return () => {
            console.log('destory1');
        }
    })
    useLayoutEffect(() => {
        console.log('effect2')
        return () => {
            console.log('destory2');
        }
    })
    return 
effect
}

挂载到Hello组件fiber上memoizedState如下:

图片

可以看到,打印出来结果和组件中声明hook的顺序是一样的,不难看出这是一个链表,这也是为什么react hook要求hook的使用不能放在条件分支语句中的原因,如果第一次mount走的是A情况,第二次updateMount走的是B情况,就会出现hooks链表混乱的情况,保证官方范式是比较重要的原因。

Hook

从上图的例子中可以看到,memorizedState的值会根据不同hook来决定。

Hook类型如下:

export type Hook = { 
    memoizedState: any, // Hook 自身维护的状态 
    baseQueue: any,
    baseState: any,
    queue: UpdateQueue | null, // Hook 自身维护的更新队列 
    next: Hook | null, // next 指向下一个 Hook 
};

创建副作用流程

基于上面的数据结构,对于use(Layout)Effect来说,React做的事情就是

第二点提到了一个重点,就是useEffect和useLayoutEffect的执行时机不一样,前者被异步调度,当页面渲染完成后再去执行,不会阻塞页面渲染。 后者是在commit阶段新的DOM准备完成,但还未渲染到屏幕之前,同步执行。

创建effect链表

useEffect的工作是在currentlyRenderingFiber加载当前的hook,具体流程就是判断当前fiber是否已经存在hook(就是判断fiber.memoizedState),存在的话则创建一个effect hook到链表的最后,也就是.next,没有的话则创建一个memoizedState。

先看一下创建一个Effect的入口函数:

function mountEffect(
    create: () => (() => void) | void,
    deps: Array | void | null
): void {
    return mountEffectImpl(
        UpdateEffect | PassiveEffect,
        HookPassive,
        create,
        deps,
    );
};

可以看到本质上是调用了mountEffectImpl函数,传了上一节所说的Effect type中的字段,这里有个问题,为什么destroy没传呢?获取上一次effect的destroy函数,也就是useEffect回调中return的函数,在创建阶段是第一次,所以为undefined。

这里看一下创建阶段调用的mountEffectImpl函数:

function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  // 创建hook对象
  const hook = mountWorkInProgressHook();
  // 获取依赖
  const nextDeps = deps === undefined ? null : deps;

  // 为fiber打上副作用的effectTag
  currentlyRenderingFiber.flags |= fiberFlags;

  // 创建effect链表,挂载到hook的memoizedState上和fiber的updateQueue
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps,
  );
}

接下来我们都知道,React或Vue都是状态改变导致页面重渲染,而useEffect or useLayoutEffect都会会根据deps变化重新执行,所以猜都猜得到,在更新时调用的updateEffectImpl函数,对比mountEffectImpl函数多出来的一部分内容其实就是对比上一次的Effect的依赖变化,以及执行上一次Effect中的destroy部分内容~代码如下:

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

  if (currentHook !== null) {
    // 从currentHook中获取上一次的effect
    const prevEffect = currentHook.memoizedState;
    // 获取上一次effect的destory函数,也就是useEffect回调中return的函数
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      // 比较前后依赖,push一个不带HookHasEffect的effect
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }

  currentlyRenderingFiber.flags |= fiberFlags;
  // 如果前后依赖有变,在effect的tag中加入HookHasEffect
  // 并将新的effect更新到hook.memoizedState上
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}

可以看到在mountEffectImpl和updateEffectImpl中,最后的结果走向都是pushEffect函数,它的工作很纯粹,就是创建出effect对象,把对象挂到链表中。

pushEffect代码如下:

function pushEffect(tag, create, destroy, deps) {
  // 创建effect对象
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    // Circular
    next: (null: any),
  };

  // 从workInProgress节点上获取到updateQueue,为构建链表做准备
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
  if (componentUpdateQueue === null) {
    // 如果updateQueue为空,把effect放到链表中,和它自己形成闭环
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    // 将updateQueue赋值给WIP节点的updateQueue,实现effect链表的挂载
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    // updateQueue不为空,将effect接到链表的后边
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}

这里的主要逻辑其实就是本节开头所说的,区分两种情况,链表为空或链表存在的情况,值得一提的是这里的updateQueue是一个环形链表。

以上,就是effect链表的构建过程。我们可以看到,effect对象创建出来最终会以两种形式放到两个地方:单个的effect,放到hook.memorizedState上;环状的effect链表,放到fiber节点的updateQueue中。两者各有用途,前者的effect会作为上次更新的effect,为本次创建effect对象提供参照(对比依赖项数组),后者的effect链表会作为最终被执行的主体,带到commit阶段处理。

提交阶段

commitRoot

当我们完成更新,进入提交重渲染视图时,主要在commitRoot函数中执行,而在这之前创建Effect以及插入到hooks链表中,useEffect和useLayoutEffect其实做的都是一样的,也是共用的,在提交阶段,我们会看出两者执行时机不同的实现点。

// src/react-reconciler/src/ReactFiberWorkLoop.js
function commitRoot(root) {
  // 已经完成构建的fiber,上面会包括hook信息
  const { finishedWork } = root;

  // 如果存在useEffect或者useLayoutEffect
  if ((finishedWork.flags & Passive) !== NoFlags) {
    if (!rootDoesHavePassiveEffect) {
      rootDoesHavePassiveEffect = true;
      // 开启下一个宏任务
      requestIdleCallback(flushPassiveEffect);
    }
  }

  console.log('start commit.');
  
  // 判断自己身上有没有副作用
  const rootHasEffect = (finishedWork.flags & MutationMask) !== NoFlags;
  // 如果自己的副作用或者子节点有副作用就进行DOM操作
  if (rootHasEffect) {
    console.log('DOM执行完毕');  
    commitMutationEffectsOnFiber(finishedWork, root);  
  
    // 执行layout Effect  
    console.log('开始执行layoutEffect');
    commitLayoutEffects(finishedWork, root);
    if (rootDoesHavePassiveEffect) {
      rootDoesHavePassiveEffect = false;
      rootWithPendingPassiveEffects = root;
    }
  }
  // 等DOM变更之后,更改root中current的指向
  root.current = finishedWork;
}

这里的rootDoesHavePassiveEffect是核心判断点,还记得Effect类型中的tag参数吗?就是依靠这个参数来标识区分useEffect和useLayoutEffect的。

rootDoesHavePassiveEffect === false,则执行宏任务,将Effect副作用推入宏任务执行栈中。我们可以简单理解成useEffect的回调函数包装在了requestIdleCallback中去异步执行,根据fiber的知识接下来会去走浏览器当前帧是否有空余时间来判断副作用函数的执行时机。

继续往下走,如果rootHasEffect === true,代表有副作用,如果是useEffect,副作用已经在上面进入宏任务队列了,所以如果是useLayoutEffect,就会在这个条件中去执行,所以在这里我们可以理解到那一句"useEffect和useLayoutEffect的区别是,前者会异步执行副作用函数不会阻塞页面更新,后者会立即执行副作用函数,会阻塞页面更新,不适合写入复杂逻辑。"的原因了。

结尾

useEffect与useLayoutEffect十分相似,就连签名都一样,不同之处就在于前者会在浏览器绘制后延迟执行,而后者会在所有DOM变更之后同步调用effect,希望你看到这里,可以对于这个结论的来源有一定的了解和学习,希望可以帮到你~

来源:量子前端内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯