高級性能

2019-08-14 14:28 更新

人們首先會考慮的是 React 是能否和其他非 React 版本一樣能快速和響應(yīng)一個項目。重新繪制組件的整個子樹來回應(yīng)每一個狀態(tài)變化的想法讓人懷疑是否這個過程中對性能產(chǎn)生負(fù)面影響。React 使用幾個巧妙的技巧,以減少所需的更新用戶界面所需要的昂貴的文檔用戶模型操作的數(shù)目。

避免調(diào)和文檔對象模型

React 使用了一個虛擬的 DOM,這是的瀏覽器中對于 DOM 樹呈現(xiàn)的一個描述符。這種并行表示形式讓 React 避免產(chǎn)生 DOM 節(jié)點和訪問現(xiàn)有的節(jié)點,比 JavaScript 對象的操作速度較慢。當(dāng)一個組件的道具或狀態(tài)改變,React 決定通過構(gòu)建一個新的虛擬 DOM 來進(jìn)行實際的 DOM 更新和舊的 DOM 相比是否必要。只有在比較結(jié)果不一樣的情況下,Reac t盡可能少的應(yīng)用轉(zhuǎn)變來融合文檔對象模型。

在此之上,React 提供了一個組件的生命周期功能,shouldComponentUpdate,這是在重新繪制過程開始之前觸發(fā)(虛擬 DOM 比較,可能最終調(diào)和 DOM),使開發(fā)人員能夠減少過程中的循環(huán)步驟。這個函數(shù)的默認(rèn)實現(xiàn)返回 true,讓 React 來執(zhí)行更新:

shouldComponentUpdate: function(nextProps, nextState) {  return true;
}

請記住,React 將非常頻繁的調(diào)用這個函數(shù),所以實現(xiàn)必須要快。 比如說你有一個由幾個聊天線程組成的消息應(yīng)用程序。假設(shè)只有一個線程已經(jīng)改變。如果我們在 ChatThread 上執(zhí)行 shouldComponentUpdate,React 可以為其他線程跳過描繪步驟:

shouldComponentUpdate: function(nextProps, nextState) {  // TODO: return whether or not current chat thread is
  // different to former one.}

因此,簡言之,React 避免調(diào)和 DOM 產(chǎn)生的復(fù)雜的 DOM 操作,允許用戶使用 shouldComponentUpdate 縮短過程中的循環(huán)步驟,而且,對于那些需要更新,通過對比虛擬的 DOM 來實現(xiàn)。

起作用的 shouldComponentUpdate

下面是組件的子樹。對于每一個組件表示 shouldComponentUpdate 的返回值,以及是否與虛擬的 DOM 是等價的。最后,圓的顏色指示組件是否必須調(diào)和。

should-component-update

在上面的例子中,由于 shouldComponentUpdate 返回值為 false,存在 C2 中,React 沒有必要產(chǎn)生新的虛擬的 DOM,并且因此,也不需要調(diào)和 DOM。需要注意的是 react 甚至沒有在 C4 和 C5 處調(diào)用 shouldComponentUpdate。

對于 C1 和 C3 shouldComponentUpdate 返回 true,所以 React 不得不深入到葉子節(jié)點并且進(jìn)行檢查。

對于 C6 它返回 true;由于和虛擬的 DOM 并不等同,它不得不調(diào)和 DOM。最后一個有趣的例子是 C8。此節(jié)點的 React 必須計算虛擬 DOM,但因為它和原來的 DOM 相同,它不需要調(diào)和 DOM。

請注意,只有 C6 需要 React 不得不對 DOM 做轉(zhuǎn)變,這是不可避免的。對于 C8 通過比較虛擬的 DOM 他不需要轉(zhuǎn)變,但是對 C2 的子樹和 C7來說,它甚至沒有計算虛擬 DOM,只需要通過執(zhí)行 shouldComponentUpdate

所以,我們應(yīng)該如何執(zhí)行 shouldComponentUpdate 呢?比如現(xiàn)有一個僅需呈現(xiàn)一個字符串值的組件:

React.createClass({
  propsTypes: {    value: React.PropTypes.string.isRequired
  },
  render: function() {    return <div>this.props.value</div>;
  }
});

我們可以很容易地實現(xiàn) shouldComponentUpdate,如下:

shouldComponentUpdate: function(nextProps, nextState) {  return this.props.value !== nextProps.value;
}

到目前為止,React 處理這類簡單的道具/態(tài)結(jié)構(gòu)非常簡單。我們甚至可以泛化一個基于淺層相等實現(xiàn),并混合到組件上。事實上,React 已經(jīng)提供了這樣實現(xiàn):PureRenderMixin。

但如果你的組件的道具或狀態(tài)是可變的數(shù)據(jù)結(jié)構(gòu)呢?比如說組件接受的道具,而不是像'bar'的這樣的字符串,而是一個是包含,如,{FOO:'bar'}這樣一個字符串的 JavaScript 對象:

React.createClass({
  propsTypes: {    value: React.PropTypes.object.isRequired
  },
  render: function() {    return <div>this.props.value.foo</div>;
  }
});

我們之前的 shouldComponentUpdate 實現(xiàn)總是不會如我們預(yù)期一樣的實現(xiàn):

/ assume this.props.value is { foo: 'bar' }// assume nextProps.value is { foo: 'bar' },// but this reference is different to this.props.valuethis.props.value !== nextProps.value; // true

問題是當(dāng)?shù)谰邔嶋H上并沒有改變時,shouldComponentUpdate 將返回 true。為了解決這個問題,我們可以用這個替代的試行方案:

shouldComponentUpdate: function(nextProps, nextState) {  return this.props.value.foo !== nextProps.value.foo;
}

基本上,我們?yōu)榇_保我們正確地跟蹤變化,最后做了深刻的對比。這種做法是在性能方面相當(dāng)昂貴和復(fù)雜的,它并不能擴(kuò)展,因為我們要為每個模型寫不同的深度相等代碼。最重要的是,如果我們不小心管理對象的引用,它甚至可能沒有工作。比如說母節(jié)點組件的引用:

React.createClass({
  getInitialState: function() {    return { value: { foo: 'bar' } };
  },
  onClick: function() {    var value = this.state.value;    value.foo += 'bar'; // ANTI-PATTERN!
    this.setState({ value: value });
  },
  render: function() {    return (
      <div>
        <InnerComponent value={this.state.value} />
        <a onClick={this.onClick}>Click me</a>
      </div>
    );
  }
});

在第一時間內(nèi)部組件得到呈現(xiàn)是 {FOO:'bar'},它將作為道具的值。如果用戶點擊,母組件的狀態(tài)將得到更新為{value:{FOO:'barbar'}},引發(fā)了內(nèi)部部件在過程中重新呈現(xiàn),將接收 {foo:“barbar'} 為道具的新值。

問題是,由于母體和內(nèi)部部件共享一個參考同一個對象,當(dāng)對象在第二行的 onClick 功能函數(shù)中突變時,道具的內(nèi)部部件具有將發(fā)生變化。這樣,當(dāng)再描繪處理開始時,shouldComponentUpdate被調(diào)用,this.props.value.foo 將等于nextProps.value.foo,因為事實上,this.props.value 和 nextProps.value引用相同的對象。

因此,由于我們錯過了道具和縮短步驟重新渲染過程中的變化,用戶界面將不會得到從 'bar' 到 'barbar' 的更新。

Immutable-JS 救援

Immutable-JS 是 Lee Byron 寫的腳本語言集合庫,其中 Facebook 最近開源 Javascript 的集合庫。它提供了通過結(jié)構(gòu)性共享一成不變持久化集合。讓我們看看這些性能:

  • 不可變的:一旦創(chuàng)建,集合不能在另一個時間點改變。

  • 持久性:新的集合可以由從早先的集合和突變結(jié)合創(chuàng)建。在創(chuàng)建新的集合后,原來集合仍然有效。

  • 結(jié)構(gòu)共享:使用新的集合創(chuàng)建為與對原始集合大致相同的結(jié)構(gòu),減少了拷貝的最低限度,以實現(xiàn)空間效率和可接受的性能。如果新的集合等于原始的集合,則通常會返回原來的集合。

不變性使得跟蹤更改方便;而變化將總是產(chǎn)生在新的對象,所以我們只需要檢查的已經(jīng)改變參考對象。例如,在這個 Javascript 代碼中:

var x = { foo: "bar" };var y = x;
y.foo = "baz";
x === y; // true

雖然 y 被修改了,但因為它是對相同對象 x 的引用,所以這個比較返回 true。然而,這段代碼可以用 immutable-JS 這樣寫:

var SomeRecord = Immutable.Record({ foo: null });var x = new SomeRecord({ foo: 'bar'  });var y = x.set('foo', 'baz');
x === y; // false

在這種情況下,由于 x 突變,當(dāng)一個新的引用被返回,我們可以安全地假設(shè) x 已經(jīng)改變。

另一種跟蹤變化的可能的方法是通過設(shè)置標(biāo)志來做 dirty 檢查。這種方法的一個問題是,它迫使你使用 setter 或者寫很多額外的代碼,或某種類工具。或者,你可以突變之前深入對象復(fù)制并深入比較,以確定是否有變化。這種方法的一個問題是 deepCopy 和 deepCompare 是耗費大且復(fù)雜的操作。

因此,不可變的數(shù)據(jù)結(jié)構(gòu)為您提供了一種廉價和更簡潔的方式來跟蹤對象的變化,這就是我們?yōu)榱藢崿F(xiàn)shouldComponentUpdate 所需要的。因此,如果我們利用 immutable-JS 提供的抽象特性來支持和聲明屬性,我們就可以使用 PureRenderMixin 并獲得 perf 的一個很好的推動。

Immutable-JS 和通量

如果你使用通量,你應(yīng)該開始使用 immutable-JS 寫你的庫??纯?a rel="nofollow" rel="external nofollow" target="_blank" target="_blank" style="box-sizing: border-box; font-family: Verdana, 'Lantinghei SC', 'Hiragino Sans GB', 'Microsoft Yahei', Helvetica, arial, 宋體, sans-serif; background-color: transparent; color: rgb(45, 133, 202); text-decoration: none;">完整的 API。

讓我們來看看使用不可變的數(shù)據(jù)結(jié)構(gòu)來模擬線程的一種可行的辦法。首先,我們需要為每一個我們正在嘗試模擬的實體定義一個 record 。記錄是不可變的容器,為一組特定的域保存值:

var User = Immutable.Record({
  id: undefined,
  name: undefined,
  email: undefined});var Message = Immutable.Record({
  timestamp: new Date(),
  sender: undefined,
  text: ''});

Record 函數(shù)接收的對象定義了對象的域和它們的默認(rèn)值。

消息可以使用以下兩個列表來跟蹤用戶和信息:

this.users = Immutable.List();this.messages = Immutable.List();

實現(xiàn)處理每個負(fù)載類型的功能應(yīng)該是相當(dāng)容易的。例如,當(dāng)庫看到負(fù)載正在顯示一個新的消息,我們只要創(chuàng)建一個新的記錄,并把它添加到消息列表中:

this.messages = this.messages.push(new Message({
  timestamp: payload.timestamp,
  sender: payload.sender,
  text: payload.text
});

請注意,由于數(shù)據(jù)結(jié)構(gòu)是不可改變的,我們需要把推送功能的結(jié)果分配到 this.messages 中。

在 React 中,如果我們也使用 immutable-JS 數(shù)據(jù)結(jié)構(gòu)來保存組件的狀態(tài)下,我們可以混合 PureRenderMixin 到所有的組件,并且縮短重新呈現(xiàn)的過程。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號