最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 探索React异步解决方案之redux-saga

    正文概述 掘金(程欢x)   2021-05-10   812

    1.redux-saga是什么?

    探索React异步解决方案之redux-saga

    顾名思义saga与redux相关,redux-saga是一个以redux中间件形式存在的一个库,主要是为了更优雅地 管理 Redux 应用程序中的 副作用(Side Effects),执行更高效,测试更简单,在处理故障时更容易。同样的,从logo也可以看出saga于redux的关系。

    关于saga的由来,它出自康奈尔大学的一篇论文(链接),是为了解决分布式系统中的长时运行事务(LLT)的数据一致性的问题。

    2.什么是SideEffects?

    映射在 Javascript 程序中,Side Effects 主要指的就是:异步网络请求本地读取 localStorage/Cookie 等外界操作:

    在 Web 应用,侧重点在于 Side Effects 的优雅管理(manage),而不是 消除(eliminate)

    3.saga与thunk有什么不同?

    探索React异步解决方案之redux-saga

    首先,比较了saga与thunk的包体积大小,二者相差10倍之多。

    无论是redux-thunk也好还是redux-saga也好,都是redux的中间件。而redux作为主体,为每个中间件,提供了统一格式,下发getState、dispatch,以及调用dispatch,收集action。

    //compose.js
    function compose(..funcs) {
      if (funcs.length === 0) {
        retyrb arg => arg
      }
      
      if (funcs.length === 1) {
        return funcs[0]
      }
      
      return funcs.reduce((a, b) => (...args) => a(b(...args)))
    }
    
    //applyMiddleware.js
    
    
    function applyMiddleware(...middlewares) {
      return (createStore) => (reducer, preloaderState, enhancer) => {
        const store = createStore(reducer, preloadedState, enhancer)
        let dispatch = store.dispatch
        let chain = []
        
        const middlewareAPI = {
          getState: store.getState,
          diapatch: (action) => dispatch(action)
        }
        chain = middlewares.map(middleware => middleware(middlewareAPI))
        dispatch = compose(...chain)(store.dispatch)
        
        return {
          ...store,
          dispatch
        }
      }
    }
    

    接着,我们先再来看看thunk函数,在阮大大的文章中有介绍到thunk函数:

    function f(m){
      return m * 2;     
    }
    
    f(x + 5);
    
    // 等同于
    
    var thunk = function () {
      return x + 5;
    };
    
    function f(thunk){
      return thunk() * 2;
    }
    

    编译器的"传名调用"实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。

    然后我们再来看看thunk的源码

    function createThunkMiddleware(extraArgument) {
      //dispath,可以用来dispatch新的action
      //getState,可以用于访问当前的state
      return ({dispatch, getState}) => (next) => (action) =>  {
        if (typeof action === 'function') {
          return action(dispatch, getState, extraArgument);
        }
        
        return next(action);
      };
    }
    
    const thunk = createThunkMiddleware();
    thunk.withExtraArgument = createThunkMiddleware;
    

    redux-thunk是个中间件,去监控传入系统中的每一个action,如果是个函数的话,那么它就会调用那个函数。这就是redux-thunk的职责。redux-thunk 选择以 middleware 的形式来增强 redux store 的 dispatch 方法(即:支持了 dispatch(function)),从而在拥有了异步获取数据能力的同时,又可以进一步将数据获取相关的业务逻辑 从 View 层分离出去。

    接着来看看redux-saga,saga模式是以命令/答复的形式与各个saga之间进行通讯,当接收到指令时会执行对应的saga,如图所示:

    探索React异步解决方案之redux-saga

    saga模式将各个服务隔离开,采用集中分布式事务的编排,能够避免服务之间的循环依赖并有利于测试。同时减少了参与者的复杂性,因为他们只需要执行/回复命令。但是,saga会产生很多无用的action.type。

    综上,redux-thunk与redux-saga都是redux的中间件,但是他们的设计思想不同,因此他们的使用方法也不同,首先来看redux-thunk的写法:

    // action.js
    // ---------
    // actionCreator(e.g. fetchData) 返回 function
    // function 中包含了业务数据请求代码逻辑
    // 以回调的方式,分别处理请求成功和请求失败的情况
    export function fetchData(someValue) {
      return (dispatch, getState) => {
        myAjaxLib.post("/someEndpoint", { data: someValue })
          .then(response => dispatch({ type: "REQUEST_SUCCEEDED", payload: response })
            .catch(error => dispatch({ type: "REQUEST_FAILED", error: error });
      };
    }
    
    
    // component.js
    // ------------
    // View 层 dispatch(fn) 触发异步请求
    // 这里省略部分代码
    this.props.dispatch(fetchData({ hello: 'saga' }));
    

    再来看redux-saga的写法,以及架构图:

    // saga.js
    // -------
    // worker saga
    // 它是一个 generator function
    // fn 中同样包含了业务数据请求代码逻辑
    // 但是代码的执行逻辑:看似同步 (synchronous-looking)
    function* fetchData(action) {
      const { payload: { someValue } } = action;
      try {
        const result = yield call(myAjaxLib.post, "/someEndpoint", { data: someValue });
        yield put({ type: "REQUEST_SUCCEEDED", payload: response });
      } catch (error) {
        yield put({ type: "REQUEST_FAILED", error: error });
      }
    }
    
    // watcher saga
    // 监听每一次 dispatch(action)               
    // 如果 action.type === 'REQUEST',那么执行 fetchData
    export function* watchFetchData() {
      yield takeEvery('REQUEST', fetchData);
    }
    
    
    // component.js
    // -------
    // View 层 dispatch(action) 触发异步请求 
    // 这里的 action 依然可以是一个 plain object
    this.props.dispatch({
      type: 'REQUEST',
      payload: {
        someValue: { hello: 'saga' }
      }
    });
    

    探索React异步解决方案之redux-saga

    综上可以看出,redux-saga相较于redux-thunk有这几点不同

    1.数据获取相关的业务逻辑被转移到单独的saga.js中,不再是参杂在action.js或component.js中。

    2.每一个saga都是一个generator function,代码采用同步书写的方式来处理异步逻辑,代码变得更易读。

    4.学习saga使用

    saga总共提供了两个MiddlewareAPI,为createSagaMiddleware、middleware.run。

    createSagaMiddleware(options): 创建一个 Redux middleware,并将 Sagas 连接到 Redux Store。其中options支持的选项有(可不提供):

    • sagaMontior:用于接收middleware传递的监视事件。

    • emmiter:用于从redux向redux-saga进给actions

    • logger:自定义日志方法(默认情况下,middleware会把所有的错误和警告记录到控制台中)。

    • onError:当提供该方法时,middleware将带着Sagas中未被捕获的错误调用它。

    middleware.run(saga, ...args): 动态地运行 saga。只能用于在 applyMiddleware 阶段之后执行Saga,其中args为提供给saga的参数。

    在安装完所有依赖后,首先将store 与saga的关联,并在最后去执行rootsaga。

    import { createStore, applyMiddleware } from 'redux';
    import createSagaMiddleware from 'redux-saga';
    
    import rootSaga from './sagas'
    import rootReducer from './reducers'
    
    const sgagMiddleware = createSagaMiddleware();
    
    const enhancer = applyMiddleware(sagaMiddleware);
    
    const store = createStore(rootReducer, enhancer);
    
    //执行rootSaga,通常是程序的初始化操作。
    sagaMiddleWare.run(rootSaga);
    

    然后,再介绍saga中比较重要的几个概念,分别为:Task、Channel、Buffer、SagaMonitor。

    1.Task

    Task 接口指定了通过 forkmiddleare.runrunSaga 运行 Saga 的结果,并提供了相应的函数方法。

    探索React异步解决方案之redux-saga

    探索React异步解决方案之redux-saga

    2.Channel

    channel 是用于在任务间发送和接收消息的对象。在被感兴趣的接收者请求之前,来自发送者的消息将被放入(put)队列;在信息可用之前,已注册的接收者将被放入队列。

    Channel 接口定义了 3 个方法:takeputclose

    Channel.take(callback): 用于注册一个 taker。

    Channel.put(message): 用于在 buffer 上放入消息。

    Channel.flush(callback): 用于从 channel 中提取所有被缓存的消息。

    Channel.close(): 关闭 channel,意味着不再允许做放入操作。

    3.Buffer

    用于为 channel 实现缓存策略。Buffer 接口定义了 3 个方法:isEmptyputtake

    • isEmpty(): 如果缓存中没有消息则返回。每当注册了新的 taker 时,channel 都会调用该方法。
    • put(message): 用于往缓存中放入新的消息。请注意,缓存可以选择不存储消息。(例如,一个 dropping buffer 可以丢弃超过给定限制的任何新消息)
    • take():用于检索任何被缓存的消息。请注意,此方法的行为必须与 isEmpty 一致。
    4.SagaMonitor

    用于由 middleware 发起监视(monitor)事件。实际上,middleware 发起 5 个事件:

    • 当一个 effect 被触发时(通过 yield someEffect),middleware 调用 sagaMonitor.effectTriggered
    • 如果该 effect 成功地被 resolve,则 middleware 调用 sagaMonitor.effectResolved
    • 如果该 effect 因一个错误被 reject,则 middleware 调用 sagaMonitor.effectRejected
    • 如果该 effect 被取消,则 middleware 调用 sagaMonitor.effectCancelled
    • 最后,当 Redux action 被发起时,middleware 调用 sagaMonitor.actionDispatched

    接着再来介绍redux-saga中的Effect创建器,在redux-saga中主要通过effect来维护,关于Effect的描述如下:

    effect 本质上是一个普通对象,包含着一些指令信息,这些指令最终会被 saga middleware 解释并执行(实际上是一个发布订阅模式)。源码解析可参考文章(juejin.cn/post/688522…

    以take为例,take是一个Effect创建器,用以创建Effect,源码如下:

    探索React异步解决方案之redux-saga

    探索React异步解决方案之redux-saga

    官方解释:

    • 以下每个Effect创建函数都会返回一个普通 Javascript 对象(plain JavaScript object),并且不会执行任何其它操作。
    • 执行是由 middleware 在上述迭代过程中进行的。
    • middleware 会检查每个 Effect 的描述信息,并进行相应的操作

    接下去简单解释一下各个Effect创建器以及Effect组合器、辅助函数的作用:

    Take: 创建一个 Effect 描述信息,用来命令 middleware 在 Store 上等待指定的 action。 在发起与 pattern 匹配的 action 之前,Generator 将暂停。

    Put: 创建一个 Effect 描述信息,用来命令 middleware 向 Store 发起一个 action。 这个 effect 是非阻塞型的,并且所有向下游抛出的错误(例如在 reducer 中),都不会冒泡回到 saga 当中。

    Call: 创建一个 Effect 描述信息,用来命令 middleware 以参数 args 调用函数 fn

    Apply: 类似Call。

    Fork: 创建一个 Effect 描述信息,用来命令 middleware 以 非阻塞调用 的形式执行 fn

    Spawn: 与fork类似,但创建的是被分离的任务。被分离的任务与其父级任务保持独立。

    Join: 创建一个 Effect 描述信息,用来命令 middleware 等待之前的一个分叉任务的结果。

    Cancel:创建一个 Effect,用以取消任务。

    Select: 创建一个 Effect,用来命令 middleware 在当前 Store 的 state 上调用指定的选择器(即返回 selector(getState(), ...args) 的结果)。

    ActionChannel: 创建一个 Effect,用来命令 middleware 通过一个事件 channel 对匹配 pattern 的 action 进行排序。

    Flush: 创建一个 Effect,用来命令 middleware 从 channel 中冲除所有被缓存的数据。被冲除的数据会返回至 saga,这样便可以在需要的时候再次被利用。

    Cancelled: 创建一个 Effect,用来命令 middleware 返回该 generator 是否已经被取消。

    setContext: 创建一个 effect,用来命令 middleware 更新其自身的上下文。

    getContext: 创建一个 effect,用来命令 middleware 返回 saga 的上下文中的一个特定属性。

    Effect组合器

    Race: 创建一个 Effect 描述信息,用来命令 middleware 在多个 Effect 间运行 竞赛(Race)(与 Promise.race([...]) 的行为类似)。

    All: 创建一个 Effect 描述信息,用来命令 middleware 并行地运行多个 Effect,并等待它们全部完成。这是与标准的 Promise#all 相当对应的 API。

    Saga辅助函数

    TakeEvery: 在发起(dispatch)到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。

    TakeLatest: 在发起到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。并自动取消之前所有已经启动但仍在执行中的 saga 任务。

    TakeLeading: 在发起到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。 它将在派生一次任务之后阻塞,直到派生的 saga 完成,然后又再次开始监听指定的 pattern。

    Throttle: 在发起到 Store 并且匹配 pattern 的一个 action 上派生一个 saga。 它在派生一次任务之后,仍然将新传入的 action 接收到底层的 buffer 中,至多保留(最近的)一个。但与此同时,它在 ms 毫秒内将暂停派生新的任务 —— 这也就是它被命名为节流阀(throttle)的原因。其用途,是在处理任务时,无视给定的时长内新传入的 action。

    5.Redux-Saga测试

    由于redux-saga将每个副作用细化到一个较小的维度,并使各个服务之间的耦合性较小。因此非常利于进行单元测试,案例如下:

    function* callApi(url) {
      const someValue = yield select(somethingFromState)
      try {
        const result = yield call(myApi, url, someValue)
        yield put(success(result.json()));
        return result.status;
      } catch (e) {
        yield put(error(e));
        return -1;
      }
    }
    
    const dispatched = [];
    
    const saga = runSaga({
      dispatch: (action) => dispatched.push(action),
      getState: () => ({ value: 'test' }),
    }, callApi, 'http://url');
    
    import sinon from 'sinon';
    import * as api from './api';
    
    test('callApi', async (assert) => {
      const dispatched = [];
      sinon.stub(api, 'myApi').callsFake(() => ({
        json: () => ({
          some: 'value'
        })
      }));
      const url = 'http://url';
      const result = await runSaga({
        dispatch: (action) => dispatched.push(action),
        getState: () => ({ state: 'test' }),
      }, callApi, url).done;
    
      assert.true(myApi.calledWith(url, somethingFromState({ state: 'test' })));
      assert.deepEqual(dispatched, [success({ some: 'value' })]);
    });
    

    最后再推荐两个,阅读官方文档后觉得比较好的小技巧的使用。

    6.Redux-Saga使用技巧

    1.ajax重试
    import { call, put, take, delay, delay } from 'redux-saga/effects'
    
    function* updateApi(data) {
      while (true) {
        try {
          const apiResponse = yield call (apiRequest, { data })
          return apiResponse;
        } catch(error) {
          yield put({
            type: 'UPDATE_RETRY',
            error
          })
          yield delay(2000)
        }
      }
    }
    
    function* updateResource({ data }) {
      const apiResponse = yield call(updateApi, data);
      yield put({
        type: 'UPDATE_SUCCESS',
        payload: apiResponse.body,
      });
    }
    
    export function* watchUpdateResource() {
      yield takeLatest('UPDATE_START', updateResource);
    }
    
    2.撤销
    import { take, put, call, spawn, race, delay } from 'redux-saga/effects'
    import { updateThreadApi, actions } from 'somewhere'
    
    function* onArchive(action) {
      
      const { threadId } = action
      const undoId =`UNDO_ARCHIVE_${threadId}`
      
      const thread = { id: threadId, archived: true}
      
      yield put(actions.showUndo(undoId))
      
      yield put(actions.updateThread(thread))
      
      const { undo, archive } = yield race({
        undo: take(action => action.type === 'UNDO' && action.undoId === undoId),
        archive: delay(5000)
      })
      
      yield put(actions.hideUndo(undoId))
      
      if (undo) {
        yield put(actions.updateThread({ id: threadId, archived: false}))
      } else if (archive) {
        yield call(updateThreadApi,thread)
      }
    }
    
    function* main() {
      while (true) {
        const action = yield take(`ARCHIVE_THREAD`)
        yield spawn(onArchive, action)
      }
    }
    
    参考文章:

    1.Redux-Saga 漫谈

    2.Saga Pattern

    3.Redux-Saga官方文档

    4.Why saga

    5.手写Redux-Saga源码


    起源地下载网 » 探索React异步解决方案之redux-saga

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元