React数据状态管理|React数据状态管理 --- 函数响应式编程Mobx

Mobx 通过透明的函数响应式编程(transparently applying functional reactive programming - TFRP)使得状态管理变得简单和可扩展。MobX背后的哲学很简单:

任何源自应用状态的东西都应该自动地获得。
React数据状态管理|React数据状态管理 --- 函数响应式编程Mobx
文章图片

React 通过提供机制把应用状态转换为可渲染组件树并对其进行渲染。而MobX提供机制来存储和更新应用状态供 React 使用。
React 提供了优化UI渲染的机制, 这种机制就是通过使用虚拟DOM来减少昂贵的DOM变化的数量。
MobX 提供了优化应用状态与 React 组件同步的机制,这种机制就是使用响应式虚拟依赖状态图表,它只有在真正需要的时候才更新并且永远保持是最新的。
概念 React数据状态管理|React数据状态管理 --- 函数响应式编程Mobx
文章图片

State(状态)
状态 是驱动应用的数据。
Derivations(衍生)
任何源自状态并且不会再有任何进一步的相互作用的东西就是衍生。 衍生以多种形式存在:
  • 用户界面
  • 衍生数据,比如剩下的待办事项的数量。
  • 后端集成,比如把变化发送到服务器端。
MobX 区分了两种类型的衍生:
  • Computed values(计算值) - 它们是永远可以使用纯函数(pure function)从当前可观察状态中衍生出的值。
  • Reactions(反应) - Reactions 是当状态改变时需要自动发生的副作用。需要有一个桥梁来连接命令式编程(imperative programming)和响应式编程(reactive programming)。或者说得更明确一些,它们最终都需要实现I / O 操作。
Actions(动作)
不同于 flux 系的一些框架,MobX 对于如何处理用户事件是完全开明的。
  • 可以用类似 Flux 的方式完成
  • 或者使用 RxJS 来处理事件
  • 或者用最直观、最简单的方式来处理事件,正如上面演示所用的 onClick
最后全部归纳为: 状态应该以某种方式来更新。
当状态更新后,MobX 会以一种高效且无障碍的方式处理好剩下的事情。
从技术上层面来讲,并不需要触发事件、调用分派程序或者类似的工作。归根究底 React 组件只是状态的华丽展示,而状态的衍生由 MobX 来管理。
store.todos.push( new Todo("Get Coffee"), new Todo("Write simpler code") ); store.todos[0].finished = true;

尽管如此,MobX 还是提供了 actions 这个可选的内置概念。它们可以帮助你把代码组织的更好,还能在状态何时何地应该被修改这个问题上帮助你做出明智的决定。
原则
MobX 支持单向数据流,也就是动作改变状态,而状态的改变会更新所有受影响的视图。
React数据状态管理|React数据状态管理 --- 函数响应式编程Mobx
文章图片

当状态改变时,所有衍生都会进行原子级的自动更新。因此永远不可能观察到中间值。
所有衍生默认都是同步更新。这意味着例如动作可以在改变状态之后直接可以安全地检查计算值。
计算值 是延迟更新的。任何不在使用状态的计算值将不会更新,直到需要它进行副作用(I / O)操作时。 如果视图不再使用,那么它会自动被垃圾回收。
所有的计算值都应该是纯净的。它们不应该用来改变状态。
import {observable, autorun} from 'mobx'; var todoStore = observable({ /* 一些观察的状态 */ todos: [],/* 推导值 */ get completedCount() { return this.todos.filter(todo => todo.completed).length; } }); /* 观察状态改变的函数 */ autorun(function() { console.log("Completed %d of %d items", todoStore.completedCount, todoStore.todos.length ); }); /* ..以及一些改变状态的动作 */ todoStore.todos[0] = { title: "Take a walk", completed: false }; // -> 同步打印 'Completed 0 of 1 items'todoStore.todos[0].completed = true; // -> 同步打印 'Completed 1 of 1 items'

核心 Observable state(可观察的状态)
observable(value)
@observable classProperty = value
此 API 只有在它可以被制作成可观察的数据结构(数组、映射或 observable 对象)时才会成功。对于所有其他值,不会执行转换。
通过使用 @observable 装饰器(ES.Next)来给你的类属性添加注解就可以简单地完成这一切。
import { observable } from "mobx"; class Todo { id = Math.random(); @observable title = ""; @observable finished = false; }

Computed values(计算值)
computed(() => expression)
computed(() => expression, (newValue) => void)
computed(() => expression, options)
@computed({equals: compareFn}) get classProperty() { return expression; }
@computed get classProperty() { return expression; }
创建计算值,expression 不应该有任何副作用而只是返回一个值。 如果任何 expression 中使用的 observable 发生改变,它都会自动地重新计算,但前提是计算值被某些 reaction 使用了。
通过@computed 装饰器或者利用 (extend)Observable 时调用 的getter / setter 函数来进行使用
class TodoList { @observable todos = []; @computed get unfinishedTodoCount() { return this.todos.filter(todo => !todo.finished).length; } }

Actions(动作)
action(fn)
action(name, fn)
@action classMethod
@action(name) classMethod
@action boundClassMethod = (args) => { body }
@action.bound boundClassMethod(args) { body }
动作可以有助于更好的组织代码。 建议在任何更改 observable 或者有副作用的函数上使用动作。 结合开发者工具的话,动作还能提供非常有用的调试信息。
对于一次性动作,可以使用 runInAction(name?, fn) , 它是 action(name, fn)() 的语法糖.
Reactions(反应) & Derivations(衍生)
计算值 是自动响应状态变化的值。 反应 是自动响应状态变化的副作用。 反应可以确保当相关状态发生变化时指定的副作用(主要是 I/O)可以自动地执行,比如打印日志、网络请求、等等。 简而言之,reactions 在 响应式编程和命令式编程之间建立沟通的桥梁。
方法 描述
observer 在组件的 render 函数中的任何已使用的 observable 发生变化时,组件都会自动重新渲染。
autorun autorun 负责运行所提供的 sideEffect 并追踪在sideEffect运行期间访问过的 observable 的状态。 将来如果有其中一个已使用的 observable 发生变化,同样的sideEffect会再运行一遍。
when condition 表达式会自动响应任何它所使用的 observable。 一旦表达式返回的是真值,副作用函数便会立即调用,但只会调用一次。
reaction 接收两个函数,第一个是追踪并返回数据,该数据用作第二个函数,也就是副作用的输入。
React 组件 如果你用 React 的话,可以把你的(无状态函数)组件变成响应式组件,方法是在组件上添加 observer 函数/ 装饰器. observer由 mobx-react 包提供的。
import React, {Component} from 'react'; import ReactDOM from 'react-dom'; import {observer} from 'mobx-react'; @observer class TodoListView extends Component { render() { return
    {this.props.todoList.todos.map(todo => )}
Tasks left: {this.props.todoList.unfinishedTodoCount}} }const TodoView = observer(({todo}) =>
  • todo.finished = !todo.finished} />{todo.title}
  • )const store = new TodoList(); ReactDOM.render(, document.getElementById('mount'));

    MobX 会确保组件总是在需要的时重新渲染。
    上面例子中的 onClick 处理方法会强制对应的 TodoView 进行渲染,如果未完成任务的数量(unfinishedTodoCount)已经改变,它将导致 TodoListView 进行渲染。
    可是,如果移除 Tasks left 这行代码(或者将它放到另一个组件中),当点击 checkbox 的时候 TodoListView 就不再重新渲染。
    自定义 reactions 使用autorunreactionwhen 函数即可简单的创建自定义 reactions,以满足你的具体场景。
    autorun(() => { console.log("Tasks left: " + todos.unfinishedTodoCount) })

    最简实现 1. 定义状态并使其可观察
    import {observable} from 'mobx'; var appState = observable({ timer: 0 });

    2. 创建视图以响应状态的变化
    appState 中相关数据发生改变时视图会自动更新。 MobX 会以一种最小限度的方式来更新视图。
    通常来说,任何函数都可以成为可以观察自身数据的响应式视图,MobX 可以在任何符合ES5的JavaScript环境中应用。
    import {observer} from 'mobx-react'; @observer class TimerView extends React.Component { render() { return ( ); }onReset() { this.props.appState.resetTimer(); } }; ReactDOM.render(, document.body);

    3. 更改状态
    不像一些其它框架,MobX 不会命令你如何如何去做。 这是最佳实践,但关键要记住一点: *MobX 帮助你以一种简单直观的方式来完成工作*。
    无论是在改变状态的控制器函数中,还是在应该更新的视图中,都没有明确的关系定义。 使用 observable 来装饰你的状态和视图,这足以让 MobX检测所有关系了。
    appState.resetTimer = action(function reset() { appState.timer = 0; }); setInterval(action(function tick() { appState.timer += 1; }), 1000);

    理解Mobx怎么作出响应
    MobX 会对在 追踪函数执行 过程中 读取现存的可观察属性做出反应。
    • “读取” 是对象属性的间接引用。
    • “追踪函数” 是 computed 表达式、observer 组件的 render() 方法和 whenreactionautorun 的第一个入参函数。
    • “过程(during)” 意味着只追踪那些在函数执行时被读取的 observable 。这些值是否由追踪函数直接或间接使用并不重要。
    换句话说,MobX 不会对其作出反应:
    • 从 observable 获取的值,但是在追踪函数之外
    • 在异步调用的代码块中读取的 observable
    MobX 追踪属性访问,而不是值 假设你有如下的 observable 数据结构(默认情况下 observable 会递归应用,所以本示例中的所有字段都是可观察的)。
    const message = observable({ title: "Foo", author: { name: "Michel" }, likes: [ "John", "Sara" ] })

    绿色框表示可观察属性。 请注意,值 本身是不可观察的!
    React数据状态管理|React数据状态管理 --- 函数响应式编程Mobx
    文章图片

    现在 MobX 基本上所做的是记录你在函数中使用的是哪个箭头。之后,只要这些箭头中的其中一个改变了(它们开始引用别的东西了),它就会重新运行。
    间接引用
    // 正确,在追踪函数内进行 const disposer = autorun(() => { console.log(message.title) // 追踪过程 trace() })// 输出: // [mobx.trace] 'Autorun@2' tracing enabledmessage.title = "Hello" // [mobx.trace] 'Autorun@2' is invalidated due to a change in: 'ObservableObject@1.title'// 错误,在追踪函数外进行间接引用 const title = message.title; autorun(() => { console.log(title) }) message.title = "Bar"

    改变了非 observable 的引用
    autorun(() => { console.log(message.title) }) message = observable({ title: "Bar" })

    这将不会作出反应。message 被改变了,但它不是 observable,它只是一个引用 observable 的变量,但是变量(引用)本身并不是可观察的。
    存储 observable 对象的本地引用而不对其追踪
    const author = message.author; autorun(() => { console.log(author.name) }) // 生效 message.author.name = "Sara"; // 失效 message.author = { name: "John" };

    常见陷阱: console.log
    const message = observable({ title: "hello" })autorun(() => { console.log(message) })// 不会触发重新运行 message.title = "Hello world"

    因为没有在 autorun 内使用。autorun 只依赖于 message,它不是 observable,而是常量。
    事实上 console.log 会打印出 messagetitle,这是让人费解的,console.log 是异步 API,它只会稍后对参数进行格式化,因此 autorun 不会追踪 console.log 访问的数据。所以,请确保始终传递不变数据 ( immutable data ) 或防御副本给 console.log
    autorun(() => { console.log(message.title) // 很显然, 使用了 `.title` observable })autorun(() => { console.log(mobx.toJS(message)) // toJS 创建了深克隆,从而读取消息 })autorun(() => { console.log({...message}) // 创建了浅克隆,在此过程中也使用了 `.title` })autorun(() => { console.log(JSON.stringify(message)) // 读取整个结构 })

    访问数组
    // 生效 // 注意这会对数组中的任何更改做出反应。 数组不追踪每个索引/属性(如 observable 对象和映射),而是将其作为一个整体追踪。 // 但前提条件必须是提供的索引小于数组长度。 autorun(() => { console.log(message.likes.length); }) message.likes.push("Jennifer"); // 失效 // MobX 不会追踪还不存在的索引或者对象属性(当使用 observable 映射(map)时除外) autorun(() => { console.log(message.likes[0]); }) message.likes.push("Jennifer");

    使用对象的非 observable 属性 MobX 4
    // 失效 // MobX 只能追踪 observable 属性,上面的 postDate 还未被定义为 observable 属性。 autorun(() => { console.log(message.postDate) }) message.postDate = new Date()// 可以使用 MobX 提供的 get 和 set 方法来使其工作: autorun(() => { console.log(get(message, "postDate")) }) set(message, "postDate",new Date())

    MobX 5
    可以追踪还不存在的属性。注意,这只适用于由 observableobservable.object 创建出的对象。 对于类实例上的新属性,还是无法自动将其变成 observable 的。
    MobX 4 和 MobX 5 的不同之处在于后者使用了 ES6 的 proxy 来追踪属性。因此,MobX 5 只能运行在支持 proxy 的浏览器上,而 MobX 4可以运行在任何支持 ES5 的环境中。
    MobX 4 的重要局限性:
    • Observable 数组并非真正的数组,所以它们无法通过 Array.isArray() 的检查。最常见的处理方法是在传递给第三方库之前,你经常需要先对其进行 .slice() 操作,从而得到一个浅拷贝的真正数组。
    • 向一个已存在的 observable 对象中添加属性不会被自动捕获。要么使用 observable 映射来替代,要么使用工具函数 中方法来对想要动态添加属性的对象进行读/写/迭代。
    MobX 只追踪同步地访问数据
    function upperCaseAuthorName(author) { const baseName = author.name; return baseName.toUpperCase(); } autorun(() => { console.log(upperCaseAuthorName(message.author)) }) message.author.name = "Chesterton"

    尽管 author.name 不是在 autorun 本身的代码块中进行直接引用的。 MobX 会追踪发生在 upperCaseAuthorName 函数里的间接引用,因为它是在 autorun 执行期间发生的。
    autorun(() => { setTimeout( () => console.log(message.likes.join(", ")), 10 ) }) message.likes.push("Jennifer");

    autorun 执行期间没有访问到任何 observable,而只在 setTimeout 执行期间访问了。 通常来说,这是相当明显的,很少会导致问题。
    MobX 只会为数据是直接通过 render 存取的 observer 组件进行数据追踪
    一个使用 observer 的常见错误是它不会追踪语法上看起来像 observer 父组件的数据,但实际上是由不同的组件渲染的。当组件的 render 回调函数在第一个类中传递给另一个组件时,经常会发生这种情况。
    const MyComponent = observer(({ message }) => {message.title}} /> )message.title = "Bar"

    起初看上去一切似乎都是没问题的,除了 实际上不是由 MyComponent(有追踪的渲染) 渲染的,而是 SomeContainer。 所以要确保 SomeContainer 的 title 可以正确对新的 message.title 作出反应,SomeContainer 应该也是一个 observer
    如果 SomeContainer 来源于外部库的话,这通常不在你的掌控之中。在这种场景下,你可以用自己的无状态 observer 组件来包裹 div 解决此问题,或通过利用 组件:
    const MyComponent = observer(({ message }) => } /> )const TitleRenderer = observer(({ message }) => {message.title}} )message.title = "Bar"

    另外一种方法可以避免创建额外组件,它同样适用了 mobx-react 内置的 Observer 组件,它不接受参数,只需要单个的 render 函数作为子节点:
    const MyComponent = ({ message }) => {() => {message.title}} } />message.title = "Bar"

    避免在本地字段中缓存 observable
    @observer class MyComponent extends React.component { author; constructor(props) { super(props) this.author = props.message.author; }render() { return {this.author.name} } }

    【React数据状态管理|React数据状态管理 --- 函数响应式编程Mobx】组件会对 author.name 的变化作出反应,但不会对 message 本身的 .author 的变化作出反应!因为这个间接引用发生在 render() 之外,而render()observer 组件的唯一追踪函数。 注意,即便把组件的 author 字段标记为 @observable 字段也不能解决这个问题,author 仍然是只分配一次。 这个问题可以简单地解决,方法是在 render() 中进行间接引用或者在组件实例上引入一个计算属性:
    @observer class MyComponent extends React.component { @computed get author() { return this.props.message.author } // ...

      推荐阅读