redux-saga 深入浅出

1,616 阅读8分钟

初识 redux-saga

在接触到一项新的技术时,我们最关心的问题是这项技术是什么?这项技术的出现是为了解决什么问题?对于 redux-saga 而言,需要解决的同样是这几个问题。

首先,redux-saga 是什么? 官网中对于 redux-saga 的介绍如下:

redux-saga 是一个用于管理 Redux 应用异步操作(side effects, 这里可以直译为副作用,但为了更好理解翻译为异步操作)的中间件(又称异步 action). redux-saga 通过创建 Saga 将所有的异步操作逻辑手机在一个地方集中处理,可以用来代替 redux-thunk 中间件。

在这段描述中,不难发现,redux-saga 是 Redux 的一个中间件。该中间件的功能是用来处理异步操作,也可以成为副作用。

我们知道,redux 是用来管理组件的状态的轻量级的库,并且 redux 中用来处理状态变化的函数是纯函数,即一个输入对应一个固定的输出。但是,redux 的这种操作在功能上会显得很单一。在很多时候,我们希望从后端服务器中获取状态,并且将状态放到 redux 中进行管理。因此,只用 redux 是没有办法完成对于异步请求的管理的。而 redux-saga 为我们提供了管理异步请求的方式。

同时使用 redux-saga 和 redux,意味着应用的逻辑会存在两个地方:

  • Reducers 负责处理 action 的 state 更新
  • saga 负责协调那些复杂或异步的操作

接下来,我们来了解一下Redux-saga 的几个基本概念

  • 生成器(generator):redux-saga 是基于生成器实现的。生成器使用 ES6 实现的,它可以在函数的内部打断函数的执行,在下一次运行时从之前被打断的地方开始重新执行。关于生成器的更多介绍,可以参考阮一峰老师的博客.
  • effects:在 redux-saga 的世界里,所有的任务都通用 yield Effect 来完成。Effect 可以看成是 redux-saga 的任务单元。Effects 都是简单的 JavaScript 对象,包含了要被 saga middleware 执行的信息。redux-saga 为各项任务提供了各种 effect 创建起,比如调用一个异步函数、发起一个 action 到 store、启动一个后台任务或者等待一个满足某些条件的未来的 action。
  • redux-saga 分类
    • root saga:启动 saga 的唯一入口
    • watcher saga:监听被 dispatch 的 actions,当接受到 action 或者知道其被触发时,调用 worker 执行任务
    • worker saga:简单来讲就是干活的,比如调用 API,进行异步请求,获取请求结果等等

redux-saga 使用实例

我们来看一个简单的 redux-saga 使用实例来感受一下它的大致用法:

该实例要实现的功能是一个异步的加法器。具体的工程构建这里就不赘述了,这里只展示一些关键的代码和文件。

  • react 入口文件的代码,主要完成了 store 创建,引入redux-saga 中间件,并且用 react-redux 将 store 进行包裹并且传递到 react 组件中,以上功能。
    // 当前路径:src/index.js

    // 第三方模块引入
    import React from 'react'  
    import ReactDom from 'react-dom'
    import { Provider } from 'react-redux'
    import { createStore, applyMiddleware } from 'redux'
    import createSagaMiddleware from 'redux-saga'

    // 自定义模块引入
    // 1,redux中的 reducer引入
    import rootReducer from './LearningSaga/reducer'
    // 2,react中 Counter组件引入
    import Counter from './LearningSaga/component'
    // 3, redux-saga中间件的 saga文件引入
    import rootSaga from './LearningSaga/saga'

    // 4,创建一个redux-saga中间件
    const sagaMiddleware = createSagaMiddleware()
    // 5,将redux-saga中间件加入到redux中
    const store = createStore(rootReducer, {}, applyMiddleware(sagaMiddleware))
    // 6,动态的运行saga,注意 sagaMiddleware.run(rootSaga) 只能在applyMiddleware(sagaMiddleware)之后进行
    sagaMiddleware.run(rootSaga)

    // 7,挂载react组件
    ReactDom.render(<Provider store={store}><Counter /></Provider>, document.getElementById('root'))
  • reducer.js:
// 当前路径:src/LearningSaga/reducer/index.js

import { combineReducers } from 'redux'

function counterReducer(state = 1, action = {}) {
    switch (action.type) {
        case "increment": return state + 1;
        default: return state
    }
}

const rootReducer = combineReducers({ counter: counterReducer })

export default rootReducer
  • react 组件:
// 当前路径:src/LearningSaga/component/index.js

import React from 'react'
import { connect } from 'react-redux'

class Counter extends React.Component {
    add = () => this.props.dispatch({ type: 'increment_saga' })
    // addAsync函数将派发一个类型为incrementAsync_saga的action
    addAsync = () => this.props.dispatch({ type: 'incrementAsync_saga' })
    render() {
        return (
            <div>
                <span>{this.props.counter}</span>
                <button onClick={this.add}>add1-sync</button>
                <button onClick={this.addAsync}>add1-async</button>
            </div>
        )
    }
}
const mapStateToProps = state => ({ counter: state.counter })
export default connect(mapStateToProps)(Counter)

  • redux-saga 创建的effects:
// 当前路径:src/LearningSaga/saga/index.js

import { all, put, takeEvery, delay } from 'redux-saga/effects'

function* increment() {
    yield put({ type: 'increment' }) // 相当于:dispatch({ type: 'increment' })
}
function* incrementAsync() {
    // 延迟1s
    yield delay(1000)
    // 1s后,dispatch({ type: 'increment' })
    yield put({ type: 'increment' })
}
function* watchIncrement() {
    yield takeEvery('increment_saga', increment) // 监听类型为increment_saga的action,监听到启动increment

    // 监听类型为incrementAsync_saga的action,监听到启动incrementAsync
    yield takeEvery('incrementAsync_saga', incrementAsync)
}
function* rootSaga() {
    yield all([watchIncrement()]) // 启动watchIncrement
}

export default rootSaga

正如在前文中提到的,该例子中的应用逻辑存储在两个地方:reducer 中对于不同 action 的处理;redux-saga 中对于 action 何时该分发的处理。本例中,我们主要关注 redux-saga 相关的内容。

跟 redux-saga 相关的内容主要集中在 saga 相关的文件中,以及在 react 应用的 index 文件中的引用。

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import rootSaga from './LearningSaga/saga'

const sageMiddleware = createSageMiddleware()
const store = createStore(rootReducer, {}, applyMiddleware(sageMiddleware))

sagaMiddleware.run(rootSaga)

上述代码完成了在 redux 的 store 中对于 saga 中间件的应用,并且将 rootSaga 在后台运行了起来。

最为关键的,即开发者需要写的代码逻辑就是在 saga 中编写的。这段代码也符合前文的描述,通过 yield 创建 effects,并且所有的 saga 分为三类。

首先,counter 组件渲染出 store 中的 counter 数据和两个 button —— add1-sync 和 add1-async;当点击 add1-sync 时,触发其绑定的 add 方法。add 方法会向 redux 分发一个 type 属性为 'increment_saga' 的 action。这个,action 被 redux-saga 拦截下来,通过 saga 中的 takeEvery() 中绑定的方法执行。该方法最终也会返回一个action。

redux-saga 常用 API

redux-saga 的常用的 API 分为两类:saga辅助函数、effect creators。

  1. saga 辅助函数:

redux-saga 提供了两个辅助函数:takeEverytakeLatest.

takeEvery 就像流水线的搬运工,过来一个货物就直接执行后面的函数,一旦调用,他就会一直执行这个工作,绝对不会停止对于货物的监听过程和触发搬运货物的函数。

takeLatest 允许多个 fetchData 实例同时启动,但是最终只会返回最新的响应结果。在任何时刻,takeLatest 只允许执行一个 fetchData 任务,并且这个任务是最后被启动的那个,如果之前已经有一个任务在执行,那么执行当前任务之前会把之前执行的任务自动取消。

  1. effect creators:
    • take(pattern)
    • put(action)
    • call(fn, ...args)
    • fork(fn, ...args)
    • select(selector, ...args)

take(pattern)

take 函数可以理解为监听未来的 action,它创建了一个命令对象,告诉 middleware 等待一个特定的 action,Generator 会暂停,直到一个与 pattern 匹配的 action 被发起,才会继续执行下面的语句

put(action)

put 函数是用来发送 action 的 effect,你可以简单地把它理解为 redux 框架中的 dispatch 函数,当 put 一个 action 后,reducer 中就会计算新的 state 并返回,注意:put 也是阻塞 effect

call(fn, ...args)

call 函数你可以把它简单的理解为可以调用其他函数的函数,它命令 middleware 来调用 fn 函数,args 为函数的参数。注意:fn 函数可以是一个 Generator 函数,也可以是一个返回 promise 的普通函数,call 也是一个阻塞 effect

fork(fn, ...args)

fork 函数和 call 函数很像,都是用来调用其他函数的,但是 fork 函数是非阻塞函数。也就是说,程序执行完 yield fork(fn, ...args) 之后,会立即执行下一行语句。

select(selector, ...args)

select 函数用来指示 middleware 调用提供的选择器获取 store 上的 state 数据,也可以简单的理解为 redux框架中获取store上的state数据一样的功能:store.getState()

redux-saga 原理

redux-saga 作为 Redux 的插件,它所完成的功能就是截获从别处发来的 action,并且对它进行封装处理。在合适的时机完成 action 的重新封装。具体的过程如图所示:

image.png

除此之外,由于 redux-saga 对于 effect 的监听是独立于 reducer 中的处理的。因此,它们可以同时运行。比如说在数据加载的时候想让页面发生一些变化(最常见的就是有个小圈,一直在转),只需要在 store 中添加相应的属性,并在合适的 action.type 发生时进行相应的设置即可。

参考