Immutable 操做在 React 中的實踐

做者簡介 Amy 螞蟻金服·數據體驗技術團隊react

最近在需求開發的過程當中,踩了不少由於更新引用數據可是頁面不從新渲染的坑,因此對這塊的內容總結了一下。git

在談及 Immutable 數據以前,咱們先來聊聊 React 組件是怎麼渲染更新的。github

React 組件的更新方式

state 的直接改變

React 組件的更新是由狀組件態改變引發,這裏的狀態通常指組件內的 state 對象,當某個組件的 state 發生改變時,組件在更新的時候將會經歷以下過程:算法

  • shouldComponentUpdate
  • componentWillUpdate
  • render()
  • componentDidUpdate

state 的更新通常是經過在組件內部執行 this.setState 操做, 可是 setState 是一個異步操做,它只是執行將要修改的狀態放在一個執行隊列中,React 會出於性能考慮,把多個 setState 的操做合併成一次進行執行。bash

props 的改變

除了 state 會致使組件更新外,外部傳進來的 props 也會使組件更新,可是這種是當子組件直接使用父組件的 props 來渲染, 例如:babel

render(){
	return <span>{this.props.text}</span>
}

複製代碼

當 props 更新時,子組件將會渲染更新,其運行順序以下:數據結構

  • componentWillReceiveProps (nextProps)
  • static getDerivedStateFromProps()
  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • getSnapshotBeforeUpdate()
  • componentDidUpdate

示例代碼 根據示例中的輸出顯示,React 組件的生命週期的運行順序能夠一目瞭然了。異步

state 的間接改變

還有一種就是將 props 轉換成 state 來渲染組件的,這時候若是 props 更新了,要使組件從新渲染,就須要在 componentWillReceiveProps 生命週期中將最新的 props 賦值給 state,例如:函數

class Example extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            text: props.text
        };
    }
    componentWillReceiveProps(nextProps) {
        this.setState({text: nextProps.text});
    }
    render() {
        return <div>{this.state.text}</div>
    }
}
複製代碼

這種狀況的更新也是 setState 的一種變種形式,只是 state 的來源不一樣。源碼分析

React 的組件更新過程

當某個 React 組件發生更新時(state 或者 props 發生改變),React 將會根據新的狀態構建一棵新的 Virtual DOM 樹,而後使用 diff 算法將這個 Virtual DOM 和 以前的 Virtual DOM 進行對比,若是不一樣則從新渲染。React 會在渲染以前會先調用 shouldComponentUpdate 這個函數是否須要從新渲染,整個鏈路的源碼分析可參照這裏,React 中 shouldComponentUpdate 函數的默認返回值是 true,因此組件中的任何一個位置發生改變了,組件中其餘不變的部分也會從新渲染。

當一個組件渲染的機構很簡單的時候,這種由於某個狀態改變引發整個組件改變的影響可能不大,可是當組件渲染很複雜的時候,好比一個不少節點的樹形組件,當更改某一個葉子節點的狀態時,整個樹形都會從新渲染,即便是那些狀態沒有更新的節點,這在某種程度上耗費了性能,致使整個組件的渲染和更新速度變慢,從而影響用戶體驗。

PureComponent 的淺比較

基於上面提到的性能問題,因此 React 又推出了 PureComponent, 和它有相似功能的是 PureRenderMixin 插件,PureRenderMixin 插件實現了 shouldComponentUpdate 方法, 該方法主要是執行了一次淺比較,代碼以下:

function shallowCompare(instance, nextProps, nextState) {
  return (
    !shallowEqual(instance.props, nextProps) ||
    !shallowEqual(instance.state, nextState)
  );
}
複製代碼

PureComponent 判斷是否須要更新的邏輯和 PureRenderMixin 插件同樣,源碼以下:

if (this._compositeType === CompositeTypes.PureClass) {
      shouldUpdate =
        !shallowEqual(prevProps, nextProps) ||
        !shallowEqual(inst.state, nextState);
 }

複製代碼

利用上述兩種方法雖然能夠避免沒有改變的元素髮生沒必要要的從新渲染,可是使用上面的這種淺比較仍是會帶來一些問題:

假如傳給某個組件的 props 的數據結構以下所示:

const data = {
   list: [{
      name: 'aaa',
      sex: 'man'
   },{
   	   name: 'bbb',
   	   sex: 'woman'
   }],
   status: true,
}

複製代碼

因爲上述的 data 數據是一個引用類型,當更改了其中的某一字段,並指望在改變以後組件能夠從新渲染的時候,發現使用 PureComponent 的時候,發現組件並無從新渲染,由於更改後的數據和修改前的數據使用的同一個內存,全部比較的結果永遠都是 false, 致使組件並無從新渲染。

解決問題的幾種方式

要解決上面這個問題,就要考慮怎麼實現更新後的引用數據和原數據指向的內存不一致,也就是使用Immutable數據,下面列舉本身總結的幾種方法;

使用 lodash 的深拷貝

這種方式的實現代碼以下:

import _ from "lodash";

 const data = {
	  list: [{
	    name: 'aaa',
	    sex: 'man'
	  }, {
	    name: 'bbb',
	    sex: 'woman'
	  }],
	  status: true,
 }
 const newData = _.cloneDeepWith(data);
 shallowEqual(data, newData) //false
 
 //更改其中的某個字段再比較
  newData.list[0].name = 'ccc';
  shallowEqual(data.list, newData.list)  //false

複製代碼

這種方式就是先深拷貝複雜類型,而後更改其中的某項值,這樣二者使用的是不一樣的引用地址,天然在比較的時候返回的就是 false,可是有一個缺點是這種深拷貝的實現會耗費不少內存。

使用 JSON.stringify()

這種方式至關於一種黑魔法了,使用方式以下:

const data = {
    list: [{
      name: 'aaa',
      sex: 'man'
    }, {
      name: 'bbb',
      sex: 'woman'
    }],
    status: true,
    c: function(){
      console.log('aaa')
    }
  }
 
 const newData = JSON.parse(JSON.stringify(data))
 shallowEqual(data, newData) //false
 
  //更改其中的某個字段再比較
  newData.list[0].name = 'ccc';
  shallowEqual(data.list, newData.list)  //false
複製代碼

這種方式其實就是深拷貝的一種變種形式,它的缺點除了和上面那種同樣以外,還有兩點就是若是你的對象裏有函數,函數沒法被拷貝下來,同時也沒法拷貝 copyObj 對象原型鏈上的屬性和方法

使用 Object 解構

Object 解構是 ES6 語法,先上一段代碼分析一下:

const data = {
    list: [{
      name: 'aaa',
      sex: 'man'
    }, {
      name: 'bbb',
      sex: 'woman'
    }],
    status: true,
  }
  
  const newData =  {...data};
  console.log(shallowEqual(data, newData));  //false
  
  console.log(shallowEqual(data, newData));  //true
  //添加一個字段
  newData.status = false;
  console.log(shallowEqual(data, newData));  //false
  //修改複雜類型的某個字段
  newData.list[0].name = 'abbb';
  console.log(shallowEqual(data, newData));  //true
複製代碼

經過上面的測試能夠發現: 當修改數據中的簡單類型的變量的時候,使用解構是能夠解決問題的,可是當修改其中的複雜類型的時候就不能檢測到(曾經踩過一個大坑)。

由於解構在通過 babel 編譯後是 Object.assign(), 可是它是一個淺拷貝,用圖來表示以下:

images | left

這種方式的缺點顯而易見了,對於複雜類型的數據沒法檢測到其更新。

使用第三方庫

業界提供了一些庫來解決這個問題,好比 immutability-helper , immutable 或者immutability-helper-x

immutability-helper

一個基於 Array 和 Object 操做的庫,就一個文件可是使用起來很方便。例如上面的例子就能夠寫成下面這種:

import update from 'immutability-helper';
    
    const data = {
    list: [{
      name: 'aaa',
      sex: 'man'
    }, {
      name: 'bbb',
      sex: 'woman'
    }],
    status: true,
  }
  
   const newData = update(data, { list: { 0: { name: { $set: "bbb" } } } });
   console.log(this.shallowEqual(data, newData));  //false

   //當只發生以下改變時
   const newData = update(data,{status:{$set: false}});
   console.log(this.shallowEqual(data, newData));  //false
   console.log(this.shallowEqual(data.list, newData.list));  //true
複製代碼

同時能夠發現當只改變 data 中的 status 字段時,比較先後二者的引用字段,發現是共享內存的,這在必定程度上節省了內存的消耗。並且 API 都是熟知的一些對 Array 和 Object 操做,比較容易上手。

immutable

相比於 immutability-helper, immutable 則要強大許多,可是與此同時,也增長了學習的成本,由於須要學習新的 API,因爲沒怎麼用過,在此再也不贅述,具體知識點可移步這裏

immutability-helper-x

最後推薦下另外一個開源庫immutability-helper-x,API更好用哦~能夠將

const newData = update(data, { list: { 0: { name: { $set: "bbb" } } } });
複製代碼

簡化爲可讀性更強的

const newData = update.$set(data, 'list.0.name', "bbb");
或者
const newData = update.$set(data, ['list', '0', 'name'], "bbb");
複製代碼

寫在最後

在 React 項目中,仍是最好使用 immutable 數據,這樣能夠避免不少渾然不知的 bug。 以上只是我的在實際開發中的一些總結和積累,若有闡述得不對的地方歡迎拍磚~

對咱們團隊感興趣的能夠關注專欄,關注github或者發送簡歷至'tao.qit####alibaba-inc.com'.replace('####', '@'),歡迎有志之士加入~

原文地址:github.com/ProtoTeam/b…

相關文章
相關標籤/搜索