[譯] 深刻解析React中state和props更新

原文連接:medium.com/react-in-de…javascript

到react fiber內部一探究竟html

在我以前文章Fiber內部:深度概述React新協調算法中,我鋪設了基礎內容,用於理解我這篇文章講解的更新處理的技術細節。java

我已經概述過將在這篇文章中用到的主要數據結構和概念,特別是Fiber節點、當前和工做過程樹、反作用和做用列表,我也對主要的算法提供過大體的說明,且解釋過**rendercommit**階段的不一樣。若是你尚未讀過,那我建議你從那裏仍是react

我也介紹過一個button的簡單應用,在屏幕上渲染一個遞增的樹:git

你能夠在這裏運行它,它實現了一個簡單的組件,經過**render方法返回兩個子元素buttonspan。當你點擊button時,組件的狀態就會在處理方法中更新,span**元素的文本更新結果:github

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如何在commit**階段添加做用(effects)來調用這個方法。算法

這篇文章中,我想向你展現React如何處理狀態更新以及構建做用列表,咱們將帶你去看看**rendercommit**階段中大體方法都作了些什麼。數組

特別的是,咱們將看到React在completeWork中是如何:瀏覽器

  • 更新**ClickCounterstate中的count**屬性。
  • 調用**render**方法來獲取子節點列表,以及執行比較。
  • 更新**span**元素的props

還有,React在commitRoot安全

  • 更新**span元素的textCount**屬性。
  • 調用**componentDidUpdate**生命週期方法。

在這以前,咱們先看看,當咱們在click處理方法中調用**setState**時,工做(就是指的work loop中的work啦)如何如何調用的。

注意,你須要知道這裏的一塊兒來用React,這篇文章是關於React如何內部工做的。

調度更新(Scheduling updates)

當咱們點擊button時,**click**事件被觸發,React執行經過props傳給button的回調方法,在咱們的應用中,它簡單的增長計數器,並更新狀態:

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

每個React組件都有相關聯**updater,它扮演組件與React內核的橋,這使得setState**在ReactDOM、React Native、服務端渲染以及測試工具中有不一樣的實現。

這篇文章中,咱們將看看ReactDOM中updater對象的實現,它使用了Fiber協調器。對於**ClickCounter**組件,它是classComponentUpdater,它負責獲取Fiber實例、隊列化更新以及調度工做。

當更新隊列化了,它們就在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節點的更新

個人前一篇文章的工做循環章節解釋了**nextUnitOfWork全局變量的角色,特別是,它持有了來自還有工做待作的workInProgress**樹中的Fiber節點。當React遍歷Fiber樹時,使用它來知道是否要有其餘未完成工做的Fiber節點。

咱們從假定**setState方法被調開始,React在ClickCounter上添加setState的回調,且調用工做,React進入render階段。它使用renderRoot方法從頂層HostRoot開始遍歷,而後它會調用以及處理過的fiber節點,直到發現尚未完成工做的節點,在這一點上,這裏只有一個fiber節點有工做作,就是ClickCounter**Fiber節點。

全部工做都在fiber的副本上執行,這個副本保存在**alternate字段中,若是這個alternate節點尚未建立,React會在處理更新以前在createWorkInProgress方法中建立這個副本。咱們來假定這個nextUnitOfWork遍歷就持有這個副本ClickCounter**Fiber節點的引用。

beginWork

首先,咱們Fiber進入beginWork方法。

由於這個方法在樹中的每一個fiber節點上都會執行,因此若是你想在**render**階段debug,那這是很好斷點位置,我常常這樣來檢查Fiber節點類型,以便於肯定我須要的那個節點。

**beginWork基本上是一個大的switch語句,根據tag來肯定每一個Fiber須要作的工做類型,而後執行各自的方法,在咱們的CountClicks**例子中,它是個類組件,因此這部分被執行:

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

咱們進入updateClassComponent方法,依賴於它是首次渲染、工做恢復繼續(work不是能夠異步打斷的嘛),或者只是更新,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, ...);
}
複製代碼

處理ClickConter Fiber的更新

咱們已經有**ClickCounter**組件的實例,因此咱們進入updateClassInstance,這是React處理類組件大部分工做的地方,方法中按順序有最重要的幾個操做:

  • 調用 UNSAFE_componentWillReceiveProps 鉤子 (棄用)
  • 執行 **updateQueue**中的更新,並生成新的state
  • 使用新的state調用**getDerivedStateFromProps**,並得到結果
  • 調用**shouldComponentUpdate確保組件是否須要更新,若是不,則跳過整個render處理,包括該組件和其子組件的render**調用,反之則用更新處理。
  • 調用**UNSAFE_componentWillUpdate** (棄用)
  • 添加一個做用(effect)來觸發**componentDidUpdate**生命週期鉤子

儘管調用**componentDidUpdate的做用在render階段添加,可是這個方法將在commit**階段被執行

  • 在組件實例上更新**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;
}
複製代碼

在上面的代碼片斷中我已經移除掉一些輔助代碼,對於實例,在調用生命週期方法或者添加觸發它們的做用前,React使用typeof操做符檢測組件是否實現了這個方法。例如,這裏即是React檢測**componentDidUpdate**,在它這個做用添加以前:

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

好,如今,我知道了**ClickCounter在render階段中有哪些操做須要執行,那咱們來看看Fiber節點上這些操做改變的值。當React開始工做時,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) => {…}
            }
        },
        ...
    }
}
複製代碼

工做結束以後,咱們獲得Fiber階段結果看起來這樣:

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

花點時間觀察一下屬性值的不一樣

在更新執行以後,count屬性的值在memoizedStateupdateQueue中的baseState上變成1,React也更新了**ClickCounter**組件實例中的state。

此刻,咱們在隊列中不在有更新,全部**firstUpdatenull,且重要的是,咱們修改了effectTag屬性的值,它不在是0,而是4,二進制中爲100,這表明第三位被設,而這一位表明Update**的反作用tag(side-effect tag)

export const Update = 0b00000000100;
複製代碼

綜述,**ClickCounter**Fiber節點的工做是,調用前置突變生命週期方法,更新state以及定義相關反作用。

ClickCounter Fiber的子協調

一旦那些完成,React進入finishClassComponent,這個方法中,React調用組件實例**render**方法,且對組件返回的孩子執行diff算法,這個文檔中有大體概述,這是相關的一部分:

當比較兩個相同類型的React DOM元素時,React觀察二者的屬性,保留DOM節點中一致的,且只更新變化的屬性。

而後若是咱們再深刻的話,咱們能夠知道它的確是比較Fiber節點和React元素,可是我如今就先不太詳細的說明了,由於這個處理至關細緻,我將會針對子協調單獨的寫文章分析。

若是你本身很好奇想知道這個細節,能夠查閱reconcileChildrenArray方法,由於在咱們的例子中,**render**方法返回React元素數組。

此刻,有兩個重要事情須要理解,首先,當React進行子協調處理時,它建立或更新了子React元素的Fiber節點,這些子元素有**render方法返回,finishClassComponent返回了當前Fiber節點的第一個孩子的引用,它將會賦值給nextUnitOfWork,便於在工做循環中以後處理;其次,React更新孩子的props是其父級上執行工做的一部分,因此爲了作這個,它要使用render**方法返回的react元素上的數據。

例如,這裏是**span元素相關的Fiber節點在React協調ClickCounter**fiber以前的樣子:

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

正如你所見,memoizedPropspendingPropschildren屬性值都是0,這是**span元素的render**方法返回的React元素結構:

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

如你所見,Fiber節點和返回的React元素的props有點不一樣,在建立fiber節點副本的createWorkInProgress方法中,React從React元素中拷貝了更新的屬性到Fiber節點

因此,在React完成**ClickCounter組件上的子協調後,spanFiber節點的pendingProps將會更新,它們將會匹配span**React元素中的值:

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

而後,當React將要執行**spanFiber節點上的工做,它將拷貝它們到memoizedProps**,並添加做用(effects)來更新DOM。

嗯,這就是在render階段中,React在**ClickCounterFiber節點上執行的全部工做,由於Button是ClickCounter組件上的第一孩子,它將賦值給nextUnitOfWork變量,因爲無事可作,因此React將會轉移到它的兄弟spanFiber節點上,根據這裏描述的算法,這發生在completeUnitOfWork**方法中。

Span Fiber的更新處理

因此,**nextUnitOfWork變量如今指向span副本,且React在它上面仍是工做,相似於在ClickCounte**上的步驟,咱們開始於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節點的完成工做

一旦**beginWork完成,該節點就進入completeWork方法,可是在此以前,React須要更新span fiber上的memoizedProps,你可能記得,當ClickCounter組件上子協調時,React更新spanFiber節點上的pendingProps**:

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

因此,一旦**spanFiber上的beginWork完成,React就是更新pendingPropsmemoizedProps**上:

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

而後,它調用**completeWork方法,這個基本上是一個大的switch語句,相似於咱們在beginWork**中看到的:

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

由於咱們的**spanFiber節點是HostComponent**,因此它執行updateHostComponent方法,在這個方法中,React基本上以下操做:

  • 準備DOM更新
  • 把它們添加到**spanfiber的updateQueue**
  • 添加更新DOM的做用

在操做執行以前,**span**Fiber節點看起來像:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    effectTag: 0
    updateQueue: null
    ...
}
複製代碼

當工做完成以後,它看起來像:

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

注意,effectTagupdateQueue字段的不一樣,它再也不是0,而是**4,二進制爲100,這表明第三位被設,第三位表明着Update的反作用tag,這是React在接下來的commit階段惟一須要作的工做,而updateQueue**字段持有的負載(payload)將會在更新時用到。

一旦,React處理完**ClickCounter和它們的孩子,它就完成了render階段,它如今就能把完成成的副本(或者叫替代-alternate)樹賦值給FiberRoot上的finishedWork屬性。這是一顆新的須要刷新在屏幕上的樹,它能夠在render**階段以後當即處理,或者掛起等瀏覽器給React空閒時間。

做用列表(Effects list)

在咱們的例子中,由於span節點和ClickCounter組件都有反作用,React會把HostFiber上的firstEffect指向spanFiber節點。

React在compliteUnitOfWork方法中構建做用列表,這裏是帶有做用的Fiber樹,這些做用是更新**span節點文本,調用ClickCounter**的鉤子:

這裏是做用節點的線性列表:


Commit 階段

這個階段開始於completeRoot方法,在它作任何工做以前,它把**FiberRoot上的finishedWork屬性設爲null**:

root.finishedWork = null;

不像**render階段,commit階段老是同步的,因此它能夠安全的更新HostRoot**來指示提交工做開始了。

**commit階段就是React更新DOM以及調動後置突變生命週期方法componentDidUpdate的地方,爲了這樣,它迭代在render**階段建立的做用列表,並應用它。

咱們在**render階段中,對spanClickCounter**節點有以下做用:

{ type: ClickCounter, effectTag: 5 }
{ type: 'span', effectTag: 4 }
複製代碼

ClickCounter的做用標籤值是5或者二進制的101,定義的**更新工做被認爲是調用類組件的componentDidUpdate生命週期方法。最低位也被設值,它表明這個Fiber節點在render**階段的全部工做都已完成。

span的做用標籤是4或者二進制100,定義的**更新工做是host組件的DOM更新,在咱們例子中的span元素,React將須要更新元素的textContent**。

應用做用(Applying effects)

咱們來看React是如何應用這些做用的,commitRoot方法,用於應用這些做用,有三個子方法組成:

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

每一個子方法都會用循環來迭代做用列表,並檢查其中做用類型,當找到有關方面目的的做用時,就應用它。在咱們的例子中,它會調用**ClickCounter組件的componentDidUpdate生命週期方法,以及更新span**元素上的文本內容。

第一個放commitBeforeMutationLifeCycles尋找**Snapshot做用,且調用getSnapshotBeforeUpdate方法,可是,由於咱們在ClickCounter組件中沒有實現這個方法,因此React不會在render**階段添加這個做用,因此在咱們的例子中,這個方法啥也沒作。

DOM更新

接着,React執行到commitAllHostEffects方法,這裏,React就會把**span元素的文本內容從0修改到1,而ClickCounter**fiber上啥也不作,由於類組件的節點沒有任何DOM更新。

這個方法大體是,選擇正確做用類型,並應用相關的操做。在個人例子中,咱們須要更新**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的負載(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更新被應用以後,React把**finishedWork賦值給HostRoot**,即把替換(alternate)樹設置爲當前樹:

root.current = finishedWork;

調用後置突變生命週期鉤子

最後一個方法是commitAllLifecycles方法,這裏React會調用後置突變生命週期方法。在**render階段,React在ClickCounter組件上添加Update做用,這是commitAllLifecycles方法尋找的做用之一,而後調用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**方法。

額……講完啦。

相關文章
相關標籤/搜索