摘要:也就是說不應該有公開的,所有都應該是私有的,只能有公開的。允許使用方法設置監聽函數,一旦發生變化,就自動執行這個函數。用一個叫做的純函數來處理事件??梢酝ㄟ^得到當前狀態。在中,同步的表現就是發出以后,立即算出。
這篇文章試著聊明白這一堆看起來挺復雜的東西。在聊之前,大家要始終記得一句話:一切前端概念,都是紙老虎。
不管是Vue,還是 React,都需要管理狀態(state),比如組件之間都有共享狀態的需要。什么是共享狀態?比如一個組件需要使用另一個組件的狀態,或者一個組件需要改變另一個組件的狀態,都是共享狀態。
父子組件之間,兄弟組件之間共享狀態,往往需要寫很多沒有必要的代碼,比如把狀態提升到父組件里,或者給兄弟組件寫一個父組件,聽聽就覺得挺啰嗦。
如果不對狀態進行有效的管理,狀態在什么時候,由于什么原因,如何變化就會不受控制,就很難跟蹤和測試了。如果沒有經歷過這方面的困擾,可以簡單理解為會搞得很亂就對了。
在軟件開發里,有些通用的思想,比如隔離變化,約定優于配置等,隔離變化就是說做好抽象,把一些容易變化的地方找到共性,隔離出來,不要去影響其他的代碼。約定優于配置就是很多東西我們不一定要寫一大堆的配置,比如我們幾個人約定,view 文件夾里只能放視圖,不能放過濾器,過濾器必須放到 filter 文件夾里,那這就是一種約定,約定好之后,我們就不用寫一大堆配置文件了,我們要找所有的視圖,直接從 view 文件夾里找就行。
根據這些思想,對于狀態管理的解決思路就是:把組件之間需要共享的狀態抽取出來,遵循特定的約定,統一來管理,讓狀態的變化可以預測。根據這個思路,產生了很多的模式和庫,我們來挨個聊聊。
Store 模式最簡單的處理就是把狀態存到一個外部變量里面,比如:this.$root.$data,當然也可以是一個全局變量。但是這樣有一個問題,就是數據改變后,不會留下變更過的記錄,這樣不利于調試。
所以我們稍微搞得復雜一點,用一個簡單的 Store 模式:
var store = { state: { message: "Hello!" }, setMessageAction (newValue) { // 發生改變記錄點日志啥的 this.state.message = newValue }, clearMessageAction () { this.state.message = "" } }
store 的 state 來存數據,store 里面有一堆的 action,這些 action 來控制 state 的改變,也就是不直接去對 state 做改變,而是通過 action 來改變,因為都走 action,我們就可以知道到底改變(mutation)是如何被觸發的,出現錯誤,也可以記錄記錄日志啥的。
不過這里沒有限制組件里面不能修改 store 里面的 state,萬一組件瞎胡修改,不通過 action,那我們也沒法跟蹤這些修改是怎么發生的。所以就需要規定一下,組件不允許直接修改屬于 store 實例的 state,組件必須通過 action 來改變 state,也就是說,組件里面應該執行 action 來分發 (dispatch) 事件通知 store 去改變。這樣約定的好處是,我們能夠記錄所有 store 中發生的 state 改變,同時實現能做到記錄變更 (mutation)、保存狀態快照、歷史回滾/時光旅行的先進的調試工具。
這樣進化了一下,一個簡單的 Flux 架構就實現了。
FluxFlux其實是一種思想,就像MVC,MVVM之類的,他給出了一些基本概念,所有的框架都可以根據他的思想來做一些實現。
Flux把一個應用分成了4個部分:
View
Action
Dispatcher
Store
比如我們搞一個應用,顯而易見,這個應用里面會有一堆的 View,這個 View 可以是Vue的,也可以是 React的,啥框架都行,啥技術都行。
View 肯定是要展示數據的,所謂的數據,就是 Store,Store 很容易明白,就是存數據的地方。當然我們可以把 Store 都放到一起,也可以分開來放,所以就有一堆的 Store。但是這些 View 都有一個特點,就是 Store 變了得跟著變。
View 怎么跟著變呢?一般 Store 一旦發生改變,都會往外面發送一個事件,比如 change,通知所有的訂閱者。View 通過訂閱也好,監聽也好,不同的框架有不同的技術,反正 Store 變了,View 就會變。
View 不是光用來看的,一般都會有用戶操作,用戶點個按鈕,改個表單啥的,就需要修改 Store。Flux 要求,View 要想修改 Store,必須經過一套流程,有點像我們剛才 Store 模式里面說的那樣。視圖先要告訴 Dispatcher,讓 Dispatcher dispatch 一個 action,Dispatcher 就像是個中轉站,收到 View 發出的 action,然后轉發給 Store。比如新建一個用戶,View 會發出一個叫 addUser 的 action 通過 Dispatcher 來轉發,Dispatcher 會把 addUser 這個 action 發給所有的 store,store 就會觸發 addUser 這個 action,來更新數據。數據一更新,那么 View 也就跟著更新了。
這個過程有幾個需要注意的點:
Dispatcher 的作用是接收所有的 Action,然后發給所有的 Store。這里的 Action 可能是 View 觸發的,也有可能是其他地方觸發的,比如測試用例。轉發的話也不是轉發給某個 Store,而是所有 Store。
Store 的改變只能通過 Action,不能通過其他方式。也就是說 Store 不應該有公開的 Setter,所有 Setter 都應該是私有的,只能有公開的 Getter。具體 Action 的處理邏輯一般放在 Store 里。
聽聽描述看看圖,可以發現,Flux的最大特點就是數據都是單向流動的。
ReduxFlux 有一些缺點(特點),比如一個應用可以擁有多個 Store,多個Store之間可能有依賴關系;Store 封裝了數據還有處理數據的邏輯。
所以大家在使用的時候,一般會用 Redux,他和 Flux 思想比較類似,也有差別。
StoreRedux 里面只有一個 Store,整個應用的數據都在這個大 Store 里面。Store 的 State 不能直接修改,每次只能返回一個新的 State。Redux 整了一個 createStore 函數來生成 Store。
import { createStore } from "redux"; const store = createStore(fn);
Store 允許使用 ?store.subscribe ?方法設置監聽函數,一旦 State 發生變化,就自動執行這個函數。這樣不管 View 是用什么實現的,只要把 View 的更新函數 subscribe 一下,就可以實現 State 變化之后,View 自動渲染了。比如在 React 里,把組件的render方法或setState方法訂閱進去就行。
Action和 Flux ?一樣,Redux 里面也有 Action,Action 就是 View 發出的通知,告訴 Store State 要改變。Action 必須有一個 type 屬性,代表 Action 的名稱,其他可以設置一堆屬性,作為參數供 State 變更時參考。
const action = { type: "ADD_TODO", payload: "Learn Redux" };
Redux 可以用 Action Creator 批量來生成一些 Action。
ReducerRedux 沒有 Dispatcher 的概念,Store 里面已經集成了 dispatch 方法。store.dispatch()是 View 發出 Action 的唯一方法。
import { createStore } from "redux"; const store = createStore(fn); store.dispatch({ type: "ADD_TODO", payload: "Learn Redux" });
Redux 用一個叫做 Reducer 的純函數來處理事件。Store 收到 Action 以后,必須給出一個新的 State(就是剛才說的Store 的 State 不能直接修改,每次只能返回一個新的 State),這樣 View 才會發生變化。這種 State 的計算過程就叫做 Reducer。
什么是純函數呢,就是說沒有任何的副作用,比如這樣一個函數:
function getAge(user) { user.age = user.age + 1; return user.age; }
這個函數就有副作用,每一次相同的輸入,都可能導致不同的輸出,而且還會影響輸入 user 的值,再比如:
let b = 10; function compare(a) { return a >= b; }
這個函數也有副作用,就是依賴外部的環境,b 在別處被改變了,返回值對于相同的 a 就有可能不一樣。
而 Reducer 是一個純函數,對于相同的輸入,永遠都只會有相同的輸出,不會影響外部的變量,也不會被外部變量影響,不得改寫參數。它的作用大概就是這樣,根據應用的狀態和當前的 action 推導出新的 state:
(previousState, action) => newState
類比 Flux,Flux 有些像:
(state, action) => state
為什么叫做 Reducer 呢?reduce ?是一個函數式編程的概念,經常和 ?map ?放在一起說,簡單來說,map ?就是映射,reduce ?就是歸納。映射就是把一個列表按照一定規則映射成另一個列表,而 reduce 是把一個列表通過一定規則進行合并,也可以理解為對初始值進行一系列的操作,返回一個新的值。
比如 ?Array 就有一個方法叫 reduce,Array.prototype.reduce(reducer, ?initialValue),把 Array 整吧整吧弄成一個 ?newValue。
const array1 = [1, 2, 3, 4]; const reducer = (accumulator, currentValue) => accumulator + currentValue; // 1 + 2 + 3 + 4 console.log(array1.reduce(reducer)); // expected output: 10 // 5 + 1 + 2 + 3 + 4 console.log(array1.reduce(reducer, 5)); // expected output: 15
看起來和 Redux 的 Reducer 是不是好像好像,Redux 的 Reducer 就是 reduce 一個列表(action的列表)和一個 initialValue(初始的 ?State)到一個新的 value(新的 ?State)。
把上面的概念連起來,舉個例子:
下面的代碼聲明了 reducer:
const defaultState = 0; const reducer = (state = defaultState, action) => { switch (action.type) { case "ADD": return state + action.payload; default: return state; } };
createStore接受 Reducer 作為參數,生成一個新的 Store。以后每當store.dispatch發送過來一個新的 Action,就會自動調用 Reducer,得到新的 State。
import { createStore } from "redux"; const store = createStore(reducer);
createStore?內部干了什么事兒呢?通過一個簡單的 createStore 的實現,可以了解大概的原理(可以略過不看):
const createStore = (reducer) => { let state; let listeners = []; const getState = () => state; const dispatch = (action) => { state = reducer(state, action); listeners.forEach(listener => listener()); }; const subscribe = (listener) => { listeners.push(listener); return () => { listeners = listeners.filter(l => l !== listener); } }; dispatch({}); return { getState, dispatch, subscribe }; };
Redux 有很多的 Reducer,對于大型應用來說,State 必然十分龐大,導致 Reducer 函數也十分龐大,所以需要做拆分。Redux ?里每一個 Reducer 負責維護 State 樹里面的一部分數據,多個 Reducer 可以通過 combineReducers 方法合成一個根 Reducer,這個根 Reducer 負責維護整個 State。
import { combineReducers } from "redux"; // 注意這種簡寫形式,State 的屬性名必須與子 Reducer 同名 const chatReducer = combineReducers({ Reducer1, Reducer2, Reducer3 })
combineReducers 干了什么事兒呢?通過簡單的 combineReducers 的實現,可以了解大概的原理(可以略過不看):
const combineReducers = reducers => { return (state = {}, action) => { return Object.keys(reducers).reduce( (nextState, key) => { nextState[key] = reducers[key](state[key], action); return nextState; }, {} ); }; };流程
再回顧一下剛才的流程圖,嘗試走一遍 ?Redux ?流程:
1、用戶通過 View 發出 Action:
store.dispatch(action);
2、然后 Store 自動調用 Reducer,并且傳入兩個參數:當前 State 和收到的 Action。 Reducer 會返回新的 State 。
let nextState = xxxReducer(previousState, action);
3、State 一旦有變化,Store 就會調用監聽函數。
store.subscribe(listener);
4、listener可以通過 ?store.getState() ?得到當前狀態。如果使用的是 React,這時可以觸發重新渲染 View。
function listerner() { let newState = store.getState(); component.setState(newState); }對比 Flux
和 ?Flux ?比較一下:Flux 中 Store 是各自為戰的,每個 Store 只對對應的 View 負責,每次更新都只通知對應的View:
Redux 中各子 Reducer 都是由根 Reducer 統一管理的,每個子 Reducer 的變化都要經過根 Reducer 的整合:
簡單來說,Redux有三大原則:
單一數據源:Flux 的數據源可以是多個。
State 是只讀的:Flux 的 State 可以隨便改。
使用純函數來執行修改:Flux 執行修改的不一定是純函數。
Redux 和 Flux 一樣都是單向數據流。
中間件剛才說到的都是比較理想的同步狀態。在實際項目中,一般都會有同步和異步操作,所以 Flux、Redux 之類的思想,最終都要落地到同步異步的處理中來。
在 ?Redux ?中,同步的表現就是:Action 發出以后,Reducer 立即算出 State。那么異步的表現就是:Action 發出以后,過一段時間再執行 Reducer。
那怎么才能 Reducer 在異步操作結束后自動執行呢?Redux 引入了中間件 Middleware 的概念。
其實我們重新回顧一下剛才的流程,可以發現每一個步驟都很純粹,都不太適合加入異步的操作,比如 Reducer,純函數,肯定不能承擔異步操作,那樣會被外部IO干擾。Action呢,就是一個純對象,放不了操作。那想來想去,只能在 View 里發送 Action 的時候,加上一些異步操作了。比如下面的代碼,給原來的 ?dispatch ?方法包裹了一層,加上了一些日志打印的功能:
let next = store.dispatch; store.dispatch = function dispatchAndLog(action) { console.log("dispatching", action); next(action); console.log("next state", store.getState()); }
既然能加日志打印,當然也能加入異步操作。所以中間件簡單來說,就是對 store.dispatch 方法進行一些改造的函數。不展開說了,所以如果想詳細了解中間件,可以點這里。
Redux 提供了一個 applyMiddleware 方法來應用中間件:
const store = createStore( reducer, applyMiddleware(thunk, promise, logger) );
這個方法主要就是把所有的中間件組成一個數組,依次執行。也就是說,任何被發送到 store 的 action 現在都會經過thunk,promise,logger 這幾個中間件了。
處理異步對于異步操作來說,有兩個非常關鍵的時刻:發起請求的時刻,和接收到響應的時刻(可能成功,也可能失敗或者超時),這兩個時刻都可能會更改應用的 state。一般是這樣一個過程:
請求開始時,dispatch ?一個請求開始 Action,觸發 State 更新為“正在請求”狀態,View 重新渲染,比如展現個Loading啥的。
請求結束后,如果成功,dispatch ?一個請求成功 Action,隱藏掉 ?Loading,把新的數據更新到 ?State;如果失敗,dispatch ?一個請求失敗 Action,隱藏掉 ?Loading,給個失敗提示。
顯然,用 ?Redux ?處理異步,可以自己寫中間件來處理,當然大多數人會選擇一些現成的支持異步處理的中間件。比如 redux-thunk 或?redux-promise?。
Redux-thunkthunk 比較簡單,沒有做太多的封裝,把大部分自主權交給了用戶:
const createFetchDataAction = function(id) { return function(dispatch, getState) { // 開始請求,dispatch 一個 FETCH_DATA_START action dispatch({ type: FETCH_DATA_START, payload: id }) api.fetchData(id) .then(response => { ? ? ? ?// 請求成功,dispatch 一個 FETCH_DATA_SUCCESS action dispatch({ type: FETCH_DATA_SUCCESS, payload: response }) }) .catch(error => { ? ? ? ? ? ? ? ?// 請求失敗,dispatch 一個 FETCH_DATA_FAILED action ? dispatch({ type: FETCH_DATA_FAILED, payload: error }) }) } } //reducer const reducer = function(oldState, action) { switch(action.type) { case FETCH_DATA_START : // 處理 loading 等 case FETCH_DATA_SUCCESS : // 更新 store 等 case FETCH_DATA_FAILED : // 提示異常 } }
缺點就是用戶要寫的代碼有點多,可以看到上面的代碼比較啰嗦,一個請求就要搞這么一套東西。
Redux-promiseredus-promise 和 redux-thunk 的思想類似,只不過做了一些簡化,成功失敗手動 dispatch 被封裝成自動了:
const FETCH_DATA = "FETCH_DATA" //action creator const getData = function(id) { return { type: FETCH_DATA, payload: api.fetchData(id) // 直接將 promise 作為 payload } } //reducer const reducer = function(oldState, action) { switch(action.type) { case FETCH_DATA: if (action.status === "success") { // 更新 store 等處理 } else { // 提示異常 } } }
剛才的什么 then、catch 之類的被中間件自行處理了,代碼簡單不少,不過要處理 Loading 啥的,還需要寫額外的代碼。
其實任何時候都是這樣:封裝少,自由度高,但是代碼就會變復雜;封裝多,代碼變簡單了,但是自由度就會變差。redux-thunk 和 redux-promise 剛好就是代表這兩個面。
redux-thunk 和 redux-promise ?的具體使用就不介紹了,這里只聊一下大概的思路。大部分簡單的異步業務場景,redux-thunk 或者 redux-promise 都可以滿足了。
上面說的 Flux 和 Redux,和具體的前端框架沒有什么關系,只是思想和約定層面。下面就要和我們常用的 Vue 或 React 結合起來了:
VuexVuex 主要用于 Vue,和 Flux,Redux 的思想很類似。
Store每一個 Vuex 里面有一個全局的 Store,包含著應用中的狀態 State,這個 State 只是需要在組件中共享的數據,不用放所有的 State,沒必要。這個 State 是單一的,和 Redux 類似,所以,一個應用僅會包含一個 Store 實例。單一狀態樹的好處是能夠直接地定位任一特定的狀態片段,在調試的過程中也能輕易地取得整個當前應用狀態的快照。
Vuex通過 store 選項,把 state 注入到了整個應用中,這樣子組件能通過 this.$store 訪問到 state 了。
const app = new Vue({ el: "#app", // 把 store 對象提供給 “store” 選項,這可以把 store 的實例注入所有的子組件 store, components: { Counter }, template: `` })
const Counter = { template: `{{ count }}`, computed: { count () { return this.$store.state.count } } }
State 改變,View 就會跟著改變,這個改變利用的是 Vue 的響應式機制。
Mutation顯而易見,State 不能直接改,需要通過一個約定的方式,這個方式在 Vuex 里面叫做 mutation,更改 Vuex 的 store 中的狀態的唯一方法是提交 mutation。Vuex 中的 mutation 非常類似于事件:每個 mutation 都有一個字符串的 事件類型 (type) 和 一個 回調函數 (handler)。
const store = new Vuex.Store({ state: { count: 1 }, mutations: { increment (state) { // 變更狀態 state.count++ } } })
觸發 mutation 事件的方式不是直接調用,比如 increment(state) ?是不行的,而要通過 store.commit 方法:
store.commit("increment")
注意:mutation 都是同步事務。
mutation 有些類似 Redux 的 Reducer,但是 Vuex 不要求每次都搞一個新的 State,可以直接修改 State,這塊兒又和 Flux 有些類似。具尤大的說法,Redux 強制的 immutability,在保證了每一次狀態變化都能追蹤的情況下強制的 immutability 帶來的收益很有限,為了同構而設計的 API 很繁瑣,必須依賴第三方庫才能相對高效率地獲得狀態樹的局部狀態,這些都是 Redux 不足的地方,所以也被 Vuex 舍掉了。
到這里,其實可以感覺到 Flux、Redux、Vuex 三個的思想都差不多,在具體細節上有一些差異,總的來說都是讓 View 通過某種方式觸發 Store 的事件或方法,Store 的事件或方法對 State 進行修改或返回一個新的 State,State 改變之后,View 發生響應式改變。
Action到這里又該處理異步這塊兒了。mutation 是必須同步的,這個很好理解,和之前的 ?reducer 類似,不同步修改的話,會很難調試,不知道改變什么時候發生,也很難確定先后順序,A、B兩個 mutation,調用順序可能是 A -> B,但是最終改變 State 的結果可能是 B -> A。
對比Redux的中間件,Vuex 加入了 Action 這個東西來處理異步,Vuex的想法是把同步和異步拆分開,異步操作想咋搞咋搞,但是不要干擾了同步操作。View 通過 store.dispatch("increment") 來觸發某個 Action,Action 里面不管執行多少異步操作,完事之后都通過 store.commit("increment") 來觸發 mutation,一個 Action 里面可以觸發多個 mutation。所以 Vuex 的Action 類似于一個靈活好用的中間件。
Vuex 把同步和異步操作通過 mutation 和 Action 來分開處理,是一種方式。但不代表是唯一的方式,還有很多方式,比如就不用 Action,而是在應用內部調用異步請求,請求完畢直接 commit mutation,當然也可以。
Vuex 還引入了 Getter,這個可有可無,只不過是方便計算屬性的復用。
Vuex 單一狀態樹并不影響模塊化,把 State 拆了,最后組合在一起就行。Vuex 引入了 Module 的概念,每個 Module 有自己的 state、mutation、action、getter,其實就是把一個大的 Store 拆開。
總的來看,Vuex 的方式比較清晰,適合 Vue 的思想,在實際開發中也比較方便。
對比ReduxRedux:
view——>actions——>reducer——>state變化——>view變化(同步異步一樣)
Vuex:
view——>commit——>mutations——>state變化——>view變化(同步操作)
view——>dispatch——>actions——>mutations——>state變化——>view變化(異步操作)
Redux 和 Flux 類似,只是一種思想或者規范,它和 React 之間沒有關系。Redux 支持 React、Angular、Ember、jQuery 甚至純 JavaScript。
但是因為 React 包含函數式的思想,也是單向數據流,和 Redux 很搭,所以一般都用 ?Redux 來進行狀態管理。為了簡單處理 ?Redux ?和 React ?UI ?的綁定,一般通過一個叫 react-redux 的庫和 React 配合使用,這個是 ?react ?官方出的(如果不用 react-redux,那么手動處理 Redux 和 UI 的綁定,需要寫很多重復的代碼,很容易出錯,而且有很多 UI 渲染邏輯的優化不一定能處理好)。
Redux將React組件分為容器型組件和展示型組件,容器型組件一般通過connect函數生成,它訂閱了全局狀態的變化,通過mapStateToProps函數,可以對全局狀態進行過濾,而展示型組件不直接從global state獲取數據,其數據來源于父組件。
如果一個組件既需要UI呈現,又需要業務邏輯處理,那就得拆,拆成一個容器組件包著一個展示組件。
因為 react-redux 只是 redux 和 react 結合的一種實現,除了剛才說的組件拆分,并沒有什么新奇的東西,所以只拿一個簡單TODO項目的部分代碼來舉例:
入口文件 index.js,把 redux 的相關 store、reducer 通過 Provider 注冊到 App 里面,這樣子組件就可以拿到 ?store ?了。
import React from "react" import { render } from "react-dom" import { Provider } from "react-redux" import { createStore } from "redux" import rootReducer from "./reducers" import App from "./components/App" const store = createStore(rootReducer) render(, document.getElementById("root") )
actions/index.js,創建 Action:
let nextTodoId = 0 export const addTodo = text => ({ type: "ADD_TODO", id: nextTodoId++, text }) export const setVisibilityFilter = filter => ({ type: "SET_VISIBILITY_FILTER", filter }) export const toggleTodo = id => ({ type: "TOGGLE_TODO", id }) export const VisibilityFilters = { SHOW_ALL: "SHOW_ALL", SHOW_COMPLETED: "SHOW_COMPLETED", SHOW_ACTIVE: "SHOW_ACTIVE" }
reducers/todos.js,創建 Reducers:
const todos = (state = [], action) => { switch (action.type) { case "ADD_TODO": return [ ...state, { id: action.id, text: action.text, completed: false } ] case "TOGGLE_TODO": return state.map(todo => todo.id === action.id ? { ...todo, completed: !todo.completed } : todo ) default: return state } } export default todos
reducers/index.js,把所有的 Reducers 綁定到一起:
import { combineReducers } from "redux" import todos from "./todos" import visibilityFilter from "./visibilityFilter" export default combineReducers({ todos, visibilityFilter, ... })
containers/VisibleTodoList.js,容器組件,connect 負責連接React組件和Redux Store:
import { connect } from "react-redux" import { toggleTodo } from "../actions" import TodoList from "../components/TodoList" const getVisibleTodos = (todos, filter) => { switch (filter) { case "SHOW_COMPLETED": return todos.filter(t => t.completed) case "SHOW_ACTIVE": return todos.filter(t => !t.completed) case "SHOW_ALL": default: return todos } } // mapStateToProps 函數指定如何把當前 Redux store state 映射到展示組件的 props 中 const mapStateToProps = state => ({ todos: getVisibleTodos(state.todos, state.visibilityFilter) }) // mapDispatchToProps 方法接收 dispatch() 方法并返回期望注入到展示組件的 props 中的回調方法。 const mapDispatchToProps = dispatch => ({ toggleTodo: id => dispatch(toggleTodo(id)) }) export default connect( mapStateToProps, mapDispatchToProps )(TodoList)
簡單來說,react-redux 就是多了個 connect 方法連接容器組件和UI組件,這里的“連接”就是一種映射:
mapStateToProps ?把容器組件的 state 映射到UI組件的 props
mapDispatchToProps 把UI組件的事件映射到 dispatch 方法
Redux-saga剛才介紹了兩個Redux 處理異步的中間件 redux-thunk 和 redux-promise,當然 redux 的異步中間件還有很多,他們可以處理大部分場景,這些中間件的思想基本上都是把異步請求部分放在了 ?action ?creator ?中,理解起來比較簡單。
redux-saga 采用了另外一種思路,它沒有把異步操作放在 action creator 中,也沒有去處理 reductor,而是把所有的異步操作看成“線程”,可以通過普通的action去觸發它,當操作完成時也會觸發action作為輸出。saga 的意思本來就是一連串的事件。
redux-saga 把異步獲取數據這類的操作都叫做副作用(Side ?Effect),它的目標就是把這些副作用管理好,讓他們執行更高效,測試更簡單,在處理故障時更容易。
在聊 redux-saga 之前,需要熟悉一些預備知識,那就是 ES6 的 Generator。
如果從沒接觸過 Generator 的話,看著下面的代碼,給你個1分鐘傻瓜式速成,函數加個星號就是 Generator?函數了,Generator 就是個罵街生成器,Generator 函數里可以寫一堆 yield 關鍵字,可以記成“丫的”,Generator 函數執行的時候,啥都不干,就等著調用 next 方法,按照順序把標記為“丫的”的地方一個一個拎出來罵(遍歷執行),罵到最后沒有“丫的”標記了,就返回最后的return值,然后標記為 done: true,也就是罵完了(上面只是幫助初學者記憶,別噴~)。
function* helloWorldGenerator() { yield "hello"; yield "world"; return "ending"; } var hw = helloWorldGenerator(); hw.next() // 先把 "hello" 拎出來,done: false 代表還沒罵完 // { value: "hello", done: false } next() 方法有固定的格式,value 是返回值,done 代表是否遍歷結束 hw.next() // 再把 "world" 拎出來,done: false 代表還沒罵完 // { value: "world", done: false } hw.next() // 沒有 yield 了,就把最后的 return "ending" 拎出來,done: true 代表罵完了 // { value: "ending", done: true } hw.next() // 沒有 yield,也沒有 return 了,真的罵完了,只能擠出來一個 undefined 了,done: true 代表罵完了 // { value: undefined, done: true }
這樣搞有啥好處呢?我們發現 Generator?函數的很多代碼可以被延緩執行,也就是具備了暫停和記憶的功能:遇到yield表達式,就暫停執行后面的操作,并將緊跟在yield后面的那個表達式的值,作為返回的對象的value屬性值,等著下一次調用next方法時,再繼續往下執行。用 Generator 來寫異步代碼,大概長這樣:
function* gen(){ var url = "https://api.github.com/users/github"; var jsonData = yield fetch(url); console.log(jsonData); } var g = gen(); var result = g.next(); // 這里的result是 { value: fetch("https://api.github.com/users/github"), done: true } // fetch(url) 是一個 Promise,所以需要 then 來執行下一步 result.value.then(function(data){ return data.json(); }).then(function(data){ // 獲取到 json data,然后作為參數調用 next,相當于把 data 傳給了 jsonData,然后執行 console.log(jsonData); g.next(data); });
再回到 redux-saga 來,可以把 saga 想象成開了一個以最快速度不斷地調用 next 方法并嘗試獲取所有 yield 表達式值的線程。舉個例子:
// saga.js import { take, put } from "redux-saga/effects" function* mySaga(){ // 阻塞: take方法就是等待 USER_INTERACTED_WITH_UI_ACTION 這個 action 執行 yield take(USER_INTERACTED_WITH_UI_ACTION); // 阻塞: put方法將同步發起一個 action yield put(SHOW_LOADING_ACTION, {isLoading: true}); // 阻塞: 將等待 FetchFn 結束,等待返回的 Promise const data = yield call(FetchFn, "https://my.server.com/getdata"); // 阻塞: 將同步發起 action (使用剛才返回的 Promise.then) yield put(SHOW_DATA_ACTION, {data: data}); }
這里用了好幾個yield,簡單理解,也就是每個 yield 都發起了阻塞,saga 會等待執行結果返回,再執行下一指令。也就是相當于take、put、call、put 這幾個方法的調用變成了同步的,上面的全部完成返回了,才會執行下面的,類似于 await。
用了 saga,我們就可以很細粒度的控制各個副作用每一部的操作,可以把異步操作和同步發起 action 一起,隨便的排列組合。saga 還提供 takeEvery、takeLatest 之類的輔助函數,來控制是否允許多個異步請求同時執行,尤其是 takeLatest,方便處理由于網絡延遲造成的多次請求數據沖突或混亂的問題。
saga 看起來很復雜,主要原因可能是因為大家不熟悉 Generator 的語法,還有需要學習一堆新增的 API 。如果拋開這些記憶的東西,改造一下,再來看一下代碼:
function mySaga(){ if (action.type === "USER_INTERACTED_WITH_UI_ACTION") { store.dispatch({ type: "SHOW_LOADING_ACTION", isLoading: true}); const data = await Fetch("https://my.server.com/getdata"); store.dispatch({ type: "SHOW_DATA_ACTION", data: data}); ? ?} }
上面的代碼就很清晰了吧,全部都是同步的寫法,無比順暢,當然直接這樣寫是不支持的,所以那些 Generator 語法和API,無非就是做一些適配而已。
saga 還能很方便的并行執行異步任務,或者讓兩個異步任務競爭:
// 并行執行,并等待所有的結果,類似 Promise.all 的行為 const [users, repos] = yield [ call(fetch, "/users"), call(fetch, "/repos") ] // 并行執行,哪個先完成返回哪個,剩下的就取消掉了 const {posts, timeout} = yield race({ ?posts: call(fetchApi, "/posts"), ?timeout: call(delay, 1000) })
saga 的每一步都可以做一些斷言(assert)之類的,所以非常方便測試。而且很容易測試到不同的分支。
這里不討論更多 saga 的細節,大家了解 saga 的思想就行,細節請看文檔。
對比 Redux-thunk比較一下 redux-thunk 和 redux-saga 的代碼:
和 redux-thunk 等其他異步中間件對比來說,redux-saga 主要有下面幾個特點:
異步數據獲取的相關業務邏輯放在了多帶帶的 saga.js 中,不再是摻雜在 action.js 或 component.js 中。
dispatch 的參數是標準的 ?action,沒有魔法。
saga 代碼采用類似同步的方式書寫,代碼變得更易讀。
代碼異常/請求失敗 都可以直接通過 try/catch 語法直接捕獲處理。
很容易測試,如果是 thunk 的 Promise,測試的話就需要不停的 mock 不同的數據。
其實 redux-saga 是用一些學習的復雜度,換來了代碼的高可維護性,還是很值得在項目中使用的。
DvaDva是什么呢?官方的定義是:dva 首先是一個基于 redux 和 redux-saga 的數據流方案,然后為了簡化開發體驗,dva 還額外內置了 react-router 和 fetch,所以也可以理解為一個輕量級的應用框架。
簡單理解,就是讓使用 react-redux 和 redux-saga 編寫的代碼組織起來更合理,維護起來更方便。
之前我們聊了 redux、react-redux、redux-saga 之類的概念,大家肯定覺得頭昏腦漲的,什么 action、reducer、saga 之類的,寫一個功能要在這些js文件里面不停的切換。
dva 做的事情很簡單,就是讓這些東西可以寫到一起,不用分開來寫了。比如:
app.model({ // namespace - 對應 reducer 在 combine 到 rootReducer 時的 key 值 namespace: "products", // state - 對應 reducer 的 initialState state: { list: [], loading: false, }, // subscription - 在 dom ready 后執行 subscriptions: [ function(dispatch) { dispatch({type: "products/query"}); }, ], // effects - 對應 saga,并簡化了使用 effects: { ["products/query"]: function*() { yield call(delay(800)); yield put({ type: "products/query/success", payload: ["ant-tool", "roof"], }); }, }, // reducers - 就是傳統的 reducers reducers: { ["products/query"](state) { return { ...state, loading: true, }; }, ["products/query/success"](state, { payload }) { return { ...state, loading: false, list: payload }; }, }, });
以前書寫的方式是創建 ?sagas/products.js, reducers/products.js 和 actions/products.js,然后把 saga、action、reducer 啥的分開來寫,來回切換,現在寫在一起就方便多了。
比如傳統的 TODO 應用,用 redux + redux-saga 來表示結構,就是這樣:
saga 攔截 add 這個 action, 發起 http 請求, 如果請求成功, 則繼續向 reducer 發一個 addTodoSuccess 的 action, 提示創建成功, 反之則發送 addTodoFail 的 action 即可。
如果使用 Dva,那么結構圖如下:
整個結構變化不大,最主要的就是把 store 及 saga 統一為一個 model 的概念(有點類似 Vuex 的 Module),寫在了一個 js 文件里。增加了一個 Subscriptions, 用于收集其他來源的 action,比如快捷鍵操作。
app.model({ namespace: "count", state: { record: 0, current: 0, }, reducers: { add(state) { const newCurrent = state.current + 1; return { ...state, record: newCurrent > state.record ? newCurrent : state.record, current: newCurrent, }; }, minus(state) { return { ...state, current: state.current - 1}; }, }, effects: { *add(action, { call, put }) { yield call(delay, 1000); yield put({ type: "minus" }); }, }, subscriptions: { keyboardWatcher({ dispatch }) { key("?+up, ctrl+up", () => { dispatch({type:"add"}) }); }, }, });
之前我們說過約定優于配置的思想,Dva正式借鑒了這個思想。
MobX前面扯了這么多,其實還都是 Flux 體系的,都是單向數據流方案。接下來要說的 MobX,就和他們不太一樣了。
我們先清空一下大腦,回到初心,什么是初心?就是我們最初要解決的問題是什么?最初我們其實為了解決應用狀態管理的問題,不管是 Redux 還是 MobX,把狀態管理好是前提。什么叫把狀態管理好,簡單來說就是:統一維護公共的應用狀態,以統一并且可控的方式更新狀態,狀態更新后,View跟著更新。不管是什么思想,達成這個目標就ok。
Flux 體系的狀態管理方式,只是一個選項,但并不代表是唯一的選項。MobX 就是另一個選項。
MobX背后的哲學很簡單:任何源自應用狀態的東西都應該自動地獲得。譯成人話就是狀態只要一變,其他用到狀態的地方就都跟著自動變。
看這篇文章的人,大概率會對面向對象的思想比較熟悉,而對函數式編程的思想略陌生。Flux 或者說 Redux 的思想主要就是函數式編程(FP)的思想,所以學習起來會覺得累一些。而 MobX 更接近于面向對象編程,它把 state 包裝成可觀察的對象,這個對象會驅動各種改變。什么是可觀察?就是 MobX 老大哥在看著 state 呢。state 只要一改變,所有用到它的地方就都跟著改變了。這樣整個 View 可以被 state 來驅動。
const obj = observable({ a: 1, b: 2 }) autoRun(() => { console.log(obj.a) }) obj.b = 3 // 什么都沒有發生 obj.a = 2 // observe 函數的回調觸發了,控制臺輸出:2
上面的obj,他的 obj.a 屬性被使用了,那么只要 obj.a 屬性一變,所有使用的地方都會被調用。autoRun 就是這個老大哥,他看著所有依賴 obj.a 的地方,也就是收集所有對 obj.a 的依賴。當 obj.a 改變時,老大哥就會觸發所有依賴去更新。
MobX 允許有多個 store,而且這些 store 里的 state 可以直接修改,不用像 Redux 那樣每次還返回個新的。這個有點像 Vuex,自由度更高,寫的代碼更少。不過它也會讓代碼不好維護。
MobX 和 Flux、Redux 一樣,都是和具體的前端框架無關的,也就是說可以用于 React(mobx-react) 或者 Vue(mobx-vue)。一般來說,用到 React 比較常見,很少用于 Vue,因為 Vuex 本身就類似 MobX,很靈活。如果我們把 MobX 用于 React ?或者 ?Vue,可以看到很多 setState() 和 this.state.xxx = 這樣的處理都可以省了。
還是和上面一樣,只介紹思想。具體 MobX 的使用,可以看這里。
對比 Redux我們直觀地上兩坨實現計數器代碼:
Redux:
import React, { Component } from "react"; import { createStore, bindActionCreators, } from "redux"; import { Provider, connect } from "react-redux"; // ①action types const COUNTER_ADD = "counter_add"; const COUNTER_DEC = "counter_dec"; const initialState = {a: 0}; // ②reducers function reducers(state = initialState, action) { switch (action.type) { case COUNTER_ADD: return {...state, a: state.a+1}; case COUNTER_DEC: return {...state, a: state.a-1}; default: return state } } // ③action creator const incA = () => ({ type: COUNTER_ADD }); const decA = () => ({ type: COUNTER_DEC }); const Actions = {incA, decA}; class Demo extends Component { render() { const { store, actions } = this.props; return (); } } // ④將state、actions 映射到組件 props const mapStateToProps = state => ({store: state}); const mapDispatchToProps = dispatch => ({ // ⑤bindActionCreators 簡化 dispatch actions: bindActionCreators(Actions, dispatch) }) // ⑥connect產生容器組件 const Root = connect( mapStateToProps, mapDispatchToProps )(Demo) const store = createStore(reducers) export default class App extends Component { render() { return (a = {store.a}
) } }
MobX:
import React, { Component } from "react"; import { observable, action } from "mobx"; import { Provider, observer, inject } from "mobx-react"; // 定義數據結構 class Store { // ① 使用 observable decorator @observable a = 0; } // 定義對數據的操作 class Actions { constructor({store}) { this.store = store; } // ② 使用 action decorator @action incA = () => { this.store.a++; } @action decA = () => { this.store.a--; } } // ③實例化單一數據源 const store = new Store(); // ④實例化 actions,并且和 store 進行關聯 const actions = new Actions({store}); // inject 向業務組件注入 store,actions,和 Provider 配合使用 // ⑤ 使用 inject decorator 和 observer decorator @inject("store", "actions") @observer class Demo extends Component { render() { const { store, actions } = this.props; return (); } } class App extends Component { render() { // ⑥使用Provider 在被 inject 的子組件里,可以通過 props.store props.actions 訪問 return (a = {store.a}
) } } export default App;
比較一下:
Redux 數據流流動很自然,可以充分利用時間回溯的特征,增強業務的可預測性;MobX 沒有那么自然的數據流動,也沒有時間回溯的能力,但是 View 更新很精確,粒度控制很細。
Redux 通過引入一些中間件來處理副作用;MobX ?沒有中間件,副作用的處理比較自由,比如依靠 autorunAsync 之類的方法。
Redux 的樣板代碼更多,看起來就像是我們要做頓飯,需要先買個調料盒裝調料,再買個架子放刀叉。。。做一大堆準備工作,然后才開始炒菜;而 MobX 基本沒啥多余代碼,直接硬來,拿著炊具調料就開干,搞出來為止。
但其實?Redux 和 MobX 并沒有孰優孰劣,Redux 比 Mobx 更多的樣板代碼,是因為特定的設計約束。如果項目比較小的話,使用 MobX 會比較靈活,但是大型項目,像?MobX 這樣沒有約束,沒有最佳實踐的方式,會造成代碼很難維護,各有利弊。一般來說,小項目建議 MobX 就夠了,大項目還是用 Redux 比較合適。
總結時光荏苒,歲月如梭。每一個框架或者庫只能陪你走一段路,最終都會逝去。留在你心中的,不是一條一條的語法規則,而是一個一個的思想,這些思想才是推動進步的源泉。
帥哥美女,如果你都看到這里了,那么不點個贊,你的良心過得去么?
參考鏈接https://cn.vuejs.org/v2/guide/state-management.html
https://vuex.vuejs.org/
https://cn.redux.js.org/docs/react-redux/
http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.html
http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_three_react-redux.html
https://redux-saga-in-chinese.js.org
https://juejin.im/post/59e6cd68f265da43163c2821
https://react-redux.js.org/introduction/why-use-react-redux
https://segmentfault.com/a/1190000007248878
http://es6.ruanyifeng.com/#docs/generator
https://juejin.im/post/5ac1cb9d6fb9a028cf32a046
https://zhuanlan.zhihu.com/p/35437092
https://github.com/dvajs/dva/issues/1
https://cn.mobx.js.org
https://zhuanlan.zhihu.com/p/25585910
http://imweb.io/topic/59f4833db72024f03c7f49b4
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/100152.html
摘要:希望大家在這浮夸的前端圈里,保持冷靜,堅持每天花分鐘來學習與思考。 今天的React題沒有太多的故事…… 半個月前出了248個Vue的知識點,受到很多朋友的關注,都強烈要求再出多些React相前的面試題,受到大家的邀請,我又找了20多個React的使用者,他們給出了328道React的面試題,由我整理好發給大家,同時發布在了前端面試每日3+1的React專題,希望對大家有所幫助,同時大...
摘要:要求通過要求數據變更函數使用裝飾或放在函數中,目的就是讓狀態的變更根據可預測性單向數據流。同一份數據需要響應到多個視圖,且被多個視圖進行變更需要維護全局狀態,并在他們變動時響應到視圖數據流變得復雜,組件本身已經無法駕馭。今天是 520,這是本系列最后一篇文章,主要涵蓋 React 狀態管理的相關方案。 前幾篇文章在掘金首發基本石沉大海, 沒什么閱讀量. 可能是文章篇幅太長了?掘金值太低了? ...
閱讀 1459·2021-11-22 13:52
閱讀 1281·2021-09-29 09:34
閱讀 2690·2021-09-09 11:40
閱讀 3031·2019-08-30 15:54
閱讀 1255·2019-08-30 15:53
閱讀 971·2019-08-30 11:01
閱讀 1354·2019-08-29 17:22
閱讀 1943·2019-08-26 10:57