这里主要介绍本身在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
中的深拷贝上来讲提高会不少。