深刻react的state和props更新

本文为意译和整理,若有误导,请放弃阅读。原文html

前言

这篇文章用一个由parent component和children component组成的例子来说述fiber架构中react将props传递给子组件的处理流程。node

正文

在我先前的文章中 Inside Fiber: in-depth overview of the new reconciliation algorithm in React 提到要想理解更新流程的技术细节,咱们需得具有必定的基础知识。而这部分的基础知识就是篇文章要讲述的内容。react

对于本文所提到的数据结构和概念,我已经在上一篇文章概述过了。这些数据结构和概念主要包括有:git

  • fiber node
  • current tree
  • work-in-progress tree
  • side-effects
  • effects list

同时,我也对主要算法进行了宏观上的阐述,也解释过render阶段和commit阶段之间的差别性。若是你尚未阅读过讲述这些东西的文章,我建议你先去阅读。github

我也引入过一个简单demo。这个demo的主要功能是经过点击button来增长界面上的一个数字。 算法

你能够这里去玩玩它。这个demo实现了一个简单的组件。这个组件的render方法返回了两个子组件:button和span。当你点击界面上的按钮的时候,咱们会在click的事件处理器中去更新组件的state。结果是,界面上span元素的文本内容获得更新。数组

class ClickCounter extends React.Component {
    constructor(props) {
        super(props);
        this.state = {count: 0};
        this.handleClick = this.handleClick.bind(this);
    }

    handleClick() {
        this.setState((state) => {
            return {count: state.count + 1};
        });
    }
    
    componentDidUpdate() {}

    render() {
        return [
            <button key="1" onClick={this.handleClick}>Update counter</button>,
            <span key="2">{this.state.count}</span>
        ]
    }
}
复制代码

在这里,我把一个componentDidUpdate的生命周期函数加入到组件中。这么作,是为了演示React是如何添加effects和在commit阶段调用这个方法的。浏览器

在本文中,我会带你看看,React是如何处理state更新和构建effects list的。咱们也对rendercommit阶段的顶层函数进行简单的讲解。安全

特别地,咱们着重看看completeWork方法:bash

  • 更新ClickCounter组件state中的count属性。
  • 调用组件实例的render方法,获取到children列表,而后执行比对。
  • 更新span元素的props。

commitRoot方法:

  • 更新span元素的textContent属性。
  • 调用componentDidUpdate这个生命周期函数。

在深刻这些东西以前,咱们快速地过一遍“当咱们在click事件处理器中调用setState的时候,work是如何被调度”的这一环节。

Scheduling updates

当咱们点击界面上的button的时候,click事件被触发了,而后React执行咱们做为props传递进去的事件回调。在咱们的demo中,这个事件回调就是简单地经过增长count字段值来更新组件的状态。

class ClickCounter extends React.Component {
    ...
    handleClick() {
        this.setState((state) => {
            return {count: state.count + 1};
        });
    }
}   
复制代码

每个React组件都有本身的updater,这个updater充当着组件与React core通信的桥梁。这种设计,使得多个render(好比:ReactDOM, React Native, server side rendering和testing utilities)去实现本身的setState方法成了可能。

在这篇文章中,咱们单独分析一下updater对象在ReactDOM中的实现。在这个实现中,就用到了Fiber reconciler。具体对于ClickCounter组件来讲,这个updater对象就是classComponentUpdater。它的职责有:1)把Fiber的实例检索回来; 2)将更新请求入队;3)对work进行调度。

当咱们说“一个更新请求被入队”,其实意思就是把一个setState的callback添加到Fiber node的“updateQueue”队列中去,等待处理。回归到本示例,ClickCounter组件所对应的Fiber node具体的数据结构:

{
    stateNode: new ClickCounter,
    type: ClickCounter,
    updateQueue: {
         baseState: {count: 0}
         firstUpdate: {
             next: {
                 payload: (state) => { return {count: state.count + 1} }
             }
         },
         ...
     },
     ...
}
复制代码

正如你所看到的那样,updateQueue.firstUpdate.next.payload引用所指向的那个函数就是咱们传给setState方法的那个callback。它表明着render阶段第一个须要被处理的“更新请求”。

处理ClickCounter Fiber node身上的更新请求

在我先前的那篇文章关于work loop的那一章节中,我已经解释过nextUnitOfWork这个全局变量所扮演的角色了。特别地,这一章节说到了这个全局变量指向的是workInProgresstree上那些有work须要去作的Fiber node。当React遍历整颗Fiber树的时候,就是用这个全局变量来判断是否还有未完成本身的work的Fiber node。

咱们从setState方法已经被调用的地方开始提及。在setState方法被调用以后,React会把咱们传给setState的callback传递ClickCounterfiber node,也就是说把这个callback添加到fiber node的updateQueue对象中。而后,就开始调度work。也是从这里开始,React开始进入了render阶段了。它调用renderRoot这个函数,从最顶层的HostRoot开始遍历整颗fiber node树。尽管是从最顶层的根节点开始,可是React会掉过那些已经处理过的 fiber node,只会处理那些还有work须要去完成的节点。此时此刻,咱们只有一个fiber node是有work须要去作的。这个node就是ClickCounterfiber node。

ClickCounterfiber node的alternate字段用于保存一个指向[当前fiber node的克隆副本]的引用。这个克隆副本上的work都是已经执行完成的了。这个克隆副本被称为当前fiber node的alternate fiber node。若是alternate fiber node尚未被建立的话,那么React就会在处理更新请求以前使用createWorkInProgress函数去完成复制工做。如今,咱们假设变量nextUnitOfWork保存着指向当前fiber node的alternate fiber node的引用。

beginWork

首先,咱们的fiber node将会被传递到beginWork 函数里面。

由于这个函数会在fiber node tree上的每个节点调用。因此,若是你想调试render阶段,这是一个打断点的好地方。我常常这么干,经过检测fiber node的type值来肯定当前节点是不是我要跟进的那个。

beginWork函数基本上就是一个大的switch语句。在这个switch语句,beginWork根据workInProgress的tag值来计算初当前fiber node所须要完成的work的类型。而后,执行相应的函数去执行这个work。在咱们的demo中,由于ClickCounter是一个class component,因此,咱们会执行如下的分支语句:

function beginWork(current$$1, workInProgress, ...) {
    ...
    switch (workInProgress.tag) {
        ...
        case FunctionalComponent: {...}
        case ClassComponent:
        {
            ...
            return updateClassComponent(current$$1, workInProgress, ...);
        }
        case HostComponent: {...}
        case ...
}
复制代码

那么,咱们会进入updateClassComponent函数中。取决于当前:1)是不是组件的首次渲染:2)是不是work正在被恢复执行;3)是不是一次React更新,React会干两件事情:

  • 要么建立一个新实例,并挂载这个组件;
  • 要么仅仅是更新它。
function updateClassComponent(current, workInProgress, Component, ...) {
    ...
    const instance = workInProgress.stateNode;
    let shouldUpdate;
    if (instance === null) {
        ...
        // In the initial pass we might need to construct the instance.
        constructClassInstance(workInProgress, Component, ...);
        mountClassInstance(workInProgress, Component, ...);
        shouldUpdate = true;
    } else if (current === null) {
        // In a resume, we will already have an instance we can reuse.
        shouldUpdate = resumeMountClassInstance(workInProgress, Component, ...);
    } else {
        shouldUpdate = updateClassInstance(current, workInProgress, ...);
    }
    return finishClassComponent(current, workInProgress, Component, shouldUpdate, ...);
}
复制代码

Processing updates for the ClickCounter Fiber

咱们已经为ClickCounter建立过一个实例了,因此,咱们的执行将会进入updateClassInstance方法。在这个方法中,React执行了class component绝大部分的work。如下是这个方法执行的最重要的操做(罗列的顺序也是代码执行的顺序):

  • 调用UNSAFE_componentWillReceiveProps生命周期函数(已弃用);
  • 处理updateQueue中的更新请求和生成一个新的state值;
  • 用一个新的state值去调用getDerivedStateFromProps,并获取调用结果。
  • 调用shouldComponentUpdate来确保一个组件是否真的想要更新。若是调用返回值为false的话,那么React将会跳过整个渲染流程包括调用组件实例和它的子组件实例的render方法。不然的话,正常走更新流程。
  • 调用UNSAFE_componentWillUpdate生命周期函数(已弃用);
  • 把生命周期函数componentDidUpdate添加成一个effect。

虽然,“调用componentDidUpdate”这个effect是在render阶段添加的,可是这个方法的实际执行是在接下来的commit阶段。

  • 更新组件实例上的state和props值。

state和props值的更新应该是在render方法调用前的。由于render的返回值是须要依赖最新的state和props值(译者注:这也是指出了一个事实,即react组件更新的本质就是用最新的state和props值去调用组件实例的render方法)。若是咱们不这么干的话,那么render方法的每一次调用的返回值都是同样的。

下面是updateClassInstance方法的精简版:

function updateClassInstance(current, workInProgress, ctor, newProps, ...) {
    const instance = workInProgress.stateNode;

    const oldProps = workInProgress.memoizedProps;
    instance.props = oldProps;
    if (oldProps !== newProps) {
        callComponentWillReceiveProps(workInProgress, instance, newProps, ...);
    }

    let updateQueue = workInProgress.updateQueue;
    if (updateQueue !== null) {
        processUpdateQueue(workInProgress, updateQueue, ...);
        newState = workInProgress.memoizedState;
    }

    applyDerivedStateFromProps(workInProgress, ...);
    newState = workInProgress.memoizedState;

    const shouldUpdate = checkShouldComponentUpdate(workInProgress, ctor, ...);
    if (shouldUpdate) {
        instance.componentWillUpdate(newProps, newState, nextContext);
        workInProgress.effectTag |= Update;
        workInProgress.effectTag |= Snapshot;
    }

    instance.props = newProps;
    instance.state = newState;

    return shouldUpdate;
}

复制代码

我已经把一些比较次要的代码移除掉了。举个例子,在调用生命周期函数和添加effect并触发它以前,React会用typeof操做符去检查这个组件是否实现了某个方法。下面的代码中,React会在添加effect以前检查componentDidUpdate方法是不是一个function:

if (typeof instance.componentDidUpdate === 'function') {
    workInProgress.effectTag |= Update;
}
复制代码

到了这里,咱们已经知道在render阶段,ClickCounter fiber node须要执行哪些操做了。下面,咱们来看看,这些操做是如何改变fiber node上的相关值的。当React开始执行work的时候,ClickCounter组件所对应的fiber node长这样的:

{
    effectTag: 0,
    elementType: class ClickCounter,
    firstEffect: null,
    memoizedState: {count: 0},
    type: class ClickCounter,
    stateNode: {
        state: {count: 0}
    },
    updateQueue: {
        baseState: {count: 0},
        firstUpdate: {
            next: {
                payload: (state, props) => {…}
            }
        },
        ...
    }
}
复制代码

当work执行完毕,ClickCounter组件所对应的fiber node已经长成这样的:

{
    effectTag: 4,
    elementType: class ClickCounter,
    firstEffect: null,
    memoizedState: {count: 1},
    type: class ClickCounter,
    stateNode: {
        state: {count: 1}
    },
    updateQueue: {
        baseState: {count: 1},
        firstUpdate: null,
        ...
    }
}
复制代码

仔细观察一下连个fiber node属性值之间的差别。咱们会发现,在处理完更新请求后,memoizedState和baseState中的count字段的属性值已经变为1了。与此同时,React也把ClickCounter的组件实例的状态也更新了。

当前,咱们在updateQueue中已经没有更新请求了,因此firstUpdate的值为null。还有很重要的一点,咱们的effectTag字段的值已经从0变为4了。4用二进制表示就是100,而这就是update这个side-effect的tag值

export const Update = 0b00000000100;
复制代码

下面作个小总结。当React在ClickCounterfiber node上执行work的时候,React要作的事有:

  • 调用pre-mutation生命周期方法
  • 更新state值
  • 定义相关的side-effect(译者注:将某些操做标记为side-effect)

Reconciling children for the ClickCounter Fiber

当上面提到的小总结的东西完成后,React执行将会进入finishClassComponent。在这个函数里面,React将会调用组件实例的render方法,而后在它的子组件实例(正是render方法返回的东西)上应用diff算法。在这篇文章里面有一个关于diff算法高质量的归纳:

当对比中的两个react DOM element(译者注:本质上就是react element,可是type的值是DOM类型的字符串)具体相同的type的时候,React会查看二者的attribute的差别性,保留底层所对应的DOM node对象,只是更新那些须要改变的attribute。

若是咱们再深究一点的话,那么,咱们会了解到其实对比是react element所对应的fiber node。在本文中,我不会讨论太多细节,由于这里面的处理流程仍是挺复杂的。我将会在一个单独的文章上专门来说述child reconciliation的处理流程。

若是你着急去了解child reconciliation细节的话,那么你能够查看这个reconcileChildrenArray函数。由于在咱们这个demo中,ClickCounter的render方法返回的是一个react element组成的数组。

当前,有两件重要的事情须要咱们去理解。第一件是,随着child reconciliation流程的执行,React会为从render方法中返回的child react element建立或者更新对应的fiber node。finishClassComponent函数会返回当前fiber node第一个child fiber node的引用。这个引用将会赋值给nextUnitOfWork,而且会在work loop的下一个循环中使用到;第二件事是,React把对子fiber node 的props的更新看成父fiber node的work的一部分。为了达成这事,React会使用从render方法返回的react element身上的数据。

举个例子,在React对ClickCounterfiber node 的children进行reconcile以前,span元素所对应的fiber node是长这样的:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    key: "2",
    memoizedProps: {children: 0},
    pendingProps: {children: 0},
    ...
}

复制代码

正如你所见的那样,memoizedPropspendingProps中的children属性值都是0 。而下面,就是调用render方法后返回的span元素所对应的react element:

{
    $$typeof: Symbol(react.element)
    key: "2"
    props: {children: 1}
    ref: null
    type: "span"
}
复制代码

正如你所见的那样,fiber node中的props与返回的react element中的props是不一样的。在createWorkInProgress函数中,这种不一样性会应用 在alternate fiber node的建立过程当中。React就是从react element上拷贝已经更新的props到alternate fiber node上的。

当React对ClickCounter组件的children完成了reconcile以后,span元素所对应的fiber node的pendingProps字段的值将获得更新。该字段值将会跟span元素所对应的react element的props值保持一致:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    key: "2",
    memoizedProps: {children: 0},
    pendingProps: {children: 1},
    ...
}
复制代码

稍后,React会span元素所对应的fiber node执行work,它会将它们复制到memoizedProps上,并向DOM更新上添加effect(add effect to DOM update)。

到此为止,咱们已经讲完了ClickCounterfiber node在render阶段所须要执行的全部的work了。由于button组件是ClickCounter组件的第一个子元素,因此,它所对应的fiber node将会被赋值给nextUnitOfWork变量。由于这个fiber node没有任何work须要去作的。因此,React会移步到它的sibling-span元素所对应的fiber node。根据这里所描述的算法能够得知,以上过程发生在completeUnitOfWork函数里面。

Processing updates for the Span fiber

因此,nextUnitOfWork变量如今指向span元素所对应的fiber node(后面简称为“span fiber node”)的alternate fiber node。React对span fiber node的更新处理流程就是从这里开始。跟ClickCounter fiber node的处理流程是同样的,咱们都是从beginWork函数开始。

由于span节点属于HostComponent类型的,因此,这一次,咱们会进入HostComponent的分支:

function beginWork(current$$1, workInProgress, ...) {
    ...
    switch (workInProgress.tag) {
        case FunctionalComponent: {...}
        case ClassComponent: {...}
        case HostComponent:
          return updateHostComponent(current, workInProgress, ...);
        case ...
}
复制代码

最终,咱们会进入updateHostComponent这个函数。往上,你能够看到咱们上面在分析ClickCounter fiber node时候的所提到的updateClassComponent,针对functional component,React会执行updateFunctionComponent等等。你能够在ReactFiberBeginWork.js文件中找到全部的这些函数的实现代码。

Reconciling children for the span fiber

在咱们的demo中,由于span节点的子节点太过简单了,因此在updateHostComponent函数中,没啥过重要的事情发生。

Completing work for the Span Fiber node

一旦beginWork执行完毕,当前fiber node就会被传递到completeWork中去。在本示例中,这个fiber node就是span fiber node。在此以前,React须要更新span fiber node上的memoizedProps字段值。你可能还记得,当React对ClickCounter组件的子组件进行reconcile的时候,它已经更新span fiber node上的pendingProps字段:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    key: "2",
    memoizedProps: {children: 0},
    pendingProps: {children: 1},
    ...
}
复制代码

因此,一旦beginWork函数在span fiber node上调用完毕的话,那么React会更新memoizedProps字段值,使得它与pendingProps字段值保持一致:

function performUnitOfWork(workInProgress) {
    ...
    next = beginWork(current$$1, workInProgress, nextRenderExpirationTime);
    workInProgress.memoizedProps = workInProgress.pendingProps;
    ...
}
复制代码

执行完beginWork函数后,React就会执行completeWork函数。这个函数的实现基本上就是一个大大的switch语句。这跟以前所提到的beginWork里面的switch语句差很少:

function completeWork(current, workInProgress, ...) {
    ...
    switch (workInProgress.tag) {
        case FunctionComponent: {...}
        case ClassComponent: {...}
        case HostComponent: {
            ...
            updateHostComponent(current, workInProgress, ...);
        }
        case ...
    }
}
复制代码

由于咱们的span fiber node(所对应的react element)是HostComponent,因此,咱们会进入到updateHostComponent函数里面。在这个函数里面,React基本上就作了如下的三件事情:

  • 为DOM更新作准备
  • 将准备的结果添加到span fiber node的updateQueue字段中;
  • adds the effect to update the DOM

在执行这行操做以前,span fiber node长这样的:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    effectTag: 0
    updateQueue: null
    ...
}
复制代码

当上面的work执行完成后,span fiber node长这样:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    effectTag: 4,
    updateQueue: ["children", "1"],
    ...
}
复制代码

请注意二者在effectTag和updateQueue字段值上的不一样。对于effectTag的值来讲,它再也不是0,而是4。用二进制表示就是100,而第三位就是update这 种side-effect所对应的二进制位。现在该位置为1,则说明span fiber node后面所须要执行的side-effect就是update。在接下来的commit阶段,对于span fiber node来讲,这也是React惟一须要帮它完成的任务了。而updateQueue字段值保存的是用于update的数据。

一旦React处理完ClickCounterfiber node和它的子fiber node们,那么render阶段算是结束了。React会把产出的alternate fiber node树赋值给FiberRoot对象的finishedWork属性。这颗新的alternate fiber node树包含了须要被flush到屏幕的东西。它会在render阶段以后立刻被处理或者稍后在浏览器分配给React的,空闲的时间里面执行。

effects list

在咱们给出的示例中,由于span fiber node和ClickCounter fiber node是有side effect的。React将会给span fiber node添加一个link,让它指向HostFiber的firstEffect属性

在函数compliteUnitWork中,react完成了effect list的构建。下面就是本示例中,带有effect的fiber node树。在这棵树上,有着两个effect:1)更新span节点的文本内容;2)调用ClickCounter组件的生命周期函数:

而下面是由具备effect的fiber node组成的线性列表:

commit阶段

这个阶段以completeRoot函数开始。在继续往下走以前,它首先将FiberRoot的finishedWork属性值置为null:

root.finishedWork = null;
复制代码

不像render阶段,commit阶段是同步执行的。因此,它能很安全地更新HostRoot,以此来指示commit工做已经开始了。

commit阶段是React进行DOM操做和调用post-mutation生命周期方法componentDidUpate的地方。为了实现上面这些目标,React会遍历render阶段所产出的effect list,并应用相应的effect。

就本示例而言,咱们在render阶段事后,咱们有如下几个effect:

{ type: ClickCounter, effectTag: 5 }
{ type: 'span', effectTag: 4 }
复制代码

ClickCounterfiber node的effect tag为5,用二进制表示就是“101”。它对应的work是update。而对于class component而言,这个work会被“翻译为”componentDidUpdate这个生命周期方法。在二进制“101”中,最低位为“1”,表明着当前这个fiber node的全部work都在render阶段执行完毕了。

span fiber node的effect tag值是4,用二进制表示是“100”。这个编号所表明的work是“update”,由于当前的span fiber node对应的是host component类型的。这个“update”work更具体点来讲就是“DOM更新”。回归到本示例,“DOM更新”更具体点是指“更新span元素的textContent属性”。

Applying effects

让咱们一块儿来看看,React是如何应用这些effect的。函数commitRoot就是用来应用effect的。它由三个子函数组成:

function commitRoot(root, finishedWork) {
    commitBeforeMutationLifecycles()
    commitAllHostEffects();
    root.current = finishedWork;
    commitAllLifeCycles();
}
复制代码

这三个子函数都实现对effect list的遍历,而且在遍历过程当中去检查effect的类型。若是它们发现当前的这个effect跟它们函数的职责相关的,那么就会应用这个effect。在咱们的示例中,具体点讲就是在ClickCounter组件上调用componentDidUpdate这个生命周期方法和更新span元素的文本内容。

第一个子函数commitBeforeMutationLifeCycles 会查找snapshot类型的effect,并调用getSnapshotBeforeUpdate方法。由于在ClickCouner组件身上,咱们并无实现这个方法,因此React并无在render阶段把这个effect添加到该组件对应的fiber node身上。因此,在咱们这个示例中,这个子函数啥事都没作。

DOM updates

接下来,React会移步到commitAllHostEffects函数上面来。就是在这个函数里面,React完成了将span元素的文本内容从“0”更新到“1”。这个函数几乎跟ClickCounter这个fiber node没有关系。由于这个fiber node对应的是class component,而class componnet是没有任何的直接的DOM更新方面的需求的。

这个函数的大致框架是对不一样类型的effect执行不一样操做。在咱们这个示例中,咱们须要更新span元素的文本内容,因此咱们是要走Update这条分支的:

function updateHostEffects() {
    switch (primaryEffectTag) {
      case Placement: {...}
      case PlacementAndUpdate: {...}
      case Update:
        {
          var current = nextEffect.alternate;
          commitWork(current, nextEffect);
          break;
        }
      case Deletion: {...}
    }
}
复制代码

顺着commitWork一路走下去,咱们最终会进入updateDOMProperties函数里面。在这个函数里面,它使用了咱们在render阶段添加到fiber node的updateQueue字段身上的payload来更新span元素的textContent属性值:

function updateDOMProperties(domElement, updatePayload, ...) {
  for (let i = 0; i < updatePayload.length; i += 2) {
    const propKey = updatePayload[i];
    const propValue = updatePayload[i + 1];
    if (propKey === STYLE) { ...} 
    else if (propKey === DANGEROUSLY_SET_INNER_HTML) {...} 
    else if (propKey === CHILDREN) {
      setTextContent(domElement, propValue);
    } else {...}
  }
}
复制代码

在[DOM更新]这个effect被应用后,React将finishedWork树赋值给HostRoot。它把alternate tree设置为current tree:

root.current = finishedWork;
复制代码

Calling post mutation lifecycle hooks

咱们剩下最后一个commitAllLifecycles要讲了。在这个函数里面,React调用了全部的post-mutational 生命周期方法。在render阶段,React往ClickCounter组件身上添加了一个叫“update”的effect。这个effect就是本函数所要查找的effect,一旦找到以后,React就会调用componentDidUpdate方法:

function commitAllLifeCycles(finishedRoot, ...) {
    while (nextEffect !== null) {
        const effectTag = nextEffect.effectTag;

        if (effectTag & (Update | Callback)) {
            const current = nextEffect.alternate;
            commitLifeCycles(finishedRoot, current, nextEffect, ...);
        }
        
        if (effectTag & Ref) {
            commitAttachRef(nextEffect);
        }
        
        nextEffect = nextEffect.nextEffect;
    }
}
复制代码

这个函数也会更新refs,可是由于咱们这个示例中并无使用到这个特性。因此相应的那部分代码(指commitAttachRef(nextEffect);)就不会被执行。对componentDidUpdate方法的调用是发生在commitLifeCycles函数里面:

function commitLifeCycles(finishedRoot, current, ...) {
  ...
  switch (finishedWork.tag) {
    case FunctionComponent: {...}
    case ClassComponent: {
      const instance = finishedWork.stateNode;
      if (finishedWork.effectTag & Update) {
        if (current === null) {
          instance.componentDidMount();
        } else {
          ...
          instance.componentDidUpdate(prevProps, prevState, ...);
        }
      }
    }
    case HostComponent: {...}
    case ...
}
复制代码

顺便你也看到,这也是React调用componentDidMount这个生命周期方法的地方。不过这个调用时机是在组件的首次挂载的过程当中而已。

相关文章
相关标签/搜索