@observer

egghead.io 第1课: observable & observer

observer 函数/装饰器可以用来将 React 组件转变成响应式组件。它用 mobx.autorun 包装了组件的 render 函数以确保任何组件渲染中使用的数据变化时都可以强制刷新组件。observer 是由单独的 mobx-react 包提供的。

  1. import {observer} from "mobx-react";
  2. var timerData = observable({
  3. secondsPassed: 0
  4. });
  5. setInterval(() => {
  6. timerData.secondsPassed++;
  7. }, 1000);
  8. @observer class Timer extends React.Component {
  9. render() {
  10. return (<span>Seconds passed: { this.props.timerData.secondsPassed } </span> )
  11. }
  12. };
  13. ReactDOM.render(<Timer timerData={timerData} />, document.body);

小贴士: 当 observer 需要组合其它装饰器或高阶组件时,请确保 observer 是最深处(第一个应用)的装饰器,否则它可能什么都不做。

注意,使用 @observer 装饰器是可选的,它和 observer(class Timer ... { }) 达到的效果是一样的。

陷阱: 组件中的间接引用值

MobX 可以做很多事,但是它无法使原始数据类型值转变成可观察的(尽管它可以用对象来包装它们,参见 boxed observables)。所以是不可观察的,但是对象的属性可以。这意味着 @observer 实际上是对间接引用(dereference)值的反应。那么在上面的示例中,如果是用下面这种方式初始化的,Timer 组件是不会有反应的:

  1. React.render(<Timer timerData={timerData.secondsPassed} />, document.body)

在这个代码片段中只是把 secondsPassed 的当前值传递给了 Timer 组件,这个值是不可变值0(JS中所有的原始类型值都是不可变的)。这个数值永远都不会改变,因此 Timer 组件不会更新。secondsPassed的值将来会发生改变,所以我们需要在组件访问它。或者换句话说: 值需要通过引用来传递而不是通过(字面量)值来传递。

ES5 支持

在ES5环境中,可以简单地使用 observer(React.createClass({ ... 来定义观察者组件。还可以参见语法指南

无状态函数组件

上面的 Timer 组件还可以通过使用 observer 传递的无状态函数组件来编写:

  1. import {observer} from "mobx-react";
  2. const Timer = observer(({ timerData }) =>
  3. <span>Seconds passed: { timerData.secondsPassed } </span>
  4. );

可观察的局部组件状态

就像普通类一样,你可以通过使用 @observable 装饰器在React组件上引入可观察属性。这意味着你可以在组件中拥有功能同样强大的本地状态(local state),而不需要通过 React 的冗长和强制性的 setState 机制来管理。响应式状态会被 render 提取调用,但不会调用其它 React 的生命周期方法,除了 componentWillUpdatecomponentDidUpdate 。如果你需要用到其他 React 生命周期方法 ,只需使用基于 state 的常规 React API 即可。

上面的例子还可以这样写:

  1. import {observer} from "mobx-react"
  2. import {observable} from "mobx"
  3. @observer class Timer extends React.Component {
  4. @observable secondsPassed = 0
  5. componentWillMount() {
  6. setInterval(() => {
  7. this.secondsPassed++
  8. }, 1000)
  9. }
  10. render() {
  11. return (<span>Seconds passed: { this.secondsPassed } </span> )
  12. }
  13. }
  14. 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 的方式已废弃。

示例:

  1. const colors = observable({
  2. foreground: '#000',
  3. background: '#fff'
  4. });
  5. const App = () =>
  6. <Provider colors={colors}>
  7. <app stuff... />
  8. </Provider>;
  9. const Button = inject("colors")(observer(({ colors, label, onClick }) =>
  10. <button style={{
  11. color: colors.foreground,
  12. backgroundColor: colors.background
  13. }}
  14. onClick={onClick}
  15. >{label}</button>
  16. ));
  17. // 稍后..
  18. colors.foreground = 'blue';
  19. // 所有button都会更新

更多资料,请参见 mobx-react 文档

何时使用 observer?

简单来说: 所有渲染 observable 数据的组件。如果你不想将组件标记为 observer,例如为了减少通用组件包的依赖,请确保只传递普通数据。

使用 @observer 的话,不再需要从渲染目的上来区分是“智能组件”还是“无脑”组件。在组件的事件处理、发起请求等方面,它也是一个很好的分离关注点。当所有组件它们自己的依赖项有变化时,组件自己会响应更新。而它的计算开销是可以忽略的,并且它会确保不管何时,只要当你开始使用 observable 数据时,组件都将会响应它的变化。更多详情,请参见 这里

observerPureComponent

如果传递给组件的数据是响应式的,observer 还可以防止当组件的 props 只是浅改变时的重新渲染,这是很有意义的。这个行为与 React PureComponent 相似,不同在于这里的 state 的更改仍然会被处理。如果一个组件提供了它自己的 shouldComponentUpdate,这个方法会被优先调用。想要更详细的解释,请参见这个 github issue

componentWillReact (生命周期钩子)

React 组件通常在新的堆栈上渲染,这使得通常很难弄清楚是什么导致组件的重新渲染。当使用 mobx-react 时可以定义一个新的生命周期钩子函数 componentWillReact(一语双关)。当组件因为它观察的数据发生了改变,它会安排重新渲染,这个时候 componentWillReact 会被触发。这使得它很容易追溯渲染并找到导致渲染的操作(action)。

  1. import {observer} from "mobx-react";
  2. @observer class TodoView extends React.Component {
  3. componentWillReact() {
  4. console.log("I will re-render, since the todo has changed!");
  5. }
  6. render() {
  7. return <div>this.props.todo.title</div>;
  8. }
  9. }
  • 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 中建议的示例配置。