从源码角度来聊聊React的生命周期(上)

学习源码的目的

很多时候我们总是会纠结要不要学源码,会不会被卷之类的问题,但是要我看,学不学还是在于自己,无论你是出于什么样的目的来学,总之学到了就是你自己的。从功利的角度来说,学习源码就是为了面试,为了获得更好薪资待遇。不过作为一个技术人,是否会有一种深究的欲望呢,总是想要了解其中神奇的功能到底是如何实现,那么就让我们带着一些“目的”来学习吧。

一些问题

先从最简单的生命周期的问题来学习源码,学习前,带着一些问题去看,有助于解决一些平时可能想不通的问题,甚至是面试中遇到的面试题,下面是自己开始学习前的一些疑问:

  • React 的挂载流程是什么样的?class 组件和 function 组件有什么不同呢?
  • React 的生命周期方法执行顺序。
  • 为什么带 will 的生命周期都是不安全的呢,比如 componentWillMount componentWillUpdate componentWillReceiveProps 都会加上了前缀 UNSAFE。
  • useEffect 与 useLayout 的区别。

React 的架构

先从 React 整体架构来看 React 的生命周期过程,React 的初次渲染到页面上的过程可以分为两个阶段:

  • render/reconcile阶段
  • commit阶段

在 render 阶段会形成 Fiber 树,commit 阶段则是遍历 Fiber 树上的 effectList 链表来建立对应的 DOM 节点,并将其渲染到页面上,这也就是 React 初次挂载的流程。

本篇也是围绕着两个流程,尝试来解决开头提出的问题。源码的版本为17.0.2。

先来祭出一张官方的生命周期图。

react_lifestyles.png

从这张图可以很清楚的看到 React 生命周期分为了三个阶段,分别是挂载阶段、更新阶段以及卸载阶段。

我们来一一分析。

挂载阶段

从上图可以看到 React 在挂载阶段主要包括了四个生命周期,按顺序分别为:

  • constructor
  • getDerivedStateFromProps
  • render
  • componentDidMount

先利用vite简单的搭建一个 React 的示例,然后在构造函数中断点调试一下:

class App extends React.Component {
  constructor(props) {
    super(props);
    debugger;
    console.log('father constructor', this);
    this.state = { count: 0 };
  }

  setCount = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    console.log('father render');
    return (
      <div className="App">
        <img src={logo} className="App-logo" alt="logo" />
        <div>
          <button onClick={this.setCount}>
            count is: {this.state.count}
          </button>
        </div>
      </div>
    )
  }
}
复制代码

image.png

从入口 render 函数到构造函数 App 中,经过了这么多的调用栈。先不去关心这个中间发生了什么,而是将关注点聚焦到构造函数中来。

React 文档中说了 constructor 一般是用来初始化 state 或者是做一些函数绑定工作,是不建议在 constructor 中去 setState的。那么我们就偏偏在 constructor 中 setState 一下。改造一下上面的构造函数。

constructor(props) {
  super(props);
  console.log('father constructor', this);
  this.state = { count: 0 };
  this.setState({
    count: 0
  });
}
复制代码

然后就发现控制台上多了一个 warning

image.png

顺着这个 warning,发现就是因为执行了 setState,很奇怪,setState变成了这样的

Component.prototype.setState = function (partialState, callback) {
  if (!(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null)) {
    {
      throw Error( "setState(...): takes an object of state variables to update or a function which returns an object of state variables." );
    }
  }

  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

var ReactNoopUpdateQueue = {
  enqueueSetState: function (publicInstance, partialState, callback, callerName) {
    warnNoop(publicInstance, 'setState');
  }
};
复制代码

setState 为什么直接执行了打印警告的方法呢,这不符合我们的期望啊,于是再次把断点打在了点击事件中,结果发现了另一个 setState

// setState的原型方法是一模一样的
var classComponentUpdater = {
  enqueueSetState: function (inst, payload, callback) {
    var fiber = get(inst);
    var eventTime = requestEventTime();
    var lane = requestUpdateLane(fiber);
    var update = createUpdate(eventTime, lane);
    update.payload = payload;

    if (callback !== undefined && callback !== null) {
      {
        warnOnInvalidCallback(callback, 'setState');
      }

      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }
};
复制代码

这才符合我们对 setState 的期望嘛,如果是这样的话,那就解释了为什么在 constructor 中执行 setState 不起作用的原因了。

不过为什么两者执行的方法不一样呢。继续下去才发现关键就在上面的 constructClassInstance 方法中,这个方法执行除了执行了构造方法外,还执行了一些其他的东西

// 这里省略了一些暂时不需要关注的代码
var isLegacyContextConsumer = false;
  var unmaskedContext = emptyContextObject;
  var context = emptyContextObject;
  var contextType = ctor.contextType;

  // 如果使用严格模式,也就是StrictMode,则会进入这里
  // 这里react说是实例化两次以帮助检测副作用,暂时没有深入了解
  if (
      debugRenderPhaseSideEffectsForStrictMode &&
      workInProgress.mode & StrictMode
  ) {
    disableLogs();
    try {
      new ctor(props, context);
    } finally {
      reenableLogs();
    }
  }

  // 执行构造函数的地方
  var instance = new ctor(props, context);
  // 获取fiber树的state
  var state = workInProgress.memoizedState = instance.state !== null && instance.state !== undefined ? instance.state : null;
  // 这里就是区分setState两种形态的方法之一
  adoptClassInstance(workInProgress, instance);

  return instance;
复制代码
function adoptClassInstance(workInProgress: Fiber, instance: any): void {
  // 这里就是把setState的方法赋给了实例的updater
  instance.updater = classComponentUpdater;
  // fiber树关联实例
  workInProgress.stateNode = instance;
  // 实例需要访问光纤,以便调度更新
  // 实际就是instance._reactInternals = workInProgress
  setInstance(instance, workInProgress);
}
复制代码

到这里就很清晰了,只有执行了 constructor 方法以后,才能将真正的 setState 的方法放到实例上去。

好了,看完了构造函数,继续走到下一个方法 getDerivedStateFromProps 中。这个方法是在哪里调用的呢。回到上面执行 constructClassInstance 方法的地方,也就是 updateClassComponent 方法中,来看看这里是怎么做的

function updateClassComponent(
  current, // 当前显示在页面上的fiber树,初次挂载的时候,current为null
  workInProgress, // 内存中构建的fiber树
  Component, // 这里就是我们的根应用,也就是App
  nextProps, // props属性,根应用没有设置props,为空
  renderLanes, // 调用优先级相关,暂不考虑
) {
  // 这里去掉了一些关于context的逻辑

  // 这个instance为null
  // 通过上面我们知道,只有执行了构造函数以后,才会把实例赋值给stateNode
  const instance = workInProgress.stateNode;
  let shouldUpdate;
  if (instance === null) {
    // 猜测是并发模式下调用的,未找到此项入口
    if (current !== null) {
      current.alternate = null;
      workInProgress.alternate = null;
      workInProgress.flags |= Placement;
    }
    // 在最初的过程中,需要构造实例
    constructClassInstance(workInProgress, Component, nextProps);
    // 这里就是getDerivedStateFromProps的入口方法了
    mountClassInstance(workInProgress, Component, nextProps, renderLanes);
    shouldUpdate = true;
  } else if (current === null) {
    // In a resume, we'll already have an instance we can reuse.
    shouldUpdate = resumeMountClassInstance(
      workInProgress,
      Component,
      nextProps,
      renderLanes,
    );
  } else {
    // setState更新的时候走这里
    shouldUpdate = updateClassInstance(
      current,
      workInProgress,
      Component,
      nextProps,
      renderLanes,
    );
  }
  // 完成当前fiber节点,返回下一个工作单元
  const nextUnitOfWork = finishClassComponent(
    current,
    workInProgress,
    Component,
    shouldUpdate,
    hasContext,
    renderLanes,
  );
  return nextUnitOfWork;
}
复制代码

走到这里的时候,就需要将前面调试的方法串联起来去看,不然可能不会明白为什么 React 要这么去做,下面画了一个简单的流程图:

react-render-process.png

我们来以一段简单易懂的话来试图描述这个阶段所做的工作:

首先 React 使用了双缓存机制,显示在页面上的节点指针为 current,在内存中构建的节点为 workingInProgress 指针。初次挂载时,current 指针是为空的,所以 React 所做的工作就是根据 JSX 创建的节点树进行深度优先遍历,创建Fiber树,对比 current 上的旧的Fiber树,但首次创建 current 是空的,因此直接将属性添加到新的 Fiber 树上即可。构建完整颗 Fiber 树以后,则 render 阶段结束,开始进入 commit 阶段。

这里只是简单的描述了 render 阶段执行的流程,里面还有很多细节,不过不是今天关注的重点,有兴趣的可以去看看这篇文章

这里的流程是 React 的 legacy 模式,而经常提到的可中断更新实际是 React 还正式发布的 concurrent 模式,关于 React 模式,文档中也提到了相关的概念,这里就直接拿过来作为参考:

  • legacy 模式: ReactDOM.render(<App />, rootNode)。这是当前 React app 使用的方式。当前没有计划删除本模式,但是这个模式可能不支持这些新功能。
  • blocking 模式: ReactDOM.createBlockingRoot(rootNode).render(<App />)。目前正在实验中。作为迁移到 concurrent 模式的第一个步骤。
  • concurrent 模式: ReactDOM.createRoot(rootNode).render(<App />)。目前在实验中,未来稳定之后,打算作为 React 的默认开发模式。这个模式开启了所有的新功能。

好了,回到要讲的生命周期上来。这里已经找到了 getDerivedStateFromProps 的入口函数,接着来看看这个入口函数做了什么:

function mountClassInstance(
  workInProgress,
  ctor,
  newProps,
  renderLanes,
): void {
  // 这里检查class类是否定义了render方法,getInitialState、getDefaultProps、propTypes、contextType格式以及一些生命周期方法的检查工作
  checkClassInstance(workInProgress, ctor, newProps);

  const instance = workInProgress.stateNode;
  instance.props = newProps;
  instance.state = workInProgress.memoizedState;
  instance.refs = emptyRefsObject;

  // 初始化了updateQueue,这是一个链表,所有的更新的内容都会放在这里
  initializeUpdateQueue(workInProgress);

  // 这里做了一个警告,就是不要直接把props赋值给state
  if (instance.state === newProps) {
    const componentName = getComponentName(ctor) || 'Component';
    if (!didWarnAboutDirectlyAssigningPropsToState.has(componentName)) {
      didWarnAboutDirectlyAssigningPropsToState.add(componentName);
      console.error(
        '%s: It is not recommended to assign props directly to state ' +
          "because updates to props won't be reflected in state. " +
          'In most cases, it is better to use props directly.',
        componentName,
      );
    }
  }

  // 这里做了一些不安全的生命周期和context的警告,不是重点省略了

  processUpdateQueue(workInProgress, newProps, instance, renderLanes);
  instance.state = workInProgress.memoizedState;

  const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
  // 这里执行了getDerivedStateFromProps生命周期函数
  if (typeof getDerivedStateFromProps === 'function') {
    applyDerivedStateFromProps(
      workInProgress,
      ctor,
      getDerivedStateFromProps,
      newProps,
    );
    instance.state = workInProgress.memoizedState;
  }

  // 这里如果有新的生命周期方法,也就是getDerivedStateFromProps和getSnapshotBeforeUpdate,
  // 那么componentWillMount就不会调用,否则的话,就会被调用
  if (
    typeof ctor.getDerivedStateFromProps !== 'function' &&
    typeof instance.getSnapshotBeforeUpdate !== 'function' &&
    (typeof instance.UNSAFE_componentWillMount === 'function' ||
      typeof instance.componentWillMount === 'function')
  ) {
    callComponentWillMount(workInProgress, instance);
    // If we had additional state updates during this life-cycle, let's
    // process them now.
    processUpdateQueue(workInProgress, newProps, instance, renderLanes);
    instance.state = workInProgress.memoizedState;
  }
}
复制代码

最后这里可以看到,当存在新的生命周期 API 时,那些带 will 的生命周期都不会被调用,稍后再来看这里的 will 生命周期,先来看看调用 getDerivedStateFromProps 的地方:

function applyDerivedStateFromProps(workInProgress, ctor, getDerivedStateFromProps, nextProps) {
  var prevState = workInProgress.memoizedState;

  var partialState = getDerivedStateFromProps(nextProps, prevState);

  {
    // 如果getDerivedStateFromProps没有返回值则会警告
    warnOnUndefinedDerivedState(ctor, partialState);
  }

  // 这里将getDerivedStateFromProps返回的对象和上次的state进行了合并
  var memoizedState = partialState === null || partialState === undefined ? prevState : _assign({}, prevState, partialState);
  workInProgress.memoizedState = memoizedState;
}
复制代码

这个逻辑很好理解,就是执行了 getDerivedStateFromProps 的生命周期方法后,将其返回值和上次的 state 进行了合并。注意这个 applyDerivedStateFromProps 方法会在更新阶段再次被调用。而 getDerivedStateFromProps 的作用官方文档也给出了其唯一的作用:就是在 props 改变时更新 state。这里会在更新阶段再次来分析。

通过上面的代码,可以知道 getDerivedStateFromProps 和 componentWillMount的方法是不会同时调用的。注释掉 getDerivedStateFromProps 的方法,添加 componentWillMount 方法再来看看其执行流程:

function callComponentWillMount(workInProgress, instance) {
  var oldState = instance.state;

  // 调用willMount的生命周期方法
  if (typeof instance.componentWillMount === 'function') {
    instance.componentWillMount();
  }

  if (typeof instance.UNSAFE_componentWillMount === 'function') {
    instance.UNSAFE_componentWillMount();
  }

  // 这里如果前后state不等,说明在willMount中给state重新赋值了,导致state引用改变了
  if (oldState !== instance.state) {
    {
      error('%s.componentWillMount(): Assigning directly to this.state is ' + "deprecated (except inside a component's " + 'constructor). Use setState instead.', getComponentName(workInProgress.type) || 'Component');
    }
    // 直接赋值的话,react会把赋值的语句变成了setState调用
    classComponentUpdater.enqueueReplaceState(instance, instance.state, null);
  }
}
复制代码

官方文档上面有这么一句话

componentWillMount 在 render() 之前调用,因此在此方法中同步调用 setState() 不会触发额外渲染。

意思是在 render 阶段的更新是不会触发实例的 render,这是因为进入 setState 更新方法的时候,会去判断当前的树和内存中的树是一个引用,说明当前阶段是 render 阶段,因此不会去执行 render 方法。

执行完了 getDerivedStateFromProps 或者是 componentWillMount 之后,就到了 render 方法的调用了。这里需要回到上面的 updateClassComponent 方法中有一个 finishClassComponent 方法,就是调用 render 方法的地方。来看看这个方法:

function finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes) {
  // 更新ref
  markRef(current, workInProgress);
  var didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags;
  
  // 这里shouldComponentUpdate如果返回的是false的话,则不会执行render,而是复用上次的fiber
  if (!shouldUpdate && !didCaptureError) {
    // Context providers should defer to sCU for rendering
    if (hasContext) {
      invalidateContextProvider(workInProgress, Component, false);
    }

    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }

  var instance = workInProgress.stateNode; // Rerender

  ReactCurrentOwner$1.current = workInProgress;
  var nextChildren;

  if (didCaptureError && typeof Component.getDerivedStateFromError !== 'function') {
    // 如果捕获到错误,但未定义getDerivedStateFromError,则卸载所有的子元素。
    // componentDidCatch将安排更新以重新呈现回退。这是暂时的,直到我们将迁移到新的API。
    // TODO: Warn in a future release.
    nextChildren = null;

    {
      stopProfilerTimerIfRunning();
    }
  } else {
    {
      setIsRendering(true);
      nextChildren = instance.render();

      // 其实是调用了两次render,react解释是为了侦测副作用,而且这里执行的render的log是不会被打印出来的
      if ( workInProgress.mode & StrictMode) {
        disableLogs();

        try {
          instance.render();
        } finally {
          reenableLogs();
        }
      }

      setIsRendering(false);
    }
  }

  if (current !== null && didCaptureError) {
    // 如果正在从错误中恢复,可以在不重用任何现有子元素的情况下进行协调。
    // 从概念上讲,普通子元素和在错误中显示的子元素是两个不同的集合,
    // 因此即使它们的标识匹配,也不应该重用普通子元素。
    forceUnmountCurrentAndReconcile(current, workInProgress, nextChildren, renderLanes);
  } else {
    // 将render返回的react元素进行协调
    reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  }

  // fiber记住实例上的state的值,作为状态缓存
  workInProgress.memoizedState = instance.state; // The context might have changed so we need to recalculate it.

  // 返回当前fiber的子元素
  return workInProgress.child;
}
复制代码

通过上面的流程图可以知道,React 挂载的过程实际就是一个自顶向下不断遍历的过程,在 legacy 模式下这种递归更新是无法打断的,在 render 方法中做一些计算工作会导致更新的速度变慢,浏览器的卡顿的情况更加明显。因此才说要保持 render 方法的纯净和简洁。

执行完了 render 方法以后,一直到 commitRoot 方法,则开始进入了 commit 阶段。

react-commit-process.png

总结一下 commit 阶段所做的事,首先就是 beforeMutation 阶段,这个阶段主要做了一些变量赋值的工作,其内部是调用了 commitBeforeMutationLifeCyles 方法,就是执行了 getSnapshotBeforeUpdate 的生命周期,不过需要注意的是,只有 state 或者 props 发生了变化的时候才会调用该方法,而首次挂载时,状态初始化,因此并不会调用该方法。

与此同时,尝试将根 App 组件改成了函数组件,并添加了一个子组件 Child

// 根组件
function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('qiugu');
  const dom = useRef(null);

  const clickHandler = () => {
    setCount(count+1);
    setName(name => name + count);
  }

  return (
    <div className="App">
      <img src={logo} className="App-logo" alt="logo" />
      <Child name={name}/>
      <div>
        <button onClick={clickHandler} ref={dom}>
          count is: {count}
        </button>
      </div>
    </div>
  )
}

// 子组件
class Child extends React.Component<{name: string}, {}> {
  constructor(props: {name: string}) {
    super(props);
    console.log('child constructor');
    this.state = { age: 20 };
  }

  getSnapshotBeforeUpdate() {
    console.log('child getSnapshotBeforeUpdate');
    return {
      name: 'qiugu2'
    }
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log(prevProps, prevState, snapshot, 'child componentDidUpdate');
    console.log('child componentDidUpdate');
  }

  render() {
    console.log('child render');
    return <div>我是一个子组件姓名:{this.props.name}年龄:{this.state.age}</div>
  }
}
复制代码

结果发现首次子组件就会调用了 getSnapshotBeforeUpdate 方法,但是如果根组件是 class 组件,首次则不会调用该方法!

紧接着则是 mutation 阶段,这个阶段主要是把fiber上携带的副作用标签进行对应的处理,这里可以不去先不去关心,有兴趣可以深入去了解如何插入节点的。但是可以知道的是,在mutation阶段,DOM 元素将会被插入页面,所以这个时候如果有 ref 之类的 DOM 引用是可以访问到了。

function commitMutationEffects(root, renderPriorityLevel) {
  while (nextEffect !== null) {
    setCurrentFiber(nextEffect);
    var flags = nextEffect.flags;

    if (flags & ContentReset) {
      commitResetTextContent(nextEffect);
    }

    if (flags & Ref) {
      var current = nextEffect.alternate;

      if (current !== null) {
        // 移除ref引用
        commitDetachRef(current);
      }
    }


    var primaryFlags = flags & (Placement | Update | Deletion | Hydrating);

    switch (primaryFlags) {
      case Placement:
          commitPlacement(nextEffect); 
          nextEffect.flags &= ~Placement;
          break;
      case PlacementAndUpdate:
          commitPlacement(nextEffect);
          nextEffect.flags &= ~Placement;
          var _current = nextEffect.alternate;
          commitWork(_current, nextEffect);
          break;
      case Hydrating:
          nextEffect.flags &= ~Hydrating;
          break;
      case HydratingAndUpdate:
          nextEffect.flags &= ~Hydrating;

          var _current2 = nextEffect.alternate;
          commitWork(_current2, nextEffect);
          break;
      case Update:
          var _current3 = nextEffect.alternate;
          commitWork(_current3, nextEffect);
          break;
      case Deletion:
          commitDeletion(root, nextEffect);
          break;
    }

    resetCurrentFiber();
    nextEffect = nextEffect.nextEffect;
  }
}
复制代码

最后则是 layout 阶段,这个阶段是在挂载好DOM元素后,同步执行了诸如 componentDidUpdate 和 useLayoutEffect 方法,并且调度 useEffect 方法。

function commitLayoutEffects(root, committedLanes) {
  while (nextEffect !== null) {
    setCurrentFiber(nextEffect);
    var flags = nextEffect.flags;

    if (flags & (Update | Callback)) {
      var current = nextEffect.alternate;
      // 执行生命周期方法,包括函数式组件的useEffect和useLayoutEffect
      commitLifeCycles(root, current, nextEffect);
    }

    if (flags & Ref) {
      // 更新DOM元素和ref的引用
      commitAttachRef(nextEffect);
    }

    resetCurrentFiber();
    nextEffect = nextEffect.nextEffect;
  }
}
复制代码

这里为什么说调度 useEffect 方法呢,进入 commitLifeCycles 来看看:

function commitLifeCycles(finishedRoot, current, finishedWork, committedLanes) {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
    case Block:
      {
        // 函数组件进入这里
        // 这里是useLayoutEffect的回调函数执行的入口
        commitHookEffectListMount(Layout | HasEffect, finishedWork);
        // 而这里则是调度useEffect的回调函数的入口
        schedulePassiveEffects(finishedWork);
        return;
      }

    case ClassComponent:
      {
        var instance = finishedWork.stateNode;

        if (finishedWork.flags & Update) {
          // 根据current是否存在区分当前是挂载还是更新
          if (current === null) {
            // 执行componentDidMount生命周期方法
            instance.componentDidMount();
          } else {
            // 执行componentDidUpdate方法
            var prevProps = finishedWork.elementType === finishedWork.type ? current.memoizedProps : resolveDefaultProps(finishedWork.type, current.memoizedProps);
            var prevState = current.memoizedState;
            instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate);
          }
        }

        var updateQueue = finishedWork.updateQueue;

        if (updateQueue !== null) {
          commitUpdateQueue(finishedWork, updateQueue, instance);
        }

        return;
      }
    // 其他的fiber类型暂且不去考虑
    case HostRoot:
        return;
    case HostComponent:
        return;
    case HostText:
        return;
    case HostPortal:
        return;
    case Profiler:
        return;
    case SuspenseComponent:
        return;
    case SuspenseListComponent:
    case IncompleteClassComponent:
    case FundamentalComponent:
    case ScopeComponent:
    case OffscreenComponent:
    case LegacyHiddenComponent:
      return;
  }
}
复制代码
function schedulePassiveEffects(finishedWork) {
  var updateQueue = finishedWork.updateQueue;
  var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;

  if (lastEffect !== null) {
    var firstEffect = lastEffect.next;
    var effect = firstEffect;

    do {
      var _effect = effect,
          next = _effect.next,
          tag = _effect.tag;

      if ((tag & Passive$1) !== NoFlags$1 && (tag & HasEffect) !== NoFlags$1) {
        // 将useEffect回调方法和卸载方法加入了队列中,并没有直接执行
        enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
        enqueuePendingPassiveHookEffectMount(finishedWork, effect);
      }

      effect = next;
    } while (effect !== firstEffect);
  }
}
复制代码

这么一看其实就很明确了,在进入layout阶段之后,根据组件类型是函数组件还是 class 组件,分别执行 useLayoutEffect 方法 或者是 componentDidMount 方法。如果函数组件中还存在了 useEffect 回调方法,则会将其加入队列中,会由 React 在合适的时机执行该方法。因此可以很明白的看到,对于 useLayoutEffect 和 useEffect 而言,一个是同步调用,另一个则是异步调用,所以如果在同步方法中执行了耗费时间的操作,则会影响页面的渲染速度,是不推荐这么做的。反之,放在 useEffect 方法中,则会有这样的问题。

至此整个 commit 阶段也就结束了。一些如何创建 Fiber,如何深度优先遍历整颗树的细节等都忽略了,不过总的来说,希望读者看完后,能对 React 的生命周期有一个新的认识,能够解答上面提出的部分问题。

未完待续

不知不觉,写的东西有点多,还有更新和卸载的阶段没有说到。本篇也是利用每天下班时间不断的调试源码,查看资料一点一点积累,无论是自己还是读者,都需要时间来消化这些内容,所以将更新和卸载放到下篇继续分析。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享