這裏主要介紹本身在React
開發中的一些總結,關於react
的渲染問題的一點研究。javascript
另外本人一直但願在React
項目中嘗試使用,所以在以前已經介紹過immutable
的API,能夠參看這裏Immutable平常操做之深刻API,算是對其的一個補充。css
本文全部代碼請參看github
倉庫:https://github.com/Rynxiao/immutable-reacthtml
這個例子主要是寫了同時渲染1000個房間,若是我添加一個房間或者修改一個房間,在react
中不一樣的實現方式下render
函數將會表現出什麼樣的結果?以及針對不一樣結果的一些思考和優化。大體的列表例子以下:生成1000個不一樣的房間盒子,顏色隨機。java
項目總體目錄結構大體是這樣的:react
下面主要來看ListDetail.js
中是如何寫的:webpack
List
RoomDetail
,子組件的功能只是純粹的渲染功能,自身並無任何操做子組件:css3
// 子組件 class RoomDetail extends React.Component { constructor(props) { super(props); } render() { let room = this.props.room; return ( <li className="list-item" style={{ backgroundColor: room.backgroundColor }}> { room.number } </li> ); } }
父組件:git
// 父組件 export default class List extends React.Component { // ... constructor(props) { super(props); this.addRoom = this.addRoom.bind(this); this.modifyRoom = this.modifyRoom.bind(this); this.state = { roomList: this.generateRooms(), newRoom: 0 }; } // ... render() { return ( <div> <h2 className="title">React的列表渲染問題</h2> <div><a className="back" href="#/">返回首頁</a></div> <div className="btn-operator"> <button onClick={ this.addRoom }>Add A Room</button> <button onClick={ this.modifyRoom }>Modify A Room</button> </div> <ul className="list-wrapper"> { this.state.roomList.map((room, index) => { return <RoomDetail key={ `roomDetail${index}` } room={ room } /> }) } </ul> </div> ); } }
下面咱們來添加一個房間試試github
// 添加房間 addRoom() { let newRoom = { number: `newRoom${++this.state.newRoom}`, backgroundColor: '#f00' }; let newList = this.state.roomList; newList.push(newRoom); this.setState({ roomList: newList }); }
這個操做主要是生成一個新的房間,而後從state
中取出當前的房間列表,而後再當前的房間列表中添加一個新的房間,最後將整個列表重新設置到狀態中。web
很顯然,此時因爲父組件的狀態發生了變化,會引發自身的render
函數執行,同時列表開始從新遍歷,而後將每個房間信息從新傳入到子組件中。是的,從新傳入,就表明了子組件將會從新渲染。咱們能夠來作一個測試,在子組件的render
方法中加入以下打印:
render() { let room = this.props.room; console.log(`.No${room.number}`); return ( // ... ); }
不出意外的發現了全部的子組件渲染的證據:
同時利用chorme
的Performance
檢測的信息以下:
調用的方法堆棧以下:
渲染子組件的時間達到764ms
,同時在堆棧中能夠看到大量的receiveComponent
和updateChildren
方法的執行。那麼有沒有什麼辦法只渲染改變的部分呢?在react官網性能監控這一小節中有提到一個方法,將子組件繼承React.PureComponent
能夠局部有效防止渲染。加上以後的代碼是這樣的:
class RoomDetail extends React.PureComponent { // ... }
全部的東西都沒有變化,只是將Component
換成了PureComponent
。下面咱們再來測試一下:
性能檢測圖以下:
效果出奇的好,果真只是渲染了一次,而且速度提高了10幾倍之多。
其中的原理是在組件的shouldComponentUpdate
方法中進行了props
與state
的比較,若是認爲他們相等,則會返回false
,不然則會返回true
。
// react/lib/ReactComponentWithPureRenderMixin.js var ReactComponentWithPureRenderMixin = { shouldComponentUpdate: function (nextProps, nextState) { return shallowCompare(this, nextProps, nextState); } };
同時官網也說了,這只是局部有效,爲何呢?由於這些值的比較都只是淺比較,也就是隻是第一層的比較。那麼會出現什麼問題,咱們來看下面的操做:
修改其中的一個房間:
// 修改房間 modifyRoom() { let newList2 = this.state.roomList; newList2[0] = { number: 'HAHA111', backgroundColor: '#0f0' }; this.setState({ roomList: newList2 }); }
很意外,當我添加了一個房間以後,發現第一個房間並無咱們想象中的發生變化。爲何?
緣由是我雖然修改了第一個房間的數據,當時我並無修改他的引用地址。相似下面這樣的:
var arr = [{ a: 1 }, { b: 2 }]; var arr2 = arr1; arr2[0] = { c: 1 }; arr === arr2; // true
所以在子組件中比較房間的時候,就會出現比較的值相等的狀況,此時將會返回false
那麼有沒有辦法改變這個問題,我找到了兩個辦法:
從數據源頭入手,即爲改造數據,將數據進行深拷貝,使得原先的引用與新獲得的對象的引用不相同便可。關於深拷貝的實現方法有不少,我這裏貼一個,以後再仔細作研究。
// 這個函數能夠深拷貝 對象和數組 var deepCopy = function(obj){ var str, newobj = obj.constructor === Array ? [] : {}; if(typeof obj !== 'object'){ return; } else if(window.JSON){ str = JSON.stringify(obj), //系列化對象 newobj = JSON.parse(str); //還原 } else { for(var i in obj){ newobj[i] = typeof obj[i] === 'object' ? cloneObj(obj[i]) : obj[i]; } } return newobj; };
在ES6中提供了一種解構方式,這種方式也能夠實現數組的深層次拷貝。相似這樣的
let arr = [1, 2, 3, 4]; let arr1 = [...arr]; arr1 === arr; // false // caution let arr = [{ a: 1 }, { b: 2 }]; let arr1 = [...arr]; arr1 === arr; // false arr1[0] = { c: 3 }; arr1[0] === arr[0]; // false arr1[1] === arr[1]; // true
所以我把modifyRoom
函數進行了如此改造:
// 修改房間 modifyRoom() { let newList2 = [...this.state.roomList]; newList2[0] = { number: 'HAHA111', backgroundColor: '#0f0' }; this.setState({ roomList: newList2 }); }
所以在比較第一個對象的時候,發現它們已經不相等了,則會從新渲染。
從子組件是否渲染條件入手,能夠不須要使用React.PureComponent
,而直接在shouldComponentUpdate
方法入手。由於兩次值改變以後,我清楚得能夠知道,改變的值只是第一個對象中的數值改變。那麼我能夠這麼寫來判斷:
class RoomDetail extends React.Component { constructor(props) { super(props); } shouldComponentUpdate(nextProps, nextState) { if (nextProps.room.number === this.props.room.number) { return false; } return true; } render() { let room = this.props.room; return ( <li className="list-item" style={{ backgroundColor: room.backgroundColor }}> { room.number } </li> ); } }
一樣得能夠達到效果。可是若是在shouldComponentUpdate
中存在着多個props
和state
中值改變的話,就會使得比較變得十分複雜。
在官網上來講,immutable
提供的數據具備不變性,被稱做爲Persistent data structure
,又或者是functional data structure
,很是適用於函數式編程,相同的輸入總會預期到相同的輸出。
在immutable
官網以及在知乎中談到爲何要使用immutable
的時候,會看到一個關鍵詞efficient
。高效地,在知乎上看到說是性能十分好。在對象深複製、深比較上對比與Javascript
的普通的深複製與比較上來講更加地節省空間、提高效率。我在這裏作出一個實驗(這裏我並不保證明驗的準確性,只是爲了驗證一下這個說法而已)。
實驗方法:我這裏會生成一個對象,對象有一個廣度與深度,廣度表明第一層對象中有多少個鍵值,深度表明每個鍵值對應的值會有多少層。相似這樣的:
{ "width0": {"key3": {"key2": {"key1": {"key0":"val0"}}}}, "width1": {"key3": {"key2": {"key1": {"key0":"val0"}}}}, "width2": {"key3": {"key2": {"key1": {"key0":"val0"}}}}, // ... "widthN": {"key3": {"key2": {"key1": {"key0":"val0"}}}} }
所以實際上在javascript
對象的複製和比較上,須要遍歷的次數實際上是width * deep
。
在複製的問題上,我作了三種比較。
最終獲得的數據爲:
deepCopy( μs ) | JSON( μs ) | Immutable( μs ) | |
---|---|---|---|
20 * 50 | 4000 | 9000 | 20 |
20 * 500 | 8000 | 10000 | 20 |
20 * 5000 | 10000 | 14000 | 20 |
在比較上,我只比較了兩種方式:
代碼以下:
let startTime1 = new Date().getTime(); let result1 = Equals.equalsObject(gObj, deepCopyObj); let endTime1 = new Date().getTime(); console.log(result1); console.log(`deep equal time ${(endTime1-startTime1)*1000}μs`); let startTime2 = new Date().getTime(); let result2 = is(this.state.immutableObj, this.state.aIObj); let endTime2 = new Date().getTime(); console.log(result2); console.log(`immutable equal time ${(endTime2-startTime2)*1000}μs`);
最終獲得的數據爲:
deepCompare( μs ) | Immutable.is( μs ) | |
---|---|---|
20 * 5 | 0 | 7000 |
20 * 50 | 1000 | 27000 |
20 * 500 | 6000 | 24000 |
20 * 5000 | 84000 | 5000 |
數據的設計上可能太過單一,沒有涉及到複雜的數據,好比說對象中再次嵌套數組,而且在每個鍵值對應的值得廣度上設計得也太過單一,只是一條直線下來。可是當數據量達到必定的程度時,其實也說明了一些問題。
總結:
Immutable
能夠說是零消耗Immutable.is()
所用的時間會更少Immutable
並快不了多少固然只是測試,平時中的縱向嵌套達到三層以上都會認爲是比較恐怖的了。
因而我去google
翻了翻,看看有沒有什麼更好的demo
,下面我摘錄一些話。
What is the benefit of immutable.js?
Immutable.js makes sure that the "state" is not mutated outside of say redux. For smaller projects, personally i don't think it is worth it but for bigger projects with more developers, using the same set of API to create new state in reduce is quite a good idea
It was mentioned many times before that Immutable.js has some internal optimizations, such as storing lists as more complex tree structures which give better performance when searching for elements. It's also often pointed out that using Immutable.js enforces immutability, whereas using Object.assign or object spread and destructuring assignments relies to developers to take care of immutability. EDIT: I haven't yet seen a good benchmark of Immutable.js vs no-library immutability. If someone knows of one please share. Sharing is caring :)
Immutable.js adds two things: Code enforcement: by disallowing mutations, you avoid strange errors in redux and react. Code is substantially easier to reason about. Performance: Mutation operations for larger objects are substantially faster as the internals are a tree structure that does not have to copy the entirety of an object every assignment. In conclusion: it's a no brainer for decently scoped applications; but for playing around it's not necessary.
https://github.com/reactjs/redux/issues/1262
yes, obviously mutable is the fastest but it won't work with how redux expects the data, which is immutable
Performance Tweaking in React.js using Immutable.js
But wait… This is can get really ugly really fast. I can think of two general cases where your
shouldComponentUpdate
can get out of hand.// Too many props and state to check! shouldComponentUpdate(nextProps, nextState) { return ( this.props.message !== nextProps.message || this.props.firstName !== nextProps.firstName || this.props.lastName !== nextProps.lastName || this.props.avatar !== nextProps.avatar || this.props.address !== nextProps.address || this.state.componentReady !== nextState.componentReady // etc... ); }
是的,我並無得出Immutable
在性能上必定會很快的真實數據。可是不得不提到的是他在配合Redux
使用的時候的一個自然優點——數據是不變的。而且在最後一個連接中也提到,在配合React
使用中經過控制shouldComponentUpdate
來達到優化項目的目的。
however,Let's write some examples about immutable used in react to make sense.
在父組件中的改變:
constructor(props) { super(props); this.addRoom = this.addRoom.bind(this); this.modifyRoom = this.modifyRoom.bind(this); this.state = { // roomList: this.generateRooms() roomList: fromJS(this.generateRooms()), newRoom: 0 }; } addRoom() { // let newRoom = { number: `newRoom${++this.state.newRoom}`, backgroundColor: '#f00' }; // let newList = this.state.roomList; // newList.push(newRoom); let newRoom = Map({ number: `newRoom${++this.state.newRoom}`, backgroundColor: '#f00' }); let newList = this.state.roomList.push(newRoom); this.setState({ roomList: newList }); } modifyRoom() { // let newList = [...this.state.roomList]; // newList[0] = { number: 'HAHA111', backgroundColor: '#0f0' }; let list = this.state.roomList; let newList = list.update(0, () => { return Map({ number: 'HAHA111', backgroundColor: '#0f0' }); }); this.setState({ roomList: newList }); }
子組件中:
shouldComponentUpdate(nextProps, nextState) { return !is(formJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state), fromJS(nextState)); }
將數據源用Immutable
初始化以後,以後再進行的數據改變都只要遵照ImmutableJS
的相關API便可,就能夠保證數據的純淨性,每次返回的都是新的數據。與源數據的比較上就不可能會存在改變源數據相關部分以後,因爲引用相等而致使數據不相等的問題。
我在項目底下新建了一個項目目錄redux-src
,同時在項目中增長了熱更新。新建了webpack.config.redux.js
,專門用來處理新加的redux
模塊。具體代碼能夠上github
上面去看。所以新的目錄結構以下:
webpack.config.redux.js
文件以下:
'use strict'; var webpack = require("webpack"); var ExtractTextPlugin = require("extract-text-webpack-plugin"); //css單獨打包 module.exports = { devtool: 'eval-source-map', entry: [ __dirname + '/redux-src/entry.js', //惟一入口文件 "webpack-dev-server/client?http://localhost:8888", "webpack/hot/dev-server" ], output: { path: __dirname + '/build', //打包後的文件存放的地方 filename: 'bundle.js', //打包後輸出文件的文件名 publicPath: '/build/' }, module: { loaders: [ { test: /\.js$/, loader: "react-hot!jsx!babel", include: /src/}, { test: /\.css$/, loader: ExtractTextPlugin.extract("style", "css!postcss")}, { test: /\.scss$/, loader: ExtractTextPlugin.extract("style", "css!postcss!sass")}, { test: /\.(png|jpg)$/, loader: 'url?limit=8192'} ] }, postcss: [ require('autoprefixer') //調用autoprefixer插件,css3自動補全 ], plugins: [ new ExtractTextPlugin('main.css'), new webpack.HotModuleReplacementPlugin() ] }
在項目中運行npm run redux
,在瀏覽器輸入localhost:8888
便可看到最新的模塊。
這裏關於如何在react
中使用redux
,這裏就很少說了,若是不明白,能夠去看 http://cn.redux.js.org/或者到我以前寫的 redux的一個小demo中去看。
重點說說如何在reducer
中使用Immutable
,以及在List.js
中如何經過發送Action
來改變store
。
import { fromJS } from 'immutable'; import { combineReducers } from 'redux'; import { ADD_ROOM, MODIFY_ROOM, MODIFY_NEWROOM_NUM } from '../const'; import { addRoom, modifyRoom, modifyNewRoomNum } from '../actions'; // ... generateRooms() const initialState = fromJS({ roomList: generateRooms(), newRoom: 0 }); function rooms(state = initialState, action) { switch(action.type) { case ADD_ROOM: return state.updateIn(['roomList'], list => list.push(action.room)); case MODIFY_ROOM: return state.updateIn(['roomList', 0], room => action.room); case MODIFY_NEWROOM_NUM: return state.updateIn(['newRoom'], num => ++num); default: return state; } } export default combineReducers({ rooms });
跟以前List.js
中的state
中聲明的最開始狀態同樣。這裏依舊維持一個最開始的房間列表以及一個新增房間的序號數。只不過這裏的最初狀態是經過Immutable.js
處理過的,因此在reducer
中的全部操做都必須按照其API
來。
其實這個文件也沒有做多處修改,基本能夠看引入了immutable
的state
管理的Detail.js
。只是在操做上顯得更加簡單了。
addRoom() { let { newRoom, onAddRoom, onModifyRoomNum } = this.props; let room = Map({ number: `newRoom${newRoom}`, backgroundColor: '#f00' }); onAddRoom(room); onModifyRoomNum(); } modifyRoom() { let { onModifyRoom } = this.props; let room = Map({ number: 'HAHA111', backgroundColor: '#0f0' }); onModifyRoom(room); }
運用Redux-DevTools
工具能夠清楚地看出當前redux
中的數據變化,以及操做。
日誌模式:
監控模式:
運用redux
的好處就是全局數據可控。在redux
中運用immutable data
也是redux
所提倡的,咱們再也不會由於值沒有深拷貝而找不到值在何處什麼時候發生了變化的狀況,接而引起的就是組件莫名其妙地不會re-render
,同時因爲immutable.js
在值複製上的高效性,所以在性能上來講,會比用傳統javascript
中的深拷貝上來講提高會不少。