关于javascript:React数据状态管理-函数响应式编程Mobx

43次阅读

共计 9497 个字符,预计需要花费 24 分钟才能阅读完成。

Mobx

通过通明的 函数响应式编程 (transparently applying functional reactive programming – TFRP) 使得状态治理变得简略和可扩大。MobX 背地的哲学很简略:

任何源自利用状态的货色都应该主动地取得。

React 通过提供机制 把利用状态转换为可渲染组件树并对其进行渲染 。而 MobX 提供机制来 存储和更新利用状态 供 React 应用。

React 提供了优化 UI 渲染的机制,这种机制就是 通过应用虚构 DOM 来缩小低廉的 DOM 变动的数量

MobX 提供了优化利用状态与 React 组件同步的机制,这种机制就是 应用响应式虚构依赖状态图表,它只有在真正须要的时候才更新并且永远放弃是最新的。

概念

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 反对单向数据流,也就是 动作 扭转 状态 ,而状态的扭转会更新所有受影响的 视图

状态 扭转时,所有 衍生 都会进行 原子级的主动 更新。因而永远不可能察看到两头值。

所有 衍生 默认都是 同步 更新。这意味着例如 动作 能够在扭转 状态 之后间接能够平安地查看计算值。

计算值 提早 更新的。任何不在应用状态的计算值将不会更新,直到须要它进行副作用(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 <div>
            <ul>
                {this.props.todoList.todos.map(todo =>
                    <TodoView todo={todo} key={todo.id} />
                )}
            </ul>
            Tasks left: {this.props.todoList.unfinishedTodoCount}
        </div>
    }
}

const TodoView = observer(({todo}) =>
    <li>
        <input
            type="checkbox"
            checked={todo.finished}
            onClick={() => todo.finished = !todo.finished}
        />{todo.title}
    </li>
)

const store = new TodoList();
ReactDOM.render(<TodoListView todoList={store} />, 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 (<button onClick={this.onReset.bind(this)}>
                Seconds passed: {this.props.appState.timer}
            </button>
        );
    }

    onReset() {this.props.appState.resetTimer();
    }
};

ReactDOM.render(<TimerView appState={appState} />, 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"]
})

绿色框示意 可察看 属性。请留神, 自身是不可察看的!

当初 MobX 基本上所做的是记录你在函数中应用的是哪个 箭头。之后,只有这些箭头中的其中一个扭转了(它们开始援用别的货色了),它就会从新运行。

间接援用

// 正确,在追踪函数内进行
const disposer = autorun(() => {console.log(message.title)
    // 追踪过程
    trace()})

// 输入:
// [mobx.trace] 'Autorun@2' tracing enabled

message.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}) =>
    <SomeContainer
        title = {() => <div>{message.title}</div>}
    />
)

message.title = "Bar"

起初看上去所有仿佛都是没问题的,除了 <div> 实际上不是由 MyComponent(有追踪的渲染) 渲染的,而是 SomeContainer。所以要确保 SomeContainer 的 title 能够正确对新的 message.title 作出反应,SomeContainer 应该也是一个 observer

如果 SomeContainer 来源于内部库的话,这通常不在你的掌控之中。在这种场景下,你能够用本人的无状态 observer 组件来包裹 div 解决此问题,或通过利用 <Observer>组件:

const MyComponent = observer(({message}) =>
    <SomeContainer
        title = {() => <TitleRenderer message={message} />}
    />
)

const TitleRenderer = observer(({message}) =>
    <div>{message.title}</div>}
)

message.title = "Bar"

另外一种办法能够防止创立额定组件,它同样实用了 mobx-react 内置的 Observer 组件,它不承受参数,只须要单个的 render 函数作为子节点:

const MyComponent = ({message}) =>
    <SomeContainer
        title = {() =>
            <Observer>
                {() => <div>{message.title}</div>}
            </Observer>
        }
    />

message.title = "Bar"

防止在本地字段中缓存 observable

@observer class MyComponent extends React.component {
    author;
    constructor(props) {super(props)
        this.author = props.message.author;
    }

    render() {return <div>{this.author.name}</div>
    }
}

组件会对 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}
// ...

正文完
 0