资讯专栏INFORMATION COLUMN

redux-saga实现与原理

itvincent / 3535人阅读

摘要:特点集中处理副作用问题异步实现为监听执行的工作形式主要是借鉴模式和使用进行实现的。返回的遍历器对象,可以依次遍历函数内部的每一个状态。为了方便,下文中是的简称。若任务仍在运行中则为任务抛出的错误。由于循环,再次执行。

介绍redux-saga使用和常用api介绍的文章很多,但是真正介绍原理的却很少,下面我用自己的思路讲一下redux-saga的执行过程。源码有很多删减,感兴趣的可自行查看。

1.react-saga是什么

redux-saga是用于管理 Side Effects(异步操作/副作用)的redux中间件

2.什么是redux中间件

redux中间件是通过改变store.dispatch使dispatch(action)时能处理副作用。在这里我们用于处理异步操作。
具体可参考http://www.ruanyifeng.com/blo...。如果对中间件不熟悉,请一定把这篇文章看完再往下进行。

3.redux-saga特点

集中处理redux副作用问题

异步实现为generator

watch/worker(监听->执行)的工作形式

redux-saga主要是借鉴 sagas模式 和使用 generators 进行实现的。

首先,我们讲讲sagas模式:解决长时间运行的事务导致的系统运行效率以及并发能力的问题,将业务分为多个独立的事务,每个业务都会确保拥有修正事务(回滚),如果业务过程遇到了错误的情况并且无法继续,它就可以执行修正事务来修正已经完成的步骤,这样以保证最终的一致性。举个栗子:A事务需要等待B事务完成之后才能执行,如果B事务需要的时间很长,那么A事务就要等很久才能执行,如果用sagas模式,可以把A事务和B事务分别执行,如果B事务执行失败,则把A事务进行回滚。

sagas模式参考文档:https://blog.csdn.net/ethanwh...

redux-saga中对sagas的借鉴:在redux-saga里,一个saga就是一个生成器函数(generator function),可以在系统内无限期运行。当特定action被dispatch时,saga就可以被唤醒。saga也可以继续dispatch额外的actions,也可以接入程序的单一状态树。也能从主应用程序启动,暂停和取消。借鉴了sagas的满足特殊条件的长事务,可回滚。

接下来讲讲generator
Generator 函数是 ES6 提供的一种异步编程解决方案。执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。为了方便,下文中gen是generator的简称。

举个简单的例子

function* helloWorldGenerator() {
  yield "hello";
  yield "world";
  return "ending";
}

var hw = helloWorldGenerator();

执行结果

hw.next()
// { value: "hello", done: false }

hw.next()
// { value: "world", done: false }

hw.next()
// { value: "ending", done: true }

hw.next()
// { value: undefined, done: true }

再来一个复杂一点的传值的例子

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}
var b = foo(5);

执行结果

b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

具体可参考文档http://es6.ruanyifeng.com/#do...,这里很重要,请了解一下。

了解完上面的基础,下面我们开始讲redux-saga的执行过程。

我们以一个demo的形式进行分析。
需要解决几个问题:

怎么执行监听执行?

如何循环把每一步执行完?

怎么处理下一次任务?

当我们点击按钮时,会从后端请求接口,将返回的数据更新到页面上

我们先自己实现一个中间件解决这个需求:

import axios from "axios";

const takers = []; // 存generator

// 执行gen
function runGen(dispatch, gen) {
    // 防止无限循环
    if (!takers.length) {
        return;
    }
    // 遍历执行generator
    takers.forEach((taker, key) => {
        const g = taker();
        // 删除已执行的taker
        takers.splice(key, 1);
        // 执行yield axios.post并返回一个promise
        const userPromise = g.next().value;
        userPromise.then((user) => {
            // 把{dispatch, user}设置为上一步的返回值,并执行yield dispatch
            g.next({ dispatch, user });
            // 执行yield takers
            g.next()  
        });
    })
}

export default function fetchMiddleware(_ref2) {
    var getState = _ref2.getState,
        dispatch = _ref2.dispatch;
    // 初始化时注册taker,把generator添加到takers,用于dispatch时执行
    fetchMiddleware.run = () => takers.push(gen)
    // 改变dispatch
    return (next) => {
        return (action) => {
            // dispatch时执行这里
            var result = next(action);
            runGen(dispatch, gen)
            return result;
        };
    };
    return fetchMiddleware;
}

怎么用呢

import fetchMiddleware from "./fetchMiddleware";
// 初始化
fetchMiddleware.run()
// 需要执行的gen
function* gen(){
    const { dispatch, user } = yield axios.post("http://rest.learncode.academy/api/wetern/users");
    const { data } = user;
    yield dispatch({ type: "UPDATE_USER", payload: data })
    yield takers.push(gen)
}

现在我们来看看这个中间件实现步骤

点击按钮,执行dispatch({ type: "FEATCH_USER })

对于上面那个例子,我们用gen.next()实现一步一步执行。

初始化时把gen添加到takers中,这样做的目的是点击按钮的时候可以执行generator

点击按钮的时候,获取generator,然后从takers中删除,防止dispatch({ type: "UPDATE_USER", payload: data })也就是更新user的时候再次执行generator,造成循环调用

gen.next()也就是执行yield axios.post("xxx"),这里返回的是一个promise

在promise.then中调用gen.next({ dispatch, user }),实际上是调用yield dispatch({ type: "UPDATE_USER", payload: data })

最后调用gen.next,实际上调用yield takers.push(gen)。这里是为了把generator添加到takers中,用于下一次上面第二步的时候用。

这里我们简单的实现了这个需求,redux-saga提供了更多更强大的api,下面我们看看redux-saga是怎么实现的吧。

先来看我们代码中如何使用redux-saga吧

./index.js
import { put, call, take } from "redux-saga/effects";
import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";

// 创建action
const action = type => store.dispatch({ type })
// 创建redux-saga中间件
const sagaMiddleware = createSagaMiddleware()
// 生成store
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware))
// 执行redux-saga中间件
sagaMiddleware.run(rootSaga)

function render() {
    ReactDOM.render(
         action("FETCH_USER")}
        />,
        document.getElementById("root"),
    )
}
./saga.js
// 创建saga
export function* rootSaga() {
    while(true) {
        yield take("FETCH_USER");
        const { data } =  yield call(axios.post, "http://rest.learncode.academy/api/wetern/users");
        yield put({ type: "UPDATE_USER", payload: data })
    }
}

首先引入redux-saga中间件,其实它就是redux的一个中间件,通过改变dispatch(action),使我们在发起action的时候处理异步操作。

  function sagaMiddleware(_ref2) {
    var getState = _ref2.getState,
        dispatch = _ref2.dispatch;

    // next = store.dispatch 从redux中间件得出
    return function (next) {
      return function (action) {
        // dispatch(action) dispatch的时候会到这一步
        var result = next(action); // hit reducers
        // emitter 简单的事件对象,subscribe订阅/emit触发
        sagaEmitter.emit(action);
        return result;
      };
    };
  }
// 发射器
export function emitter() {
  var subscribers = [];

  // 存储需要订阅的方法,并返回一个取消订阅的方法
  function subscribe(sub) {
    subscribers.push(sub);
    return function () {
      return remove(subscribers, sub);
    };
  }

  // 这里执行所有订阅方法,我们点击页面上的按钮,执行dispatch的时候会执行订阅器里的函数
  function emit(item) {
    var arr = subscribers.slice();
    for (var i = 0, len = arr.length; i < len; i++) {
      arr[i](item);
    }
  }
}

dispatch的时候会执行订阅器里的函数,那么订阅器里的函数是什么呢,我们接着看第二步

初始化的时候调用一次saga,这个只调用一次。调用saga的目的把获取管道中taker的方法push到订阅函数中,同时获得一个task。Task 接口指定了通过 fork,middleare.run 或 runSaga 运行 Saga 的结果。

方法 返回值
task.isRunning() 若任务还未返回或抛出了一个错误则为 true
task.isCancelled() 若任务已被取消则为 true
task.result() 任务的返回值。若任务仍在运行中则为 undefined
task.error() 任务抛出的错误。若任务仍在执行中则为 undefined
task.done 一个 Promise,以下二者之一:1.以任务的返回值 2.resolve以任务抛出的错误 reject
task.cancel() 取消任务(如果任务仍在执行中)
export function runSaga(storeInterface, saga) {
  // iterator是封装后的rootSaga
  var task = proc(iterator, subscribe, wrapSagaDispatch(dispatch), getState, context,
   { sagaMonitor: sagaMonitor, logger: logger, onError: onError }, effectId, saga.name);

  return task;
}

下面看看执行proc中发生了什么
上面的iterator是对rootSaga这个generator函数的封装,在proc里redux-saga会执行gen.next,就会执行到我们的yield take("FETCH_USER");然后会返回value={TAKE:{pattern: "FETCH_USER"}},根据返回的值,判断会执行runTakeEffect()函数,在这个函数里,会注册一个taker,并把next添加到管道的taker中,到这里就结束了调用,并返回一个task。

export default function proc(iterator) {
  // 执行这里会把获取管道中taker的方法(chan.put)push到subscribers,所以上面第一步执行订阅中的方法实际上是执行chan.put(input)
  var stdChannel = _stdChannel(subscribe);
  // 这里的task在第一次执行的时候直接返回
  var task = newTask(parentEffectId, name, iterator, cont);

  next();

  function next(arg, isErr) {
      var result = void 0;
      // iterator是封装后的rootSaga,这里执行到的是yield take("FETCH_USER");
      // 返回value={TAKE:{pattern: "FETCH_USER"}}
      result = iterator.next(arg);
      // 根据返回的value,这里会执行
      runEffect(result.value, parentEffectId, "", next);
  }
}
  // 这里cb是next
  function runTakeEffect(_ref2, cb) {
    var channel = _ref2.channel,
        pattern = _ref2.pattern,
        maybe = _ref2.maybe;

    channel = channel || stdChannel;
    var takeCb = function takeCb(inp) {
      return cb(inp);
    };
    // 给管道注册taker,把next函数放到takers数组中
    channel.take(takeCb, matcher(pattern));
  }

点击获取用户信息的按钮(onFetchUser={() => action("FETCH_USER")}),因为我们加入了saga中间件,让我们发起store.dispatch({ type: FETCH_USER })的时候会处理异步操作,

下面是saga中间件的主要代码

    // next = store.dispatch 从redux中间件得出
    return function (next) {
      return function (action) {
        // dispatch(action)
        var result = next(action); // hit reducers
        // 发射所有订阅方法
        sagaEmitter.emit(action);
        return result;
      };
    };

下面我们来看看sagaEmitter.emit(action)会做什么

// 发射器
export function emitter() {
    ...
  // 发射所有订阅方法
  function emit(item) {
    var arr = subscribers.slice();
    for (var i = 0, len = arr.length; i < len; i++) {
      arr[i](item);
    }
  }
}

这里会遍历订阅函数,并执行订阅函数里的方法。在初始化的是我们已经把获取管道中taker的方法push到订阅函数了,

所以我们这里执行的是获取管道中的taker。

  function put(input) {
      ...
    for (var i = 0; i < takers.length; i++) {
      var cb = takers[i];
      if (!cb[MATCH] || cb[MATCH](input)) {
        takers.splice(i, 1); // 删除已经执行过的taker
        return cb(input); // 这里cb实际上是next
      }
    }
  }

在初始化执行yield take("FETCH_USER")的时候,已经把gen.next放入到takers中,这里cb(input)实际上是执行gen.next({ type: FETCH_USER }),
因为在初始化的时候gen函数已经执行了一次gen.next,现在执行gen.next则为const { data } = yield call(axios.post, "http://rest.learncode.academy..."),同时把{ type: FETCH_USER }作为上一步的值传入。执行yeild call返回value
{CALL:{args: [url]}},根据返回值,

这里会执行源码中的

  function runCallEffect(_ref4, effectId, cb) {
    const result = fn.apply(context, args);
    // 这里执行结果是promise
    return resolvePromise(result, cb);
  }
  function resolvePromise(promise, cb) {
    // cb是gen.next,这里把yield call的返回值传递给gen.next
    promise.then(cb, function (error) {
      return cb(error, true);
    });
  }

接下来gen.next执行到的是yield put({ type: "UPDATE_USER", payload: data }),执行的返回值value
{PUT:{action:{payload:{id: "xx",type:"UPDATE_USER"}}}},根据返回值,

这里会执行源码中的

  function runPutEffect(_ref3, cb) {
    var channel = _ref3.channel,
        action = _ref3.action;

    asap(function () {
      var result = (channel ? channel.put : dispatch)(action); // 实际上我们演示的这段代码,这里会执行dispatch(action)

      return cb(result);
    });
  }

执行dispatch(action)这里又会回到中间件中再次进入第三步的开始过程。并完成更新。
这次执行到遍历takers的地方,takers已经为空数组,会直接return,至此完成了整个获取接口到更新数据。
由于while(true)循环,再次执行yield take("FETCH_USER")。

下面附上两张执行流程图

saga初始化

dispatch

这里只解释了执行流程和几个api,更多的请参考文档https://redux-saga-in-chinese...

本文通过简单实现了几个effect方法来地介绍了redux-saga的原理,要真正做到redux-saga的所有功能,只需要再添加一些细节就可以了

注:上面的源码均为删减版,可自行查看源码。个人文章,转载请注明出处。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98089.html

相关文章

  • 超级易懂的redux-saga原理解析

    摘要:原文地址前言笔者最近在做一些后台项目,使用的是,其使用了处理异步数据流,本文将对的原理做一个简单的解读,并将实现一个简易版的。函数的自动流程控制在中,是指一些长时操作,用函数表示。 原文地址 前言 笔者最近在做一些后台项目,使用的是Ant Design Pro,其使用了redux-saga处理异步数据流,本文将对redux-saga的原理做一个简单的解读,并将实现一个简易版的redux...

    wendux 评论0 收藏0
  • 精益 React 学习指南 (Lean React)- 3.4 掌控 redux 异步

    摘要:举例来说一个异步的请求场景,可以如下实现任何异步的逻辑都可以,如等等也可以使用的和。实际上在中,一个就是一个函数。 书籍完整目录 3.4 redux 异步 showImg(https://segmentfault.com/img/bVyou8); 在大多数的前端业务场景中,需要和后端产生异步交互,在本节中,将详细讲解 redux 中的异步方案以及一些异步第三方组件,内容有: redu...

    JouyPub 评论0 收藏0
  • redux-saga框架使用详解及Demo教程

    摘要:通过创建将所有的异步操作逻辑收集在一个地方集中处理,可以用来代替中间件。 redux-saga框架使用详解及Demo教程 前面我们讲解过redux框架和dva框架的基本使用,因为dva框架中effects模块设计到了redux-saga中的知识点,可能有的同学们会用dva框架,但是对redux-saga又不是很熟悉,今天我们就来简单的讲解下saga框架的主要API和如何配合redux框...

    Nosee 评论0 收藏0
  • React手稿之 React-Saga

    摘要:相当于一个放置在与中的垫片。之所以称之谓副作用呢,就是为了不让触发一个时,立即执行。也就是在与之间做一个事情,比如异步获取数据等。使用了中的功能,避免了像的回调地狱。把放入中最后再实现相就的即可在线示例推荐阅读手稿 Redux-Saga redux-saga 是一个用于管理应用程序副作用(例如异步获取数据,访问浏览器缓存等)的javascript库,它的目标是让副作用管理更容易,执行更...

    notebin 评论0 收藏0

发表评论

0条评论

itvincent

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<