mobx中跟新了数据视图没变化_【第1781期】MobX 简明教程
前言
SEEConf,2020年1月4號杭州見,C位搶票見文章末尾。今日早讀文章由騰訊@whinc投稿分享。
正文從這開始~~
導讀:MobX 是一個優秀的響應式狀態管理庫,在流行的狀態管理庫 Redux 之外為我們提供了其他選擇。如果你還沒有嘗試過 MobX,我強烈建議你繼續閱讀本文,并跟著示例動手實踐體驗一下。本文是 MobX 的入門教程,文章包含 MobX 的設計哲學、狀態派生模型、核心API以及與 React 的集成。
MobX 是一個簡單、可伸縮的響應式狀態管理庫。通過 MobX 你可以用最直觀的方式修改狀態,其他的一切 MobX 都會為你處理好(如自動更新UI),并且具有非常高的性能。
MobX 文檔的概念和 API 比較多,初次接觸時可能會感覺無從下手或者抓不住重點,本文嘗試提供一份 MobX 核心的知識的簡明教程,作為閱讀官方文檔之前的熱身。
MobX 的設計哲學
在學習使用 MobX API 之前,我們首先要了解 MobX 的設計哲學,它是我們在思考 MobX 應用時的心智模型,可以幫助我們更好的使用 MobX API。
MobX 的設計哲學概括起來就一句話:Anything that can be derived from the application state, should be derived. Automatically. (任何可以從應用狀態中派生的內容,都應當自動地被派生。)
理解這句話的關鍵是搞清楚【派生】的含義是什么?在 MobX 中【派生】的含義比較廣泛,包括:
用戶接口(UI),如組件、頁面、圖表
派生數據(computed data),如從數組中計算得到的數組長度
副作用(side effect),如發送網絡請求、設置定時任務、打印日志等
我們平時常看到的狀態響應模型,其中的響應就可以看做是狀態的一種派生,MobX 將這種模型進行泛化,形成更通用的狀態派生模型,接下來會詳細介紹。
MobX 狀態響應模型
狀態響應模型概括起來主要包含三個要素:定義狀態、響應狀態、修改狀態(如下圖所示)。
MobX 中通過observable來定義可觀察狀態, 它接受任意 JS 值(包括Object、Array、Map、Set),返回原始數據的代理對象,代理對象與原始數據具有相同的接口,你可以把代理對象當做原始數據使用。
// 定義狀態
const store = observable({
count: 0
});
MobX 中通過autorun來定義狀態變化時要執行的響應操作,它接受一個函數。此后,每當observable中定義的狀態發生變化時,MobX 都會立即執行該函數。
// 響應狀態
autorun(() => {
console.log("count:", store.count);
});
MobX 中修改狀態和修改原始數據的方式沒什么區別,這也是 MobX 的優點——符合直覺的操作方式。
// 修改狀態
store.count += 1;
把上面的部分串起來就是一個最簡單的 MobX 示例了(如下),示例中每次修改 count 的值時會自動打印一條日志,并且日志包含最新的 count 值。這個示例揭示了 MobX 中最核心的功能。
import { observable, autorun } from "mobx";
// 1. 定義狀態
const store = observable({
count: 0
});
// 2. 響應狀態
autorun(() => {
console.log("count:", store.count);
});
// count: 0
// 3. 修改狀態
store.count += 1;
// count: 1
上面例子中,首先通過 MobX 提供的observable函數定義狀態,例子的狀態是{count: 0}。然后通過通過 MobX 提供的autorun函數定義狀態響應函數,例子中是一條打印當前count值得的語句,當定義的狀態中的任何值發生變化時,該響應函數會立即執行,并且是由 MobX 自動完成的。最后是修改狀態,和操作對象屬性一樣,例子中是對count屬性進行自增操作。
MobX 狀態派生模型
從上一節中我們知道 MobX 的狀態響應函數是在狀態變化時執行某些操作。實際應用中這些操作可分為兩類:帶副作用(如打印日志、渲染 UI、請求網絡等)和不帶副作用(如計算數組的長度)。MobX 將這些操作統稱為派生(Derivations),即可從應用狀態中派生出來的任何內容。
其中不帶副作用的操作是一個純函數,一般是根據當前狀態計算返回一個新的值。為了區分帶副作用和不帶副作用的兩類情況,MobX 將 Derivations 概念細分成兩個概念 Reactions 和 Computed values 來區分二者。此外,MobX 還提供了一個可選的概念 Action 來表示對 State 的修改操作,用于約束和預測應用狀態的修改行為。這些概念匯總在一起如下圖:
下面是一個簡單的示例,完整展示了上圖中涉及的所有概念。
index.html 文件
id="container">
index.js 文件
import { observable, autorun, computed, action } from "mobx"
const containerEl = document.querySelector("#container");
// State
const store = observable({
count: 0
});
// Actions
window.increaseCount = action(() => store.count++);
// Computed values
const doubleCount = computed(() => 2 * store.count);
// Reactions
autorun(() => {
containerEl.innerHTML = `
${store.count} * 2 = ${doubleCount.get()}
+1
`;
});
// 0 * 2 = 0
// (點擊按鈕)
// 1 * 2 = 2
// (點擊按鈕)
// 2 * 2 = 4
這個示例展示了一個簡單的乘法,點擊按鈕后數據會自增,同時計算的結果也隨之更新。相比上一個例子,它有幾個值得注意的區別:
通過computed定義計算值,并返回計算值對象doubleCount,通過其get/set方法訪問內部計算值。computed接受一個返回計算值的函數,在函數內部可以使用observable定義的狀態數據(如count),每當count的值發生變化時,doubleCount內部的計算值會自動更新。
通過action定義狀態修改操作,action接受一個函數,并返回一個簽名相同的函數。在函數內部可直接修改obsevable定義的狀態(如count)觸發autorun和computed重新運行。
autorun中的響應操作替換成了渲染 UI,每當狀態發生變化時重新渲染 UI。
MobX 核心 API 解析
MobX 的 API 可分為四類:定義狀態(observable)、響應狀態(autorun, computed)、修改狀態(action)、輔助函數。下面挑出最核心的 API 進行重點介紹,但不會涉及 API 的詳細用法(請參考MobX Api Reference)。
MobX 目前同時支持 v4 和 v5 兩個版本,這兩個版本的 API 是相同的(功能相同),他們的區別在于內部實現數據響應的方式不同,如果使用 v4 版本時需要注意文檔中標注的注意情況。
observable
observable用于定義可觀察狀態,其類型定義如下(已簡化):
extends Object>(value: T): T & IObservableObject;
= any>(value: T[]): IObservableArray;
= any, V = any>(value: Map, V>): ObservableMap, V>;
= any>(value: Set): ObservableSet;
它接受Object/Array/Map/Set類型的數據作為參數,并返回對應數據的代理對象,代理對象和原數據類型具有相同的接口,你可以像使用原始數據一樣使用代理對象。例如:
import { observable } from "mobx";
const object = observable({a: 1})
console.log(object.a)
const array = observable([1, 2])
console.log(array[0])
const map = observable(new Map({a: 1}))
console.log(map.get('a'))
const set = observable(new Set([1, 2]))
console.log(set.has(1))
observable觀察的數據可以嵌套,嵌套的數據也會被觀察。例如:
import { observable, autorun } from "mobx";
const store = observable({
a: {
b: [1, 2]
}
})
autorun(() => console.log(store.a.b[0]))
// 1
store.a.b[0] += 1
// 2
observable支持動態添加可觀察狀態。例如:
import { observable, autorun } from "mobx";
const store = observable({});
autorun(() => {
console.log("a =", store.a);
});
// a = undefined
store.a = 1;
// a = 1
動態添加可觀察狀態,僅適用于 MobX v5+ 版本,MobX v4 及以下版本需要借助輔助函數,詳見Direct Observable manipulation。
autorun
autorun用于定義響應函數,其類型定義如下(已簡化):
autorun(reaction: () => any): IReactionDisposer;
autorun接受一個響應函數 reaction,并在定義時立即執行一次 reaction 函數, reaction 函數內部可以執行帶有副作用的操作。以后,每當依賴狀態發生變化時,autorun自動重新運行 reaction 函數。autorun第一次運行 reaction 函數是為了搜集依賴狀態——運行 reaction 過程中實際使用的狀態(通過obj.name或obj['name']解引用方式使用的狀態)。
例如下面例子,autorun中使用了狀態a,因此當狀態a的值發生變化時,會執行響應函數。而狀態b雖然被列為可觀察狀態,但由于在autorun中沒有被實際使用,因此當狀態b的值發生變化時,不會執行 響應函數 。這是 MobX 的“聰明”之處,它能根據狀態的實際使用情況,細粒度地控制更新范圍。由于減少了不必要的執行開銷,從而提升了程序性能。
import { observable, autorun } from "mobx";
const store = observable({
a: 1,
b: 2
});
autorun(() => {
console.log("a =", store.a);
});
// a = 1
store.a += 1;
// a = 2
store.b += 1;
// (無輸出)
讓我們回顧一下 MobX 的使用,通過observable定義狀態,在autorun中使用狀態,當autorun中使用到的狀態發生變化時,該autorun重新執行。絕大部分情況下它都工作的很好,如果你遇到修改了狀態而autorun沒有如你預期的那樣運行,這時候你需要深入了解 MobX 是如何響應狀態變化的,推薦閱讀What does MobX react to?。
computed
computed用于定義計算值,其類型定義如下(已簡化):
(func: () => T) => { get(): T, set(value: T): void}
computed與autorun相似,他們都會在依賴的狀態發生變化時會重新運行,不同之處是computed接收的是純函數并且返回一個計算值,這個計算值在狀態變化時會自動更新,計算值可以在autorun中使用。
例如下面例子中,ca是一個計算值,它依賴狀態a的值,當狀態a的值發生變化時,ca會重新計算值。計算值ca是一個“裝箱”對象,需要通過get/set訪問內部值,只有這樣才能保持計算值的引用不變而內部值又是可變的。
import { observable, autorun, computed } from "mobx";
const store = observable({
a: 1
});
const ca = computed(() => {
return 10 * store.a;
});
autorun(() => {
console.log(`${store.a} * 10 = ${ca.get()}`);
});
// 1 * 10 = 10
store.a += 1;
// 2 * 10 = 20
store.a += 1;
// 3 * 10 = 30
由于computed被視作是純函數,MobX 提供了許多開箱即用的優化措施,例如對計算值的緩存和惰性計算。
computed值會被緩存
每當讀取computed值時,如果其依賴的狀態或其他computed值未發生變化,則使用上次的緩存結果,以減少計算開銷,對于復雜的computed值,緩存可以大大提高性能。
例如下面例子中,computed值ca和cb分別依賴狀態a和b,第一次執行autorun時,ca和cb都會重新計算,然后修改狀態a的值,第二次執行autorun時,只有ca會重新計算,而cb則使用上次的緩存結果。
import { observable, autorun, computed } from "mobx";
const store = observable({
a: 1,
b: 1
});
const ca = computed(() => {
console.log("recomputed ca");
return 10 * store.a;
});
const cb = computed(() => {
console.log("recomputed cb");
return 10 * store.b;
});
autorun(() => {
console.log(
`a = ${store.a}, ca = ${ca.get()}, b = ${store.b}, cb = ${cb.get()}`
);
});
// recomputed ca
// recomputed cb
// a = 1, ca = 10, b = 1, cb = 10
store.a += 1;
// recomputed ca
// a = 2, ca = 20, b = 1, cb = 10
為了觀察computed的計算過程,插入了打印日志的語句,這會帶有副作用,實際中不要這樣做
computed值會惰性計算
只有computed值被使用時才重新計算值。反言之,即使computed值依賴的狀態發生了變化,但是它暫時沒有被使用,那么它不會重新計算。
例如下面例子中,computed值ca依賴狀態a。當狀態a的值小于 3 時,autorun運行時只打印狀態a的值,由于computed值ca未被使用,所以ca不會從新計算。當狀態a的值增長到 3 以后,autorun運行時同時打印狀態a和computed值ca,由于computed值ca被使用了,所以ca會重新計算。
import { observable, autorun, computed } from "mobx";
const store = observable({
a: 1
});
const ca = computed(() => {
console.log("recomputed ca");
return 10 * store.a;
});
autorun(() => {
if (store.a >= 3) {
console.log(`a = ${store.a}, ca = ${ca.get()}`);
} else {
console.log(`a = ${store.a}`);
}
});
// a = 1
store.a += 1;
// a = 2
store.a += 1;
// recomputed ca
// a = 3, ca = 30
action
action用于定義狀態修改操作,其類型定義如下(已簡化):
extends Function>(fn: T) => T
雖然沒有action也可以直接修改狀態,但是通過action顯式地修改狀態,使得狀態的變化可預測(狀態的變化能定位到是哪個action引起的)。此外,action函數是事務型的,通過action修改狀態時,響應函數不會立即執行,而是等到action結束后才執行,這有助于提升性能。
例如下面例子中,展示了修改狀態的兩種方式,一種是直接修改狀態a,另一種是通過調用預先定義的action修改狀態。值得注意的是,在一次action中連續兩次修改狀態a的值,只會觸發一次autorun的執行。
import { observable, autorun, action } from "mobx";
const store = observable({
a: 1
});
autorun(() => {
console.log(`a = ${store.a}`);
});
// a = 1
store.a += 1;
// a = 2
store.a += 1;
// a = 3
const increaseA = action(() => {
store.a += 1;
store.a += 1;
});
increaseA();
// a = 5
對 MobX 進行一些配置后,可以使action成為修改狀態的唯一方式,這可以避免不受約束的修改狀態行為發生,有利于提升項目的可維護性。
例如下面例子中,配置 enforceActions 為"always"后,就只能通過action修改狀態了,如果嘗試直接修改狀態將會觸發異常。
import { observable, autorun, action, configure } from "mobx";
// 強制只能通過 action 修改狀態
configure({
enforceActions: "always"
});
const store = observable({
a: 1
});
autorun(() => {
console.log(`a = ${store.a}`);
});
// a = 1
// store.a += 1;
// 直接修改狀態,將會拋出如下異常
// Error: [mobx] Since strict-mode is enabled, changing observed
// observable values outside actions is not allowed.
// Please wrap the code in an `action` if this change is intended.
const increaseA = action(() => {
store.a += 1;
});
increaseA();
// a = 2
MobX 與 React 集成
MobX 是框架無關的,你可以單獨使用它,也可以與任何流行的 UI 框架進行一起使用,MobX 官方提供了 React/Vue/Angular 等流行框架的綁定實現。MobX 最常見的是與 React 一起使用,他們的綁定實現是 mobx-react(或 mobx-react-lite)。
mobx-react 提供了一個observer方法, 它是一個高階組件,它接收 React 組件并返回一個新的 React 組件,返回的新組件能響應(通過observable定義的)狀態的變化,即組件能在可觀察狀態變化時自動更新。observer方法是對 MobX 提供的autorun方法和 React 組件更新機制的封裝,以便于在 React 中使用,你依然可以在 React 中使用autorun來更新組件。下面是observer方法的類型聲明,它支持組件類和函數組件。
function observer<T extends React.ComponentClass | React.FunctionComponent>(target: T): T
下面是組件類使用 MobX 的示例,通過observable定義可觀察狀態,并通過observer包裹組件,之后組件事件處理方法中修改狀態后,組件會自動更新,無需手動調用 React 的setState()來更新組件。
import React from "react";
import { observable } from "mobx";
import { observer } from "mobx-react";
class Counter extends React.Component {
constructor(props) {
super(props);
this.store = observable({
count: 0
});
}
render() {
return (
<button onClick={() => store.count++}>
{store.count}
button>
)
}
}
export default observer(Counter);
下面是函數組件使用 MobX 的示例,與上面類組件類似,區別是使用 mobx-react 提供的useLocalStore定義客觀察狀態,useLocalStore內部也是使用observable定義可觀察狀態。
import React, { useMemo } from "react";
import { observable } from "mobx";
import { observer, useLocalStore } from "mobx-react";
const Counter = () => {
const store = useLocalStore(() => ({
count: 0
}));
// 等價于下面
// const store = useMemo(() => observable({ count: 0 }), []);
return (
<button onClick={() => store.count++}>
{store.count}
button>
)
};
export default observer(Counter);
小結
MobX 的設計哲學是“可從應用狀態中派生的任何內容都應當自動的被派生”,后半句有兩個關鍵字:自動和派生。心中秉持這一設計哲學,再來看 MobX 的派生狀態模型就比較清晰了,Computed values 和 Reactions 都可以視作是從 State 中派生出的,State 變化時觸發 Computed values 的重新計算和 Reations 的重新運行。為了讓派生能自動的進行,MobX 通過Object.definePropery或Proxy方式攔截對象的讀寫操作,從而允許用戶以自然的方式來修改狀態,MobX 負責更新派生的內容。
MobX 提供了幾個核心 API 來幫助定義狀態(observable)、響應狀態(autorun, computed)和修改狀態(action),通過這些 API 可以讓程序立即具備響應式能力。這些 API 接口并不復雜,但要熟練使用,需要深入理解 MobX 響應機制,文中通過一些簡單的示例來輔助理解這些 API 的行為。
MobX 可以單獨使用,也可以與任何流行的 UI 框架一起使用,Github 上可以找到 MobX 與流行框架的綁定實現。不過 MobX 最常見的是與 React 一起使用,mobx-react 是流行的 MobX 和 React 的綁定實現庫,本文介紹了它在組件類和函數組件上的一些基本用法。
參考
MobX Documentations
mobx-react
關于本文 作者:@whinc 原文:https://github.com/whinc/blog/issues/16
他曾分享過
【第1611期】前端路由原理解析和實現
為你推薦
【第1586期】基于Redux/Vuex/MobX等庫的通用化狀態OOP
【第1302期】基于 MobX 構建視圖框架無關的數據層-與 Vue 的結合
【第1736期】現代 JavaScript 教程 - 代碼風格
總結
以上是生活随笔為你收集整理的mobx中跟新了数据视图没变化_【第1781期】MobX 简明教程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python json是什么_pytho
- 下一篇: 51单片机按键控制数码管0~9_51单片