最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • [译]深入了解React中的state和props更新

    正文概述 掘金(金名的铭)   2021-08-10   498

    在我的上篇文章 Inside Fiber: 深入了解React新协调算法中介绍了理解更新过程细节的所需的基础知识,我将在本文中描述这个更新过程。

    我已经概述了将在本文中使用的主要数据结构和概念,特别是Fiber节点,currentwork-in-progress树,副作用(side-effects)以及effects链表(effects list)。我也提供了主要算法的高级概述和render阶段与commit阶段的差异。如果你还没有阅读过它,我推荐你从那儿开始。

    我还向你介绍了带有一个按钮的示例程序,这个按钮的功能就是简单的增加数字。

    [译]深入了解React中的state和props更新

    你可以在这查看在线代码。它的实现很简单,就是一个render函数中返回buttonspan元素的类组件。当你点击按钮的时候,在点击事件的处理函数中更新组件的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是如何处理状态更新和创建effects list的。我们可以看到render阶段和commit阶段的高级函数中发生了什么。

    尤其是在React的completeWork函数中:

    • 更新ClickCounterstate中的count属性
    • 调用render方法获取子元素列表并比较
    • 更新span元素的props

    以及,在React的commitRoot 函数中:

    • 更新span元素的文本内容属性
    • 调用componentDidUpdate生命周期方法

    但是在那之前,我们先快速看看当我们在点击处理函数中调用setState时工作是如何调度的。

    请注意,你无需了解这些来使用React。本文是关于React内部是如何运作的。

    调度更新

    当我们点击按钮时,click事件被触发,React执行传递给按钮props的回调。在我们的程序中,它只是简单的增加计数器和更新state

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

    每个组件都有相应的updater,它作为组件和React核心之间的桥梁。这允许setState在ReactDOM,React Native,服务端渲染和测试程序中是不同的实现。(译注:从源码可以看出,setState内部是调用updater.enqueueSetState,这样在不同平台,我们都可以调用setState来更新页面)

    本文中,我们关注ReactDOM中实现的updater对象,它使用Fiber协调器。对于ClickCounter组件,它是classComponentUpdater。它负责获取Fiber实例,为更新入列,以及调度work。

    当更新排队时,它们基本上只是添加到Fiber节点的更新队列中进行处理。在我们的例子中,ClickCounter组件对应的Fiber节点将有下面的结构:

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

    如你所见,updateQueue.firstUpdate.next.payload中的函数就我我们在ClickCounter组件中传递给setState的回调。它代表在render阶段中需要处理的第一个更新。

    处理ClickCounter Fiber节点的更新

    我上篇文章中的work循环部分中解释了全局变量nextUnitOfWork的角色。尤其是,这个变量保存workInProgress树中有work待做的Fiber节点的引用。当React遍历树的Fiber时,它使用这个变量知道是否存在其他有未完成work的Fiber节点。

    我们假定setState方法已经被调用。 React将setState中的回调添加到ClickCounterfiber节点的updateQueue中,然后调度work。React进入render阶段。它使用renderRoot函数从最顶层HostRootFiber节点开始遍历。然而,它会跳过已经处理过得Fiber节点直到遇到有未完成work的节点。基于这点,只有一个节点有work待做。它就是ClickCounterFiber节点。

    所有的work都是基于保存在Fiber节点的alternate字段的克隆副本执行的。如果alternate节点还未创建,React在处理更新前调用createWorkInProgress函数创建副本。我们假设nextUnitOfWork变量保存代替ClickCounterFiber节点的引用。

    beginWork

    首先, 我们的Fiber进入beginWork函数。

    beginWork函数大体上是个大的switch语句,通过tag确定Fiber节点需要完成的work的类型,然后执行相应的函数来执行work。在这个例子中,CountClicks是类组件,所以会走这个分支:

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

    我们进入updateClassComponent函数。取决于它是首次渲染、恢复work还是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'll already have an instance we can reuse.
            shouldUpdate = resumeMountClassInstance(workInProgress, Component, ...);
        } else {
            shouldUpdate = updateClassInstance(current, workInProgress, ...);
        }
        return finishClassComponent(current, workInProgress, Component, shouldUpdate, ...);
    }
    

    处理ClickCounter Fiber更新

    我们已经有了ClickCounter组件实例,所以我们进入updateClassInstance。这是React为类组件执行大部分work的地方。以下是在这个函数中按顺序执行的最重要的操作:

    • 调用UNSAFE_componentWillReceiveProps()钩子(已废弃)
    • 处理updateQueue中的更新以及生成新state
    • 使用新state调用getDerivedStateFromProps并得到结果
    • 调用shouldComponentUpdate确定组件是否需要更新;如果返回结果为false,跳过整个渲染过程,包括在该组件和它的子组件上调用render;否则继续更新
    • 调用UNSAFE_componentWillUpdate(已废弃)
    • 添加一个effect来触发componentDidUpdate生命周期钩子
    • 更新组件实例的stateprops

    组件实例的stateprops应该在render方法调用前更新,因为render方法的输出通常依赖于stateprops。如果我们不这样做,它每次都会返回一样的输出。

    下面是该函数的简化版本:

    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;
    }
    

    上面代码片段中我删除了一些辅助代码。对于实例,调用生命周期方法或添加effects来触发它们前,React使用typeof操作符检查组件是否实现了这些方法。比如,这是React添加effect前如何检查componentDidUpdate方法:

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

    好的,我们现在知道了render阶段中为ClickCounterFiber节点执行了什么操作。现在让我们看看这些操作如何改变Fiber节点的值。当React开始work,ClickCounter组件的Fiber节点类似这样:

    {
        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完成后,我们得到一个长这样的Fiber节点:

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

    花点时间观察属性值的差异

    更新被应用后,memoizedStateupdateQueuebaseState的属性count的值变为1。React也更新了ClickCounter组件实例的state。

    至此,队列中不再有更新,所以firstUpdatenull。更重要的是,我们改变了effectTag属性。它不再是0,它的是为4。 二进制为100,意味着第三位被设置了,代表Update副作用标记:

    export const Update = 0b00000000100;
    

    可以得出结论,当执行ClickCounterFiber节点的work时,React低啊用变化前生命周期方法,更新state,定义有关的副作用。

    协调ClickCounter Fiber的子组件

    在那之后,React进入finishClassComponent。这是调用组件实例render方法和在子组件上使用diff算法的地方。文档中对此有高级概述。以下是相关部分:

    然而,如果我们深入挖掘,会知道它实际是对比Fiber节点和React元素。但是我现在不会详细介绍因为过程相当复杂。我会单独些篇文章,特别关注子协调过程。

    至此,有两个很重要的事需要理解。第一,当React进行子协调时,它会为从render函数返回的子React元素创建或更新Fiber节点。finishClassComponent函数当前Fiber节点的第一个子节点的引用。它被赋值给nextUnitOfWork并在稍后的work循环中处理。第二,React更新子节点的props作为父节点执行的一部分work。为此,它使用render函数返回的React元素的数据。

    举例来说,这是React协调ClickCounterfiber子节点之前span元素对应的Fiber节点看起来的样式

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

    可以看到,memoizedPropspendingPropschildren属性都是0。这是render函数返回的span元素对应的React元素的结构。

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

    可以看出,Finer节点和返回的React元素的props是有差异的。createWorkInProgress内部用这创建替代的Fiber节点,React把React元素中更新的属性复制到Fiber节点

    因此,在React完成ClickCounter组件子协调后,span的Fiber节点的pendingProps更新了。它们将匹配spanReact元素中的值。

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

    稍后,React会为spanFiber节点执行work,它将把它们复制到memoizedProps以及添加effects来更新DOM。

    好的,这就是render阶段React为ClickCounterfiber节点所执行的所有work。因为button是ClickCounter组件的第一个子节点,它会被赋值给nextUnitOfWork变量。button上无事可做,所有React会移动到它的兄弟节点spanFiber节点上。根据这里描述的算法,这发生在completeUnitOfWork函数内。

    处理Span fiber的更新

    nextUnitOfWork变量现在指向spanfiber的alternate,React基于它开始工作。和ClickCounter执行的步骤类似,开始于beginWork函数。

    因为span节点是HostComponent类型,这次在switch语句中React会进入这条分支:

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

    结束于updateHostComponent函数。(在这个函数内)你可以看到一系列和类组件调用的updateClassComponent函数类似的函数。对于函数组件是updateFunctionComponent。你可以在ReactFiberBeginWork.js文件中找到这些函数。

    协调Span fiber子节点

    在我们的例子中,span节点在updateHostComponent里没什么重要事的发生。

    完成Span Fiber节点的work

    一旦beginWork完成,节点就进入completeWork函数。但是在那之前,React需要更新span Fiber节点的memoizedProps属性。你应该还记得协调ClickCounter组件子节点时更新了spanFiber节点的pendingProps

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

    所以一旦spanfiber的beginWork完成,React会将pendingProps更新到memoizedProps

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

    然后调用的completeWork和我们看过的beginWork相似,基本上是一个大的switch语句。

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

    由于spanFiber节点是HostComponent,它会执行updateHostComponent函数。在这个函数中React大体上做了这些事:

    • 准备DOM更新
    • 把它们加到spanfiber的updateQueue
    • 添加effect用于更新DOM

    在这些操作执行前,spanFiber节点看起来像这样:

    {
        stateNode: new HTMLSpanElement,
        type: "span",
        effectTag: 0
        updateQueue: null
        ...
    }
    

    works完成后它看起来像这样:

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

    注意effectTagupdateQueue字段的差异。它不再是0,它的值是4。用二进制表示是100,意味着设置了第3位,正是Update副作用的标志位。这是React在接下来的commit阶段对这个节点唯一要做的任务。updateQueue保存着用于更新的载荷。

    一旦React处理完ClickCounter级它的子节点,render阶段结束。现在它可以将完成的替代树赋值给FiberRootfinishedWork属性。这是需要被刷新到屏幕上的新树。它可以在render阶段之后马上被处理,或这当React被浏览器给予时间时再处理。

    Effects list

    在我们的例子中,由于span节点ClickCounter组件有副作用,React将添加指向spanFiber节点的链接到HostFiberfirstEffect属性。

    React在compliteUnitOfWork函数内构建effects list。这是带有更新span节点文本和调用ClickCounter上hooks副作用的Fiber树看起来的样子:

    [译]深入了解React中的state和props更新

    这是由有副作用的节点组成的线性列表: [译]深入了解React中的state和props更新

    Commit阶段

    这个阶段开始于completeRoot函数。它在做其他工作之前,它将FiberRootfinishedWork属性设为null

    root.finishedWork = null;
    

    于之前的render阶段不同的是,commit阶段总是同步的,这样它可以安全地更新HostRoot来表示commit work开始了。

    commit阶段是React更新DOM和调用突变后生命周期方法componentDidUpdate的地方。为此,它遍历在render阶段中构建的effects list并应用它们。

    有以下在render阶段为spanClickCounter定义的effects:

    { type: ClickCounter, effectTag: 5 }
    { type: 'span', effectTag: 4 }
    

    ClickCounter的effect tag的值是5或二进制的101,定义了对于类组件基本上转换为componentDidUpdate生命周期方法的Update工作。最低位也被设置了,表示该Fiber节点在render阶段的所有工作都已完成。

    span的effect tag的值是4或二进制的100,定义了原生组件DOM更新的update工作。这个例子中的span元素,React需要更新这个元素的textContent

    应用effects

    让我们看看React如何应用这些effects。commitRoot函数用于应用这些effects,由3个子函数组成:

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

    每个子函数都实现了一个循环,该循环用于遍历effects list并检查这些effects的类型。当发现effect和函数的目的有关时就应用它。我们的例子中,它会调用ClickCounter组件的componentDidUpdate生命周期方法,更新span元素的文本。

    第一个函数 commitBeforeMutationLifeCycles 寻找 Snapshot effect然后调用getSnapshotBeforeUpdate方法。但是,我们在ClickCounter组件中没有实现该方法,React在render阶段没有添加这个effect。所以在我们的例子中,这个函数不做任何事。

    DOM更新

    接下来React执行 commitAllHostEffects 函数。这儿是React将span元素的t文本由0变为1的地方。ClickCounter fiber没有要做的,因为类组件的节点没有任何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节点的updateQueue载荷更新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更新后,React将finishedWork赋值给HostRoot。它将替代树是设为当前树:

    root.current = finishedWork;
    

    调用突变后生命周期hooks

    剩下的函数是commitAllLifecycles。这是 React 调用突变后生命周期方法的地方。在render阶段,React为ClickCounter组件添加Update effect。这是commitAllLifecycles寻找的effects之一并调用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,但是由于我们没有使用这个特性,所以没什么作用。这个方法在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方法的函数。


    起源地下载网 » [译]深入了解React中的state和props更新

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元