redux-saga

1 概述

redux-saga 是 redux 一个中间件,用于解决异步问题。

2 es6 Generator

解决地狱回调问题,通过 yield 关键字,可以让函数的执行流挂起。

使用在线工具验证:https://jsbin.com/musedihito/edit?js,console

参考:
https://blog.csdn.net/tcy83/article/details/80427195

2.1 基础示例

function* geo() { // 声明一个 Generator 函数
  const x = yield 'xxx';
  console.log(x); // 返回的是 undefined
  const y = yield 'yyy'
}
const Geo = geo(); // 调用,返回 Generator 对象赋值给 Geo
console.log(Geo.next()); // 通过 next() 方法,执行Generator 中的函数体。
console.log(Geo.next());
console.log(Geo.next());


结果

2.1.1 进一步分析

yield和next方法
例子1
例子2
function* geo() {
  const post = yield $.getJSON("https://jsonplaceholder.typicode.com/posts");
  console.log(post[0].title);
  const users = yield $.getJSON("https://jsonplaceholder.typicode.com/users");
  console.log(users[0]);
  
}

run(geo);

function run(geo) {
  const Geo = geo(); 
  
  function handle(yielded) {
    if(!yielded.done) {
      yielded.value.then(function(data){
        return handle(Geo.next(data));
      })
    }
  }
  
  handle(Geo.next());
  
}


2.3 区别 yield* 和 yield

区别yield和yield*

4 简单理解 'redux-saga/effects' 中的几个关键字:fork,call, put,takeEvery,takeLatest,all

4.1 fork

参考:https://redux-saga-in-chinese.js.org/docs/advanced/ForkModel.html

创建一个新的进程或者线程,并发发送请求。

关键代码:

function* user() {
  yield takeEvery('FETCH_REQUEST', fetch_user); // 监听 FETCH_REQUEST action
}

// 并发发送请求
function* fetch_user() {
  const [users, todos] = [
    yield fork(fetchResource, 'https://jsonplaceholder.typicode.com/users'),
    yield fork(fetchResource, 'https://jsonplaceholder.typicode.com/todos')
  ]
}


function* fetchResource(resource) {
  const data = yield call(axios.get, resource);
  
  // 获取 call 数据,触发成功后的 action
  yield put({ type: 'FETCH_SUCESS', uu: data });
  
}

4.2 call

发送 api 请求

4.3 put

发送对应的 dispatch,触发对应的 action

4.4 takeEvery

  • 监听对应的 action;
  • 每一次 dispatch 都会触发;例如:点击一个新增的按钮,2s 后触发新增动作,在2s内不断点击按钮,这时候,每一次点击,都是有效的。

yield takeEvery('FETCH_USER', fetch_user);

4.5 takeLatest

  • 监听对应的 action;
  • 只会触发最后一次 dispatch;例如:点击一个新增的按钮,2s 后触发新增动作,在2s内不断点击按钮,这时候,只有最后一次点击是有效的。

yield takeLatest('FETCH_USER', fetch_user);

4.6 all

跟 fork 一样,同时并发多个 action,没有顺序。

yield all([ // 同时并发多个
  ...rootUser,
  add()
])

5 demo 示例

使用 create-react-app 脚手架初始化;
在线请求API:https://jsonplaceholder.typicode.com/

目录结构

src/index.js:项目入口文件配置 redux 和 saga

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

import { createStore, applyMiddleware } from 'redux'; // 中间件和 store
import { Provider } from 'react-redux'; // provider
import { composeWithDevTools } from 'redux-devtools-extension'; // 调试工具
import rootReducer from './reducers'; // reducers

import createSagaMiddleware from 'redux-saga'; // 1:saga 引入createSagaMiddleware
import rootSaga from './sagas'; // 5:自己写的根 rootSaga


// 2:创建saga中间件
const sagaMiddleware = createSagaMiddleware()

const store = createStore(
  rootReducer,
  composeWithDevTools( // 3:把 sagaMiddleware 当做一个中间件,引用调试工具
    applyMiddleware(sagaMiddleware)
  )
)

// 4:启动 saga
sagaMiddleware.run(rootSaga);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

serviceWorker.unregister();


src/App.js:页面

import React, { Component } from 'react';
import './App.css';
import { connect } from 'react-redux';
import { increate, increateAsync, fetch_user } from './actions/counter';

class App extends Component {

  render() {
    const { isFetch, error, user } = this.props.users;
    let data = "";
    if (isFetch) {
      data = '正在加载中。。。'
    } else if (user) {
      data = user.data[0]['name'];
    } else if (error) {
      data = error.message;
    }
    return (
      <div className="App">
        {/* 触发dispatch,发送对应的action */}
        <div style={{ marginBottom: 20 }}>
          <p>{this.props.counter}</p>
          <button onClick={() => this.props.increate()}>新增</button>
          &nbsp;&nbsp;&nbsp;
          <button onClick={() => this.props.increateAsync()}>异步新增</button>
          &nbsp;&nbsp;&nbsp;
          <button onClick={() => this.props.fetch_user()}>axios请求</button>
        </div>
        <h2>{data}</h2>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    counter: state.counter, // state 对应的 key, 在 reducers/index.js 中声明。
    users: state.us
  }
}

// mapStateToProps,在 reducers/index.js 中,通过 connect 导入对应的 state
// { increate, increateAsync, fetch_user } ,通过 connect 导入对应的action,在view触发相应的action
export default connect(mapStateToProps, { increate, increateAsync, fetch_user })(App);



constants/index.js:常量

export const INCREMENT = "INCREMENT";
export const INCREMENT_ASYNC = "INCREMENT_ASYNC";


actions/counter.js:action

import { INCREMENT, INCREMENT_ASYNC } from '../constants';

export const increate = () => {
  return {
    type: INCREMENT
  }
}

export const increateAsync = () => {
  return {
    type: INCREMENT_ASYNC
  }
}


export const fetch_user = () => {
  return {
    type: 'FETCH_REQUEST'
    // type: 'FETCH_USER'
  }
}



reducers/index.js:reducers 的入口文件

import { combineReducers } from 'redux';
import us from './users';
import counter from './counter';

export default combineReducers({
  us,
  counter
})

reducers/counter.js:

import { INCREMENT } from '../constants';

const counter = (state = 1, action = {}) => {
  switch (action.type) {
    case INCREMENT:
      return state + 1;
    default: return state
  }
}

export default counter;


reducers/users.js:

const initialState = {
  isFetch: false,
  error: null,
  user: null
}

const u = (state = initialState, action = {}) => {
  switch (action.type) {
    case 'FETCH_REQUEST':
      return state = {
        isFetch: true,
        error: null,
        user: null
      };
    case 'FETCH_SUCESS':
      return state = {
        isFetch: false,
        error: null,
        user: action.uu
      };
    case 'FETCH_FAIL':
      return state = {
        isFetch: false,
        error: action.errors,
        user: null
      };
    default: return state
  }
}

export default u;

sagas/index.js:sagas 的入口文件

import { all, fork } from 'redux-saga/effects';
import rootUser from './fetchUser';
import { add } from './counter';

export default function* rootSaga() {
  yield all([ // 同时并发多个
    ...rootUser,
    add()
  ])
}


sagas/counter.js:

import { INCREMENT_ASYNC, INCREMENT } from '../constants';
import { delay } from 'redux-saga';
import { call, put, takeEvery} from 'redux-saga/effects';

function* increase() {
  yield call(delay, 1000); // 需要执行异步的时候,直接调用 call
  yield put({ type: INCREMENT });
}

// 直接 export 函数,没有做整理
export function* add() {
  yield takeEvery(INCREMENT_ASYNC, increase);
}


sagas/fetchUser.js:

使用数组导出函数,就不用一个一个函数导出,优化了代码!

import { call, takeEvery, put, fork } from 'redux-saga/effects';
import { delay } from 'redux-saga';
import axios from 'axios'

function* fetch_user() {
  try {
    const users = yield call(axios.get, 'https://jsonplaceholder.typicode.com/users'); // axios.get('https://jsonplaceholder.typicode.com/users')      
    yield put({ type: 'FETCH_SUCESS', uu: users });
  } catch (e) {
    yield put({ type: 'FETCH_FAIL', errors: e });
  }
}

function* fetch_todo() {
  const todos = yield call(axios.get, 'https://jsonplaceholder.typicode.com/todos'); // axios.get('https://jsonplaceholder.typicode.com/users')      
  console.log(todos);
}

function* user() {
  yield takeEvery('FETCH_REQUEST', fetch_user); // 正在加载数据
}

function* todo() {
  yield takeEvery('FETCH_TODO', fetch_todo);
}

// 使用数组导出
const rootUser = [
  user(),
  todo()
]

export default rootUser;


package.json:

{
  "name": "redux-saga-demo",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "axios": "^0.18.0",
    "react": "^16.6.0",
    "react-dom": "^16.6.0",
    "react-redux": "^5.1.0",
    "react-scripts": "2.1.0",
    "redux": "^4.0.1",
    "redux-devtools-extension": "^2.13.5",
    "redux-saga": "^0.16.2"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
  ]
}


效果

6 总结体会

1:使用同步的操作,处理异步的请求;
2:使用 redux + redux-saga,在入口文件 index.js 配置 saga;
3:在 saga 中,使用 takeEvery 或者 takeLatest 在项目启动的时候,监听对应的 action,触发对应的 action;
4:当页面触发了对应的 action 时,除了会去寻找对应的 reducer(找不到也没事),进行操作;也会触发 saga 监听的 action,进行异步请求等操作;

代码:
https://github.com/hongzelin/redux-saga-demo

7 参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容