React 技巧和最佳实践
不管如何,你都应该去尝试一下当下炙手可热的 React。这篇文章我会总结一些关于 React 的技巧和最佳实践。
使用 PureComponent
React 在 15.3 版本中被引入了类 PureComponent
,表明之前社区中奖 shouldComponentUpdate
和 PureRenderMixin
作为一种最佳实践是一种共识。
当然两点需要简单说明的地方:
首先,你需要了解的是当 state 改变时,React 是如何更新我们的组件的。简单的说,当一个组件的状态改变时,它以及它所有后代的组件的 render 方法都会被调用一次,然后生成一个虚拟的 DOM 树再将它与内存中的虚拟的 DOM 树进行比较,然后采用最小代价的操作来更新浏览器中真实 DOM。因此,当我们通过 setState
方法更新状态的时候,React 看起来像是更新了整个 DOM 树。shouldComponentUpdate
提供了一种我们可以侵入到这个过程的能力,当该方法返回 false 的时候,该组件的 render 方法不会被调用,不会产生新的虚拟 DOM,该组件自然也没有后续虚拟 DOM 的 diff 和真实 DOM 更新。
其次,PureRenderMixin 只是浅比较 state 和 props ,只意味着你的 render 在相同的 state 和 props 的情况下必须要返回一致的输出,当你使用了复杂的数据类型来作为 state 和 prop 的时候,你可能需要引入 Immutable.js 这样的不可变数据类型的库。
通常情况下,它能够大幅提高你的应用的性能,尤其是你在构建一个复杂的应用的时候。当你使用类似于 Redux 这样的库用于集中管理你应用的 state 时,数据流总是从顶部单向流向各个层级的组件,因此意味着每一次 store 的变化,都会导致整个组件树中所有组件的 render 方法被调用一次,这也意味着引入 PureComponent 能够避免那些不必要的 render 方法被调用。
状态设计和无状态组件
关于状态的设计有些时候并不是一目了然,尤其是对新手来说。但是总结起来只有一条核心的原则,那就是尽量保证你的组件无状态 。这会让你的组件更容易理解和维护,也会让你整个应用的数据流更加清晰。
具体可以拆分为两条具体的原则,状态最小子集 和 状态集中管理 。状态最小集要求我们找到影响视图变化的状态的最小集合,而状态集中管理则要求我们使用类似 redux 这样的状态容器来集中管理我们的状态。实践中具体的技巧大概有以下 3 条:
消除组件内重复的状态
其核心是:
任何可以被计算或推导的数据不应该作为状态。
如果将可以被计算和推导出来的数据作为状态,那么就意味着你要维护和保持这些状态之间的同步,这通常是 bug 产生的地方,而且排查起来也很不容易。
消除组件间重复的状态
其核心是:
_ 任何除自身以及后代以外的组件会关注的数据不应该作为自身的状态_。
也就是通常提到的「向上顶」的原则。
这一条没有上一条那么直观,但却是很容易犯错的地方。任何除自身以及后代以外的组件通常意外着父组件或者其兄弟组件。
我举一个简单的例子,现在你要设计一个筛选器组件,它会涉及到很多筛选项,然后当用户提交的时候,页面下方的列表会更新。我们会很自然的将当前用户筛选的值作为该组件内部的一个状态,由该组件内部跟新和维护,然后在用户点击的确定的时候,通过暴露一个 onSubmit
事件,将参数传递给父组件,如组件更新列表。类似如下:
class Filter extends React.Component {
constructor(props) {
super(props)
this.state = {
foo: props.defaultFoo,
bar: props.defaultBar
}
}
handleBarChange(newBar) {
this.setState({
bar: newBar
})
}
handleFooChange(newFoo) {
this.setState({
foo: newFoo
})
}
handleSubmit() {
const { bar, foo } = this.state
this.props.onSubmit({ bar, foo })
}
render () {
//...
}
}
这样做会带来很多问题,比如如组件如果还接受其他的筛选条件来更新列表,比如翻页,那么意味这父组件必须要在 onSubmit 事件中不得不保存该筛选条件,可能将它存到 state 里面,那就意味同一份数据在父组件中也保存了一份相同的引用,同时意味着你要时刻小心的保持两者的同步。再比如,我现在用一个弹窗或者下拉框的形式来展现这些筛选条件,也就是说,但用户筛选完成之后,如果并没有点击提交,而是直接关闭了弹窗。那么当用户再次点开弹窗的时候,你需要重置会已生效的筛选条件,这个时候你又不得不将 props 同步给 state。
接下来我们进行改进,筛选项的值父组件是会关注的,所以「向上顶」放到父组件里面去。大概如下:
class Filter extends React.Component {
constructor(props) {
super(props)
}
handleBarChange(newBar) {
const {value, onChange} = this.props
onChange(Object.assign({}, value, {bar: newBar}))
}
handleFooChange(newFoo) {
const {value, onChange} = this.props
onChange(Object.assign({}, value, {foo: newFoo}))
}
handleSubmit() {
this.props.onSubmit()
}
render () {
//...
}
}
然后 Filter 组件变得很清晰明了,仅仅负责将父组件传入的筛选项信息 value 正确的渲染,同时暴露 onChange 和 onSubmit 父组件,将筛选状态的维护工作拆分给了父组件。而我上面提到的 2 个问题也就迎刃而解了。
通过以上 2 条技巧,我们很容易消除那些重复的状态,保证我们应用处于最小状态集合中。同时能够衍生出一条简单的准则帮助你发现问题,那就是当你发现你在维护两个状态的同步时,通常意味着你的组件设计有问题,如果两个状态在同一个组件内,参考技巧 1,否则参考技巧 2。
现在保证了状态最小集合,但要达到无状态组件,还需要我们将这些状态移除我们的组件,也就是接下来第 3 条技巧。
使用状态容器管理状态
这一条技巧很简单,就是使用例如 redux 这样的状态容器来集中管理我们应用中的状态,尤其是在构建复杂的应用的时候。关于 redux 的使用,社区里面有很多相关的教程,这里不再赘述。
最后,值得一提的是,我们是否应该将所有的状态都移除组件,达到真正的无状态组件。答案是否定的,因为这样不仅将会导致实践中流程极其繁琐,甚至某些时候也是不必要的。
但某些时候,又是什么时候?
简单的想一下我们浏览器自带的 select
元素,选择器是否展开必然应该作为一个状态。但是我们没有需要将其作为一个属性传给元素来控制元素是否展开,而且 select
元素确实也没有这个属性。因此,这个原则就是:
当某个状态之后该组件自身会关注,其余组件(父类和子类等等)都不关注的时候,那么你可以将它作为状态保留到组件内部。
关注 ComponentDidUpdate
最后,我想说明一下 ComponentDidUpdate 这个方法,这个方法经常被大家忽略,但其实应该有着更大的用武之地。
对于 React 而言,理想情况下应该是,我们设计好状态以及状态和视图的关联关系以后,在随后时间推移和用户交互的过程中我们只需要更改我们的状态就可以了。但实际情况是,我们很多时候在更改状态(例如,筛选项等)之后还需要去服务器上请求相应的数据。我们最常见的做法是在 setState
的回调函数里,如果使用了 类似于 redux 的状态管理容器时,我们可能派发一个异步 Action。
// setState 回调
function handlePageChange(newPage) {
this.setState(
{ page: newPage },
() => this.fetchData()
);
}
// dipatch 异步的 action
function fetchDataWithPageChange(page) {
return async (dispatch, getState) => {
// 同步的 action
dispatch(pageChange(page));
const query = getState().query;
// 异步操作
let res = await get(url, Object.assign({ page }, query));
// 同步 action
dispatch(dataChange(res));
}
}
这样做最大的问题是,我们将我们改变状态的行为和数据请求耦合在了一起,而当其他同样需要请求相同数据的状态改变之后,我们也必须记着调用相同的数据请求的方法,需要小心翼翼,不能遗忘。
如果我们把状态变化与数据请求的关系写在 componentDidUpdate 里面,那么我们就能够专注的更新我们的状态,因为状态更新会自动触发数据请求。看到了吗?这个我们处理状态和视图的关系如此相似,一旦指定了映射关系,那么只需要简单更新状态,就能自动触发另一方相应的动作。
改进如果下:
class Com extends PureComponent {
// ...
handlePageChange(newPage) {
this.setState({
page: newPage
});
}
componentDidUpdate(preProps, preState) {
const { page } = preState;
if (page !== this.state.page) {
this.fetchData();
}
}
// ...
}
// 或者
class Com extends PureComponent {
// ...
handlePageChange(newPage) {
this.props.dispatch(changePage(newPage));
}
componentDidUpdate(preProps) {
const { page } = preProps;
if (page !== this.props.page) {
this.props.dispatch(fetchData());
}
}
// ...
}
function changePage(page) {
return {
type: 'CHANGE_PAGE',
index: page
};
}
总结
关于 React 的总结和最佳实践暂时想到的就这些,从使用了 React 之后,在我需要构建一个在线的应用之前(不管是否使用 React ),我都会先首先梳理页面视图随着时间推移和用户的交互时是如何变化的,具体到 React 就是整个 app 的数据流。它们就像是一栋建筑的设计稿和结构图,等到你深入其中的细枝末节的时候仍然给能够对整体了然于胸,不会被一叶蔽目。我很享受这个过程,希望你们也能喜欢。
网友评论