简单明了学会使用Redux Saga

751 阅读6分钟

Saga的来源

我们都知道,技术都是为了解决某个或某类问题而产生的。

Saga作为Redux的功能增强,所以,我们先来看看单独使用Redux会遇到什么问题。

1、Redux基础知识回顾

Redux中三个核心概念:ActionReducerStore,Redux专门用来管理状态。

这句话貌似很难理解,没有使用Redux之前我们会怎么做?

没有使用Redux之前,我们会直接对数据进行操作与修改,所有的功能操作都在一个地方进行处理,当应用越来越复杂的时候,应用状态会变得非常非常乱

就比如说,一个饭店想要的是新鲜的猪肉,饭店需要自己去养猪,把猪养大之后还需要屠宰,清洗等等...什么事都自己干。 除了新鲜的猪肉,饭店还要新鲜的青菜,它还需要去种菜,摘菜...另外,饭店可能还需要海鲜、辅料...等等食材。

忙不过来是一回事,会增大出错的成本:比如摘来的菜不小心喂猪去了。

所以就有了Redux

Redux有三个核心的概念——StoreActionReducer就像人的器官一样,各种细胞进行了分化,专门的器官负责专门的事情,高效且不容易出错。

在Redux中,Store dispatch 一个 Action,交予Reducer进行处理,Reducer处理完把结果返回更新Store

就像是这个饭店,它发出了一个Action说:“我要新鲜的猪肉和青菜!”。随即Reducer接收到这个Action之后进行处理,最终给饭店带来的新鲜的猪肉与青菜,具体过程在Reducer中进行处理,饭店不用过问。

这样一来,Redux就帮我们解决了一个状态混乱的问题。👏

2、Saga来了

那单独用Redux还是不够的! Reducer需要处理的事情就太多了:主要业务逻辑、sideEffects(副作用, 例如异步获取数据,访问浏览器缓存等。)

在这个例子中,Reducer中除了养猪还要屠宰,而屠宰还需要在养猪之后进行。同样的,Reducer除了种菜还要摘菜,但是摘菜需要菜长大了才能摘。这样一来,Reducer会变得很臃肿,里面也会乱成一团麻。

这个时候,我们需要对Reducer的功能进行进一步的分工处理,这时候,Saga出场了。

Saga负责养猪、种菜...把一些异步处理,读取缓存区数据的操作交给Saga去做,在技术上叫做“sideEffects”(副作用):

那么Reducer怎么知道猪养好了,菜长大了呢?天天守在那里吗?

不需要Saga负责养猪还有种菜之外,等到猪长大和菜可以摘了,就告知Reducer一声:来屠宰了!来摘菜了!Reducer接收到信息后进行相关的操作。也不需要Reducer天天守在那里看猪长大没,菜可以吃了没...(会阻塞进程的。)

那么Saga如何通知Reducer呢?

我们知道,Reducer接收的是state还有action两个对象作为参数,然后通过计算返回一个新的state,对于其他格式的“通知信息”是没办法识别。因此,Saga通知Reducer的时候,肯定也是dispatch一个Action交予Reducer进行处理。

这样一来,很多语法规则的由来都是有章可循的。不知道你看懂了没?

Saga的基础知识

Saga是一个 Redux中间件,意味着这个线程可以通过正常的Redux Action从主应用程序启动,暂停和取消,它能访问完整的Redux state,也可以dispatch Redux Action。 一个 Saga 就像是应用程序中一个单独的线程,它独自负责处理副作用。

中间件

中间件就是非业务的技术类组件。它介于底层逻辑与业务之间,相当于中介的作用。

如果应用了中间件,程序会先经过中间件的处理之后再返回给具体业务进行处理。我们可以把应用公用部分,或者是需要进行异步操作的部分在中间件中进行处理,例如用户登录、数据请求等。

在Redux中,当我们dispatch 一个Redux Action的时候,Saga作为处理的中间件,它可以接收到Action之后进行相关的处理,并把相关的处理交给对应的Reducer再进行处理。

Saga核心API

1、辅助函数

takeEvery

例如:每次点击 “1秒后加1” 按钮时,我们发起一个 incrementAsync 的 action。

首先我们创建一个将执行异步 action 的任务:

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

function* incrementAsync() {
    // 延迟1s
    yield delay(1000)

    yield put({
        type: 'increment'
    }) 
}

然后在每次 incrementAsync action 被发起时启动上面的任务.

import { takeEvery } from 'redux-saga'

function* watchIncrementAsync() {
    yield takeEvery('incrementAsync', incrementAsync)
}

takeLatest

在上面的例子中,takeEvery 是每次发起“incrementAsync” action的时候都会执行。如果我们只想得到最新那个请求的响应,我们可以使用 takeLatest 辅助函数

import { takeLatest } from 'redux-saga'

function* watchIncrementAsync() {
    yield takeLatest('incrementAsync', incrementAsync)
}

2、Effect Creators

redux-saga框架提供了很多创建effect的函数,下面是开发中最常用的几种:

  • take(pattern)
  • put(action)
  • fork(fn, ...args)

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

put(action) put函数是用来发送action的 effect,你可以简单的把它理解成为Redux框架中的dispatch函数,当put一个action后,reducer中就会计算新的state并返回。

function* incrementAsync() {
    // 延迟1s
    yield delay(1000)

    yield put({
        type: 'increment'
    }) 
}

fork(fn, ...args) fork 函数是用来调用其他函数的,但是fork函数是非阻塞函数,也就是说,程序执行完 yield fork(fn, args) 这一行代码后,会立即接着执行下一行代码语句。

Redux-Saga语法

讲完了Saga的语法,我们来看看Redux与Saga配合使用的语法知识:

1、创建一个最简单的Saga

// saga.js
function* helloSaga() {
    console.log('Hello saga!')
}
export default helloSaga

2、把Saga作为中间件使用

这里使用的是createSagaMiddleware函数,创建一个saga中间件:

import createSagaMiddleware from 'redux-saga'

const sagaMiddleware = createSagaMiddleware()

3、Redux如何添加中间件

这里使用的是applyMiddleware函数,应用saga中间件:

import { createStore, applyMiddleware } from 'redux'

const store = createStore(reducer,
  applyMiddleware(sagaMiddleware)
)

4、运行

import helloSaga from './saga'

sagaMiddleware.run(helloSaga)

完整🌰代码示例

myComponent.js

import React from 'react'
import ReactDOM from 'react-dom'

class MyComponent extends React.Component {
    render() {
        return (
            <div className="index">
                <p>{this.props.count}</p>
                <button onClick={this.props.increment}>加1</button>
                <button onClick={this.props.incrementAsync}>1秒后加1</button>
            </div>
        )
    }
}

export default MyComponent

reducer.js

export default function reducer(state = {
  count: 10
}, action) {
  switch (action.type) {
    case 'increment': {
      return {
       count: state.count + 1
      }
    }
    default:
      return state
  }
}

App.js

import { connect } from 'react-redux'
import MyComponent from './myComponent'

// Map Redux state to component props
function mapStateToProps(state) {
  console.log('state', state)
  return {
    count: state.count
  }
}

// Map Redux actions to component props
function mapDispatchToProps(dispatch) {
  return {
    increment: () => dispatch({
      type: 'increment'
    }),
    incrementAsync: () => dispatch({
      type: 'incrementAsync'
    })
  }
}

// Connected Component
const App = connect(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent)

export default App

main.jsx

import "regenerator-runtime/runtime"
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import createSagaMiddleware from 'redux-saga'
import rootSaga from './saga'

import App from './App'
import reducer from './reducer'

const sagaMiddleware = createSagaMiddleware()
// Store
const store = createStore(reducer,
  applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.body.appendChild(document.createElement('div'))
)

saga.js

import { delay, put, takeEvery, all } from 'redux-saga/effects'
function* helloSaga() {
    console.log('Hello saga!')
}

function* incrementAsync() {
    // 延迟1s
    yield delay(1000)

    yield put({
        type: 'increment'
    }) 
}

function* watchIncrementAsync() {
    yield takeEvery('incrementAsync', incrementAsync)
}

export default function* rootSaga() {
    yield all([
        helloSaga(),
        watchIncrementAsync()
    ])
}

Github源码参考:github.com/kexinWeb/re…