在开始学习本教程之前,我们假定你已经能够熟练使用Generator、redux和react。
redux-saga 是一个在 React/Redux 应用中,可以优雅地处理 side effect(副作用:异步等)的库,你可以在一个地方创建sagas来专门处理side effect。
一个简单的例子
在store中引入中间件redux-saga
//store import { createStore, applyMiddleware } from 'redux'; import createSagaMiddleware, { runSaga } from 'redux-saga'; import reducer from './reducers'; import saga from './sagas'; const sagaMiddleware = createSagaMiddleware(); const store = createStore(reducer, applyMiddleware(sagaMiddleware)); sagaMiddleware.run(saga); //sagas.js function* helloSagas(){ console.log('hello saga'); } export default helloSagas
运行程序,就可以在控制台看到hello saga。现在让我们的saga开始捕获action,下面是一个简单的Counter组件,对于这种同步的action,我们无需借助saga,但是当我们点击按钮时,saga仍然能够监听到action的执行。运行下面的程序,当点击按钮时,在控制台会打印相应的action,但action并不是由saga来调用的。
// component import React, { Component } from 'react' import {connect} from 'react-redux' import * as actions from '@/actions' import { getSagaCounter } from '@/reducers' class Counter extends Component { render() { const { increment, decrement } = this.props return ( <div> <button onClick={increment}>increment</button> <button onClick={decrement}>decrement</button> <br /> { this.props.counter } </div> ) } } export default connect( (state) => ({counter: getSagaCounter(state)}), actions )(Counter) //reducer import { combineReducers } from 'redux'; const sagaCounter = (state = 0, action) => { switch(action.type){ case 'INCREMENT': return state + 1 case 'DECREMENT': return state - 1 default: return state } } export default combineReducers({ sagaCounter }) export const getSagaCounter = state => state.sagaCounter //action export const increment = () => ({ type: 'INCREMENT' }) export const decrement = () => ({ type: 'DECREMENT' }) //saga import {takeEvery} from 'redux-saga' function* listenAction(action){ console.log(action); } function* incrementSagas(){ //在每次 dispatch `INCREMENT` action 時,执行listenAction。 yield takeEvery('INCREMENT', listenAction) //在每次 dispatch `DECREMENT` action 時,执行listenAction。 yield takeEvery('DECREMENT', listenAction) } export default incrementSagas;
现在我们对做一点小改动,定义一个新的action:MY_INCREMENT,这个action的作用就是执行原来的INCREMENT,我们新添加一个按钮,点击时dispatch这个action
const { increment, decrement, myIncrement } = this.props return ( ... <button onClick={myIncrement}>myIncrement</button> ... ) //action export const myIncrement = () => ({ type: 'MY_INCREMENT' }) //sagas import {takeEvery, put} from 'redux-saga/effects' function* increment(action){ yield put({type:'INCREMENT'})//dispatch } function* incrementSagas(){ yield takeEvery('MY_INCREMENT', increment) } export default incrementSagas;
当saga拿到MY_INCREMENT时,执行increment函数,而increment函数就是dispatch INCREMENT,从而实现的counter的增加。此时我们就发现,如果要MY_INCREMENT延迟执行的话,只需要在increment函数中yeild一个延迟函数,当一个Promise在saga中yield时,saga将暂停执行,知道这个Promise被resolve,然后saga继续执行。
const delay = ms => new Promise(resolve => setTimeout(resolve, ms)) function* increment(action){ yield delay(1000) //延迟1s yield api.fetchSomeData() // 等待数据返回 yield put({type:'INCREMENT'}) //上面的都完成后,dispatch `INCREMENT` action }
现在回过头,看看我们最开始的helloSagas,如果此时要在加上这个saga该如何实现呢,在redux-saga/effects里面有一个all方法,只需要将多个saga放入all里面就可以了,因此很容易实现saga文件的拆分
export default function* rootSaga() { yield all([ helloSaga(), incrementSagas() ]) }
到这里,我们大致已经了解了saga的基本原理,后面的教程将通过一个todolist详细介绍具体的细节。 上面的源码点这里
准备工作
-
安装:react react-dom react-redux redux redux-saga react-router-dom react-router axios
-
node服务器
最开始的代码就是这个样子:
//App.js import React from 'react' import {BrowserRouter} from 'react-router-dom' import TodoList from './components/TodoList' const App = () => (<div> <BrowserRouter> <div> <h1>Hello Saga</h1> <div> <TodoList /> </div> </div> </BrowserRouter> </div>) export default App //TodoList.js import React from 'react' import AddTodo from './AddTodo' import VisibleTodoList from './VisibleTodoList' import Footer from './Footer' const TodoList = () => ( <div> <AddTodo /> <VisibleTodoList /> <Footer /> </div> ) export default TodoList //AddTodo.js import React from 'react' const AddTodo = () => { let input return ( <div> <input ref={node => { input = node }} /> <button>add todo</button> </div> ) } export default AddTodo //VisibleTodoList.js import React from 'react' const VisibleTodoList = () => ( <div> <p> <span>todo</span> <a href="#">×</a> </p> </div> ) export default VisibleTodoList //Footer import React from 'react'; const Footer = () => ( <p> <a href="#">all</a> {' '} <a href="#">active</a> {' '} <a href="#">completed</a> </p> ) export default Footer
现在,让我们来做第一件事,请求todos列表。需要想服务端发起请求,这里需要三个actions,首先是发起请求,然后是请求成功和请求失败。
export const fetchTodo = (filter) => ({ type:'FETCH_TODO', filter }) export const fetchTodoSuccess= (todos) => ({ type: 'FETCH_TODO_SUCCESS', todos }) export const fetchTodoError= () => ({ type: 'FETCH_TODO_ERROR' })
我们的列表只会在FETCH_TODO_SUCCESS的时候才会改变,因此相应的reducers就会想这样,我们每一个reducer都要暴露一些方法用于获取store中的数据,在component中使用。
const todos = (state = [], action) => { switch(action.type){ case 'FETCH_TODO_SUCCESS': return action.todos default: return state } } export const getTodos = (state) => state.todos
现在我们重构VisibleTodoList,使其能够重store中获取数据,并将数据显示在列表中。
import React, { Component } from 'react' import { connect } from 'react-redux' import { fetchTodo } from '@/actions' import {getTodos} from '@/reducers' class VisibleTodoList extends Component { componentDidMount(){ this.props.fetchTodo(); } render() { const { todos } = this.props return ( <div> {todos.map(todo => ( <p key={todo.id}> <span>{todo.text}</span> <a href="#">×</a> </p> ))} </div> ) } } export default connect( (state) => ({ todos: getTodos(state) }), { fetchTodo } )(VisibleTodoList)
通过connect向组件的props传入todos值和fetchTodo方法,fetchTodo方法在组件装载完毕后调用,而此时,我们的saga监听到这个action,并对其做相应的处理
import {takeEvery, put} from 'redux-saga/effects' import * as api from '@/api' function* fetchTodo(action){ const {todos, error} = yield api.getTodos() if(todos){ yield put({type:'FETCH_TODO_SUCCESS', todos}) } else { yield put({type: 'FETCH_TODO_ERROR'}) } } function* fetchTodoSaga(){ yield takeEvery('FETCH_TODO', fetchTodo) } //api export const getTodos = (params) => axios.get('/api/list', {params}) .then(result => ({todos: result.data})) .catch(err => ({err}))
当saga监听到FETCH_TODO触发,就执行fetchTodos函数,函数中向服务器发起请求,如果成功得到数据后,dispatch FETCH_TODO_SUCCESS,否则执行dispatch FETCH_TODO_ERROR
此时我们能够从浏览中看到我们从数据库中取得的数据,现在,我们要对VisibleTodoList组件做优化,使其与Footer组件配合,能够根据footer的状态显示相应的数据,这里通过引入react-router来实现,这里直接给代码: 源码
import {takeEvery, put} from 'redux-saga/effects' import * as api from '@/api' function* fetchTodo(action){ const {todos, error} = yield api.getTodos({filter:action.filter}) if(todos){ yield put({type:'FETCH_TODO_SUCCESS', todos}) } else { yield put({type: 'FETCH_TODO_ERROR'}) } } function* fetchTodoSaga(){ yield takeEvery('FETCH_TODO', fetchTodo) } export default fetchTodoSaga
现在我们仔细揣测下这个saga,每当收到FETCH_TODO就执行请求,返回后就分发FETCH_TODO_SUCCESS,然而这个请求不可控,当我们快速切换filter时,连续发起多个请求,此时页面显示的结果是最后执行FETCH_TODO_SUCCESS的结果,但这可能跟我们预想的不一样。正确的做法是,每当发起一个FETCH_TODO就取消上一次,让结果永远是最新的FETCH_TODO的结果。因此我们不能采用takeEvery,需要替换成takeLatest,其作用就是执行最新的FETCH_TODO时会取消上一次的FETCH_TODO,即上一次请求返回后不会再dispatch其他action。
