@observer
egghead.io 第1课: observable & observer
observer
函数/装饰器可以用来将 React 组件转变成响应式组件。它用 mobx.autorun
包装了组件的 render 函数以确保任何组件渲染中使用的数据变化时都可以强制刷新组件。observer
是由单独的 mobx-react
包提供的。
import {observer} from "mobx-react";
var timerData = observable({
secondsPassed: 0
});
setInterval(() => {
timerData.secondsPassed++;
}, 1000);
@observer class Timer extends React.Component {
render() {
return (<span>Seconds passed: { this.props.timerData.secondsPassed } </span> )
}
};
ReactDOM.render(<Timer timerData={timerData} />, document.body);
小贴士: 当 observer
需要组合其它装饰器或高阶组件时,请确保 observer
是最深处(第一个应用)的装饰器,否则它可能什么都不做。
注意,使用 @observer
装饰器是可选的,它和 observer(class Timer ... { })
达到的效果是一样的。
陷阱: 组件中的间接引用值
MobX 可以做很多事,但是它无法使原始数据类型值转变成可观察的(尽管它可以用对象来包装它们,参见 boxed observables)。所以值是不可观察的,但是对象的属性可以。这意味着 @observer
实际上是对间接引用(dereference)值的反应。那么在上面的示例中,如果是用下面这种方式初始化的,Timer
组件是不会有反应的:
React.render(<Timer timerData={timerData.secondsPassed} />, document.body)
在这个代码片段中只是把 secondsPassed
的当前值传递给了 Timer
组件,这个值是不可变值0
(JS中所有的原始类型值都是不可变的)。这个数值永远都不会改变,因此 Timer
组件不会更新。secondsPassed
的值将来会发生改变,所以我们需要在组件中访问它。或者换句话说: 值需要通过引用来传递而不是通过(字面量)值来传递。
ES5 支持
在ES5环境中,可以简单地使用 observer(React.createClass({ ...
来定义观察者组件。还可以参见语法指南。
无状态函数组件
上面的 Timer
组件还可以通过使用 observer
传递的无状态函数组件来编写:
import {observer} from "mobx-react";
const Timer = observer(({ timerData }) =>
<span>Seconds passed: { timerData.secondsPassed } </span>
);
可观察的局部组件状态
就像普通类一样,你可以通过使用 @observable
装饰器在React组件上引入可观察属性。这意味着你可以在组件中拥有功能同样强大的本地状态(local state),而不需要通过 React 的冗长和强制性的 setState
机制来管理。响应式状态会被 render
提取调用,但不会调用其它 React 的生命周期方法,除了 componentWillUpdate
和 componentDidUpdate
。如果你需要用到其他 React 生命周期方法 ,只需使用基于 state
的常规 React API 即可。
上面的例子还可以这样写:
import {observer} from "mobx-react"
import {observable} from "mobx"
@observer class Timer extends React.Component {
@observable secondsPassed = 0
componentWillMount() {
setInterval(() => {
this.secondsPassed++
}, 1000)
}
render() {
return (<span>Seconds passed: { this.secondsPassed } </span> )
}
}
ReactDOM.render(<Timer />, document.body)
对于使用可观察的局部组件状态更多的优势,请参见为什么我不再使用 setState
的三个理由。
使用 inject
将组件连接到提供的 stores
egghead.io 第8课: 使用 Provider 注入 stores
mobx-react
包还提供了 Provider
组件,它使用了 React 的上下文(context)机制,可以用来向下传递 stores
。要连接到这些 stores,需要传递一个 stores 名称的列表给 inject
,这使得 stores 可以作为组件的 props
使用。
注意: 从 mobx-react 4开始,注入 stores 的语法发生了变化,应该一直使用 inject(stores)(component)
或 @inject(stores) class Component...
。直接传递 store 名称给 observer
的方式已废弃。
示例:
const colors = observable({
foreground: '#000',
background: '#fff'
});
const App = () =>
<Provider colors={colors}>
<app stuff... />
</Provider>;
const Button = inject("colors")(observer(({ colors, label, onClick }) =>
<button style={{
color: colors.foreground,
backgroundColor: colors.background
}}
onClick={onClick}
>{label}<button>
));
// 稍后..
colors.foreground = 'blue';
// 所有button都会更新
更多资料,请参见 mobx-react
文档。
何时使用 observer
?
简单来说: 所有渲染 observable 数据的组件。如果你不想将组件标记为 observer,例如为了减少通用组件包的依赖,请确保只传递普通数据。
使用 @observer
的话,不再需要从渲染目的上来区分是“智能组件”还是“无脑”组件。在组件的事件处理、发起请求等方面,它也是一个很好的分离关注点。当所有组件它们自己的依赖项有变化时,组件自己会响应更新。而它的计算开销是可以忽略的,并且它会确保不管何时,只要当你开始使用 observable 数据时,组件都将会响应它的变化。更多详情,请参见 这里。
observer
和 PureComponent
如果传递给组件的数据是响应式的,observer
还可以防止当组件的 props 只是浅改变时的重新渲染,这是很有意义的。这个行为与 React PureComponent 相似,不同在于这里的 state 的更改仍然会被处理。如果一个组件提供了它自己的 shouldComponentUpdate
,这个方法会被优先调用。想要更详细的解释,请参见这个 github issue。
componentWillReact
(生命周期钩子)
React 组件通常在新的堆栈上渲染,这使得通常很难弄清楚是什么导致组件的重新渲染。当使用 mobx-react
时可以定义一个新的生命周期钩子函数 componentWillReact
(一语双关)。当组件因为它观察的数据发生了改变,它会安排重新渲染,这个时候 componentWillReact
会被触发。这使得它很容易追溯渲染并找到导致渲染的操作(action)。
import {observer} from "mobx-react";
@observer class TodoView extends React.Component {
componentWillReact() {
console.log("I will re-render, since the todo has changed!");
}
render() {
return <div>this.props.todo.title</div>;
}
}
componentWillReact
不接收参数componentWillReact
初始化渲染前不会触发 (使用componentWillMount
替代)componentWillReact
对于 mobx-react@4+, 当接收新的 props 时并在setState
调用后会触发此钩子
优化组件
请参见相关章节。
MobX-React-DevTools
结合 @observer
,可以使用 MobX-React-DevTools ,它精确地显示了何时重新渲染组件,并且可以检查组件的数据依赖关系。详情请参见 开发者工具 。
observer 组件特性
- Observer 仅订阅在上次渲染期间活跃使用的数据结构。这意味着你不会订阅不足(under-subscribe)或者过度订阅(over-subscribe)。你甚至可以在渲染方法中使用仅在未来时间段可用的数据。 这是异步加载数据的理想选择。
- 你不需要声明组件将使用什么数据。相反,依赖关系在运行时会确定并以非常细粒度的方式进行跟踪。
- 通常,响应式组件没有或很少有状态,因为在与其他组件共享的对象中封装(视图)状态通常更方便。但你仍然可以自由地使用状态。
@observer
以和PureComponent
同样的方式实现了shouldComponentUpdate
,因此子组件可以避免不必要的重新渲染。- 响应式组件单方面加载数据,即使子组件要重新渲染,父组件也不会进行不必要地重新渲染。
@observer
不依赖于 React 的上下文系统。- mobx-react@4+ 中,observer 组件的props 对象和 state 对象都会自动地转变为 observable,这使得创建 @computed 属性更容易,@computed 属性是根据组件内部的 props 推导得到的。如果在
@observer
组件中包含 reaction(例如autorun
) 的话,当 reaction 使用的特定属性不再改变时,reaction 是不会再重新运行的,在 reaction 中使用的特定 props 一定要间接引用(例如const myProp = props.myProp
)。不然,如果你在 reaction 中引用了props.myProp
,那么 props 的任何改变都会导致 reaction 的重新运行。对于 React-Router 的典型用例,请参见这篇文章。
在编译器中启用装饰器
在使用 TypeScript 或 Babel 这些等待ES标准定义的编译器时,默认情况下是不支持装饰器的。
- 对于 typescript,启用
--experimentalDecorators
编译器标识或者在tsconfig.json
中把编译器属性experimentalDecorators
设置为true
(推荐做法) - 对于 babel5,确保把
--stage 0
传递给 Babel CLI - 对于 babel6,参见此 issue 中建议的示例配置。