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