你的React项目是怎么被运行在浏览器的?😀😀😀

构建阶段

在前面的文章中我们讲到了 Schedule 调度,在文章中讲到了 scheduleUpdateOnFiber(...) 函数是 React 应用处理更新的入口,无论是首次渲染,还是后续更新操作,都会进入到该函数,而首次渲染是经过 updateContainer(...) 函数然后在该函数体中调用 scheduleUpdateOnFiber(...) 函数,而在该函数中又调用了 ensureRootIsScheduled(...) 函数,如果是同步任务,会调用 performSyncWorkOnRoot(...) 函数,如果并发模式下会调用 performConcurrentWorkOnRoot() 函数。

首先我们看看 performSyncWorkOnRoot(...) 函数,该函数源码看起来很多,初次构建中真正的用到的却不是很多。

该函数是 reconciler 阶段的所有的执行入口,首次渲染将进入 renderRootSync,该函数具体代码如下所示:

function renderRootSync(root: FiberRoot, lanes: Lanes) {
  const prevExecutionContext = executionContext;
  executionContext |= RenderContext;
  const prevDispatcher = pushDispatcher();
  // 如果fiberRoot变动, 或者update.lane变动, 都会刷新栈帧, 丢弃上一次渲染进度
  if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
    if (enableUpdaterTracking) {
      if (isDevToolsPresent) {
        const memoizedUpdaters = root.memoizedUpdaters;
        if (memoizedUpdaters.size > 0) {
          restorePendingUpdaters(root, workInProgressRootRenderLanes);
          memoizedUpdaters.clear();
        }
        movePendingFibersToMemoized(root, lanes);
      }
    }

    workInProgressTransitions = getTransitionsForLanes(root, lanes);
    prepareFreshStack(root, lanes);
  }

  if (enableSchedulingProfiler) {
    markRenderStarted(lanes);
  }

  do {
    try {
      // 以同步的方式开始构建 fiber 树
      workLoopSync();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);
  resetContextDependencies();

  executionContext = prevExecutionContext;
  popDispatcher(prevDispatcher);

  if (enableSchedulingProfiler) {
    markRenderStopped();
  }
  // 重置全局变量, 表明render结束
  workInProgressRoot = null;
  workInProgressRootRenderLanes = NoLanes;

  return workInProgressRootExitStatus;
}

在该函数中主要创建了 HostRootFiber.alternate,重置全局变量 workInProgressworkInProgressRoot 等,并且会在该函数调用 workLoopSync(...) 函数,从此进入循环阶段。

循环构造

workLoopSync

代码逻辑来到了 workLoopSync() 函数里,该函数执行一个 while 循环,如果 workInProgress 不为空,会循环调用 performUnitOfWork(workInProgress),该函数的具体代码如下所示:

function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

performUnitOfWork

最终无论是同步执行任务,还是可中断地执行任务,都会进入 performUnitOfWork 函数中,该函数会以 fiber 作为单元,进行协调过程,每次 beginWork 执行后都会更新 workingProgress,直到 fiber 树遍历完成之后,workingProgress 此时设置为 null,执行 completeUnitOfWork(...) 函数,表明 fiber 树构建完成,performUnitOfWork 该函数的具体实现如下:

function  performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate;

  setCurrentDebugFiberInDEV(unitOfWork);

  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  }

  resetCurrentDebugFiberInDEV();
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // 如果没有子节点,表示当前的 fiber 已经完成了 
    completeUnitOfWork(unitOfWork);
  } else {
    // 如果有子节点,就让子节点称为下一个工作单元
    workInProgress = next;
  }

  ReactCurrentOwner.current = null;
}

探寻阶段 beginWork

  1. 根据 ReactElement 对象创建所有的 fiber 节点,最终构造出 fiber 属性结构,并设置 returnsibling 指针,例如这个玩意:

你的React项目是怎么被运行在浏览器的?😀😀😀

  1. 设置 fiber.flags,该值是一个二进制形式变量,用来标记 fiber 节点的状态,等待 completeWork 阶段处理;
  2. 设置 fiber.stateNode 局部状态。

beginWork 函数中有很多 updateXXX 函数,例如 updateHostRoot,updateClassComponent等,虽然case较多,但是主要逻辑可以概括为三个步骤:

  1. 获取 fiber.pendingProps,pendingProps 是函数初始化时设置的 props,和fiber.updateQueue 等输入状态,计算 fiber.memoizedState(memoizedProps是函数执行结束时props的值)作为输出状态;

  2. 获取夏季 ReactElement 对象:

    1. Class类型的 fiber 阶段:

      • 构建React.Component实例;
      • 把新实例挂载到 fiber.stateNode 上;
      • 执行 render 之前的生命周期函数;
      • 执行 render 方法,获取夏季 reactElement;
      • 根据实际情况,设置 fiebr.flags;
    2. function 类型的 fiber 节点:

      • 执行function,获取下级 reactElement;
      • 根据实际情况,设置 fiber.flags;
    3. HostComponent 类型(如: div, span, button 等)的 fiber 节点:

      • pendingProps.children 作为下级 reactElement;
      • 如果下级节点是文本节点,则设计下级节点为null,准备进入 completeUnitOfWork 阶段;
      • 根据实际情况,设置 fiber.flags;
  3. 根据 ReactElement 对象,调用 reconcileChildren 生成 fiber 子节点;

回溯阶段 completeWork

beginWork 已经创建完成 fiber 节点时,会将该 fiber 节点赋值给 next,如果为 null,说明没有子节点了,表示当前的 fiber 节点已经完成了,会调用 completeUnitOfWork(...)

completeUnitOfWork

该函数是在 beginWork 执行到第一个叶子节点的时候开始执行的,从子到父,构建其余节点的 fiber 树。

function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  do {
    // 获取备份节点
    // 初始化渲染非根 fiber 对象没有备份节点,所以 current 为 null
    const current = completedWork.alternate;
    // 父级 fiber 树,非根 fiber 对象都有父级
    const returnFiber = completedWork.return;

    // 检查是否存在异常
    if ((completedWork.flags & Incomplete) === NoFlags) {
      setCurrentDebugFiberInDEV(completedWork);
      let next;
      if (
        !enableProfilerTimer ||
        (completedWork.mode & ProfileMode) === NoMode
      ) {
          // 创建节点真实 DOM 对象并将其添加到 stateNode 属性中
        next = completeWork(current, completedWork, subtreeRenderLanes);
      } else {
        startProfilerTimer(completedWork);
        next = completeWork(current, completedWork, subtreeRenderLanes);
        stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
      }
      resetCurrentDebugFiberInDEV();
      if (next !== null) {
        workInProgress = next;
        return;
      }
    } else {
      const next = unwindWork(current, completedWork, subtreeRenderLanes);
      if (next !== null) {
        next.flags &= HostEffectMask;
        workInProgress = next;
        return;
      }
      if (
        enableProfilerTimer &&
        (completedWork.mode & ProfileMode) !== NoMode
      ) {
        stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
        let actualDuration = completedWork.actualDuration;
        let child = completedWork.child;
        while (child !== null) {
          actualDuration += child.actualDuration;
          child = child.sibling;
        }
        completedWork.actualDuration = actualDuration;
      }

      if (returnFiber !== null) {
        returnFiber.flags |= Incomplete;
        returnFiber.subtreeFlags = NoFlags;
        returnFiber.deletions = null;
      } else {
        // We've unwound all the way to the root.
        workInProgressRootExitStatus = RootDidNotComplete;
        workInProgress = null;
        return;
      }
    }

    // 获取下一个同级 fiber 对象
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // 如果下一个同级 fiber 树存在
      workInProgress = siblingFiber;
      return;
    }
    // Otherwise, return to the parent
    completedWork = returnFiber;
    // 否则退回父级
    workInProgress = completedWork;
  } while (completedWork !== null);
  if (workInProgressRootExitStatus === RootInProgress) {
    workInProgressRootExitStatus = RootCompleted;
  }
}

接下来分析 fiber 处理函数 completeWork(...),该函数的核心是根据 fiber.tag 做不同的处理,核心是 HostComponent 的实现:

  • 调用 createInstance 函数,创建了一个 DOM 元素;
  • 调用 appendAllChildren 函数,将所有的子级追加到 instance 父级中;
  • fiber 树添加 stateNode 属性;
  • 设置 DOM 对象的属性,绑定事件等;
  • 设置 fiber.flags,判断当前 fiber 是否有副作用,如果存在需要将当前 fiber 阶段加入到父节点的 effects 队列中,等待 commit 阶段处理。

举个例子,在我们的项目有如下的代码:

import React, { Component } from "react";

class App extends Component {
  constructor() {
    super();
  }

  render() {
    return (
      <div>
        <h2>count的值为 1</h2>
        <div>
          <h3>hello</h3>
        </div>
      </div>
    );
  }
}

export default App;

DOM 树的构造方式正是深度遍历,遍历到最终的子节点,当子节点为空时,寻找它的兄弟节点,继续前面的方法,最终生成一个完整的 DOM 树,具体请看调用 createInstance(...) 函数后返回的 instance 在浏览器输出:

你的React项目是怎么被运行在浏览器的?😀😀😀

对比更新

在前面的内容中有讲到,无论是 首次渲染 还是 对比更新,最后都会调用 scheduleUpdateOnFiber(...) 函数,并且会在该函数中调用 ensureRootIsScheduled(...) 函数,该函数又调用 performConcurrentWorkOnRoot(...) 函数,如果当前是首次更新,会调用 renderRootSync(...) 函数,但如果是对比更新,会调用 renderRootConcurrent(...) 函数。

如果要发起主动更新,有三种常见方式:

  1. Class 组件中调用 setState;
  2. Function 组件中调用 hook 对象暴露出来的 dispatchAction;
  3. container 节点上重复调用 render;

举个例子,当我们调用 setState 的时候,会调用 enqueueSetState(...) 函数,该函数里面会调用 enqueueUpdate(...) 函数将任务推入更新队列,最后会在该函数的结尾返回enqueueConcurrentClassUpdate(...) 函数的调用结果,在该函数中,主要做的作用就又是调用 markUpdateLaneFromFiberToRoot(...) 函数并返回结果,这个函数很关键,接下来我们看看它到底都干了些啥?

构建阶段

markUpdateLaneFromFiberToRoot

该函数,只在 对比更新 阶段才发挥出它的作用,它找出了 fiber 树中受到本次 update 更新的影响的所有节点,并设置这些节点的 fiber.lanesfiber.childLanes 以备 fiber 树构造阶段使用。

function markUpdateLaneFromFiberToRoot(
  sourceFiber: Fiber, // sourceFiber表示被更新的节点
  lane: Lane,// lane表示update优先级
): FiberRoot | null {
  console.log(sourceFiber);
   // 将 update 优先级设置到 sourceFiber.lanes
  sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
  let alternate = sourceFiber.alternate;
  if (alternate !== null) {
     // 同时设置sourceFiber.alternate的优先级
    alternate.lanes = mergeLanes(alternate.lanes, lane);
  }
 // 当前的fiber
  let node = sourceFiber;
 // 父fiber
  let parent = sourceFiber.return;
   // 从 sourceFiber 开始, 向上遍历所有节点, 直到HostRoot. 设置沿途所有节点(包括alternate)的childLanes
  while (parent !== null) {
    // fiber 的childLanes 合并
    parent.childLanes = mergeLanes(parent.childLanes, lane);
    alternate = parent.alternate;
    if (alternate !== null) {
      alternate.childLanes = mergeLanes(alternate.childLanes, lane);
    }
    node = parent;
    parent = parent.return;
  }
  if (node.tag === HostRoot) {
    const root: FiberRoot = node.stateNode;// 最终返回一个新的 fiberRoot
    return root;
  } else {
    return null;
  }
}

还是之前的例子,当我们调用 setState 发起更新的时候,会执行到该函数,通过打印 sourceFiber,发现控制台下会有以下输出:

你的React项目是怎么被运行在浏览器的?😀😀😀

在红色框那个地方标记着本次更新的节点。

这个时候我们不妨把关注点放回到 performConcurrentWorkOnRoot(...) 函数中,如果不是首次渲染,会触发 renderRootConcurrent(...) 函数,这个函数做的事情真的很简单,只不过是加了一些 do...while循环 把任务交给小弟 workLoopConcurrent 去做。

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

探寻阶段 beginWork

在前面的 workLoopConcurrent 函数中会循环调用 performUnitOfWork(...) 函数,该函数又调用了 beginWork,这前面不是讲了吗? 是的,有讲过,但是这次和上一次的不同。

首次渲染 时,currentnull 或者 current.memoizedProps 的值为 null

beginWork 函数中,根据 workInProgress.tag 的不同,返回不同的值,但是注意看到有很 updateXXX 函数,其中很多个函数都会调用 bailoutOnAlreadyFinishedWork(...) 函数。

bailoutOnAlreadyFinishedWork

该函数的主要的作用是做性能优化的,那么什么时候会触发该函数呢?

举个例子,当 workInProgress.tag === FunctionComponent 是会调用 updateFunctionComponent 函数,该函数的核心逻辑如下:

  1. 处理 context,然后执行 renderWithHooks 函数;
  2. 如果 current 不为 null 并且 didReceiveUpdatefalse,则执行 bailoutHooks(...) 函数和 bailoutOnAlreadyFinishedWork(...) 函数,并返回后者;
  3. 执行 reconcileChildren 函数;
  4. 返回 workInProgress.child;

到这里.我们来聊聊 bailout 逻辑,在 Reactbailout 用于判断子树节点是否完全复用,如果可以复用,则会略过 fiber 树构造。

与初次创建不同,在对比更新过程中,如果是老节点,那么 current!==null 成立,需要进行对比,然后决定是否复用老节点及其子树。

那么我们看看 bailoutOnAlreadyFinishedWork() 函数,该函数的主要作用是作对比更新:

  • 如果同时满足 !includesSomeLane(renderLanes, workInProgress.childLanes),表明该 fiber 节点及其子树都无需更新,可直接进入回溯阶段;
  • 如果不满足 !includesSomeLane(renderLanes, workInProgress.childLanes),意味着子节点需要更新,clone 并返回子节点;
function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  if (current !== null) {
    workInProgress.dependencies = current.dependencies;
  }
  if (enableProfilerTimer) {
    stopProfilerTimerIfRunning(workInProgress);
  }
  markSkippedUpdateLanes(workInProgress.lanes);
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    if (enableLazyContextPropagation && current !== null) {
      lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
      if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
        return null;
      }
    } else {
       // 渲染优先级不包括 workInProgress.childLanes, 表明子节点也无需更新. 返回null, 直接进入回溯阶段.
      return null;
    }
  }
  // 本fiber虽然不用更新, 但是子节点需要更新. clone并返回子节点
  cloneChildFibers(current, workInProgress);
  return workInProgress.child;
}

cloneChildFibers(...) 函数内部调用 createWorkInProgress,在构造 fiber 节点时会优先复用 workInProgress.alternate (不开辟新的内存空间), 否则才会创建新的fiber对象。

回溯阶段 completeWork

completeUnitOfWork(unitOfWork) 函数在 初次创建对比更新 逻辑一致,都是处理 beginWork 阶段已经创建出来的 fiber 节点,最后创建或更新 DOM 对象,并上移副作用队列。

fiber 树渲染

fiber 树的渲染阶段整个渲染逻辑都在 commitRoot 函数中,具体代码如下:

function commitRoot(
  root: FiberRoot,
  recoverableErrors: null | Array<CapturedValue<mixed>>,
  transitions: Array<Transition> | null,
) {
  const previousUpdateLanePriority = getCurrentUpdatePriority();
  const prevTransition = ReactCurrentBatchConfig.transition;

  try {
    ReactCurrentBatchConfig.transition = null;
    setCurrentUpdatePriority(DiscreteEventPriority);
    commitRootImpl(
      root,
      recoverableErrors,
      transitions,
      previousUpdateLanePriority,
    );
  } finally {
    ReactCurrentBatchConfig.transition = prevTransition;
    setCurrentUpdatePriority(previousUpdateLanePriority);
  }

  return null;
}

这个 commitRoot(...) 函数只是一个桥梁,最终的实现还是需要通过 commitRootImpl(...) 函数。

所以 commit 阶段又分为三个子阶段,它们分别是:

  • before mutation 阶段,执行 DOM 操作前;
  • mutation 阶段,执行 DOM 操作;
  • layout 阶段,执行 DOM 后;

进入 commit 和阶段,先清空之前未执行的 useEffect

before mutation 阶段

这个阶段的入口自函数的 commitBeforeMutationEffects 函数,该函数又有如下的调用栈:

  • commitBeforeMutationEffects
  • commitBeforeMutationEffects_begin
  • commitBeforeMutationEffects_complete

在这里就贴出第一个函数的代码,具体自行查阅源码:

export function commitBeforeMutationEffects(
  root: FiberRoot,
  firstChild: Fiber,
) {
  focusedInstanceHandle = prepareForCommit(root.containerInfo);

  nextEffect = firstChild;
  commitBeforeMutationEffects_begin();

  const shouldFire = shouldFireAfterActiveInstanceBlur;
  shouldFireAfterActiveInstanceBlur = false;
  focusedInstanceHandle = null;

  return shouldFire;
}

commitBeforeMutationEffects_begin 中会向下遍历 child 指针,为每个遍历到的节点指向 ensureCorrectReturnPointer(...) 函数,以确立当前父节点的指针,若不存在子节点则调用 commitBeforeMutationEffects_complete(...) 函数。

function commitBeforeMutationEffects_complete() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    setCurrentDebugFiberInDEV(fiber);
    try {
      commitBeforeMutationEffectsOnFiber(fiber);
    } catch (error) {
      captureCommitPhaseError(fiber, fiber.return, error);
    }
    resetCurrentDebugFiberInDEV();

    const sibling = fiber.sibling;
    if (sibling !== null) {
      sibling.return = fiber.return;
      nextEffect = sibling;
      return;
    }

    nextEffect = fiber.return;
  }

在该函数中主要的作用是调用 commitBeforeMutationEffectsOnFiber(...) 函数以调用 getSnapshotBeforeUpdate 生命周期钩子。

因此我们在这里可以得知 before mutation 阶段的主要职责就是执行class组件的 getsnapshotBeforeUpdate 生命周期。

mutation 阶段

这个阶段就是 DOM 操作阶段,通过调用 commitMutationEffects(...) 函数,这个函数根据不同的 finishedWork.tag 进入不同的入口,对于 HostRoot,首先会有一些公共逻辑会先执行:

  • 调用 recursivelyTraverseMutationEffects 函数深度遍历执行删除操作;
  • 调用 commitReconciliationEffects 函数执行插入操作;

layout 阶段

layout 阶段的入口函数是 commitLayoutEffects,它的实现如下代码所示:

export function commitLayoutEffects(
  finishedWork: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
): void {
  inProgressLanes = committedLanes;
  inProgressRoot = root;
  nextEffect = finishedWork;

  commitLayoutEffects_begin(finishedWork, root, committedLanes);

  inProgressLanes = null;
  inProgressRoot = null;
}

before mutation 阶段一样,先深度优先递归,找最后一个 LayoutMask 标记的 fiber。然后从下往上调用 complete 逻辑,确保逻辑是从底部到顶部,从子到父。

complete 的函数是 commitLayoutMountEffects_complete,它的具体实现如下代码所示:

function commitLayoutMountEffects_complete(
  subtreeRoot: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
) {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    if ((fiber.flags & LayoutMask) !== NoFlags) {
      const current = fiber.alternate;
      setCurrentDebugFiberInDEV(fiber);
      try {
        commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
      } catch (error) {
        captureCommitPhaseError(fiber, fiber.return, error);
      }
      resetCurrentDebugFiberInDEV();
    }

    if (fiber === subtreeRoot) {
      nextEffect = null;
      return;
    }

    const sibling = fiber.sibling;
    if (sibling !== null) {
      sibling.return = fiber.return;
      nextEffect = sibling;
      return;
    }

    nextEffect = fiber.return;
  }
}

它的核心工作就是调用 commitLayoutEffectOnFiber,它会根据组件类型不同执行不同逻辑:

  • 如果是函数组件,会调用 useLayoutEffect 的回调函数,调度 useEffect销毁回调 函数;
  • 如果是类组件的情况:
    1. 如果是首次渲染,调用 instance.componentDidMount 方法;
    2. 如果是更新,提取 preProps 等参数传入到 componentDidUpdate 里调用;

最后会取出 updateQueue 里的 effect,一次调用 effect.callback 函数,这个 callback 其实就是 setState 方法的第二个参数。

在上面的事情处理完成之后,调用 commitAttachRef 获取 DOM 实例,更新 ref。该函数的具体实现如下代码所示:

function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    const instance = finishedWork.stateNode;
    // 获取DOM实例
    let instanceToUse;
    switch (finishedWork.tag) {
      case HostComponent:
        instanceToUse = getPublicInstance(instance);
        break;
      default:
        instanceToUse = instance;
    }
    // Moved outside to ensure DCE works with this flag
    if (enableScopeAPI && finishedWork.tag === ScopeComponent) {
      instanceToUse = instance;
    }
    if (typeof ref === "function") {
      let retVal;
      if (
        enableProfilerTimer &&
        enableProfilerCommitHooks &&
        finishedWork.mode & ProfileMode
      ) {
        try {
          startLayoutEffectTimer();
          // 如果ref是函数形式,调用回调函数
          retVal = ref(instanceToUse);
        } finally {
          recordLayoutEffectDuration(finishedWork);
        }
      } else {
        retVal = ref(instanceToUse);
      }
    } else {
      // 如果ref是ref实例形式,赋值ref.current
      ref.current = instanceToUse;
    }
  }
}

useEffect

现在还差 useEffect 没调用了,它不在同步的 commit 阶段中执行,它是异步的,被 schuduler 异步调度执行。

举个例子,在我们的项目中如下定义:

import React, { useEffect, useLayoutEffect, useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log(1);
    return () => {
      console.log(5);
    };
  }, [count]);

  useLayoutEffect(() => {
    console.log(2);
  }, [count]);
  setTimeout(() => {
    console.log(6);
  }, 0);
  console.log(3);

  new Promise((resolve) => {
    resolve(4);
  }).then((res) => {
    console.log(res);
  });
  return <div onClick={() => setCount(count + 1)}>{count}</div>;
};

export default App;

当我们运行项目时首次输出会有以下这玩意:

你的React项目是怎么被运行在浏览器的?😀😀😀

你会发现 settimeout 函数都会被优先执行了,useEffect 再最后再执行,当我们通过 onLlick 事件调用 useState 时,又会有如下输出:

你的React项目是怎么被运行在浏览器的?😀😀😀

在这里我们应该知道一点,useEffect 的销毁函数会交给下一次渲染中执行,所以当我们点击时会首先执行这个(忽略2和3),由于离散事件优先级最高,需要和用户优先进行交互,这个时候 useEffect 变为了同步优先级,优先执行。

在文章的最后再贴一直叼图吧!!!

你的React项目是怎么被运行在浏览器的?😀😀😀

到此为止,本章内容也到这里结束了。

总结

没有总结,你自己总结吧。

就是说,学生党别乱读源码,还不如安安分分写个项目,读这源码对我找工作目前没有任何作用,项目还是用的之前写的垃圾网易云音乐呜呜呜呜呜…..

参考文章

本文正在参加「金石计划」

原文链接:https://juejin.cn/post/7216644653126713405 作者:Moment

(0)
上一篇 2023年4月1日 上午10:22
下一篇 2023年4月1日 下午4:00

相关推荐

发表回复

登录后才能评论