React原始碼分析 - 元件更新與事務

_zon_發表於2018-02-25

在React中,元件的更新本質上都是由setState操作改變state引起的。因此元件更新的入口在於setState,同樣經過擼原始碼和打斷點分析畫了以下的元件更新的流程圖:

default

setState的定義在元件mountComponent的時候定義:

inst = new Component(publicProps, publicContext, ReactUpdateQueue);
複製程式碼
function ReactComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

ReactComponent.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};
複製程式碼

所以setState的真正的定義在 ReactUpdateQueue.js

enqueueSetState: function (publicInstance, partialState) {
  var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
  if (!internalInstance) {
    return;
  }
  var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
  // 將state新增到對應的component的_pendingStateQueue陣列中。
  queue.push(partialState);
  enqueueUpdate(internalInstance);
}

enqueueCallback: function (publicInstance, callback, callerName) {
  var internalInstance = getInternalInstanceReadyForUpdate(publicInstance);
  if (!internalInstance) {
    return null;
  }
  // 將callback新增到對應的component的_pendingCallbacks陣列中。
  if (internalInstance._pendingCallbacks) {
    internalInstance._pendingCallbacks.push(callback);
  } else {
    internalInstance._pendingCallbacks = [callback];
  }
  enqueueUpdate(internalInstance);
}
複製程式碼

兩個方法最後都呼叫enqueueUpdate:

function enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance);
}

function enqueueUpdate(component) {
  // 確認需要的事務是否注入了。
  ensureInjected();
  // batchingStrategy.isBatchingUpdates為false的時候,
  // 或者說當不處於批量更新的時候,用事務的方式批量的進行component的更新。
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // 當處於批量更新階段時,不進行state的更新操作,而是將需要更新的component新增到dirtyComponents陣列中
  dirtyComponents.push(component);
}
複製程式碼

這裡需要注意enqueueUpdate中根據batchingStrategy.isBatchingUpdates分別進入不同的流程,當isBatchingUpdates為true的時候表示已經處於批量更新的過程中了,這時候會將所有的有改動的元件push到dirtyComponents中。當isBatchingUpdates為false的時候會執行更新操作,這裡先認為當isBatchingUpdates為false的時候進行的操作是更新元件,實際上的過程是更復雜的,稍後馬上解釋具體的過程。這裡我們先理解下React中的事務的概念,事務的概念根據原始碼中的註釋就可以非常清楚的瞭解了:

 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
複製程式碼

簡單來說當使用transaction.perform執行方法method的時候會按順序先執行WRAPPER裡面的initialize方法然後執行method最後再執行close方法。

React提供了基礎的事務物件Transaction,不同的事務的區別就在於initialize和close方法的不同,這個可以通過定義getTransactionWrappers方法來傳入WRAPPER陣列,具體的用法看下原始碼就好了,不過實際使用中是不會要自己去定義事務的,當然要的話也阻止不了~。

回到enqueueUpdate,其呼叫的batchingStrategy.batchedUpdates方法在ReactDefaultBatchingStrategy 中定義了:

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,
  batchedUpdates: function (callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    if (alreadyBatchingUpdates) {
      callback(a, b, c, d, e);
    } else {
      transaction.perform(callback, null, a, b, c, d, e);
    }
  }
};
複製程式碼

可以看到isBatchingUpdates的初始值是false的,在呼叫batchedUpdates方法的時候會將isBatchingUpdates變數設定為true。然後根據設定之前的isBatchingUpdates的值來執行不同的流程。

對於enqueueUpdate的效果就是,當執行enqueueUpdate的時候如果isBatchingUpdates為true的話(已經處於批量執行操作),則不會進行更新操作,而是將改動的component新增到dirtyComponents陣列中;如果isBatchingUpdates為false的話,會執行batchedUpdates將isBatchingUpdates置為true然後呼叫enqueueUpdate方法,這個時候會用事務的方式來執行enqueueUpdate。

根據流程圖可以知道,事務ReactDefaultBatchingStrategyTransaction的initialize是foo沒有任務操作,接著會執行method即:將改動的元件push到dirtyComponent中,最後執行close方法執行flushBatchedUpdate方法再把isBatchingUpdates重置為false。在flushBatchedUpdates方法中事務執行runBatchedUpdates方法將dirtyComponent中的component依次(先父元件在子元件的順序)進行更新操作。這裡具體的更新的過程看流程圖就可以理解了,需要注意的是updateChildren方法這個方法是virtual DOM的Diff演算法的核心程式碼,作用就是根據更新前後元件的不同進行有效的更新,具體的部分,之後單獨的文章再介紹。

在更新的過程中需要注意的一個方法是_processPendingState方法:

_processPendingState: function (props, context) {
   var inst = this._instance;
   var queue = this._pendingStateQueue;
   var replace = this._pendingReplaceState;
   this._pendingReplaceState = false;
   this._pendingStateQueue = null;

   if (!queue) {
     return inst.state;
   }

   if (replace && queue.length === 1) {
     return queue[0];
   }

   var nextState = _assign({}, replace ? queue[0] : inst.state);
   for (var i = replace ? 1 : 0; i < queue.length; i++) {
     var partial = queue[i];
     _assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
   }

   return nextState;
 }
複製程式碼

可以看到當setState傳入的是函式的時候,函式被呼叫的時候的傳入的引數是merge了已經遍歷的queue的state的nextState,如果傳入的不是函式則直接merge state至nextState。這也解釋了,為什麼用回撥函式的形式使用setState的時候可以解決state是按照順序最新的state了。

從流程圖可以看到在保證元件更新完畢後會將setState中傳入的callback按照順序依次push到事務的callback queue佇列中,在事務結束的時候close方法中notifyAll就是執行這些callbacks,這樣保證了回撥函式是在元件完全更新完成後執行的,也就是setState的回撥函式傳入的state是更新後的state的原因。

在瞭解了以上的元件更新的流程後,可以看一個場景,栗子如下:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));

import React, { Component } from 'react';
import Hello from './Hello';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      appText: 'hello App',
      helloText: 'heiheihei'
    };
  }

  handleAppClick = () => {
    console.log('App is clicked ~');
    this.setState({
      appText: 'App is clicked ~'
    });
  }

  render() {
    const { appText, helloText } = this.state;
    console.log('render App');
    return (
      <div className="app-container">
        <div          
          onClick={this.handleAppClick}
        >{appText}</div>
        <Hello
          text={helloText}
          handleAppClick={this.handleAppClick}
        />
      </div>
    );
  }
}

export default App;

import React, { Component } from 'react';

class Hello extends Component {
    constructor(props) {
        super(props);
        this.state = {
            text: 'hello Hello'
        };
    }

    componentWillReceiveProps(nextProps) {
        this.setState({
          text: nextProps.text + '~'
        });
    }

    handleClick = () => {
        this.setState({
            text: 'Hello is clicked ~'
        });
        this.props.handleAppClick();
    }

    render() {
        const { text } = this.state;
        console.log('render Hello');
        return (
            <div>
                <div
                    onClick={this.handleClick}
                    style={{ color: '#e00' }}
                >{text}</div>
            </div>
        );
    }
}

export default Hello;
複製程式碼

點選

hello Hello
複製程式碼

後元件的渲染如下,可以看到父元件到子元件按順序更新了一次:

render App
render Hello
複製程式碼

而不是:

render Hello
render App
render Hello
複製程式碼

example

批量更新的時候元件的順序由:

dirtyComponents.sort(mountOrderComparator);
複製程式碼

處理的。

到這裡你需要知道這個結果產生的原因在於不是隻有setState的呼叫棧會改變isBatchingUpdates的值

回顧《React事件機制》的流程圖可以知道事件的統一回撥函式dispatchEvent呼叫了ReactUpdates.batchedUpdates用事務的方式進行事件的處理,也就是說點選事件的處理本身就是在一個大的事務中,在setState執行的時候isBatchingUpdates已經是true了,setState做的就是將更新都統一push到dirtyComponents陣列中,在事務結束的時候按照上述的流程進行批量更新,然後將批量執行關閉結束事務。

事務的機制在這裡也保證了React更新的效率,此外在更新元件的時候的virtual DOM的Diff演算法也起到很大的作用,這個在後續的文章再介紹。

參考資料

相關文章