核心API
了解了observable
, computed
, reactions
and actions之后,
你
就能够掌握MobX,并将其运用在你的应用中了。
1.observable(value
)
用法:
observable(value)
@observable classProperty = value
被观测的数据可以是JS所有数据类型。通过这个api可以将数据转为可观察数据,转换的规则如下,不过你也可以使用修饰器修改转换结果
- 如果数据被包裹在修饰器asMap中,就会返回一个新的可观察的Map。当你不仅想要关注属性的变化,也想要关注属性的增加或者删除时,可观察的Map就会非常有用。
- 如果数据是一个数组,就会返回一个可观察的数组。
- 如果是一个没有原型的对象,那么这个对象当前的所有属性都会被转为可观察的。
- 如是数据是一个有原型的对象,一个JS简单类型数据或者一个函数,那么就会返回一个 Boxed Observable。
这些规则看起来也许很复杂。但在实际的运用中,它们用起来会非常直观。
- 如果你想要创建一个拥有动态key的对象,就一定要使用asMap修饰器!只有初始化就已经存在的属性才会被转为可观察的,当然你也可以使用extendObservable来新增一个属性。
- 使用@observable修饰器时,当然要确保你的编码器支持解析修饰器
- 默认情况下,把数据转为可观察的这一些行为是具有扩散性的,也就是说所有的数据都会自动转为可观察的,无论是已包含的还是将来会包含的。这一行为使可以通过修饰器来改变的。
一些例子:
const map = observable(asMap({ key: "value"})); map.set("key", "new value"); const list = observable([1, 2, 4]); list[2] = 3; const person = observable({ firstName: "Clive Staples", lastName: "Lewis" }); person.firstName = "C.S."; const temperature = observable(20); temperature.set(25);
2.
@observable
在使用ES7或者TypeScrip语法时,你可以使用修饰器来将类的属性转为可观察的。你可以在实例或者可读取的属性上使用@observable。这能让你精确的控制,对象的哪一部分变成可观察的状态。
import {observable} from "mobx"; class OrderLine { @observable price:number = 0; @observable amount:number = 1; constructor(price) { this.price = price; } @computed get total() { return this.price * this.amount; } }
如果你的环境的不支持修饰器或field initializers,就使用extendObservable(this, { key: value })
。实际上@observable key = value是一个语法糖。
3.(@)computed
计算属性就是那些基于状态或者其他计算属性运算而来的值。
从概念上讲,这很像单元表格里的公式。不要小看了计算属性,它能让你的状态尽可能的小。除此之外,计算属性十分高效,所以尽可能的使用它。
不要将computed和autorun搞混了,尽管它们都是被动调用的表达式。当你想产生新的值以供其他的观察者用时,请使用@computed。如果你不是想要创建一个新值而是想要执行一些会产生副作用的操作时,请用autorun。常见的会产生副作用的操作有,输出日志,发送网络请求。
当和计算属性有关的状态改变时,它会自动更新。很多情况下,MobX会暂停很多计算属性的管理。例如没有任何地方用到这个计算属性,那它就不会在被更新。在这种情况下MobX会停止对计算属性的观察。
这种自动暂停对计算属性的观察是非常方便的。例如,如果MobX停止了对一个计算属性的观察,那它也会同时回收这个属性所涉及到的UI。而使用autorun的话,这些都需要你自己处理。对于初学者来说这是很迷惑的,如果你创建了一个计算属性但是又没有使用过他,MobX不会缓存这个值也不会重新计算。迄今为止,这是最好的设计了。通过使用observe或者keepAlive,强制性地让你计算属性保持活跃状态。
注意计算属性是不可枚举的,他们也不能在原型链中被重写。
@computed
如果你的环境支持修饰器,你可以使用@computed修饰器,在类的getter属性上面来定义计算属性。
computed修饰器
如果你的环境不支持修饰器,你可以使用computed(expression)和extendObservable
/ observable来创建一个计算属性。
@computed get propertyName() { }实际上是extendObservable(this, { propertyName: get func() { } })
的一个语法糖。
计算属性的setter
有时候我们会需要定义计算属性的setter用于设置计算属性的值。注意这些setters不能用来直接改变计算属性的值,但是可以作为逆计算来改变源的值,例如
const box = observable({ length: 2, get squared() { return this.length * this.length; }, set squared(value) { this.length = Math.sqrt(value); } });
等同于
class Foo { @observable length: 2, @computed get squared() { return this.length * this.length; } set squared(value) { //this is automatically an action, no annotation necessary this.length = Math.sqrt(value); } }
注意,定义setters需要MobX2.5.1及其以上。
computed的配置项
computed的修饰器或者computed的函数,可以接受一些可选的参数:
- name: 字符串,使用调试工具时展示出的调试名
- context:在表达式中使用的this值
- setter:setter函数。如果没有setter函数就不能给计算属性赋新值了。如果传入的第二个参数是函数,这个函数就会被设定为setter.
- compareStructural: 默认为false.当值设为true时,会在数据更新前,打印出旧数据和新数据的结构对比。这就确保了如果新返回的数据和之前的一直,计算属性的观察器就不会重新计算。
用于结构比较的@computed.struct
@computed修饰器不接受参数。如果你想要创建一个有结构比较的计算属性,请使用@computed.struct.
在错误处理上写注释
如果一个计算属性在计算过程中抛出一个异常,这个异常就会被缓存下来,并在这个值下次被读取时再重新抛出。强烈推荐每次都抛出新的错误,这样之前的追踪也能保存下来了。例如:使用throw new Error("Uhoh")而不是throw "Uhoh"。抛出异常并不会中断程序,所以进行争取操作后,例如传入正确的值后,程序就能从异常中恢复。
4.Autorun
如果你想要在状态改变的同时执行一些操作,你就可以用autorun。autorun可以帮助你在操作和命令式代码间建立起联系,例如打印日志,视图更新。当使用autorun的时候,所定义的函数会一直处于激活状态,也就是说只要状态改变,函数就会被执行。但是如果是计算的属性话,它只有在被使用时才会重新计算它的值。一个好的建议就是:当你想要创建一个可以自动执行的函数但不产生新值时,就用autorun。其余的情况就用computed。如果给autorun的第一个参数传了一个字符串,这个字符串会被用在调试名。
5.@observer
observer函数/修饰器用于将React组件转化为处于激活状态的组件。MobX用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> ) } }); React.render(<Timer timerData={timerData} />, document.body);
注意:如果在你的组件中还使用了其他修饰器,请确保observer在最里层。
同样的如果不能使用修饰器可以用observer(class Timer ... { })代替。
组件中的解引用值
MobX不能把js原始数据类型的值转为可观察的。所以不是值可以被被监测,而是对象的属性能被检测。所以如果将上个例子的初始代码改成下面的这种写法,组件永远不会更新。
React.render(<Timer timerData={timerData.secondsPassed} />, document.body)
在这个代码段中,传给Timer组件的只是secondsPassed的值,数字0,数字是不可变的(所有js原始类型的值都是不可变的)。因为这个数字永远都不会发生变化所以这个组件也就不会更新。所以我们传递给组件的一定要是一个引用,不能是一个值。
监测局部的组件状态
就像普通的类那样,你可以使用@observable定义可观察的属性。这样你就可以不用React管理state的哪一套了。只有render函数会感应到数据变化,react生命周期的其他点例如:componentShouldUpdate和componentWillUpdate都不会感应到。如果你想在其他点也感应到,那就使用React管理局部state的那一套咯。
上面的例子可以改成这个样子:
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> ) } }) React.render(<Timer />, document.body)
连接observer和stores
mobx-react包也提供了Provider组件,用于注入stores。为了连接这些stores,需要传递一个含有store名字的数组,这样你就可以通过props来访问了。
const colors = observable({ foreground: '#000', background: '#fff' }); const App = () => <Provider colors={colors}> <app stuff... /> </Provider>; const Button = observer(["colors"], ({ colors, label, onClick }) => <button style={{ color: colors.foreground, backgroundColor: colors.background }} onClick={onClick} >{label}<button> ); // later.. colors.foreground = 'blue'; // all buttons updated
什么时候使用observer
最简单的准则就是:所有渲染数据的组件。如果你不想让一个组件处于激活状态,只要你传下去的是简单数据就行了。使用observer,你就不需要在组件渲染的时候区分聪明的组件和愚蠢的组件。但任然需要你区分什么地方执行事件,什么地方发送请求。所有的组件在他们的依赖发生变化时,都会更新。这种花销是可以忽略不计的,这可以确保无论什么时候使用可观察的数据,组件都可以响应它。
observer和
PureRenderMixin
observer可以在组件渲染的时候比较props有没有发生变化,如果没有任何变化就不会重新渲染组件。但是这种比较是一种浅比较。如果传入组件的数据是响应式的,这种机制就会产生出大量的检测。这种行为和React的PureRender mixmin很像,除了状态改变一直需要处理。如果组件自己本身就有了shouldComponentUpdate,那就以shouldComponentUpdate里的判断为准。
componentWillReact(生命周期钩子)
React组件的渲染机制,让我们很难分辨是什么原因导致组件重新渲染。 所以你可以使用mobx-react来定义一个新的生命周期的钩子componentWillReact。当一个组件是因为数据改变而被压入渲染的堆栈的时候,componentWillReact就会被触发。这让我们可以很容易的追踪到是哪一个action导致组件重新渲染。
MobX-React-DevTools
结合使用@observer和MobX-React-DevTools ,可以准确的展示出组件什么时候重新渲染以及组件所依赖的数据。
观察组件的特征
- 观察组件只会订阅在最近一次渲染中使用过的数据,这意味着不会过少或者过多的订阅。你甚至可以在渲染的时候使用之后才能拿到的数据。这对异步加载数据来说是最好的设计。
- 你不需要声明组件用了哪些数据,在运行的时候依赖就能被自动定义,并以细粒度的方式在追踪。
- 响应式的组件通常没有或者有极少的局部state,因为将state放在可以共享给其他组件的对象中更方便。当然这是建议,你还是可以自由的使用state.
- @observer用和PureRenderMixin同样的方式实现了shouldComponentUpdate,这会减少子组件不必要的重新渲染。
- 组件侧向加载数据;父组件不会作没必要的重复渲染即使子组件会更新。
- @observe并不依赖React的运行机制。
6.action
用法:
action(fn)
action(name, fn)
@action classMethod() {}
@action(name) classMethod () {}
@action boundClassMethod = (args) => { body }
@action(name) boundClassMethod = (args) => { body }
@action.bound classMethod() {}
@action.bound(function() {})
任何修改state的操作都称为actions。action接收一个函数,并用untracked,untracked和allowStateChanges包裹后再返回。
@action createRandomContact() { this.pendingRequestCount++; superagent .get('https://randomuser.me/api/') .set('Accept', 'application/json') .end(action("createRandomContact-callback", (error, results) => { if (error) console.error(error); else { const data = JSON.parse(results.text).results[0]; const contact = new Contact(this, data.dob, data.name, data.login.username, data.picture) contact.addTag('random-user'); this.contacts.push(contact); this.pendingRequestCount--; } })); }
redux使用dispatch来修改数据,而action修改数据只需简单的赋值,流程上精简了很多。
异步actins和runInAction
action只对当前正运行的函数有效,对于在当前函数中定义而未执行的函数是没有作用的。也就是说如果你函数中的setTimeout,promise的回调函数也需要更新状态,就需要把这些回调也包含在action中。
如果你使用async
/ await(es7实现异步调用),这就有点棘手了,因为没有办法将异步函数体包裹在action中。这种情况下使用runInAction更方便。用runInAction将你想要更新状态的地方包裹起来就行了。
@action /*optional*/ updateDocument = async () => { const data = await fetchDataFromUrl(); /* required in strict mode to be allowed to update state: */ runInAction("update state after fetching data", () => { this.data.replace(data); this.isSaving = true; }) }
绑定ations
用于绑定this值
例:
class Ticker { @observable this.tick = 0 @action.bound increment() { this.tick++ // 'this' will always be correct } } const ticker = new Ticker() setInterval(ticker.increment, 1000)
注意:使用action.bind的时候不要用箭头函数,因为箭头函数已经绑定过this值了。