redux-saga上手門檻較高,這對於redux-saga在團隊內推廣是不利的,對新人來講redux-saga的官方資料沒有給出使人信服的理由讓咱們能夠去放棄redux-thunk或者async/await方案。 這篇文章是個人學習過程和了解項目的過程當中總結出來的學習筆記,經過分析redux的做用,對比分析redux中處理反作用的方案,說明了redux-saga是什麼,有什麼用和實現原理。前端
reduxtypescript
redux下降了大型項目的維護難度。面向對象中有一種常見的解耦方式: "接口與實現分離"。即經過定義接口,類開發者只須要將接口暴露給業務方,業務方只須要知道接口定義了什麼行爲就能夠了,而不須要知道其背後的實現,而類做者在更新的時候也只須要保證明現對應的接口便可。其集大成的模式就是依賴注入(如Spring和Angular)。redux和這種模式其實很是類似:若是咱們把接口理解爲action,具體實現是reducer,業務方只能經過action去操做整個store的狀態,對業務方來講,也不須要知道reducer的具體實現,更新reducer對業務方來講也是無感的。redux這種編程模式是另種形式的"接口與實現分離"。編程
"接口與實現分離"的優點相信各位已經很熟悉,就不在這裏多說了。redux
然而這種模式不是完好陷的。redux被詬病的地方在於代碼量太大,在第一次寫的時候,寫代碼速度是不如直接在組件中處理業務邏輯的,所以redux不適合小型或者快速試錯的項目。社區提出了一些緩和這一缺點的方案,如redux-action。設計模式
反作用api
無反作用是指在函數中,除了返回值以外,這個函數不會對調用方產生附加的影響,好比修改函數入參,讀取或者修改全局環境變量,寫IO,讀IO都是反作用。關於無反作用函數的優勢這裏已經說得很清楚了。數組
redux官方教程中要求reducer是無反作用的。這是爲何呢?下面兩段代碼都表示將一個數組所有元素先加1,而後將值大於10的元素所有剔除數組,而後返回這個數組。promise
function transformArr(arr) {
// 有反作用
for (let i = 0; i < arr.length; i++) {
arr[i] += 1;
if (arr[i] > 10) {
arr.splice(i, 1);
}
}
return arr;
}
function transformArr(arr) {
// 無反作用
const greaterThen10Arr = arr.map(e => e + 1).filter(e => e > 10);
return greaterThen10Arr;
}
複製代碼
能夠看出,無反作用的代碼可以更清晰地表達做者的意圖,試想若是你在別人的代碼中看到一段對數組的for循環遍歷,除了看這個循環對數組作了什麼,更讓人擔驚受怕的是這個循環有沒有對數組之外的東西進行了處理,而無反作用的代碼只須要看變量名便明白了做者但願獲得一個什麼樣的數組。一樣,若是在一個reducer是有反作用的,那麼後續維護,你頗有可能要回去看這個reducer作了什麼,會不會對新增的功能產生影響。這是違背redux接口與實現分離的理念的。bash
反作用處理方案網絡
redux誕生時,JS社區對如何處理異步的問題還在討論中,大概是由於這個緣由致使redux沒有對反作用的處理方案,然而前端不可能作到全部代碼無反作用,那麼反作用該放哪?下面是社區的方案。
async/await
這是小型項目最簡單的方案,如如下代碼,在獲取用戶id後,根據用戶id得到對應數據,最後發出action將數據保存到store中。
function fetchData(userId) {
// 返回一個promise, 內容是數據
}
function fetchUser() {
// 返回一個promise, 內容是用戶信息
}
class Component {
componentDidMount() {
const { userInfo: { userId } } = await fetchUser();
store.dispatch({type: 'UPDATE_USER_ID', payload: userId});
const { data } = await fetchData(userId);
store.dispatch({type: 'UPDATE_DATA', payload: data});
}
}
複製代碼
那問題來了,若是其餘組件也須要複用這個獲取數據的邏輯該怎麼辦呢,稍好的方案是創建一個類來實現複用,以下:
class DataHandler {
static fetchData() {
const { userInfo: { userId } } = await fetchUser();
store.dispatch({type: 'UPDATE_USER_ID', payload: userId});
const { data } = await fetchData(userId);
store.dispatch({type: 'UPDATE_DATA', payload: data});
}
}
class ComponentA {
componentDidMount() {
DataHandler.fetchData();
}
}
class ComponentB {
componentDidMount() {
DataHandler.fetchData();
}
}
複製代碼
可是這樣就會致使了維護的問題,DataHandler該如何擴展?好比ComponentB但願網絡請求結束後,但願對上方第五行中的data進行變換後更新store的其餘字段,那你可能須要在DataHandler添加一個給B的專屬函數用來處理,長期下來DataHandler就會變得愈來愈臃腫,最重要是組件對DataHandler是依賴關係,DataHandler的每一次修改都須要檢查全部引用DataHandler的組件,DataHandler會愈來愈變得不可維護。由於咱們須要redux。
redux-thunk
redux-thunk用法就不在這裏介紹了,主要優勢是靈活好用,團隊推廣難度低,克服DataHandler的缺點,很是適合小型項目使用。
缺點是對不能對異步進行粒度小的控制(下文會說明),不容易測試。須要說的是這篇文章沒法也不會去說明redux-saga比redux-thunk的絕對優點,使用哪一個徹底取決具體場景和團隊的選擇。
redux-saga最大的缺點是絕對高的理解門檻和缺少能夠公開討論的使用場景----小型項目根本用不上,大型項目不必定適合在公衆場合討論具體的技術細節,這就致使學習資源很缺少----除了API使用外。
redux-saga是個很是強大處理反作用的工具。它提供了對異步流程更細粒度的控制,對每一個異步流程他能夠實現暫停、中止、啓動三種狀態。此外redux-saga利用了generator,對每一個saga,其測試方式能夠很是簡單。更重要的是其異步處理邏輯放在了saga中,咱們能夠監聽action觸發,而後產生反作用。action依然是普通的redux action,不破壞redux對action的定義。
redux-saga如何工做
redux-saga基於generator。咱們確定都認識generator是什麼,可是概念很虛,讓人看得雲裏霧裏。咱們隨便拉一個generator例子:
function* gen() {
const x = yield 1;
const y = yield 2;
}
const g = gen();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
複製代碼
這是很簡單的generator例子,但咱們仍是不知道generator能夠用來作什麼。其實咱們能夠把gen當成一個任務清單,而後有一我的拿到這個任務清單而後開始執行任務。好比如下代碼:
function* gen() {
const user = {userName: 'xiaoming'};
const article = {articles: [{title: '文章'}]};
const x = yield Promise.resolve(user);
console.assert(x === user); // true
const y = yield Promise.resolve(article);
console.assert(y === article); // true
}
async function worker(gen) {
const g = gen();
let task = g.next();
while(!task.done) {
const response = await task.value;
task = g.next(response);
}
}
worker(gen);
複製代碼
worker就是執行任務的那我的,gen就是那個任務清單。若是聯想一下redux-saga,redux-saga就是那個worker,咱們須要作的就是編寫任務清單-----編寫saga,那saga是什麼呢?
saga是什麼
saga是個奇怪的名字,下面是字典和論文的定義:
字典:A saga is a long story, account, or sequence of events.
論文:A LLT(Long lived transactions) is a saga if it can be written as a sequence of transactions that can be interleaved with other transactions.
複製代碼
簡單解釋就是,saga是一個任務列表,任務執行順序是有序的,每一個任務的狀態能夠被改變。有序好理解,那什麼是能夠被改變呢?下面代碼是項目關於網絡請求的代碼:
function* request(action: PayloadAction) {
try {
yield put(requestActions.start(action));
const response = yield call(apiRequestAndReturnPromise);
yield put({type: `${action.type}`_SUCCESS, payload: response});
} catch (error) {
yield put({type: `${action.type}`_FAIL, payload: error});
}
}
function* cancelSendRequestOnAction(abortOn: string | string[], task: any) {
const { abortingAction } = yield race({
abortingAction: take(abortOn), // 能夠是一個數組
taskFinished: join(task),
timeout: call(delay, 10000), // taskFinished doesn't work for aborted tasks
});
if (abortingAction) {
yield cancel(task);
}
}
function* requestWatcher() {
const newTask = yield fork(apiRequest, action); // Task 1
yield fork(cancelSendRequestOnAction, abortOn, newTask); // Task 2
}
複製代碼
這段代碼可能會有點繞,可能須要你多看幾回。
首先requestWatcher用fork啓動了兩個異步任務,由於使用了fork,Task 2不會等待Task 1結束,而是會在Task 1開始以後立刻執行。
fork返回的newTask是一個saga的任務對象,咱們能夠對這個任務進行處理,好比取消。
Task 1是發起網絡請求獲取數據。沒有什麼特別的。
Task 2是重點,能夠解釋什麼是saga的狀態可改變。Task2的意思是,abortingAction、taskFinished、timeout任意一個action觸發時,就返回abortingAction,若是abortingAction就取消剛纔發起的網絡請求任務。
這是一個頗有意思的特性,在前端除了網絡請求,還有如下的異步行爲:Promise、setTimeout、setInterval、點擊事件(歡迎補充),那利用redux-saga能簡單實現很複雜的交互,特別是在編輯器中的應用。
PS:熱烈歡迎批評