dva实现原理及自定义实现

# 原生dva实现

create-react-app demo
npm start
1
2

基本计数器项目

index.js

import React from 'react';
import dva, { connect } from 'dva'

// 1. Initialize
const app = dva()

// 2. Plugins
// app.use({});

// 3. Model
// app.model(require('./models/example').default);
app.model({
  namespace: 'counter',
  state: { number: 0 },
  reducers: {
    //接收老状态,返回新状态
    add(state) {
      //dispatch({type:'add'});
      return { number: state.number + 1 }
    },
    minus(state) {
      //dispatch({type:'minus'})
      return { number: state.number - 1 }
    }
  }
})

const Counter = connect(state => state.counter)(props => (
  <div>
    <p>{props.number}</p>
    <button onClick={() => props.dispatch({ type: 'counter/add' })}>+</button>
    <button onClick={() => props.dispatch({ type: 'counter/minus' })}>-</button>
  </div>
))

// 4. Router
// app.router(require('./router').default);
app.router(() => <Counter />)

// 5. Start
app.start('#root')
// ReactROM.render(() => <Counter />, document.querySelector('#root'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 手写实现

# demo引入

import React from 'react';
// import dva, { connect } from 'dva'
import dva, { connect } from './dva'

// 1. Initialize
const app = dva()
console.log(app);
1
2
3
4
5
6
7

# 初始化项目

create-react-app dva
cd dva
npm i -S dva redux react-redux react-saga react-router-dom react-router-redux history
npm start
#注意history用4.x版本,不要用5.0版本;npm i -S history@4.10.1
1
2
3
4
5

# 实现reducers同步

要点:

  • let reducer = combineReducers(reducers) //合并reducers
  • let store = createStore(reducer) //创建仓库
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, combineReducers } from 'redux'
import { Provider, connect } from 'react-redux'
const NAMESPACE_SEPERATOR = '/'

export { connect }

//https://github.com/dvajs/dva/blob/master/packages/dva-core/src/prefixNamespace.js
function prefix(reducers, namespace) {
  // return Object.keys(reducers).reduce((memo, key) => {
  //   const newKey = `${namespace}${NAMESPACE_SEPARATOR}${key}`
  //   memo[newKey] = reducers[key]
  //   return memo
  // }, {})
  let newReducers = {}
  for (const key in reducers) {
    const newKey = `${namespace}${NAMESPACE_SEPARATOR}${key}`
    newReducers[newKey] = reducers[key]
  }
  return newReducers
}

export default function () {
  let app = {
    model,
    _models: [], // 存储传入的model
    router,
    _router: null, // 存储传入的路由配置
    start
  }

  function model(model) {
    app._models.push(model)
  }

  function router(routeConfig) {
    app._router = routeConfig
  }

  // 启动渲染
  function start(root) {
    let reducers = {}
      // 实现reducer同步功能
    for (const model of app._models) {
      let { namespace, state: initState, reducers: modelReducers } = model
      let reducerWithPrefix = prefix(modelReducers, namespace)
      // 新方式
      reducers[namespace] = function (state = initState, action) {
        let reducer = reducerWithPrefix[action.type] //type = "counter/add"
        if (reducer) {
          return reducer(state, action) //如匹配到reducer就处理
        }
        return state //没有匹配到的话,返回老状态
      }
      // 老方式
      // reducers[model.namespace] = function (state = model.state, action) {
      //   let actionType = action.type
      //   const [namespace, type] = actionType.split(NAMESPACE_SEPERATOR)
      //   if (namespace === model.namespace) {
      //     let reducer = model.reducers[type]
      //     if (reducer) {
      //       return reducer(state, action) //如匹配到reducer就处理
      //     }
      //   }
      //   return state
      // }
    }
    let reducer = combineReducers(reducers) //合并reducers
    let store = createStore(reducer) //创建仓库
    let App = app._router //获取要渲染的组件
    //执行渲染
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.querySelector(root)
    )
  }
  return app
}

/**
combineReducers({
  a:function(state, action){},
  b:function(state, action){},
})
{
  a:xxx,
  b:yyy,
}
 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

# 实现effects异步

要点:

  • yield effects.takeEvery
  • createStore(reducer, applyMiddleware(sagaMiddleware)) //创建仓库
  • sagaMiddleware.run(rootSaga)

demo/index.js

+ function delay(ms) {
+    return new Promise((resolve,reject) => {
+        setTimeout(function () {
+            resolve();
+        },ms);
+    });
+ }
+    effects:{
+        *asyncAdd(action,{call,put}){
+          yield call(delay,1000);
+          yield put({type:'counter/add'});
+        }
+    }
+        <button onClick={()=>props.dispatch({type:"counter/asyncAdd"})}>异步+</button>
+//index.js:1 Warning: [sagaEffects.put] counter/add should not be prefixed with namespace counter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

src/dva/index.js

+import {createStore,combineReducers,applyMiddleware} from 'redux';
import {connect,Provider} from 'react-redux';
+import createSagaMiddleware from 'redux-saga';
+import * as sagaEffects from 'redux-saga/effects';
        let rootReducer = combineReducers(reducers);
+        let sagaMiddleware = createSagaMiddleware();
+        function* rootSaga(){
+            let {takeEvery} =  sagaEffects;
+            for(const model of app._models){
+                for(const key in model.effects){
+                    yield takeEvery(model.namespace+'/'+key,function*(action){
+                        yield model.effects[key](action,sagaEffects);
+                    });
+                }
+            }
+        }
+        let store = createStore(rootReducer,applyMiddleware(sagaMiddleware));
+        sagaMiddleware.run(rootSaga);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 实现路由功能

demo/index.js

import React from 'react';
import dva,{connect} from 'dva';
+import {Router,Route} from 'dva/router';
+const Home = ()=><div>Home</div>
+app.router(({history})=>(
+    <Router history={history}>
+        <>
+            <Route path="/" exact={true} component={Home}/>
+            <Route path="/counter" component={Counter}/>
+        </>
+    </Router>
+));
1
2
3
4
5
6
7
8
9
10
11
12

src/index.js

+import {createHashHistory} from 'history';
+        let history = createHashHistory();
+        let App = app._router({history,app});
+        ReactDOM.render(<Provider store={store}>{App}</Provider>,document.querySelector(root));
1
2
3
4

src\dva\router.js

export * from 'react-router-dom';
1

# 实现跳转功能

demo/index.js

+ import {Router,Route,routerRedux} from './dva/router';
+   *goto({to}, { put }) {
+      yield put(routerRedux.push(to));
+   }
 <button onClick={()=>props.dispatch({type:"counter/goto",to:'/'})}>跳转到/</button>
1
2
3
4
5

src/index.js

react-router-redux改用【connected-react-route】

+        let history = createHashHistory();
+        const reducers = {
+            router: connectRouter(history)
+        };
+        let store = createStore(rootReducer,applyMiddleware(routerMiddleware(history),sagaMiddleware));
        sagaMiddleware.run(rootSaga);
+        let App = app._router({history,app});
+        ReactDOM.render(
+        <Provider store={store}>
+            <ConnectedRouter history={history}>
+              {App}
+            </ConnectedRouter >
+        </Provider>,document.querySelector(root));
1
2
3
4
5
6
7
8
9
10
11
12
13

src\dva\router.js

+module.exports = require('react-router-dom');
+module.exports.routerRedux = require('connected-react-router');
1
2

# 实现自定义effects功能

demo/index.js

// 自定义effects示范; 加完三次后自动断开;
addWatchers: [
    function* ({ take, put, call }) {
        for (let i = 0; i < 3; i++) {
            const { payload } = yield take('counter/addWatcher')
            yield call(delay, 1000)
            yield put({ type: 'counter/add', payload })
        }
        alert('不能再加了')
    },
    { type: 'watcher' }
]
<br />
<button onClick={() => props.dispatch({ type: 'counter/addWatcher', payload: 2 })}>自定义异步+2</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

src/index.js

for (const key in model.effects) {
    let effect = model.effects[key]
    if (!Array.isArray(effect)) {
        yield takeEvery(`${model.namespace}${NAMESPACE_SEPERATOR}${key}`, function* (action) {
            yield model.effects[key](action, sagaEffects)
        })
    } else {
        const [func, typeObj] = effect // 支持自定义effects
        if (typeObj.type === 'watcher') {
            yield sagaEffects.fork(func, sagaEffects)
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 实现钩子初始化

demo/index.js

+import { message } from 'antd';
+import logger from 'redux-logger';
+import {createBrowserHistory} from 'history';
+import 'antd/dist/antd.css'
+const SHOW = 'SHOW';
+const HIDE = 'HIDE';
+const initialState = {
+    global: false,
+    models: {},
+    effects: {}
+};
+const app = dva({
+    history:createBrowserHistory(),//自定义history
+    initialState:{counter:{number:5}},//自定义初始状态
+    //effect 执行错误或 subscription 通过 done 主动抛错时触发,可用于管理全局出错状态
+    onError:((err, dispatch) => {message.error(err.message)}),
+    //在 action 被 dispatch 时触发,用于注册 redux 中间件。支持函数或函数数组格式
+    onAction:logger,
+    //state 改变时触发,可用于同步 state 到 localStorage,服务器端等
+    onStateChange:(state)=>{console.log(state)},
+    //封装 reducer 执行。比如借助 redux-undo 实现 redo/undo
+    onReducer:reducer=>(state,action)=>{//增加额外的reducer
+      localStorage.setItem('action',JSON.stringify(action));
+      return reducer(state,action);
+    },
+    //封装 effect 执行。比如 dva-loading 基于此实现了自动处理 loading 状态。
+    onEffect:(effect, { put }, model, actionType)=>{
+      const { namespace } = model;
+      return function*(...args) {
+        yield put({ type: SHOW, payload: { namespace, actionType } });
+        yield effect(...args);
+        yield put({ type: HIDE, payload: { namespace, actionType } });
+      };
+    },
+    //指定额外的 reducer,比如 redux-form 需要指定额外的 form reducer:
+    extraReducers:{
+      loading(state = initialState, { type, payload }) {
+        const { namespace, actionType } = payload || {};
+        switch(type){
+          case SHOW:
+            return {global:true,models: { ...state.models, [namespace]: true }, effects: { ...state.effects, [actionType]: true },};
+          case HIDE:
+            const effects = { ...state.effects, [actionType]: false };
+            const models = { ...state.models, [namespace]: false };
+            const global = Object.keys(models).some(namespace => {
+                return models[namespace];
+            });
+            return {global,models,effects};
+          default:
+            return state;  
+        }
+      }
+    },
+    //指定额外的 StoreEnhancer ,比如结合 redux-persist 的使用:
+    //指定额外的 StoreEnhancer
+    //它的参数是创建store的函数(store creator),返回值是一个可以创建功能更加强大的store的函数(enhanced store creator)
+    extraEnhancers:(createStore)=>{
+       return (...args)=>{
+           let store = createStore(...args);
+           console.log('返回更强大的store')
+           return {...store,more(){console.log('更强大的强能')}};
+       }
+    }
+});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

src/index.js

react-router-redux改用【connected-react-route】

  function start(root) {
+  let history = options.history||createHashHistory();
-  let history = createHashHistory();
+    let reducers = {
+      router: connectRouter(history)
+    };
+    if(options.extraReducers){
+        reducers = {...reducers,...options.extraReducers};
+    }
    for (let i = 0; i < app._models.length; i++) {
      let model = app._models[i];
      reducers[model.namespace] = function(state = model.state, action) {
        let actionType = action.type;
        let values = actionType.split(NAMESPACE_SEPARATOR);
        if (values[0] === model.namespace) {
          let reducer = model.reducers[values[1]];
          if (reducer) {
            return reducer(state, action);
          }
        }
        return state;
      };
    }
+    let combinedReducer  = combineReducers(reducers);
+    let rootReducer = function(state,action){
+        let newState =  combinedReducer(state,action);
+        options.onStateChange&&options.onStateChange(newState);
+        return newState;
+    }
+    if(options.onReducer){
+        rootReducer = options.onReducer(rootReducer);
+    }
+    if(options.onAction){
+        if(typeof options.onAction == 'function'){
+            options.onAction = [options.onAction];
+        }
+    }else{
+        options.onAction=[];
+    }
+    let enhancedCreateStore;
+    if(options.extraEnhancers){
+        enhancedCreateStore = options.extraEnhancers(createStore);
+    }
+    let store = enhancedCreateStore(
+      rootReducer,
+      applyMiddleware(routerMiddleware(history), sagaMiddleware,...options.onAction)
+    );
    sagaMiddleware.run(rootSaga);
    let App = app._router({ history, app });
    ReactDOM.render(
      <Provider store={store}>
        <ConnectedRouter history={history}>{App}</ConnectedRouter>
      </Provider>,
      document.querySelector(root)
    );
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

# 最后方法抽取到各文件

简化主文件`index.js

image-20200920231855001

上次更新: 2022/04/15, 05:41:29
×