深入react的state和props更新 您所在的位置:网站首页 state和status 深入react的state和props更新

深入react的state和props更新

2023-02-08 15:16| 来源: 网络整理| 查看: 265

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

前言

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

正文

在我先前的文章中 深入React Fiber架构的reconciliation 算法 提到要想理解更新流程的技术细节,我们需得具备一定的基础知识。而这部分的基础知识就是篇文章要讲述的内容。

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

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

同时,我也对主要算法进行了宏观上的阐述,也解释过render阶段和commit阶段之间的差异性。如果你还没有阅读过讲述这些东西的文章,我建议你先去阅读。

我也引入过一个简单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 [ Update counter, {this.state.count} ] } } 复制代码

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

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

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

更新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}, ... } 复制代码

正如你所见的那样,memoizedProps和pendingProps中的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这个生命周期方法的地方。不过这个调用时机是在组件的首次挂载的过程中而已。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有