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

在個人上篇文章 Inside Fiber: 深刻了解React新協調算法中介紹了理解更新過程細節的所需的基礎知識,我將在本文中描述這個更新過程。html

我已經概述了將在本文中使用的主要數據結構和概念,特別是Fiber節點,currentwork-in-progress樹,反作用(side-effects)以及effects鏈表(effects list)。我也提供了主要算法的高級概述和render階段與commit階段的差別。若是你尚未閱讀過它,我推薦你從那兒開始。react

我還向你介紹了帶有一個按鈕的示例程序,這個按鈕的功能就是簡單的增長數字。git

你能夠在這查看在線代碼。它的實現很簡單,就是一個render函數中返回buttonspan元素的類組件。當你點擊按鈕的時候,在點擊事件的處理函數中更新組件的state。結果就是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如何添加effects並在commit階段調用這個方法。在本文中,我想向你展現React是如何處理狀態更新和建立effects list的。咱們能夠看到render階段和commit階段的高級函數中發生了什麼。算法

尤爲是在React的completeWork函數中:數組

  • 更新ClickCounterstate中的count屬性
  • 調用render方法獲取子元素列表並比較
  • 更新span元素的props

以及,在React的commitRoot 函數中:瀏覽器

  • 更新span元素的文本內容屬性
  • 調用componentDidUpdate生命週期方法

可是在那以前,咱們先快速看看當咱們在點擊處理函數中調用setState時工做是如何調度的。安全

請注意,你無需瞭解這些來使用React。本文是關於React內部是如何運做的。markdown

調度更新

當咱們點擊按鈕時,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函數。

由於這個函數對樹中每一個節點執行,因此若是你想調試render階段,它是放置斷點的好地方。 我常常這樣作,還有檢查Fiber節點的type來肯定我須要的節點。

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生命週期鉤子

儘管調用componentDidUpdate的effect是在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;
}
複製代碼

上面代碼片斷中我刪除了一些輔助代碼。對於實例,調用生命週期方法或添加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算法的地方。文檔中對此有高級概述。如下是相關部分:

當比較兩個相同類型的React DOM元素時,React查看二者的屬性(attributes),保留DOM節點,僅更新變化了的屬性。

然而,若是咱們深刻挖掘,會知道它實際是對比Fiber節點和React元素。可是我如今不會詳細介紹由於過程至關複雜。我會單獨些篇文章,特別關注子協調過程。

若是你想本身學習細節,請查看reconcileChildrenArray函數,由於在咱們的程序中render方法返回一個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樹看起來的樣子:

這是由有反作用的節點組成的線性列表:

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方法的函數。

相關文章
相關標籤/搜索