redux-saga

21 4月

redux-saga和redux-thunk一样都是异步action的中间件。之前写了Redux教程,saga实在懒得写,项目中也不用。

现在苦练基本功,而且接手的dva框架构建的项目用到saga,补充一下。saga本质上仍旧是在处理异步流,将异步处理集中在了一起(thunk也一样),更适合写测试用例。

  • 基本概念
  • 引入saga
  • Effect
    • call / apply / cps
    • put / putResolve
    • take / takeEvery / takeLatest / takeLeading
    • fork / spawn / cancel / cancelled
    • all / race

基本概念

saga是基于generator函数来创建的中间件,可以理解为一个在后台运行的进程,通过takeEvery来监听action,所有的操作都通过yield Effects来完成,通过call来调用异步方法,通过put来触发reducer,通过select来获取state。

Effect:是个含有一些方法(例如call, put, takeEvery,fork等)的plain Object,包含了要被saga执行的信息。

Task:通过fork / spawn创建的的进程。saga在后台可以并行执行这些进程。

Blocking/Non-blocking call:call方法默认都是阻塞式的,yield call(…)会等到执行完并返回结果后,才执行下一个generator。可以用fork / spawn创建非阻塞式的call。具体哪些方法是阻塞还是非阻塞,参照这里

import {call, cancel, join, take, put} from "redux-saga/effects";

function* saga() {
    yield take(ACTION)                           // 阻塞
    yield call(ApiFn, ...args)                   // 阻塞
    yield put(...)                               // 非阻塞,dispatch一个action出去
    const task = yield fork(otherSaga, ...args)  // 非阻塞,不会等待otherSaga
    yield cancel(task)                           // 非阻塞,不会等待取消task的结果
    // or
    yield join(task)                             // 阻塞,会等待task运行中止
}

Watcher/Worker:Watcher会监听actions,Worker用于执行action。

function* watcher() {
    while (true) {
        const action = yield take(ACTION)
        yield fork(worker, action.payload)
    }
}

function* worker(payload) {
    ... 
}

引入saga

saga是redux的中间件,引入方式并没有什么特殊。

import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootSaga from '../sagas';        // 业务相关的操作

const saga = createSagaMiddleware();

const store = createStore(reducer, compose(
    applyMiddleware(saga, /* ... 其他中间件 ... */),
    window.devToolsExtension ? window.devToolsExtension() : (f) => f,
));

saga.run(rootSaga);

Effect

Effect是saga的核心,generator yield的就是这个plain object。提供了几乎所有方法,例如call,put,take,takeEvery,takeLatest,fork,cancel,throttle,debounce,还有race,all方法。

call / apply / cps

call / apply / cps是阻塞型的。就如同redux中用actionCreator创建action对象。call方法创建了effect对象,能够让saga执行fn,并当fn resolve/reject后,继续执行下一个generator。

call(fn, …args):参数fn可以是普通方法(此时saga会立即resolve返回值),也可以是generator方法。

yield call(fetchDataApi, 'redux-saga');

支持几个变种:

call([context, fn], …args) / call({context, fn}, …args) / apply(context, fn, [args]):支持传入context作为fn的this。

yield call({context: localStorage, fn: localStorage.getItem}, 'redux-saga');

call([context, fnName], …args):支持fn传入string,常用在调用对象的方法时。

yield call([localStorage, 'getItem'], 'redux-saga');

cps(fn, …args):调用node风格的fn,即fn(…arg, cb),参数cb(null, result)。

yield cps(readFile, '/path/to/file');

cps([context, fn], …args) / cps({context, fn}, …args):支持传入context作为fn的this。

put / putResolve

put是非阻塞型的,putResolve是阻塞型的。

put(action):dispatch action去reducer。非阻塞型。

yield put({ type: 'RECEIVE_DATA', data });

putResolve(action):和put的区别就是,它是阻塞型的。

put(channel, action):将action放入channel对象内。channel是个通用概念,后续很多方法都支持channel。如果我们有这样的需求:如果我们同时激活了四个同名的action,希望一个一个的执行action任务,第一个REQUEST执行完,再执行第二个,可以将它加入channel里。

take / takeEvery / takeLatest / takeLeading

take(pattern) / take(channel):监听匹配到的action时会被触发。

function* watchAndLog() {
    while (true) {
        const action = yield take('*');
        const state = yield select();
        console.log('action', action);
        console.log('state after', state);
    }
}

takeEvery(pattern, saga, …args) / takeEvery(channel, saga, …args):会为每次dispatch action都派生一个进程去匹配,而且是并发的,如果dispatch相同的action,即使前一个还没结束,也没任何影响。上面的while (true)并不会无限循环,执行完一个才会进入下一个,下面takeEvery的例子和上面是等价的。

function* watchAndLog() {
    yield takeEvery('*', function* logger(action) {
        const state = yield select();
        console.log('action', action);
        console.log('state after', state);
    });
}

这个打log的例子两者是等价的,但其实take比takeEvery更能精确地控制。例如有login,logout功能,如果用takeEvery,就像传统redux-thunk一样,我们需要写两个独立的task,但用take时,可以将两个功能写在一起,代码阅读起来更加通顺:

function* loginFlow() {
    while (true) {
        yield take('LOGIN')
        ...   // perform the login logic
        yield take('LOGOUT')
        ...   // perform the logout logic
    }
}

本质上takeEvery就是封装了fork,take的高阶函数:

const takeEvery = (patternOrChannel, saga, ...args) => fork(function*() {
    while (true) {
        const action = yield take(patternOrChannel);
        yield fork(saga, ...args.concat(action));
    }
})

takeLatest(pattern, saga, …args) / takeLatest(channel, saga, …args):会fork个新的task,并取消之前创建的task。本质上takeLatest就是封装了fork,take的高阶函数:

const takeLatest = (patternOrChannel, saga, ...args) => fork(function*() {
    let lastTask;
    while (true) {
        const action = yield take(patternOrChannel);
        if (lastTask) {
            yield cancel(lastTask);   // cancel is no-op if the task has already terminated
        }
        lastTask = yield fork(saga, ...args.concat(action));
    }
})

fork / spawn / cancel / cancelled

fork(fn, …args) / fork([context, fn], …args) / fork({context, fn}, …args):创建一个非阻塞的call,支持传入context作为fn的this,返回一个task对象。默认call是阻塞的,执行时,saga会等待generator的执行结果返回。但fork执行时,saga会立即继续执行后续的代码。fork和race都是saga并发执行的核心。关于非阻塞的例子,参照这里

yield fork(authorize, user, password);

spawn(fn, …args) / spawn([context, fn], …args):和fork的区别是:fork是创建attached task,会依附于父task,父task会等待fork task完成。spawn是创建detached task,和父task是相同层级的,父task不会等待spawn task完成,即取消父task,spawn task不会受到任何影响,同样spawn task出错,也不会冒泡到父task。支持传入context作为fn的this。参照这里

cancel(task) / cancel([…tasks]) / cancel():取消task继续执行,不传参数表示取消所有fork的task。同样cancel也是非阻塞的,就像fork一样,不会等task取消完成再返回。

import { take, put, call, fork, cancel } from 'redux-saga/effects'

function* loginFlow() {
    while (true) {
        const {user, password} = yield take('LOGIN_REQUEST');
        const task = yield fork(authorize, user, password);
        const action = yield take(['LOGOUT', 'LOGIN_ERROR']);
        if (action.type === 'LOGOUT') {
            yield cancel(task);
        }
        yield call(Api.clearItem, 'token');
    }
}

cancelled():常用在finally中

function* saga() {
    try {
        ...
    } finally {
        if (yield cancelled()) {
            ...  // logic that should execute only on Cancellation
        }
        ...  // logic that should execute in all situations (e.g. closing a channel)
    }
}

all / race

all([…effects]) / all(effects):并行执行Effects

import { all, call } from 'redux-saga/effects';

function* mySaga() {
    const [customers, products] = yield all([
        call(fetchCustomers),
        call(fetchProducts)
    ])
}

race([…effects]) / race(effects):竞争执行Effects。

import { race, take, call } from 'redux-saga/effects';

function* fetchUsersSaga() {
    const [response, cancel] = yield race([
        call(fetchUsers),
        take(CANCEL_FETCH)
    ])
}

function* fetchUsersSaga() {
    const { response, cancel } = yield race({
        response: call(fetchUsers),
        cancel: take(CANCEL_FETCH)
    })
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注