天天看點

MobX 和 React 十分鐘快速入門

MobX

是一種簡單的、可擴充的、久經考驗的狀态管了解決方案。

這個教程将在十分鐘内向你詳解 MobX 的所有重要概念。MobX 是一個獨立的庫,但是大部分人将它和 React 共同使用,是以本教程将重點講解他們的結合使用。

核心理念

State 是所有應用的核心,沒有任何途徑比“建立不穩定的 State 或者建立與周圍本地變量不同步的State”更容易産生 bug 叢生、不可管理的應用了。

是以,許多 State 管了解決方案試圖限制可以變更狀态的方法,例如使其不可變(immutable)。

但這帶來了新的問題:資料需要規範化,無法保證引用的完整性,使用原型之類的強大概念幾乎是不可能的。

MobX 通過解決根本問題重新簡化了 State 管理工作:我們根本無法建立不穩定的 State。

達到這一目标的政策很簡單:保證從應用程式狀态派生出的所有内容都可以被自動地推導出來。

原理上,MobX 将你的應用看做是一個電子表格:

MobX 和 React 十分鐘快速入門
  1. 首先,我們看應用狀态(application state)。對象,數組,原型,引用組成了你的應用程式的 model。
  2. 其次,看看推導(derivations)。講道理,所有可以通過應用程式 state 自動計算出來的值都算推導。這些推導或計算的值,範圍包括從簡單的值(如未完成的 todo 數量),到複雜的值(如一個表示 todo 的可視化 HTML)。從電子表格的角度看:這些是應用程式的公式和圖表。
  3. 響應(Reactions) 與推導很類似。主要的差別是這些函數不産生值,而是自動地執行一些任務,這些任務通常與 I/O 相關。他們保證了在正确的時間自動地更新 DOM 或者發起網絡請求。
  4. 最後我們看看 行動(actions)。行動是所有改變 state 的事情。MobX 将保證所有由你的操作觸發的 state 變更都可以被所有的派生和響應處理。這個過程是同步且無故障的。

一個簡單的 todo store

理論講完了,實際操作試試可能比仔細閱讀上面的東西更能說明問題。出于創意,讓我們從一個非常簡單的 todo store 做起。注意:下面所有的代碼塊是可編輯的,可以點選 run code 按鈕執行它們(譯者注:臣妾做不到……詳細執行結果請參考原文)。下面是一個很簡單的

TodoStore

用來管理待辦事項。還沒有加入 MobX。

class TodoStore {
    todos = [];

    get completedTodosCount() {
        return this.todos.filter(
            todo => todo.completed === true
        ).length;
    }

    report() {
        if (this.todos.length === 0)
            return "<none>";
        return `Next todo: "${this.todos[0].task}". ` +
            `Progress: ${this.completedTodosCount}/${this.todos.length}`;
    }

    addTodo(task) {
        this.todos.push({
            task: task,
            completed: false,
            assignee: null
        });
    }
}

const todoStore = new TodoStore();           

複制

我們剛剛建立了一個包含

待辦事項

清單的一個

todoStore

執行個體。是時候給它填充一些對象了。為了保證我們可以看到我們改變的影響,我們在每個變更之後調用

todoStore.report

并列印它。注意這個報告故意隻列印第一個任務。這使得這個例子看起來有點别扭,但是你将看到它可以很好地說明 MobX 的依賴跟蹤是動态的。

todoStore.addTodo("read MobX tutorial");
console.log(todoStore.report());

todoStore.addTodo("try MobX");
console.log(todoStore.report());

todoStore.todos[0].completed = true;
console.log(todoStore.report());

todoStore.todos[1].task = "try MobX in own project";
console.log(todoStore.report());

todoStore.todos[0].task = "grok MobX tutorial";
console.log(todoStore.report());           

複制

Becoming reactive

到目前為止,這段代碼沒什麼特殊的。但是如果我們不需要明确地調用

report

,而是生命我們希望它在每次狀态的改變時被調用呢?這将使我們不再需要糾結在所有可能影響報告的地方調用 report。我們想要保證最新的報告被列印。但是我們不想糾結于怎麼去組織它。

值得慶幸的是,這正是 MobX 可以為你做到的。自動執行完全依賴 state 的代碼。是以我們的

report

函數像電子表格中的圖表一樣自動更新。為了實作這一目标,

TodoStore

需要變成可監視的(observable)以保證 MobX 可以追蹤到所有改變。讓我們一起改改代碼來實作它。

進一步,

completedTodosCount

屬性可以由 todo list 自動推導而來。我們可以使用

@observable

@computed

裝飾器為一個對象增加 observable 屬性:

class ObservableTodoStore {
    @observable todos = [];
    @observable pendingRequests = 0;

    constructor() {
        mobx.autorun(() => console.log(this.report));
    }

    @computed get completedTodosCount() {
        return this.todos.filter(
            todo => todo.completed === true
        ).length;
    }

    @computed get report() {
        if (this.todos.length === 0)
            return "<none>";
        return `Next todo: "${this.todos[0].task}". ` +
            `Progress: ${this.completedTodosCount}/${this.todos.length}`;
    }

    addTodo(task) {
        this.todos.push({
            task: task,
            completed: false,
            assignee: null
        });
    }
}


const observableTodoStore = new ObservableTodoStore();           

複制

搞定啦!我們為 MobX 标記了一些

@observable

屬性,這些屬性的值可以随時改變。計算值是用

@computed

标記以表示他們可以由 state 推導出來。

pendingRequests

assignee

屬性現在還沒被使用,但是将會在本教程的後面被使用。為了簡潔,本頁中的例子都使用了 ES6、JSX 和裝飾器(decorators)。但是不要擔心,MobX 中所有的裝飾器對應有 ES5 的形式。

在構造函數中,我們建立了一個小函數來列印

report

并用

autorun

包裹它。autorun 建立了一個 響應(Reaction) 并執行一次,之後這個函數中任何 observable 資料變更時,響應都會被自動執行。由于

report

使用了 observable

todos

屬性,是以它将會在所有合适的時刻列印 report。下面的例子可以說明這一點,隻需要點選一下 run 按鈕(譯者:……):

observableTodoStore.addTodo("read MobX tutorial");
observableTodoStore.addTodo("try MobX");
observableTodoStore.todos[0].completed = true;
observableTodoStore.todos[1].task = "try MobX in own project";
observableTodoStore.todos[0].task = "grok MobX tutorial";           

複制

很好玩對不對?

report

自動地列印了,這個過程是自動的且沒有中間變量洩露。如果你仔細研究日志,你會發現第四行沒有生成新的日志行。因為 report 并沒有 真正地 因為重命名而改變,盡管底層資料确實變了。而變更第一個 todo 的名字改變了 report,因為它的 name 被 report 使用了。這充分地說明了

autorun

不隻監聽了

todo

數組,而且還監聽了 todo 元素中的個别屬性。

讓 React 變得有響應(reactive)

好了,目前為止我們建立了一個簡單的響應式 report。是時候在這個 store 周圍構造一個響應式的使用者接口了。React 元件無法對外界作出反應(除了自己的名字)。

mobx-react

包的

@observer

裝飾器通過将 React 元件的

render

方法包裹在

autorun

中解決了這一問題,它自動地保持你的元件和 state 同步。理論上這和我們之前對

report

的做法沒什麼差別。

下面的例子定義了一些 React 元件。這些元件中隻有

@observer

是屬于的 MobX 的。但它足以保證所有的元件都可以在相關資料變更時獨立地重新渲染。你不再需要調用

setState

,也不必考慮如何通過配置選擇器或高階元件來訂閱應用程式 state 的适當部分。可以說,所有的元件都變得智能化。不過他們是以愚蠢的聲明的方式定義的。

點選 Run code 按鈕檢視下面代碼的結果。這個例子是可編輯的,是以你可以随便在裡面玩耍。試着删掉所有的

@oberver

或者隻删掉裝飾

TodoView

的那一個。右邊預覽中的數字會在每次元件重新渲染的時候高亮。

@observer
class TodoList extends React.Component {
  render() {
    const store = this.props.store;
    return (
      <div>
        { store.report }
        <ul>
        { store.todos.map(
          (todo, idx) => <TodoView todo={ todo } key={ idx } />
        ) }
        </ul>
        { store.pendingRequests > 0 ? <marquee>Loading...</marquee> : null }
        <button onClick={ this.onNewTodo }>New Todo</button>
        <small> (double-click a todo to edit)</small>
        <RenderCounter />
      </div>
    );
  }

  onNewTodo = () => {
    this.props.store.addTodo(prompt('Enter a new todo:','coffee plz'));
  }
}

@observer
class TodoView extends React.Component {
  render() {
    const todo = this.props.todo;
    return (
      <li onDoubleClick={ this.onRename }>
        <input
          type='checkbox'
          checked={ todo.completed }
          onChange={ this.onToggleCompleted }
        />
        { todo.task }
        { todo.assignee
          ? <small>{ todo.assignee.name }</small>
          : null
        }
        <RenderCounter />
      </li>
    );
  }

  onToggleCompleted = () => {
    const todo = this.props.todo;
    todo.completed = !todo.completed;
  }

  onRename = () => {
    const todo = this.props.todo;
    todo.task = prompt('Task name', todo.task) || todo.task;
  }
}

ReactDOM.render(
  <TodoList store={ observableTodoStore } />,
  document.getElementById('reactjs-app')
);           

複制

下一個例子完美地展現了我們不需要做任何别的事情就可以改變我們的資料。MobX 将會從 store 的 state 中自動地派生并更新使用者界面相關的部分。

const store = observableTodoStore;
store.todos[0].completed = !store.todos[0].completed;
store.todos[1].task = "Random todo " + Math.random();
store.todos.push({ task: "Find a fine cheese", completed: true });
// etc etc.. add your own statements here...           

複制

使用引用

到目前為止,我們已經建立了 observable 對象(包括原型和普通對象),數組和原語。你可能會驚訝,MobX 是如何操作這些引用的?是我們的 state 可以被用于建立一個圖表嗎?在上面的例子中,你可能發現 todo 上有一個

assignee

屬性。讓我們通過引入另一個包含人員資訊的“store”(其實,它隻是一個美化的數組)來給他們一些值,并将任務配置設定給他們。

var peopleStore = mobx.observable([
    { name: "Michel" },
    { name: "Me" }
]);
observableTodoStore.todos[0].assignee = peopleStore[0];
observableTodoStore.todos[1].assignee = peopleStore[1];
peopleStore[0].name = "Michel Weststrate";           

複制

我們現在擁有兩個獨立的 store。一個包含人員資訊,另一個包含 todo 資訊。為人員 store 中的一個人賦予一個

assignee

,我們隻需要添加一個引用。這些改變會被

TodoView

自動擷取。在 MobX 的幫助下,我們不需要先格式化資料并寫相應的選擇器以保證我們的元件可以被更新。實際上,甚至是資料的存儲位置也并不重要。隻要對象被設定為 obervable,MobX 将可以追蹤他們。真實的 JavaScript 引用将會起作用。如果它們與一個派生有關,那麼 MobX 将自動地追蹤它們。為了測試這一點,隻需要嘗試改變下面的 input 框中的名字(測試前先確定你點選了 Run Code 按鈕!)。

異步操作

由于我們的 Todo 小應用中的所有資料都是派生自 state,是以 state 何時改變并不重要。這使得建立異步操作變得異常簡單。點選下面的按鈕(多次)以模拟異步地建立新的待辦項。

Load todo

後面的代碼給常簡單。我們首先通過更新

pendingRequests

這一 store 屬性使 UI 顯示目前的加載狀态。當加載結束之後,沃恩跟新 store 中的待辦項并再次減少

pendingRequests

計數。将這段代碼與上面的

TodoList

定義相比較以學習如何使用 pendingRequests 屬性。

observableTodoStore.pendingRequests++;
setTimeout(function() {
    observableTodoStore.addTodo('Random Todo ' + Math.random());
    observableTodoStore.pendingRequests--;
}, 2000);           

複制

開發工具

mobx-react-devtools

包提供了一個被用于 MobX + ReactJS 應用的顯示在螢幕右上角的開發工具。點選第一個按鈕将會高亮每一個被重新渲染的

@observer

元件。如果你點選第二個按鈕,預覽中的元件依賴樹将會顯示出來,你可以在任何時候準确地檢測出它正在觀察的是哪一段資料。

結論

就這麼多!沒有樣闆。隻有一些簡單的聲明式元件用來形成我們整體的 UI。這份 UI 完全響應式地派生自我們的 state。你現在可以開始在你的應用中使用

mobx

mobx-react

包啦。下面對你目前學到的東西做一個簡要總結:

  1. 使用

    @observable

    裝飾器或

    observable(objectorarray)

    函數使 MobX 可以追蹤對象。
  2. @computed

    裝飾器可被用于建立基于 state 自動計算值的函數。
  3. 使用

    autorun

    來自動地運作依賴于 observable state 的函數。這對于打日志、發起網絡請求等來說很有用。
  4. 使用

    mobx-react

    包中的

    @observer

    裝飾器将你的 React 元件變得真正的可響應。他們将會自動并有效地更新。即使是在用夠大量資料的大型複雜項目中。

多花點時間玩玩上面的可編輯代碼塊,以對 MobX 如何對你的操作作出響應有一個基本的概念。例如,你可以為 report 函數增加一個 log 語句來看它什麼時候被調用;或者完全不要顯示

report

來看看會對

TodoList

的渲染造成什麼影響;或者在某些情況下不要顯示它……

MobX 不是一個 state 容器

人們通常将 MobX 當做是 Redux 的替代。但是請注意,MobX 隻是一個解決技術問題的庫,其本身并沒有 state 容器。從這個意義上說,上面的例子是人為設計的,是以我們建議您使用适當的工程實踐,如在方法中封裝邏輯、在 store 或控制器中組織它們等等。或者,像 HackerNews 中某位使用者說的:

“MobX,它總是被提起,但我忍不住要贊美它。使用 MobX 寫東西意味着它可以完成所有控制器(controllers) / 排程員(dispatchers) / 操作(actions) / 管理程式(supervisors) 以及其他管理資料流需要考慮的工作,而不隻是完成一個 Todo 應用預設要求完成的那些工作。”
往期精選文章
使用虛拟dom和JavaScript建構完全響應式的UI架構
擴充 Vue 元件
使用Three.js制作酷炫無比的無窮隧道特效
一個治愈JavaScript疲勞的學習計劃
全棧工程師技能大全
WEB前端性能優化常見方法
一小時内搭建一個全棧Web應用架構
幹貨:CSS 專業技巧
四步實作React頁面過渡動畫效果
讓你分分鐘了解 JavaScript 閉包

小手一抖,資料全有。長按二維碼關注京程一燈,閱讀更多技術文章和業界動态。