本文作者:IMWeb 黄qiong 原文出处:IMWeb社区 未经同意,禁止转载
React渲染机制
前面这篇文章,我已经介绍了React的渲染机制.
简单来说,就是在页面一开始打开的时候,React会调用render函数构建一棵Dom树,在
state/props
发生改变的时候,render函数会被再次调用渲染出另外一棵virtula Dom
,接着,React会用对两棵树进行对比,计算出对DOM树的最少修改,然后批量改动。
找出优化点
注意这里面,如果可以在渲染virtual DOM之前就可以判断渲染结果不会有变化,那么可以直接不进行virtual DOM的渲染和比较,速度会更快。
shouldComponentUpdate
怎么可以做到呢?
这里需要用到React生命周期里的shouldComponentUpdate,当这个函数返回false的时候,DOM tree直接不需要重新渲染,从而节省大量的计算资源。 由于每个React组件的逻辑都有自己的特点,所以需要根据组件逻辑来定制shouldComponentUpdate函数的行为.
以官网的TodoList为例: http://cn.redux.js.org/docs/basics/ExampleTodoList.html
代码语言:javascript复制import React, { Component, PropTypes } from 'react'
export default class Todo extends Component {
render() {
return (
<li
onClick={this.props.onClick}
style={{
textDecoration: this.props.completed ? 'line-through' : 'none',
cursor: this.props.completed ? 'default' : 'pointer'
}}>
{this.props.text}
</li>
)
}
}
Todo.propTypes = {
onClick: PropTypes.func.isRequired,
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired
}
对于这个todo来说,只要completed
跟text
都没有发生改变,那么这个组件渲染的结果就不会发生改变,因此,shouldComponentUpdate
就可以这样写:
shouleComponentUpdate(nextProps, nextState) {
return (nextProps.completed !== this.props.completed) ||
(nextProps.text !== this.props.text)
}
两个问题
接下来就有两个问题需要我们思考了:
- 是不是所有的组件都需要这个
shouldComponentUpdate
- 每个需要
shouldComponentUpdate
的组件都需要自己写逻辑是不是太麻烦了
问题一:
关于这个问题,我在前一篇文章其实已经作答,使用React Pref
,或者why-did-you-update
都可以找到无需被重新渲染的组件,这个组件就是需要使用shouldComponetUpdate优化的组件。
问题二:
确实麻烦啊,能偷懒就偷懒的我们怎么能忍受。 所以可以直接使用React-Redux的connect帮助我们.
React-Redux的connect其实会自动做一个对props的比较。过程如下:
简而言之,它会根据传入进来的props
,state
通过各种计算得到nextProps
跟上一个props
做比较,如果不相等,shouldComponentUpdate
才会返回false
。
所以,有了它,todo.js就可以这样写
代码语言:javascript复制import React, { Component, PropTypes } from 'react'
class Todo extends Component {
render() {
return (
<li
onClick={this.props.onClick}
style={{
textDecoration: this.props.completed ? 'line-through' : 'none',
cursor: this.props.completed ? 'default' : 'pointer'
}}>
{this.props.text}
</li>
)
}
}
Todo.propTypes = {
onClick: PropTypes.func.isRequired,
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired
}
export default connect()(Todo);
跟踪问题
这里我使用React Pref跟踪问题,加上之后,查看控制台,可以看到浪费的渲染时间由TodoList -> Todo
转变到了Connect(Todo)> Todo
,要理解这个现象就要理解connect
里shouldComponentUpdate
的实现方式。
注意:
这里对props的对比只是一个浅比较,所以要让react-redux认为前后的对象是相同的,必须指向同一个js对象。
例如:
代码语言:javascript复制<Foo style={color: 'red'}>
这样每次传入进来的style都是一个新的对象,所以即使里面的值相等,react-redux的浅比较仍然认为它不等需要重新渲染。
再回来看看TodoList里是怎么用Todo的
代码语言:javascript复制 <ul>
{this.props.todos.map((todo, index) =>
<Todo {...todo}
key={index}
onClick={() => this.props.onTodoClick(index)} />
)}
</ul>
不知道大家发现了没有?这里的每一个onClick都是一个新的函数,即使Todo被装备了shouldComponentUpdate的实现,浅比较的时候props总是不相等,依旧躲不过每次更新都要被重新渲染的命运。
如何改进
有两种做法:
方法一:
先来看看这个onTodoClick是怎么被一层层传下来的:
代码语言:javascript复制// App.js
<TodoList
todos={visibleTodos}
onTodoClick={index =>
dispatch(toggleTodo(index))}
/>
// TodoList.js
<Todo {...todo}
key={index}
onClick={() => this.props.onTodoClick(index)}
/>
// todo.js
<li
onClick={this.props.onClick}
style={{
textDecoration: this.props.completed ? 'line-through' : 'none',
cursor: this.props.completed ? 'default' : 'pointer'
}}>
{this.props.text}
</li>
可以在TodoList的时候,不构造匿名函数直接将onTodoClick传下来,然后index可以放在新加的属性id里。
代码语言:javascript复制// TodoList.js
<Todo {...todo}
key={index}
*id={item.id}*
onClick={onTodoClick}
/>
// todo.js的mapDispatchToProps需要做对应的改变
const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: () => ownProps.onToggle(ownProps.id)
})
方法二:
直接让TodoList不要给todo传递任何函数类型的prop,点击事件完全由todo组件自己搞定。
代码语言:javascript复制// TodoList只需要穿一个id出来
<Todo {...todo}
key={index}
*id={item.id}*
/>
// todo中,自己通过react-redux派发action即可
const mapDispatchToProps = (dispatch, ownProps) => {
const {id} = this.props;
return {
onClick: () => dispatch(toggleTodo(id))
}
}
对比两种方式,其实对于todo来说,都需要使用react-redux
,都需要todoList传入一个id,区别只在于actions是由父组件还是有组件自己导入。
相比而言,没有必要一层层传递这个action,第二种方式让todo处理自己的一切事务,更符合高内聚的要求。
总结
说了那么多,总之就是通过React Pref帮我们找到需要优化的组件,然后用connect帮助我们做优化偷个懒。
参考: <深入浅出React和Redux> -- 程墨