dva使用+源码解析

519 阅读7分钟

简介

dva是用来进行状态管理的(umi内置的有dva所以我们不需要另外再引入dva)

使用dva

使用dva我们首先要在umi的src下创建一个model文件夹,用来管理我们的状态

 //modules/system
 export default {
      // models命名空间,需全局唯一
  namespace: 'system',           
  // models存储的数据store                  
  state: {
      dataList: 1,
      lists:2
  },      
  // 更新store,用新数据合并state的旧数据                                
  reducers: {
      Lists(state,{payload}){
          return {...state,...payload}
      },
      save(state, {payload}) {                    
         
          // console.log(state);
          // console.log(payload);
           return { ...state,...payload};
      },
    
  },
 }
 // 在组件中使用
 import {connect} from 'umi';
  export default Index=(props)=>{
    return (
        <div>{props.system.dataList}</div>
    )
  }
 connect(({ system }) => ({ system }))(Index)

namespace (命名空间)

指定model的名字,一定要是唯一的

state

存储数据,

reducers

这里是用来修改数据的,

//models/index.ts
  reducers:{
        save(state,{payload}){
            return {...state,...payload}
        }
  }
 // pages / index.ts
   props.dispatch({
       type:'index/save',
       payload:{
           //要修改的数据
       }
   })

subscriptions

每次进入项目都会执行subscriptions

  subscriptions:{
      set(dispatch,history){
          //可以实现对history的监听
          history.listen((a)=>{

          })
      }
  }

effects

effects是由action触发的,比如我们有些数据是通过请求获得的,那么我们就需要在请求完之后再去调用dispatch修改数据,所以我们可以在effects里执行 ,那么我们在调用dispatch的时候就需要调用effects里的函数,在通过effects去调用reducers

 effects:{
    add({payload},{call,put,select}){
        //payload,接受过来要修改的参数
        //call 传进去一个函数并执行
        //put 调用reducers
        //select 获取所有的state
       //  yield是ES6的新关键字,使生成器函数执行暂停,yield关键字后面的表达式的值返回给生成器的调用者。它可以被认为是一个基于生成器的版本的return关键字。
        const states=yield select (state=>state);
       const {data}=  yield  call( get/post ) //可以在call里去执行请求函数,获取到数据后
       //通过put函数去调用reducers更改state
       yield put('refucer名字',payload:{
           //要修改的数据
       })
    }

 }

dva-cli

dva源码解析

dva其实就是实现了对redux和redux-sga的封装他还内置了路由和fetch(相当于axios),所以dva也是一个企业级别的微型框架,研究dva的源码首先我们根据package.json查看dva都引入了哪些东西

dva/package.json

dva里分别引入了这些东西
  "@babel/runtime": "7.0.0-beta.46", //编译时的文件,编译过后使得代码的体积更小
    "@types/isomorphic-fetch": "^0.0.34", //请求用的库
    "@types/react-router-dom": "^4.2.7",
    "@types/react-router-redux": "^5.0.13",
    "dva-core": "^1.4.0", //dva源码的核心功能实现的库
    "global": "^4.3.2",
    "history": "^4.6.3", //borswerRouter和hashRouter
    "invariant": "^2.2.2",//断言库
    "isomorphic-fetch": "^2.2.1",
    "react-redux": "5.0.7",//提供了一个高阶组件方便在各处调用store
    "react-router-dom": "^4.1.2", //路由
    "react-router-redux": "5.0.0-alpha.9", //reduce的中间件
    "redux": "^3.7.2" //redux

然后我们来看index.js

dva/src/index.js

 index.js返回一个app方法,app挂载了dva的所有方法,app返回三个方法start,model,use,router

 ```javaScript
 //额外的plugin插件
 app.use({})
 //models层

app.model(require('./models/example').default);

// 4. Router 配置路由 app.router(require('./router').default);

// 5. Start 挂载 app.start('#root');

     ```

dva

当我们引入dva的时候,dva先导出的是一个方法,我们需要调用这个方法得到app,然后才能通过app开启我们的应用,
 我们首先来看导出的这个方法
export default  function (opts={}){
   const history = opts.history || createHashHistory();
    //创建app时需要传入的参数
    const createOpts = {
    initialReducer: {
      routing,
    },
    setupMiddlewares(middlewares) {
      return [
        routerMiddleware(history),
        ...middlewares,
      ];
    },
     setupApp(app) {
            app._history = patchHistory(history);
       },
     };
        const app = core.create(opts, createOpts); //通过引入的dva-core创建一个app
       const oldAppStart = app.start; //给app绑定上start方法
       app.router = router; //给app绑定上router
      app.start = start; //给app绑定start方法
     return app; //返回app
}

start

start方法接收一个参数,就是我们需要挂载的Dom,它可以是一个字符串就是dom的id或者class,也可以是一个真实的dom,start之后开始models(这一层是通过dva-core实现的),然后通过getProvider函数给根组件绑定上provider组件(provider组件是React-redux提供给我们的)给跟组件绑定上provider组件我们就可以在任意组件拿到state,然后调用react的render函数
   function start(container) {
    // 允许 container 是字符串,然后用 querySelector 找元素
    if (isString(container)) {
      //判断传进来的参数是否是字符串
      container = document.querySelector(container);
      //invariant是用来判断传入参数的重要性能
      invariant(
        container,
        `[app.start] container ${container} not found`,
      );
    }

    // 并且是 HTMLElement
    invariant(
      !container || isHTMLElement(container),
      `[app.start] container should be HTMLElement`,
    );

    // 路由必须提前注册
    //判断app是否提前注册了路由
    invariant(
      app._router,
      `[app.start] router must be registered before app.start()`,
    );

    if (!app._store) {
      //给app绑定store,并且传入this
      oldAppStart.call(app);
    }
    const store = app._store;
        //将数据层于view层进行绑定
    // export _getProvider for HMR
    // ref: https://github.com/dvajs/dva/issues/469
    app._getProvider = getProvider.bind(null, store, app);
         
    // If has container, render; else, return react component
    if (container) {
      render(container, store, app, app._router);
      app._plugin.apply('onHmr')(render.bind(null, container, store, app));
    } else {
      return getProvider(store, this, this._router);
    }
  }
}

//getprovider函数

function getProvider(store, app, router) {
  //此处的provider是react-redux的我们之所以使用dva在任意组件里去进行行provider和connect,而不是在通过reactr-redux去给根节点绑定是因为
  //dva在此处就已经帮我们给根节点绑定了
  const DvaRoot = extraProps => (
    //Provider组件是react提供给我们的,通过parovider组件给根组件绑定我们就可以在任意地方拿到state了
    <Provider store={store}>
      { router({ app, history: app._history, ...extraProps }) }
    </Provider>
  );
  return DvaRoot;
}
 //render函数
function render(container, store, app, router) {
  //调用reactr-dom的render方法来进行渲染组件
  const ReactDOM = require('react-dom');  // eslint-disable-line
  ReactDOM.render(React.createElement(getProvider(store, app, router)), container);
}

router

router方法接收的是路由,给app的绑定上路由

// 这个方法是在dva导出的函数里的,我们通过调用app.router传进来路由组件就可以实现绑定了
 function router(router) {
 //同样通过invariant来进行判断
 invariant(
   isFunction(router),
   `[app.router] router should be function, but got ${typeof router}`,
 );
 //给app方法绑定上路由
 app._router = router;
}

经过上面的源码,我们可以看到dva/src/index.js只是实现了对mode和view的绑定,真正实现了dva里的model的是dva-core所以我们接下来看dva-core的源码

dva-core/src/index.js

create

dva-core导出了create方法,这个create方法就是我们整个创建model的过程,create方法接收两个参数opts 是使用者添加的控制选项,createOpts 则是初始化了 reducer 与 redux 的中间件
create创建了一个app并给他绑定model(用于注册model),plugin(各种插件基于dva的声明周期函数的),start(启动app),最终create放回app
  export function create(hooksAndOpts = {}, createOpts = {}) {
  const { initialReducer, setupApp = noop } = createOpts;
//plugin里包含了一些钩子函数
  const plugin = new Plugin(); //所有的中间件
  plugin.use(filterHooks(hooksAndOpts));
 //create创建了一个app并给他绑定了model和start方法
  const app = {
    _models: [prefixNamespace({ ...dvaModel })],
    _store: null,
    _plugin: plugin,
    use: plugin.use.bind(plugin),
    model,
    start,
  };
  return app;

plugin

在create函数里创建了plugin并通过filterHooks去掉不合法的plugin,plugin里是这样做的,

   1.plugin里首先定义一乐hooks函数,用于保存所有生命周期函数的名称(主要用于判断用户传进来的是否是合法的,以及给plugin这个类进行绑定)
   2.声明一个filterhooks函数用于过滤掉用户传进来的不合法的函数
   3.声明plugin类并进行导出,在constructor函数给plugin类的私有hooks绑定hooks数组
   4.定义plugin的类方法用来注册中间件,也就是将用户传进来的方法push到对应的hooks数组里
//定义hooks包含了所有声明周期函数的名字
const hooks = [
  'onError',
  'onStateChange',
  'onAction',
  'onHmr',
  'onReducer',
  'onEffect',
  'extraReducers',
  'extraEnhancers',
  '_handleActions',
];
//判断是否是hooks里有的,如果hooks没有就被视为不合法的剔除掉
export function filterHooks(obj) {
  return Object.keys(obj).reduce((memo, key) => {
    if (hooks.indexOf(key) > -1) {
      memo[key] = obj[key];
    }
    return memo;
  }, {});
}

export default class Plugin {
  constructor() {
    this._handleActions = null;
    this.hooks = hooks.reduce((memo, key) => {
      memo[key] = [];
      return memo;
    }, {});
  //定义每一个插件函数为数组,就是相当于
   // this.hooks={
      //onError=[],
      //onStartChange=[]  
   //}
  }

  use(plugin) {
    //判断传进来的plugin是否是一个对象
    invariant(
      isPlainObject(plugin),
      'plugin.use: plugin should be plain object'
    );
    const hooks = this.hooks;
    //挨个放入数组
    for (const key in plugin) {
      //调用hasOwnProperty方法,是为了防止用户在的pligun里自己写一个,也就是怕用户传入进项相同名称的然后覆盖掉之前的函数,所以通过hasOwnProperty调用
      if (Object.prototype.hasOwnProperty.call(plugin, key)) {
        invariant(hooks[key], `plugin.use: unknown plugin property: ${key}`);
        if (key === '_handleActions') {
          this._handleActions = plugin[key];
        } else if (key === 'extraEnhancers') {
          hooks[key] = plugin[key];
        } else {
          hooks[key].push(plugin[key]);
        }
      }
    }
  }

  apply(key, defaultHandler) {
    const hooks = this.hooks;
    const validApplyHooks = ['onError', 'onHmr'];
    invariant(
      validApplyHooks.indexOf(key) > -1,
      `plugin.apply: hook ${key} cannot be applied`
    );
    const fns = hooks[key];

    return (...args) => {
      if (fns.length) {
        for (const fn of fns) {
          fn(...args);
        }
      } else if (defaultHandler) {
        defaultHandler(...args);
      }
    };
  }

  get(key) {
    const hooks = this.hooks;
    invariant(key in hooks, `plugin.get: hook ${key} cannot be got`);
    if (key === 'extraReducers') {
      return getExtraReducers(hooks[key]);
    } else if (key === 'onReducer') {
      return getOnReducer(hooks[key]);
    } else {
      return hooks[key];
    }
  }
}

接下来我们来看model,我们是如何注册model的

model

注册model是通过model函数,model函数首先通过checkmodel来检查model是否合法 一个model是否合法需要以下几点 1.namespace: //必须传,必须为字符串,而且是唯一的 2.state 必须传,可以是任何数据 3.effect //可传可不传 必须是一个对象 4.subscriptions //可传可不传,必须是一个对象

然后model函数通过profixednamespace函数给model加上前缀,最后将model添加到app的_model就完成了注册
    function model(m) {
    if (process.env.NODE_ENV !== 'production') {
      checkModel(m, app._models);
    }
    const prefixedModel = prefixNamespace({ ...m });
    app._models.push(prefixedModel);
    return prefixedModel;
  }

start方法(start方法是dva-core的核心)

dva的start方法是完成了model和view的绑定,dva-core的start就是完成了对react-redux的调用和redux-saga的调用,在dva的导出函数里就执行了dva-core的start的调用

1.start方法首先定义了一个全局的错误处理函数
2.通过createSagaMiddleware函数创建saga中间件
3.通过createPromisemiddleware函数(createPromisemiddleware函数用于判断action的type是否属于effect的如果属于返回promise,并且封装action在getsaga中使用)(看这个函数的时候我们可以想一下effect的用法,就是当我们在dispatch的时候我们的type指向的是effect)
4.sagas  (sagas是一个数组里边的元素都是被getsaga处理过的东西)(这一步的getsga处理内容较多,可以跳到getsga先看完再转过来看)
5.挂在recuder和effect并添加到sgags数组里
6.遍历sagas执行sagamiddle.run实现监听
7.上述几步已经实现了对异步数据流的处理接下来我们该组建我们的store了,dva提供了自己的createstore的方法
8.创建store主要是通过dva自己的参数(redux提供的reducer,plugin,create时的配置,core提供的sagamiddlewore和promisemiddleword)
9.组建完store,dva又添加了订阅模式subscription(dva/index中将history代理到了app._history上,因此每次history改变,都会通知到redux,触发state的更新。并且subscription中的函数接受app对象,可以订阅到history的变化。)

redux-saga

 这里插入以下redux-sags是什么,redux-saga是redux的中间件,为什么要用到这个中间件呢就是比如我们在dispatch到action的时候需要做一些额外的操作,这时候我们就用到了我们的中间件

getsaga

getsaga是对model添加监听,getsaga返回一个generator函数

 
export default function getSaga(effects, model, onError, onEffect) {
  //getsgas接收四个参数
  // effects :model的effects
  // model : model本身
  // onerror : 全局的错误处理函数
  // oneffect: 生命周期函数

  return function*() {
    for (const key in effects) {
      //遍历所有的effects
      if (Object.prototype.hasOwnProperty.call(effects, key)) {
        const watcher = getWatcher(key, effects[key], model, onError, onEffect);
        //给每一个effects是添加watcher,单独开辟一个线程监听action执行effects然后在必要的时候取消监听
        //getwatcher函数返回一个generator函数
        const task = yield sagaEffects.fork(watcher);

          
        // fork方法接受一个generator函数或者返回promise对象的普通函数
        // 执行传入的函数,以此开辟新的线程,创建一个分叉任务,
        // 在dva内,这里完成了派发异步action自动执行对应effect的功能
        // 之后又创建一个分叉任务管理这个task对象,在必要时候卸载task
        yield sagaEffects.fork(function*() {
          yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
          yield sagaEffects.cancel(task);
        });
      }
    }
  };
}

function getWatcher(key, _effect, model, onError, onEffect) {
  let effect = _effect;
  let type = 'takeEvery';
  let ms;
  //判断是数组的情况
  if (Array.isArray(_effect)) {
    effect = _effect[0];
    const opts = _effect[1];
    if (opts && opts.type) {
      type = opts.type;
      if (type === 'throttle') {
        invariant(
          opts.ms,
          'app.start: opts.ms should be defined if type is throttle'
        );
        ms = opts.ms;
      }
    }
    invariant(
      ['watcher', 'takeEvery', 'takeLatest', 'throttle'].indexOf(type) > -1,
      'app.start: effect type should be takeEvery, takeLatest, throttle or watcher'
    );
  }

  function noop() {}
  //sagawitchCatch内部调用用户的effect方法,并通知redux-saga已经将action封装为promise并在结束后调用promise对应状态的方法
  function* sagaWithCatch(...args) {
    //通过sagawithCatch方法达到通知saga的目的
    const { __dva_resolve: resolve = noop, __dva_reject: reject = noop } =
      args.length > 0 ? args[0] : {};
    try {
      yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` });
      //执行用户传入的effect
      const ret = yield effect(...args.concat(createEffects(model)));
      yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@end` });
      resolve(ret);
    } catch (e) {
      onError(e, {
        key,
        effectArgs: args,
      });
      if (!e._dontReject) {
        reject(e);
      }
    }
  }

  const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);
//根据用户传入的type返回想对应的generator函数
  switch (type) {
    case 'watcher':
      return sagaWithCatch;
    case 'takeLatest':
      return function*() {
        yield takeLatest(key, sagaWithOnEffect);
      };
    case 'throttle':
      return function*() {
        yield throttle(ms, key, sagaWithOnEffect);
      };
      // // 在effect不是数组的情况下,
    // 监听每一个发出的action,getWatcher的返回值走的都是default
    // 即,每次发出指向effect的action时都会调用sagaWithOnEffect
    // takeEvery()方法接受两个参数,
    // 要匹配的action和一个saga(一个saga就是一个generator函数)
    // takeEvery监听action,在每次这个action被发起时,
    // 创建一个新的saga任务
    // 因此,dva项目中,所有的指向effect的action的派发,
    // 都会在这里创建一个实时任务
    default:
      return function*() {
        yield takeEvery(key, sagaWithOnEffect);
      };
  }
}
//主要是对type的检查
function createEffects(model) {
  function assertAction(type, name) {
    invariant(type, 'dispatch: action should be a plain Object with type');
    warning(
      type.indexOf(`${model.namespace}${NAMESPACE_SEP}`) !== 0,
      `[${name}] ${type} should not be prefixed with namespace ${
        model.namespace
      }`
    );
  }
  function put(action) {
    const { type } = action;
    assertAction(type, 'sagaEffects.put');
    return sagaEffects.put({ ...action, type: prefixType(type, model) });
  }

  // The operator `put` doesn't block waiting the returned promise to resolve.
  // Using `put.resolve` will wait until the promsie resolve/reject before resuming.
  // It will be helpful to organize multi-effects in order,
  // and increase the reusability by seperate the effect in stand-alone pieces.
  // https://github.com/redux-saga/redux-saga/issues/336
  function putResolve(action) {
    const { type } = action;
    assertAction(type, 'sagaEffects.put.resolve');
    return sagaEffects.put.resolve({
      ...action,
      type: prefixType(type, model),
    });
  }
  put.resolve = putResolve;

  function take(type) {
    if (typeof type === 'string') {
      assertAction(type, 'sagaEffects.take');
      return sagaEffects.take(prefixType(type, model));
    } else if (Array.isArray(type)) {
      return sagaEffects.take(
        type.map(t => {
          if (typeof t === 'string') {
            assertAction(t, 'sagaEffects.take');
            return prefixType(t, model);
          }
          return t;
        })
      );
    } else {
      return sagaEffects.take(type);
    }
  }
  return { ...sagaEffects, put, take };
}