React生態,dva原始碼閱讀

yuxiaoliang發表於2019-02-16

  dva的思想還是很不錯的,大大提升了開發效率,dva整合了Redux以及Redux的中介軟體Redux-saga,以及React-router等等。得益於Redux的狀態管理,以及Redux-saga中通過Task和Effect來處理非同步的概念,dva在這些工具的基礎上高度封裝,只暴露出幾個簡單的API就可以設計資料模型。

  最近看了一下Redux-saga的原始碼,結合以及之前在專案中一直採用的是redux-dark模式來將reducers和sagas(generator函式處理非同步)拆分到不同的子頁面,每一個頁面中同一個檔案中包含了該頁面狀態的reducer和saga,這種簡單的封裝已經可以大大的提升專案的可讀性。

  最近看了dva原始碼,熟悉了dva是在上層如何做封裝的。下面會從淺到深,淡淡在閱讀dva原始碼過程中自己的理解。

  • redux-dark模式
  • dva 0.0.12版本的使用和原始碼理解

本文的原文地址為: github.com/fortheallli… 歡迎star


一、redux-dark模式

  在使用redux和redux-saga的時候,特別是如何存放reducer函式和saga的generator函式,這兩個函式是直接跟如何處理資料掛鉤的。

  回顧一下,在redux中使用非同步中介軟體redux-saga後,完整的資料和資訊流向:

default

  在存在非同步的邏輯下,在UI Component中發出一個plain object的action,然後經過redux-saga這個中介軟體處理,redux-saga會將這個action傳入相應channel,通過redux-saga的effect方法(比如call、put、apply等方法)生成一個描述物件,然後將這個描述物件轉化成具有副作用的函式並執行。

  在redux-saga執行具有副作用的函式時,又可以dispatch一個action,這個action也是一個plain object,會直接傳入到redux的reducer函式中進行處理,也就是說在redux-saga的task中發出的action,就是同步的action。

簡單的概括:從UI元件上發出的action經過了2層的處理,分別是redux-saga中介軟體和redux的reducer函式。

  redux-dark模式很簡單,就是將同一個子頁面下的redux-saga處理action的saga函式,以及reducer處理該子頁面下的state的reducer函式,放在同一個檔案中。

舉例來說:

 import { connect } from 'react-redux';
 class Hello extends React.Component{
     componentDidMount(){
       //發出一個action,獲取非同步資料
       this.props.dispatch({
          type:'async_count'
       })
     }
     
 
 }
 export default connect({})(Hello);
 
複製程式碼

從Hello元件中發出一個type = 'async_count'的action,我們用redux-dark模式來將saga和reducer函式放在同一個檔案中:

    import { takeEvery } from 'redux-saga/effects';
    
    //saga
    function * asyncCount(){
      console.log('執行了saga非同步...')
      //發出一個原始的action
      yield put({
        type:'async'
      });
    }
    function * helloSaga(){
        //接受從UI元件發出的action
        takeEvery('async_count',asyncCount);
    }
    
    //reducer
    function helloReducer(state,action){
       if(action.type === 'count');
       return { ...state,count + 1}
    }

複製程式碼

上述就是一個將saga和reducer放在同一個檔案裡面的例子。redux-dark模式來組織程式碼,可以顯得比較直觀,統一了資料的處理層。分拆子頁面後,每一個子頁面對應一個檔案。可讀性很高。

二、dva 0.0.12版本的使用和原始碼理解

  上述的redux-dark模式,就是一種簡單的處理,而dva的話是做了更近一步的封裝,dva不僅封裝了redux和redux-saga,還有react-router-redux、react-router等等。使得我們可以通過很簡單的配置,就能使用redux、redux-saga、react-router等。

下面首先以dva的初始版本為例來理解一下dva的原始碼。

(1)、dva 0.0.12的使用

來看官網給的使用dva 0.0.12的例子:

// 1. Initialize
const app = dva();

// 2. Model
app.model({
  namespace: 'count',
  state: 0,
  effects: {
    ['count/add']: function*() {
      console.log('count/add');
      yield call(delay, 1000);
      yield put({
        type: 'count/minus',
      });
    },
  },
  reducers: {
    ['count/add'  ](count) { return count + 1 },
    ['count/minus'](count) { return count - 1 },
  },
  subscriptions: [
    function(dispatch) {
      //..處理監聽等等函式
    }
  ],
  
});

// 3. View
const App = connect(({ count }) => ({
  count
}))(function(props) {
  return (
    <div>
      <h2>{ props.count }</h2>
      <button key="add" onClick={() => { props.dispatch({type: 'count/add'})}}>+</button>
      <button key="minus" onClick={() => { props.dispatch({type: 'count/minus'})}}>-</button>
    </div>
  );
});

// 4. Router
app.router(({ history }) =>
  <Router history={history}>
    <Route path="/" component={App} />
  </Router>
);

// 5. Start
app.start(document.getElementById('root'));
複製程式碼

只要三步就完成了一個例子,如何處理action呢,我們以一個圖來表示:

default

也就是做UI元件上發出的物件型別的action,先去根據型別匹配=model初始化時候,effects屬性中的action type。

  • 如果在effects的屬性中有相應的action type的處理函式,那麼先執行effects中的相應函式,在執行這個函式裡面可以二次發出action,二次發出的action會直接傳入到reducer函式中。
  • 如果effects的屬性中沒有相應的action type的處理函式,那麼會直接從reducer中尋找有沒有相應型別的處理函式。

在dva初始化過程中的effects屬性中的函式,其實就是redux-saga中的saga函式,在該函式中處理直接的非同步邏輯,並且該函式可以二次發出同步的action。

此外dva還可以通過router方法初始化路由等。

(2)、dva 0.0.12的原始碼閱讀

下面來直接讀讀dva 0.0.12的原始碼,下面的程式碼是經過我精簡後的dva的原始碼:

//Provider全域性注入store
import { Provider } from 'react-redux';
//redux相關的api
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
//redux-saga相關的api,takeEvery和takeLatest監聽等等
import createSagaMiddleware, { takeEvery, takeLatest } from 'redux-saga';
//react-router相關的api
import { hashHistory, Router } from 'react-router';
//在react-router4.0之後已經較少使用,將路由的狀態儲存在store中
import { routerMiddleware, syncHistoryWithStore, routerReducer as routing } from 'react-router-redux';
//redux-actions的api,可以以函式式描述reducer等
import { handleActions } from 'redux-actions';
//redux-saga非阻塞呼叫effect
import { fork } from 'redux-saga/effects';

function dva() {
  let _routes = null;
  const _models = [];
  //new dva暴露了3個方法
  const app = {
    model,
    router,
    start,
  };
  return app;
  //新增models,一個model物件包含了effects,reducers,subscriptions監聽器等等
  function model(model) {
    _models.push(model);
  }
  //新增路由
  function router(routes) {
    _routes = routes;
  }

  
  function start(container) {

    let sagas = {};
    //routing是react-router-redux的routerReducer別名,用於擴充套件reducer,這樣以後擴充套件後的reducer就可以處理路由變化。
    let reducers = {
      routing
    };
    _models.forEach(model => {
      //對於每一個model,提取其中的reducers和effects,其中reducers用於擴充套件redux的reducers函式,而effects用於擴充套件redx-saga的saga處理函式。
      reducers[model.namespace] = handleActions(model.reducers || {}, model.state);
      //擴充套件saga處理函式,sagas是包含了所有的saga處理函式的物件
      sagas = { ...sagas, ...model.effects }; ---------------------------(1)
    });

    reducers = { ...reducers };
    
    //獲取決定使用React-router中的那一個api
    const _history = opts.history || hashHistory;
    //初始化redux-saga
    const sagaMiddleware = createSagaMiddleware();
    //為redux新增中介軟體,這裡新增了處理路由的中介軟體,以及redux-saga中介軟體。
    const enhancer = compose(
      applyMiddleware.apply(null, [ routerMiddleware(_history), sagaMiddleware ]),
      window.devToolsExtension ? window.devToolsExtension() : f => f
    );
    const initialState = opts.initialState || {};
    //通過combineReducers來擴充套件reducers,同時生成擴充套件後的store例項
    const store = app.store = createStore(
      combineReducers(reducers), initialState, enhancer
    );

    // 執行model中的監聽函式,監聽函式中傳入store.dispatch
    _models.forEach(({ subscriptions }) => {
      if (subscriptions) {
        subscriptions.forEach(sub => {
         store.dispatch, onErrorWrapper);
        });
      }
    });
    
     // 根據rootSaga來啟動saga,rootSaga就是redux-saga執行的主task
    sagaMiddleware.run(rootSaga);
    
    
    //建立history例項子,可以監聽store中的state的變化。
    let history;
    history = syncHistoryWithStore(_history, store); --------------------------------(2)
    

    // Render and hmr.
    if (container) {
      render();
      apply('onHmr')(render);
    } else {
      const Routes = _routes;
      return () => (
        <Provider store={store}>
          <Routes history={history} />
        </Provider>
      );
    }

    function getWatcher(k, saga) {
      let _saga = saga;
      let _type = 'takeEvery';
      if (Array.isArray(saga)) {
        [ _saga, opts ] = saga;
    
        _type = opts.type;
      }

      function* sagaWithErrorCatch(...arg) {
        try {
          yield _saga(...arg);
        } catch (e) {
          onError(e);
        }
      }

      if (_type === 'watcher') {
        return sagaWithErrorCatch;
      } else if (_type === 'takeEvery') {
        return function*() {
          yield takeEvery(k, sagaWithErrorCatch);
        };
      } else {
        return function*() {
          yield takeLatest(k, sagaWithErrorCatch);
        };
      }
    }

    function* rootSaga() {
      for (let k in sagas) {
        if (sagas.hasOwnProperty(k)) {
          const watcher = getWatcher(k, sagas[k]);
          yield fork(watcher);
        }                      -----------------------------(3)
      }
    }

    function render(routes) {
      const Routes = routes || _routes;
      ReactDOM.render((
        <Provider store={store}>
          <Routes history={history} />
        </Provider>
      ), container);
    }
  }
}

export default dva;

複製程式碼

程式碼的閱讀在上面都以注視的方式給出,值得注意的主要有一下3點:

  • 在註釋(1)處, handleActions是通過redux-actions封裝後的一個API,用於簡化reducer函式的書寫。下面是一個handleActions的例子:
const reducer = handleActions(
  {
    INCREMENT: (state, action) => ({
      counter: state.counter + action.payload
    }),
​
    DECREMENT: (state, action) => ({
      counter: state.counter - action.payload
    })
  },
  { counter: 0 }
);
複製程式碼

INCREMENT和DECREMENT屬性的函式就可以分別處理,type = "INCREMENT"和type = "DECREMENT"的action。

  • 在註釋 (2) 處,通過react-router-redux的api,syncHistoryWithStore可以擴充套件history,使得history可以監聽到store的變化。

  • 在註釋(3)處是一個rootSaga, 是redux-saga執行的時候的主Task,在這個Task中我們這樣定義:

function* rootSaga() {
  for (let k in sagas) {
    if (sagas.hasOwnProperty(k)) {
      const watcher = getWatcher(k, sagas[k]);
      yield fork(watcher);
    }                     
  }
}
複製程式碼

從全域性的包含所有saga函式的sagas物件中,獲取相應的屬性,並fork相應的監聽,這裡的監聽常用的有takeEvery和takeLatest等兩個redux-saga的API等。

總結:上面就是dva最早版本的原始碼,很簡潔的使用了redux、redux-saga、react-router、redux-actions、react-router-redux等.其目的也很簡單:

簡化redux相關生態的繁瑣邏輯

參考原始碼地址:github.com/dvajs/dva/t…

相關文章