先上一張經典圖為敬
redux-saga 是一個用於管理redux應用異步操作代替 redux-thunk 的中間件
- 集中處理 redux 副作用問題。reducer負責處理action的更新,saga負責協調那些復雜或者異步的操作
- 使用 generator 函數執行異步,generator不是線程
- watch/worker(監聽->執行) 的工作形式
redux-saga 啟動的任務可以在任何時候通過手動來取消,也可以把任務和其他的Effects放到 race 方法里以自動取消
createSagaMiddleware(...sagas:[])
createSagaMiddleware的作用是創建一個redux中間件,並將sagas與Redux store建立鏈接
effect
只要是被 yield 的值,都可以被稱為 effect。例如 yield 1 中,數字 1 就是 effect。
所有的 effect 都是通過 yield 語法傳遞給 effect-runner 的,effect-runner 處理該 effect 並決定什么時候恢復生成器。
redux-saga里所有effect返回的值,都是一個帶類型的純object對象。
generator的特點是執行到某一步時,可以把控制權交給外部代碼,由外部代碼拿到返回結果后,決定該怎么做。
put
作用和 redux 中的 dispatch 相同。
yield put({ type: 'CLICK_BTN' });
take
等待 redux dispatch 匹配某個 pattern 的 action 。
在這個例子中,先等待一個按鈕點擊的 action ,然后執行按鈕點擊的 saga:
while (true) {
yield take('CLICK_BUTTON');
yield fork(clickButtonSaga);
}
fork
當接收到 BEGIN_COUNT 的 action,則開始倒數,而接收到 STOP_COUNT 的 action, 則停止倒數。
function* count(number) {
let currNum = number;
while (currNum >= 0) {
console.log(currNum--);
yield delay(1000);
}
}
function countSaga* () {
while (true) {
const { payload: number } = yield take(BEGIN_COUNT);
const countTaskId = yield fork(count, number);
yield take(STOP_TASK);
yield cancel(countTaskId);
}
}
call
有阻塞地調用 saga 或者返回 promise 的函數。
const project = yield call(fetch, { url: UrlMap.fetchProject });
const members = yield call(fetchMembers, project.id);
select
作用和 redux thunk 中的 getState 相同。
const id = yield select(state => state.id);
阻塞調用和無阻塞調用
其中 fork 是無阻塞型調用,call 是阻塞型調用。
channel
channel是對事件源的抽象,作用是先注冊一個take方法,當put觸發時,執行一次take方法,然后銷毀take。
export function channel(buffer = buffers.expanding()) {
let closed = false
let takers = []
function put(input) {
if (closed) {
return
}
if (takers.length === 0) {
return buffer.put(input)
}
const cb = takers.shift()
cb(input)
}
function take(cb) {
if (closed && buffer.isEmpty()) {
cb(END)
} else if (!buffer.isEmpty()) {
cb(buffer.take())
} else {
takers.push(cb)
cb.cancel = () => {
remove(takers, cb)
}
}
}
function flush(cb) {
if (closed && buffer.isEmpty()) {
cb(END)
return
}
cb(buffer.flush())
}
function close() {
if (closed) {
return
}
closed = true
const arr = takers
takers = []
for (let i = 0, len = arr.length; i < len; i++) {
const taker = arr[i]
taker(END)
}
}
return {
take,
put,
flush,
close,
}
}
當put觸發時,如果channel里已經有注冊了的taker,taker就會執行。
我們需要在put觸發之前,先調用channel的take方法,注冊實際要運行的方法。
const chan = channel();
btn.addEventListener('click', () => {
const action =`action data${i++}`;
chan.put(action);
}, false);
function* mainSaga() {
const action = yield take();
console.log(action);
}
這個take是saga里的一種effect類型。
task
task是generator方法的執行環境,所有saga的generator方法都跑在task里。
簡易實現如下:
function task(iterator) {
const iter = iterator();
function next(args) {
const result = iter.next(args);
if (!result.done) {
const effect = result.value;
if (effect.type === 'take') {
runEffect(result.value, next);
}
}
}
next();
}
數據流轉
// saga.js
// 它是一個 generator function
// fn 中同樣包含了業務數據請求代碼邏輯
function* fetchData(action) {
const { payload: { someValue } } = action;
try {
const result = yield call(myAjaxLib.post, "/someEndpoint", { data: someValue });
yield put({ type: "REQUEST_SUCCEEDED", payload: response });
} catch (error) {
yield put({ type: "REQUEST_FAILED", error: error });
}
}
// watcher saga
// 監聽每一次 dispatch(action)
// 如果 action.type === 'REQUEST',那么執行 fetchData
export function* watchFetchData() {
yield takeEvery('REQUEST', fetchData);
}
// component.js
// View 層 dispatch(action) 觸發異步請求
// 這里的 action 依然可以是一個 plain object
this.props.dispatch({
type: 'REQUEST',
payload: {
someValue: { hello: 'saga' }
}
});
對比 thunk
thunk 采用的是擴展action的方式:使得redux的store能dispatch的內容從普通對象擴展到函數
saga 采用的方案更接近於redux的全局思想
saga 采用發布訂閱模式,需要一個經紀人Broker,用於監聽組件發出的action,將監聽到的action1轉發給對應的訂閱者,再由訂閱者執行具體任務,任務執行完后,再發出另一個action2交由reducer修改state