<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      手寫Redux-Saga源碼

      上一篇文章我們分析了Redux-Thunk的源碼,可以看到他的代碼非常簡單,只是讓dispatch可以處理函數(shù)類型的action,其作者也承認(rèn)對于復(fù)雜場景,Redux-Thunk并不適用,還推薦了Redux-Saga來處理復(fù)雜副作用。本文要講的就是Redux-Saga,這個(gè)也是我在實(shí)際工作中使用最多的Redux異步解決方案。Redux-SagaRedux-Thunk復(fù)雜得多,而且他整個(gè)異步流程都使用Generator來處理,Generator也是我們這篇文章的前置知識(shí),如果你對Generator還不熟悉,可以看看這篇文章

      本文仍然是老套路,先來一個(gè)Redux-Saga的簡單例子,然后我們自己寫一個(gè)Redux-Saga來替代他,也就是源碼分析。

      本文可運(yùn)行的代碼已經(jīng)上傳到GitHub,可以拿下來玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux-saga

      簡單例子

      網(wǎng)絡(luò)請求是我們經(jīng)常需要處理的異步操作,假設(shè)我們現(xiàn)在的一個(gè)簡單需求就是點(diǎn)擊一個(gè)按鈕去請求用戶的信息,大概長這樣:

      Sep-11-2020 16-31-55

      這個(gè)需求使用Redux實(shí)現(xiàn)起來也很簡單,點(diǎn)擊按鈕的時(shí)候dispatch出一個(gè)action。這個(gè)action會(huì)觸發(fā)一個(gè)請求,請求返回的數(shù)據(jù)拿來顯示在頁面上就行:

      import React from 'react';
      import { connect } from 'react-redux';
      
      function App(props) {
        const { dispatch, userInfo } = props;
      
        const getUserInfo = () => {
          dispatch({ type: 'FETCH_USER_INFO' })
        }
      
        return (
          <div className="App">
            <button onClick={getUserInfo}>Get User Info</button>
            <br></br>
            {userInfo && JSON.stringify(userInfo)}
          </div>
        );
      }
      
      const matStateToProps = (state) => ({
        userInfo: state.userInfo
      })
      
      export default connect(matStateToProps)(App);
      

      上面這種寫法都是我們之前講Redux就介紹過的Redux-Saga介入的地方是dispatch({ type: 'FETCH_USER_INFO' })之后。按照Redux一般的流程,FETCH_USER_INFO被發(fā)出后應(yīng)該進(jìn)入reducer處理,但是reducer都是同步代碼,并不適合發(fā)起網(wǎng)絡(luò)請求,所以我們可以使用Redux-Saga來捕獲FETCH_USER_INFO并處理。

      Redux-Saga是一個(gè)Redux中間件,所以我們在createStore的時(shí)候?qū)⑺刖托校?/p>

      // store.js
      
      import { createStore, applyMiddleware } from 'redux';
      import createSagaMiddleware from 'redux-saga';
      import reducer from './reducer';
      import rootSaga from './saga';
      
      const sagaMiddleware = createSagaMiddleware()
      
      let store = createStore(reducer, applyMiddleware(sagaMiddleware));
      
      // 注意這里,sagaMiddleware作為中間件放入Redux后
      // 還需要手動(dòng)啟動(dòng)他來運(yùn)行rootSaga
      sagaMiddleware.run(rootSaga);
      
      export default store;
      

      注意上面代碼里的這一行:

      sagaMiddleware.run(rootSaga);
      

      sagaMiddleware.run是用來手動(dòng)啟動(dòng)rootSaga的,我們來看看rootSaga是怎么寫的:

      import { call, put, takeLatest } from 'redux-saga/effects';
      import { fetchUserInfoAPI } from './api';
      
      function* fetchUserInfo() {
        try {
          const user = yield call(fetchUserInfoAPI);
          yield put({ type: "FETCH_USER_SUCCEEDED", payload: user });
        } catch (e) {
          yield put({ type: "FETCH_USER_FAILED", payload: e.message });
        }
      }
      
      function* rootSaga() {
        yield takeEvery("FETCH_USER_INFO", fetchUserInfo);
      }
      
      export default rootSaga;
      

      上面的代碼我們從export開始看吧,export的東西是rootSaga這個(gè)Generator函數(shù),這里面就一行:

      yield takeEvery("FETCH_USER_INFO", fetchUserInfo);
      

      這一行代碼用到了Redux-Saga的一個(gè)effect,也就是takeEvery,他的作用是監(jiān)聽每個(gè)FETCH_USER_INFO,當(dāng)FETCH_USER_INFO出現(xiàn)的時(shí)候,就調(diào)用fetchUserInfo函數(shù),注意這里是每個(gè)FETCH_USER_INFO。也就是說如果同時(shí)發(fā)出多個(gè)FETCH_USER_INFO,我們每個(gè)都會(huì)響應(yīng)并發(fā)起請求。類似的還有takeLatesttakeLatest從名字都可以看出來,是響應(yīng)最后一個(gè)請求,具體使用哪一個(gè),要看具體的需求。

      然后看看fetchUserInfo函數(shù),這個(gè)函數(shù)也不復(fù)雜,就是調(diào)用一個(gè)API函數(shù)fetchUserInfoAPI去獲取數(shù)據(jù),注意我們這里函數(shù)調(diào)用并不是直接的fetchUserInfoAPI(),而是使用了Redux-Sagacall這個(gè)effect,這樣做可以讓我們寫單元測試變得更簡單,為什么會(huì)這樣,我們后面講源碼的時(shí)候再來仔細(xì)看看。獲取數(shù)據(jù)后,我們調(diào)用了put去發(fā)出FETCH_USER_SUCCEEDED這個(gè)action,這里的put類似于Redux里面的dispatch,也是用來發(fā)出action的。這樣我們的reducer就可以拿到FETCH_USER_SUCCEEDED進(jìn)行處理了,跟以前的reducer并沒有太大區(qū)別。

      // reducer.js
      
      const initState = {
        userInfo: null,
        error: ''
      };
      
      function reducer(state = initState, action) {
        switch (action.type) {
          case 'FETCH_USER_SUCCEEDED':
            return { ...state, userInfo: action.payload };
          case 'FETCH_USER_FAILED':
            return { ...state, error: action.payload };
          default:
            return state;
        }
      }
      
      export default reducer;
      

      通過這個(gè)例子的代碼結(jié)構(gòu)我們可以看出:

      1. action被分為了兩種,一種是觸發(fā)異步處理的,一種是普通的同步action

      2. 異步action使用Redux-Saga來監(jiān)聽,監(jiān)聽的時(shí)候可以使用takeLatest或者takeEvery來處理并發(fā)的請求。

      3. 具體的saga實(shí)現(xiàn)可以使用Redux-Saga提供的方法,比如callput之類的,可以讓單元測試更好寫。

      4. 一個(gè)action可以被Redux-SagaReducer同時(shí)響應(yīng),比如上面的FETCH_USER_INFO發(fā)出后我還想讓頁面轉(zhuǎn)個(gè)圈,可以直接在reducer里面加一個(gè)就行:

        ...
        case 'FETCH_USER_INFO':
              return { ...state, isLoading: true };
        ...
        

      手寫源碼

      通過上面這個(gè)例子,我們可以看出,Redux-Saga的運(yùn)行是通過這一行代碼來實(shí)現(xiàn)的:

      sagaMiddleware.run(rootSaga);
      

      整個(gè)Redux-Saga的運(yùn)行和原本的Redux并不沖突,Redux甚至都不知道他的存在,他們之間耦合很小,只在需要的時(shí)候通過put發(fā)出action來進(jìn)行通訊。所以我猜測,他應(yīng)該是自己實(shí)現(xiàn)了一套完全獨(dú)立的異步任務(wù)處理機(jī)制,下面我們從能感知到的API入手,一步一步來探尋下他源碼的奧秘吧。本文全部代碼參照官方源碼寫成,函數(shù)名字和變量名字盡量保持一致,寫到具體的方法的時(shí)候我也會(huì)貼出對應(yīng)的代碼地址,主要代碼都在這里:https://github.com/redux-saga/redux-saga/tree/master/packages/core/src

      先來看看我們用到了哪些API,這些API就是我們今天手寫的目標(biāo):

      1. createSagaMiddleware:這個(gè)方法會(huì)返回一個(gè)中間件實(shí)例sagaMiddleware
      2. sagaMiddleware.run: 這個(gè)方法是真正運(yùn)行我們寫的saga的入口
      3. takeEvery:這個(gè)方法是用來控制并發(fā)流程的
      4. call:用來調(diào)用其他方法
      5. put:發(fā)出action,用來和Redux通訊

      從中間件入手

      之前我們講Redux源碼的時(shí)候詳細(xì)分析了Redux中間件的原理和范式,一個(gè)中間件大概就長這個(gè)樣子:

      function logger(store) {
        return function(next) {
          return function(action) {
            console.group(action.type);
            console.info('dispatching', action);
            let result = next(action);
            console.log('next state', store.getState());
            console.groupEnd();
            return result
          }
        }
      }
      

      這其實(shí)就相當(dāng)于一個(gè)Redux中間件的范式了:

      1. 一個(gè)中間件接收store作為參數(shù),會(huì)返回一個(gè)函數(shù)
      2. 返回的這個(gè)函數(shù)接收老的dispatch函數(shù)作為參數(shù)(也就是上面的next),會(huì)返回一個(gè)新的函數(shù)
      3. 返回的新函數(shù)就是新的dispatch函數(shù),這個(gè)函數(shù)里面可以拿到外面兩層傳進(jìn)來的store和老dispatch函數(shù)

      依照這個(gè)范式以及前面對createSagaMiddleware的使用,我們可以先寫出這個(gè)函數(shù)的骨架:

      // sagaMiddlewareFactory其實(shí)就是我們外面使用的createSagaMiddleware
      function sagaMiddlewareFactory() {
        // 返回的是一個(gè)Redux中間件
        // 需要符合他的范式
        const sagaMiddleware = function (store) {
          return function (next) {
            return function (action) {
              // 內(nèi)容先寫個(gè)空的
              let result = next(action);
              return result;
            }
          }
        }
        
        // sagaMiddleware上還有個(gè)run方法
        // 是用來啟動(dòng)saga的
        // 我們先留空吧
        sagaMiddleware.run = () => { }
      
        return sagaMiddleware;
      }
      
      export default sagaMiddlewareFactory;
      

      梳理架構(gòu)

      現(xiàn)在我們有了一個(gè)空的骨架,接下來該干啥呢?前面我們說過了,Redux-Saga很可能是自己實(shí)現(xiàn)了一套完全獨(dú)立的異步事件處理機(jī)制。這種異步事件處理機(jī)制需要一個(gè)處理中心來存儲(chǔ)事件和處理函數(shù),還需要一個(gè)方法來觸發(fā)隊(duì)列中的事件的執(zhí)行,再回看前面的使用的API,我們發(fā)現(xiàn)了兩個(gè)類似功能的API:

      1. takeEvery(action, callback):他接收的參數(shù)就是actioncallback,而且我們在根saga里面可能會(huì)多次調(diào)用它來注冊不同action的處理函數(shù),這其實(shí)就相當(dāng)于往處理中心里面塞入事件了。
      2. put(action)put的參數(shù)是action,他唯一的作用就是觸發(fā)對應(yīng)事件的回調(diào)運(yùn)行。

      可以看到Redux-Saga這種機(jī)制也是用takeEvery先注冊回調(diào),然后使用put發(fā)出消息來觸發(fā)回調(diào)執(zhí)行,這其實(shí)跟我們其他文章多次提到的發(fā)布訂閱模式很像。

      手寫channel

      channelRedux-Saga保存回調(diào)和觸發(fā)回調(diào)的地方,類似于發(fā)布訂閱模式,我們先來寫個(gè):

      export function multicastChannel() {
        const currentTakers = [];     // 一個(gè)變量存儲(chǔ)我們所有注冊的事件和回調(diào)
      
        // 保存事件和回調(diào)的函數(shù)
        // Redux-Saga里面take接收回調(diào)cb和匹配方法matcher兩個(gè)參數(shù)
        // 事實(shí)上take到的事件名稱也被封裝到了matcher里面
        function take(cb, matcher) {
          cb['MATCH'] = matcher;
          currentTakers.push(cb);
        }
      
        function put(input) {
          const takers = currentTakers;
      
          for (let i = 0, len = takers.length; i < len; i++) {
            const taker = takers[i]
      
            // 這里的'MATCH'是上面take塞進(jìn)來的匹配方法
            // 如果匹配上了就將回調(diào)拿出來執(zhí)行
            if (taker['MATCH'](input)) {
              taker(input);
            }
          }
        }
        
        return {
          take,
          put
        }
      }
      

      上述代碼中有一個(gè)奇怪的點(diǎn),就是將matcher作為屬性放到了回調(diào)函數(shù)上,這么做的原因我想是為了讓外部可以自定義匹配方法,而不是簡單的事件名稱匹配,事實(shí)上Redux-Saga本身就支持好幾種匹配模式,包括字符串,Symbol,數(shù)組等等。

      內(nèi)置支持的匹配方法可以看這里:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/matcher.js

      channel對應(yīng)的源碼可以看這里:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/channel.js#L153

      有了channel之后,我們的中間件里面其實(shí)只要再干一件事情就行了,就是調(diào)用channel.put將接收的action再發(fā)給channel去執(zhí)行回調(diào)就行,所以我們加一行代碼:

      // ... 省略前面代碼
      
      const result = next(action);
      
      channel.put(action);     // 將收到的action也發(fā)給Redux-Saga
      
      return result;
      
      // ... 省略后面代碼
      

      sagaMiddleware.run

      前面的put是發(fā)出事件,執(zhí)行回調(diào),可是我們的回調(diào)還沒注冊呢,那注冊回調(diào)應(yīng)該在什么地方呢?看起來只有一個(gè)地方了,那就是sagaMiddleware.run。簡單來說,sagaMiddleware.run接收一個(gè)Generator作為參數(shù),然后執(zhí)行這個(gè)Generator,當(dāng)遇到take的時(shí)候就將它注冊到channel上面去。這里我們先實(shí)現(xiàn)taketakeEvery是在這個(gè)基礎(chǔ)上實(shí)現(xiàn)的。Redux-Saga中這塊代碼是單獨(dú)抽取了一個(gè)文件,我們仿照這種做法吧。

      首先需要在中間件里面將ReduxgetStatedispatch等參數(shù)傳遞進(jìn)去,Redux-Saga使用的是bind函數(shù),所以中間件方法改造如下:

      function sagaMiddleware({ getState, dispatch }) {
        // 將getState, dispatch通過bind傳給runSaga
        boundRunSaga = runSaga.bind(null, {
          channel,
          dispatch,
          getState,
        })
      
        return function (next) {
          return function (action) {
            const result = next(action);
      
            channel.put(action);
      
            return result;
          }
        }
      }
      

      然后sagaMiddleware.run就直接將boundRunSaga拿來運(yùn)行就行了:

      sagaMiddleware.run = (...args) => {
        boundRunSaga(...args)
      }
      

      注意這里的...args,這個(gè)其實(shí)就是我們傳進(jìn)去的rootSaga。到這里其實(shí)中間件部分就已經(jīng)完成了,后面的代碼就是具體的執(zhí)行過程了。

      中間件對應(yīng)的源碼可以看這里:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/middleware.js

      runSaga

      runSaga其實(shí)才是真正的sagaMiddleware.run,通過前面的分析,我們已經(jīng)知道他的作用是接收Generator并執(zhí)行,如果遇到take就將它注冊到channel上去,如果遇到put就將對應(yīng)的回調(diào)拿出來執(zhí)行,但是Redux-Saga又將這個(gè)過程分為了好幾層,我們一層一層來看吧。runSaga的參數(shù)先是通過bind傳入了一些上下文相關(guān)的變量,比如getState, dispatch,然后又在運(yùn)行的時(shí)候傳入了rootSaga,所以他應(yīng)該是長這個(gè)樣子的:

      import proc from './proc';
      
      export function runSaga(
        { channel, dispatch, getState },
        saga,
        ...args
      ) {
        // saga是一個(gè)Generator,運(yùn)行后得到一個(gè)迭代器
        const iterator = saga(...args);
      
        const env = {
          channel,
          dispatch,
          getState,
        };
      
        proc(env, iterator);
      }
      

      可以看到runSaga僅僅是將Generator運(yùn)行下,得到迭代器對象后又調(diào)用了proc來處理。

      runSaga對應(yīng)的源碼看這里:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/runSaga.js

      proc

      proc就是具體執(zhí)行這個(gè)迭代器的過程,Generator的執(zhí)行方式我們之前在另一篇文章詳細(xì)講過,簡單來說就是可以另外寫一個(gè)方法next來執(zhí)行Generatornext里面檢測到如果Generator沒有執(zhí)行完,就繼續(xù)執(zhí)行next,然后外層調(diào)用一下next啟動(dòng)這個(gè)流程就行。

      export default function proc(env, iterator) {
        // 調(diào)用next啟動(dòng)迭代器執(zhí)行
        next();
      
        // next函數(shù)也不復(fù)雜
        // 就是執(zhí)行iterator
        function next(arg, isErr) {
          let result;
          if (isErr) {
            result = iterator.throw(arg);
          } else {
            result = iterator.next(arg);
          }
      
          // 如果他沒結(jié)束,就繼續(xù)next
          // digestEffect是處理當(dāng)前步驟返回值的函數(shù)
          // 繼續(xù)執(zhí)行的next也由他來調(diào)用
          if (!result.done) {
            digestEffect(result.value, next)
          }
        }
      }
      

      digestEffect

      上面如果迭代器沒有執(zhí)行完,我們會(huì)將它的值傳給digestEffect處理,那么這里的result.value的值是什么的呢?回想下我們前面rootSaga里面的用法

      yield takeEvery("FETCH_USER_INFO", fetchUserInfo);
      

      result.value的值應(yīng)該是yield后面的值,也就是takeEvery("FETCH_USER_INFO", fetchUserInfo)的返回值,takeEvery是再次包裝過的effect,他包裝了take,fork這些簡單的effect。其實(shí)對于像take這種簡單的effect來說,比如:

      take("FETCH_USER_INFO", fetchUserInfo);
      

      這行代碼的返回值直接就是一個(gè)對象,類似于這樣:

      {
        IO: true,
        type: 'TAKE',
        payload: {},
      }
      

      所以我們這里digestEffect拿到的result.value也是這樣的一個(gè)對象,這個(gè)對象就代表了我們的一個(gè)effect,所以我們的digestEffect就長這樣:

      function digestEffect(effect, cb) {    // 這個(gè)cb其實(shí)就是前面?zhèn)鬟M(jìn)來的next
          // 這個(gè)變量是用來解決競爭問題的
          let effectSettled;
          function currCb(res, isErr) {
            // 如果已經(jīng)運(yùn)行過了,直接return
            if (effectSettled) {
              return
            }
      
            effectSettled = true;
      
            cb(res, isErr);
          }
      
          runEffect(effect, currCb);
        }
      

      runEffect

      可以看到digestEffect又調(diào)用了一個(gè)函數(shù)runEffect,這個(gè)函數(shù)會(huì)處理具體的effect:

      // runEffect就只是獲取對應(yīng)type的處理函數(shù),然后拿來處理當(dāng)前effect
      function runEffect(effect, currCb) {
        if (effect && effect.IO) {
          const effectRunner = effectRunnerMap[effect.type]
          effectRunner(env, effect.payload, currCb);
        } else {
          currCb();
        }
      }
      

      這點(diǎn)代碼可以看出,runEffect也只是對effect進(jìn)行了檢測,通過他的類型獲取對應(yīng)的處理函數(shù),然后進(jìn)行處理,我這里代碼簡化了,只支持IO這種effect,官方源碼中還支持promiseiterator,具體的可以看看他的源碼:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/proc.js

      effectRunner

      effectRunner是通過effect.type匹配出來的具體的effect的處理函數(shù),我們先來看兩個(gè):takefork

      runTakeEffect

      take的處理其實(shí)很簡單,就是將它注冊到我們的channel里面就行,所以我們建一個(gè)effectRunnerMap.js文件,在里面添加take的處理函數(shù)runTakeEffect:

      // effectRunnerMap.js
      
      function runTakeEffect(env, { channel = env.channel, pattern }, cb) {
        const matcher = input => input.type === pattern;
      
        // 注意channel.take的第二個(gè)參數(shù)是matcher
        // 我們直接寫一個(gè)簡單的matcher,就是輸入類型必須跟pattern一樣才行
        // 這里的pattern就是我們經(jīng)常用的action名字,比如FETCH_USER_INFO
        // Redux-Saga不僅僅支持這種字符串,還支持多種形式,也可以自定義matcher來解析
        channel.take(cb, matcher);
      }
      
      const effectRunnerMap = {
        'TAKE': runTakeEffect,
      };
      
      export default effectRunnerMap;
      

      注意上面代碼channel.take(cb, matcher);里面的cb,這個(gè)cb其實(shí)就是我們迭代器的next,也就是說take的回調(diào)是迭代器繼續(xù)執(zhí)行,也就是繼續(xù)執(zhí)行下面的代碼。也就是說,當(dāng)你這樣寫時(shí):

      yield take("SOME_ACTION");
      yield fork(saga);
      

      當(dāng)運(yùn)行到yield take("SOME_ACTION");這行代碼時(shí),整個(gè)迭代器都阻塞了,不會(huì)再往下運(yùn)行。除非你觸發(fā)了SOME_ACTION,這時(shí)候會(huì)把SOME_ACTION的回調(diào)拿出來執(zhí)行,這個(gè)回調(diào)就是迭代器的next,所以就可以繼續(xù)執(zhí)行下面這行代碼了yield fork(saga)

      runForkEffect

      我們前面的示例代碼其實(shí)沒有直接用到fork這個(gè)API,但是用到了takeEverytakeEvery其實(shí)是組合takefork來實(shí)現(xiàn)的,所以我們先來看看forkfork的使用跟call很像,也是可以直接調(diào)用傳進(jìn)來的方法,只是call會(huì)等待結(jié)果回來才進(jìn)行下一步,fork不會(huì)阻塞這個(gè)過程,而是當(dāng)前結(jié)果沒回來也會(huì)直接運(yùn)行下一步:

      fork(fn, ...args);
      

      所以當(dāng)我們拿到fork的時(shí)候,處理起來也很簡單,直接調(diào)用proc處理fn就行了,fn應(yīng)該是一個(gè)Generator函數(shù)。

      function runForkEffect(env, { fn }, cb) {
        const taskIterator = fn();    // 運(yùn)行fn得到一個(gè)迭代器
      
        proc(env, taskIterator);      // 直接將taskIterator給proc處理
      
        cb();      // 直接調(diào)用cb,不需要等待proc的結(jié)果
      }
      

      runPutEffect

      我們前面的例子還用到了put這個(gè)effect,他就更簡單了,只是發(fā)出一個(gè)action,事實(shí)上他也是調(diào)用的Reduxdispatch來發(fā)出action

      function runPutEffect(env, { action }, cb) {
        const result = env.dispatch(action);     // 直接dispatch(action)
      
        cb(result);
      }
      

      注意我們這里的代碼只需要dispatch(action)就行了,不需要再手動(dòng)調(diào)channel.put了,因?yàn)槲覀兦懊娴闹虚g件里面已經(jīng)改造了dispatch方法了,每次dispatch的時(shí)候都會(huì)自動(dòng)調(diào)用channel.put

      runCallEffect

      前面我們發(fā)起API請求還用到了call,一般我們使用axios這種庫返回的都是一個(gè)promise,所以我們這里寫一種支持promise的情況,當(dāng)然普通同步函數(shù)肯定也是支持的:

      function runCallEffect(env, { fn, args }, cb) {
        const result = fn.apply(null, args);
      
        if (isPromise(result)) {
          return result
            .then(data => cb(data))
            .catch(error => cb(error, true));
        }
      
        cb(result);
      }
      

      這些effect具體處理的方法對應(yīng)的源碼都在這個(gè)文件里面:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/effectRunnerMap.js

      effects

      上面我們講了幾個(gè)effect具體處理的方法,但是這些都不是對外暴露的effect API。真正對外暴露的effect API還需要單獨(dú)寫,他們其實(shí)都很簡單,都是返回一個(gè)帶有type的簡單對象就行:

      const makeEffect = (type, payload) => ({
        IO: true,
        type,
        payload
      })
      
      export function take(pattern) {
        return makeEffect('TAKE', { pattern })
      }
      
      export function fork(fn) {
        return makeEffect('FORK', { fn })
      }
      
      export function call(fn, ...args) {
        return makeEffect('CALL', { fn, args })
      }
      
      export function put(action) {
        return makeEffect('PUT', { action })
      }
      

      可以看到當(dāng)我們使用effect時(shí),他的返回值就僅僅是一個(gè)描述當(dāng)前任務(wù)的對象,這就讓我們的單元測試好寫很多。因?yàn)槲覀兊拇a在不同的環(huán)境下運(yùn)行可能會(huì)產(chǎn)生不同的結(jié)果,特別是這些異步請求,我們寫單元測試時(shí)來造這些數(shù)據(jù)也會(huì)很麻煩。但是如果你使用Redux-Sagaeffect,每次你代碼運(yùn)行的時(shí)候得到的都是一個(gè)任務(wù)描述對象,這個(gè)對象是穩(wěn)定的,不受運(yùn)行結(jié)果影響,也就不需要針對這個(gè)造測試數(shù)據(jù)了,大大減少了工作量。

      effects對應(yīng)的源碼文件看這里:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/io.js

      takeEvery

      我們前面還用到了takeEvery來處理同時(shí)發(fā)起的多個(gè)請求,這個(gè)API是一個(gè)高級(jí)API,是封裝前面的takefork來實(shí)現(xiàn)的,官方源碼又構(gòu)造了一個(gè)新的迭代器來組合他們,不是很直觀。官方文檔中的這種寫法反而很好理解,我這里采用文檔中的這種寫法:

      export function takeEvery(pattern, saga) {
        function* takeEveryHelper() {
          while (true) {
            yield take(pattern);
            yield fork(saga);
          }
        }
      
        return fork(takeEveryHelper);
      }
      

      上面這段代碼就很好理解了,我們一個(gè)死循環(huán)不停的監(jiān)聽pattern,即目標(biāo)事件,當(dāng)目標(biāo)事件過來的時(shí)候,就執(zhí)行對應(yīng)的saga,然后又進(jìn)入下一次循環(huán)繼續(xù)監(jiān)聽pattern

      總結(jié)

      到這里我們例子中用到的API已經(jīng)全部自己實(shí)現(xiàn)了,我們可以用自己的這個(gè)Redux-Saga來替換官方的了,只是我們只實(shí)現(xiàn)了他的一部分功能,還有很多功能沒有實(shí)現(xiàn),不過這已經(jīng)不妨礙我們理解他的基本原理了。再來回顧下他的主要要點(diǎn):

      1. Redux-Saga其實(shí)也是一個(gè)發(fā)布訂閱模式,管理事件的地方是channel,兩個(gè)重點(diǎn)APItakeput
      2. take是注冊一個(gè)事件到channel上,當(dāng)事件過來時(shí)觸發(fā)回調(diào),需要注意的是,這里的回調(diào)僅僅是迭代器的next,并不是具體響應(yīng)事件的函數(shù)。也就是說take的意思就是:我在等某某事件,這個(gè)事件來之前不許往下走,來了后就可以往下走了。
      3. put是發(fā)出事件,他是使用Redux dispatch發(fā)出事件的,也就是說put的事件會(huì)被ReduxRedux-Saga同時(shí)響應(yīng)。
      4. Redux-Saga增強(qiáng)了Reduxdispatch函數(shù),在dispatch的同時(shí)會(huì)觸發(fā)channel.put,也就是讓Redux-Saga也響應(yīng)回調(diào)。
      5. 我們調(diào)用的effects和真正實(shí)現(xiàn)功能的函數(shù)是分開的,表層調(diào)用的effects只會(huì)返回一個(gè)簡單的對象,這個(gè)對象描述了當(dāng)前任務(wù),他是穩(wěn)定的,所以基于effects的單元測試很好寫。
      6. 當(dāng)拿到effects返回的對象后,我們再根據(jù)他的type去找對應(yīng)的處理函數(shù)來進(jìn)行處理。
      7. 整個(gè)Redux-Saga都是基于Generator的,每往下走一步都需要手動(dòng)調(diào)用next,這樣當(dāng)他執(zhí)行到中途的時(shí)候我們可以根據(jù)情況不再繼續(xù)調(diào)用next,這其實(shí)就相當(dāng)于將當(dāng)前任務(wù)cancel了。

      本文可運(yùn)行的代碼已經(jīng)上傳到GitHub,可以拿下來玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux-saga

      參考資料

      Redux-Saga官方文檔:https://redux-saga.js.org/

      Redux-Saga源碼地址: https://github.com/redux-saga/redux-saga/tree/master/packages/core/src

      文章的最后,感謝你花費(fèi)寶貴的時(shí)間閱讀本文,如果本文給了你一點(diǎn)點(diǎn)幫助或者啟發(fā),請不要吝嗇你的贊和GitHub小星星,你的支持是作者持續(xù)創(chuàng)作的動(dòng)力。

      歡迎關(guān)注我的公眾號(hào)進(jìn)擊的大前端第一時(shí)間獲取高質(zhì)量原創(chuàng)~

      “前端進(jìn)階知識(shí)”系列文章:https://juejin.im/post/5e3ffc85518825494e2772fd

      “前端進(jìn)階知識(shí)”系列文章源碼GitHub地址: https://github.com/dennis-jiang/Front-End-Knowledges

      QR1270

      posted @ 2020-10-19 15:20  _蔣鵬飛  閱讀(654)  評(píng)論(0)    收藏  舉報(bào)
      主站蜘蛛池模板: 合山市| 人妻熟女一区二区aⅴ向井蓝| 久久精产国品一二三产品| 国产精品中文字幕免费| 狠狠噜天天噜日日噜无码| 中文字幕亚洲人妻一区| 久久国产成人午夜av影院| 国产免费午夜福利在线播放| 美日韩精品综合一区二区| 国产亚洲精品成人无码精品网站| 无套内谢少妇高清毛片| 国产自在自线午夜精品| 色综合色综合色综合频道| 成人性影院| 亚洲色欲色欱WWW在线| 国产69久久精品成人看| 中文字幕乱码中文乱码毛片 | 国产线播放免费人成视频播放| 91孕妇精品一区二区三区| 欧美黑人巨大videos精品| 日本不卡不二三区在线看| 国产成人午夜福利在线播放| 久久99国产精品尤物| 网友自拍视频一区二区三区| 蜜芽亚洲AV无码精品国产午夜 | 临泉县| 爆乳喷奶水无码正在播放| 亚洲爆乳WWW无码专区| 亚洲精品国产自在现线最新 | 国产精品黄色片| 中文字幕制服国产精品| 成人性无码专区免费视频| 国产高清在线精品一区| 无码熟妇人妻av影音先锋| 性做久久久久久久| 亚洲AV毛片一区二区三区| 少妇被躁爽到高潮无码文 | 人妻人人澡人人添人人爽人人玩| 少妇av一区二区三区无码| 国产精品无遮挡一区二区| 日本丰满少妇高潮呻吟|