什麼是前端狀態管理?

近兩年前端技術的發展如火如荼,大量的前端項目都在使用或轉向 Vue 和 React 的陣營,由前端渲染頁面的單頁應用佔比也越來越高,這就代表 前端工作的複雜度也在直線上升,前端頁面上展示的信息越來越多也越來 越複雜。我們知道,任何狀態都需要進行管理,那麼今天我們來聊聊前端 狀態管理。

爲什麼需要狀態管理?

舉個例子:

圖書館的管理,原來是開放式的,所有人可以隨意進出書庫借書還書,如 果人數不多,這種方式可以減少流程,增加效率,一旦人數變多就勢必造 成混亂。

Flux 思想就像是給這個圖書館加上了一個管理員(究竟什麼是 flux 思想後 面會說),所有借書還書的行爲都需要委託管理員去做,管理員會規範對 書庫的操作行爲,也會記錄每個人的操作,減少混亂的現象。

一個比喻:

我們寄一件東西的過程

沒有快遞時: 打包準備好要送出去的東西 直接到朋友家,把東西送給朋友 很直接很方便,很費時間

有了快遞公司: 打包準備好要送出去的東西 到快遞公司,填寫物品,收件人等基本信息 快遞公司替你送物品到你的朋友家,我們的工作結束了。

多了快遞公司,讓快遞公司給我們送快遞。 當我們只寄送物品給一個朋友,次數較少,物品又較少的時候,我們直接 去朋友家就挺好的。但當我們要頻繁寄送給很多朋友很多商品的時候,問 題就複雜了。

軟件工程的本質即是管理複雜度。使用狀態管理類框架會有一定的學習成 本而且通常會把簡單的事情做複雜,但如果我們想做複雜一點的事情(同時寄很多物品到多個不同地址),對我們來說,快遞會讓複雜的事情變的 簡單。

這同時也解釋了,是否需要添加狀態管理框架,我們可以根據自己的業務 實際情況和技術團隊的偏好而添加,有些情況下,創建一個全局對象就能 解決很多問題。

在工作中多個狀態互相綁定的應用場景也是蠻多的,大家可以看看各大電 商的購物車結算頁面,包括大家定外賣的購物車頁面。複雜之處主要在於 業務狀態多(比如商品狀態、店鋪優惠券和組合套餐等)、跨層級或位置 狀態聯動多(勾選購物車商品,先請求獲得被勾選商品最新價格、狀態等, 再計算價格等在上面和下面展示總價等)。隨着不同操作,分佈在不同地 方的代碼響應不同的邏輯和數據。

如何解決頁面之間的多狀態問題?

在 Web 應用開發中,AngularJS 扮演了重要角色。然而 AngularJS 數據和 視圖的雙向綁定基於髒檢測的機制,在性能上存在短板,任何數據的變更 都會重繪整個視圖。但是,由狀態反應視圖、自動更新頁面的思想是先進 的,爲了解決性能上的問題,Facebook 的工程師們提出了 Virtual DOM 的 思想。將 DOM 放到內存中,state 發生變化的時候,根據 state 生成新 的 Virtual DOM,再將它和之前的 Virtual DOM 通過一個 diff 算法進行對 比,將被改變的內容在瀏覽器中渲染,避免了 JS 引擎頻繁調用渲染引擎 的 DOM 操作接口,充分利用了 JS 引擎的性能。有了 Virtual DOM 的支 持,React 也誕生了。

有了 React,「state => view」的思想也就有了很好的實踐,但反過來呢, 怎麼在 view 中合理地修改 state 成爲了一個新的問題,爲此,Facebook 提出了 Flux 思想。

是的,Flux 不是某一個 JS 庫的名稱,而是一種架構思想,很多 JS 庫則 是這種思想的實現,例如 Alt、Fluxible 等,它用於構建客戶端 Web 應用, 規範數據在 Web 應用中的流動方式。

那麼這個和狀態管理有什麼關係呢?我們知道,React 或是 Vue 只是一個 視圖層的庫,並沒有對數據層有任何的限制,換言之任何視圖組件中都可 能存在改變數據層的代碼,而過度放權對於數據層的管理是不利的,另外一旦數據層出現問題將會很難追溯,因爲不知道變更是從哪些組件發起的。 另外,如果數據是由父組件通過 props 的方式傳給子組件的話,組件之間 會產生耦合,違背了模塊化的原則。

而 Flux 的思維方式是單向的,將之前放權到各個組件的修改數據層的 controller 代碼收歸一處,統一管理,組件需要修改數據層的話需要去觸 發特定的預先定義好的 dispatcher,然後 dispatcher 將 action 應用到 model 上,實現數據層的修改。然後數據層的修改會應用到視圖上,形成 一個單向的數據流。

Flux 思想的實現

Flux 的實現有很多,不同的實現也各有亮點,下面介紹一些比較流行的 Flux 的實現。

Flux

這應該是 Flux 的一個比較官方”的實現,顯得中規中矩,實現了 Flux 架 構文檔裏的基本概念。它的核心是 Dispatcher,通過 Dispatcher,用戶可 以註冊需要相應的 action 類型,對不同的 action 註冊對應的回調,以及 觸發 action 並傳遞 payload 數據。

Redux

Redux 實際上相當於 Reduce + Flux,和 Flux 相同,Redux 也需要你維護 一個數據層來表現應用的狀態,而不同點在於 Redux 不允許對數據層進 行修改,只允許你通過一個 Action 對象來描述需要做的變更。在 Redux 中,去掉了 Dispatcher,轉而使用一個純函數來代替,這個純函數接收原 state tree 和 action 作爲參數,並生成一個新的 state tree 代替原來的。 而這個所謂的純函數,就是 Redux 中的重要概念 —— Reducer。

在函數式編程中,Reduce 操作的意思是通過遍歷一個集合中的元素並依 次將前一次的運算結果代入下一次運算,並得到最終的產物,在 Redux 中, reducer 通過合併計算舊 state 和 action 並得到一個新 state 則反映了 這樣的過程。

因此,Redux 和 Flux 的第二個區別則是 Redux 不會修改任何一個 state, 而是用新生成的 state 去代替舊的。這實際上是應用了不可變數據 (Immutable Data),在 reducer 中直接修改原 state 是被禁止的, Facebook 的 Immutable 庫可以幫助你使用不可變數據,例如構建一個可 以在 Redux 中使用的 Store。

下面是一個用 Redux 構建應用的狀態管理的示例:

const { List } = require('immutable')
const initialState = {
    books: List([])
}
import { createStore } from 'redux'

// action
const addBook = (book) => {
    return {
        type: ADD_BOOK,
        book
    }
}

// reducer
const books = (state = initialState, action) => {
    switch (action.type) {
        case ADD_BOOK:
        return Object.assign({}, state, {
            books: state.books.push(action.book)
        })
    }
    return state
}

// store
const bookStore = createStore(books, initialState)

// dispatching action
store.dispatch(addBook({/* new book */}))

Redux 的工作方式遵循了嚴格的單向數據流原則,從上面的代碼示例中可 以看出,整個生命週期分爲:

在 store 中調用 dispatch,並傳入 action 對象。action 對象是一個描述 變化的普通對象,在示例中,它由一個 creator 函數生成。

接下來,store 會調用註冊 store 時傳入的 reducer 函數,並將當前的 state 和 action 作爲參數傳入,在 reducer 中,通過計算得到新的 state並返回。 store 將 reducer 生成的新 state 樹保存下來,然後就可以用新的 state 去生成新的視圖,這一步可以藉助一些庫的幫助,例如官方推薦的 React Redux。

如果一個應用規模比較大的話,可能會面臨 reducer 過大的問題。這時候 我們可以對 reducer 進行拆分,例如使用 combineReducers,將多個 reducer 作爲參數傳入,生成新的 reducer。當觸發一個 action 的時候, 新 reducer 會觸發原有的多個 reducer:

const book(state = [], action) => {
    // ...
    return newState
}
const author(state = {}, action) => {
    // ...
    return newState
}
const reducer = combineReducers({ book, author })

關於 Redux 的更多用法,可以仔細閱讀文檔,這裏就不多介紹了。

Vuex

中國前端業務中使用 Vue 的比例是最高的,說到 Vue 中的狀態管理就不 得不提到 Vuex。Vuex 也是基於 Flux 思想的產品,所以在某種意義上它 和 Redux 很像,但又有不同,下面通過 Vuex 和 Redux 的對比來看看 Vuex 有什麼區別。

首先,和 Redux 中使用不可變數據來表示 state 不同,Vuex 中沒有 reducer 來生成全新的 state 來替換舊的 state,Vuex 中的 state 是可以 被修改的。這麼做的原因和 Vue 的運行機制有關係,Vue 基於 ES5 中的 getter/setter 來實現視圖和數據的雙向綁定,因此 Vuex 中 state 的變更 可以通過 setter 通知到視圖中對應的指令來實現視圖更新。

另外,在 Vuex 中也可以記錄每次 state 改變的具體內容,state 的變更可 被記錄與追蹤。例如 Vue 的官方調試工具中就集成了 Vuex 的調試工具, 使用起來和 Redux 的調試工具很相似,都可以根據某次變更的 state 記 錄實現視圖快照。

上面說到,Vuex 中的 state 是可修改的,而修改 state 的方式不是通過 actions,而是通過 mutations。一個 mutation 是由一個 type 和與其對應 的 handler 構成的,type 是一個字符串類型用以作爲 key 去識別具體的 某個 mutation,handler 則是對 state 實際進行變更的函數。

// store
const store = {
    books: []
}
// mutations
const mutations = {
    [ADD_BOOKS](state, book) {
        state.books.push(book)
    }
}

那麼 action 呢?Vuex 中的 action 也是 store 的組成部分,它可以被看 成是連接視圖與 state 的橋樑,它會被視圖調用,並由它來調用 mutation handler,向 mutation 傳入 payload。

這時問題來了,Vuex 中爲什麼要增加 action 這一層呢,是多此一舉嗎?

Vuex 核心的概念——mutation 必須是同步函數,而 action 可以包含任意的異步操作。 回到這個問題本身,如果在視圖中不進行異步操作(例如調用後端 API) 只是觸發 action 的話,異步操作將會在 action 內部執行:

const actions = {
    addBook({ commit }) {
        request.get(BOOK_API).then(res => commit(ADD_BOOK, res.body.new_book))
    }
}

可以看出,這裏的狀態變更相當於是 action 產生的副作用,mutation 的 作用是將這些副作用記錄下來,這樣就形成了一個完整數據流閉環,數據 流的順序如下:

在視圖中觸發 action,並根據實際情況傳入需要的參數。

在 action 中觸發所需的 mutation,在 mutation 函數中改變 state。

通過 getter/setter 實現的雙向綁定會自動更新對應的視圖。

MobX

MobX 是一個比較新的狀態管理庫,它的前身是 Mobservable,實際上 MobX 相當於是 Mobservable 的 2.0 版本。

Mobx 和 Redux 相比,差別就比較大了。如果說 Redux 吸收併發揚了很 多函數式編程思想的話,Mobx 則更多體現了面向對象及的特點。MobX 的特點總結起來有以下幾點:

Observable。它的 state 是可被觀察的,無論是基本數據類型還是引用數 據類型,都可以使用 MobX 的 (@)observable 來轉變爲 observable value。

Reactions。它包含不同的概念,基於被觀察數據的更新導致某個計算值 (computed values),或者是發送網絡請求以及更新視圖等,都屬於響應的範疇,這也是響應式編程(Reactive Programming)在 JavaScript 中的一 個應用。

Actions。它相當於所有響應的源頭,例如用戶在視圖上的操作,或是某個 網絡請求的響應導致的被觀察數據的變更。 和 Redux 對單向數據流的嚴格規範不同,Mobx 只專注於從 store 到 view 的過程。在 Redux 中,數據的變更需要監聽(可見上文 Redux 示例 代碼),而 Mobx 的數據依賴是基於運行時的,這點和 Vuex 更爲接近。

總結

狀態管理的研究並不是前端領域獨有的問題,實際上前端狀態管理的很多 思想都是借鑑於成熟很多的軟件開發體系。相對於軟件開發,前端還是一 個很新的領域,只有多學習其他領域的優秀經驗前端界才能發展得更好。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章