dva.js 源码理解

作者: Jason_Zeng | 来源:发表于2018-11-01 16:42 被阅读3次

    一. 先从官方快速上手 dva-cli 说起,建立起工程,参考 dva官网, 然后打开 index.js看到下面, 其中有五个步骤,下面就一一分析

    import dva from 'dva';
    import './index.css';
    // 1. Initialize
    const app = dva();
    // 2. Plugins
    app.use({});
    // 3. Model
    app.model(require('./models/products').default);
    // 4. Router
    app.router(require('./router').default);
    // 5. Start
    app.start('#root');
    

    1. Initialize :const app = dva();

    初始化了一个dva对象,跳转到源码index.d.ts,发现是下面TS的声明文件,学习了发现这跟c++头文件一样,这是对外接口的描述,TS里面只需要把d.ts和源文件放一个目录就可以都不需要引用,比如这里和index.js放一起

    export interface DvaInstance {
      use: (hooks: Hooks) => void,
      model: (model: Model) => void,
      unmodel: (namespace: string) => void,
      router: (router: Router) => void,
      start: (selector?: HTMLElement | string) => any,
    }
    export default function dva(opts?: DvaOption): DvaInstance;
    

    那么具体实现部分就看看index.js, 又是一个引用,应该是为了方便组织代码,我们就看./lib下面的文件,注意上面的export default, 我们要找这个default输出

    module.exports = require('./lib');
    module.exports.connect = require('react-redux').connect;
    

    打开./lib文件夹,发现怎么有个dynamic.js, 查了资料也不知道搞嘛的,发现有个redux-container-builder里面有defaultLoadingComponent这里面用到了,应该是动态加载模块什么,暂时没搞清楚,可以先不管,直接看里面的index.js, 找到export default就是你需要的dva, 发现跟上面的声明文件对上了,少了些对象,应该是core.create创建的,

    export default function (opts = {}) {
      // ...省略
      const app = core.create(opts, createOpts);
      const oldAppStart = app.start;
      app.router = router;
      app.start = start;
      return app;
      // ...省略
    }
    

    找到dva-core里面的实现index.js, 可以看到dva完整啦,这下有dva有了use, model, router, start几个关键属性,没错,接下的步骤就是调用这些属性方法的呀!!

    这里把dva-core独立应该是为了隔离react, dva-core里面是针对react的实现,这样dva就变成一个代码组织的框架,利于以后复用

    function create() {
      //...省略
      var app = {
        _models: [(0, _prefixNamespace.default)((0, _objectSpread2.default)({}, dvaModel))],
        _store: null,
        _plugin: plugin,
        use: plugin.use.bind(plugin),
        model: model,
        start: start
      };
      return app;
     //...省略
    }
    

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

    经过上面初始化,已经拿到dva生成的app对象啦, 由于use方法在dva-core中,我们就看这里面use了什么,下面关键代码就是定义一个数组hook钩子,经过reduce(什么是reduce? 那你应该百度了)后,每个事件都是一个数组,use的作用就是遍历插件for (const key in plugin),把相应的插件实现的方法放入这个数组hooks[key].push(plugin[key]);,留做以后调用.

    什么是插件: 简单的说在应用跑起来后会有各种生命周期,比如当出错时onError, 当发起请求时onEffect, 不同的应用对于这些不同时机想要做的事是不一样的,怎么解决这一问题呢,这就是插件的由来, dva在各时机进行拦截并取好名字,如hook里面的名字,用户按照规范写插件,来一一触发,比如官方的dva-loading, 就在注册了在onEffect时候打开和关闭遮罩,具体可看源码

    const hooks = [
      'onError',
      'onStateChange',
      'onAction',
      'onHmr',
      'onReducer',
      'onEffect',
      'extraReducers',
      'extraEnhancers',
      '_handleActions',
    ];
    // ...省略
    export default class Plugin {
      constructor() {
        this._handleActions = null;
        this.hooks = hooks.reduce((memo, key) => {
          memo[key] = [];
          return memo;
        }, {});
      }
    
      use(plugin) {
        invariant(
          isPlainObject(plugin),
          'plugin.use: plugin should be plain object'
        );
        const hooks = this.hooks;
        for (const key in plugin) {
          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]);
            }
          }
        }
      }
    

    dva-loading插件举例,比如app.use(createLoading())后, onEffect里面就被压入了实现的函数内容,压入后的hook对象如下,等到要用的时候dva内部会顺序调用,这样插件的准备工作就做好了.

     hooks = {
          extraEnhancers: [],
          extraReducers: [],
          onAction: [],
          onEffect: [
            function onEffect(effect, { put }, model, actionType) {
                // dva-laoding 里面实现的部分
            },
          ],
          onError: [],
          onHmr: [],
          onReducer: [],
          onStateChange: [],
          _handleActions: [],
    }
    

    3. Model : app.model(require('./models/products').default);

    按照官方的步骤,这里的products建立如下

    export default {
        namespace: "products",
        state : [],
        reducers: {
            "delete"(state,{payload: id}) {
                return state.filter(item => item.id !== id);
            }
        }
    };
    

    来到dva-core里面看model的源码,更简单,首先判断下是否满足model的定义checkModel(m, app._models);,不满足报错,满足后根据model里面的namespace给每个model加前缀名字const prefixedModel = prefixNamespace({ ...m });,然后再放入内部的app._models里面留有后用

      function model(m) {
        if (process.env.NODE_ENV !== 'production') {
          checkModel(m, app._models);
        }
        const prefixedModel = prefixNamespace({ ...m });
        app._models.push(prefixedModel);
        return prefixedModel;
      }
    

    model的定义如下

      const {
        namespace,
        reducers,
        effects,
        subscriptions,
      } = model;
    

    4. Router: app.router(require('./router').default);

    到了路由了, 源码更简单,直接存了一下, 但是router是个啥子呢??

      function router(router) {
        invariant(
          isFunction(router),
          `[app.router] router should be function, but got ${typeof router}`,
        );
        app._router = router;
      }
    

    一看这结构,是不是跟react-router-dom里面的很像,没错就是一个模子,查看源码发现就是react-router-dom里面的路由,名都没改

    import React from 'react';
    import { Router, Route, Switch } from 'dva/router';
    import IndexPage from './routes/IndexPage';
    import Products from './routes/Products';
    
    function RouterConfig({ history }) {
      return (
        <Router history={history}>
          <Switch>
            <Route path="/" exact component={IndexPage} />
            <Route path="/products" exact component={Products} />
          </Switch>
        </Router>
      );
    }
    export default RouterConfig;
    

    dvarouter.js文件如下

    module.exports = require('react-router-dom');
    module.exports.routerRedux = require('react-router-redux');
    

    5. Start: app.start('#root');

    先贴上start的源码, 这是最后也是最重要的一步啦,加油!!

      function start(container) {
        // 允许 container 是字符串,然后用 querySelector 找元素
        if (isString(container)) {
          container = document.querySelector(container);
          invariant(
            container,
            `[app.start] container ${container} not found`,
          );
        }
    
        // 并且是 HTMLElement
        invariant(
          !container || isHTMLElement(container),
          `[app.start] container should be HTMLElement`,
        );
    
        // 路由必须提前注册
        invariant(
          app._router,
          `[app.start] router must be registered before app.start()`,
        );
    
        if (!app._store) {
          oldAppStart.call(app);
        }
        const store = app._store;
    
        // 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);
        }
      }
    

    下面我们一步一步分解来看简单的分为以下几小步

    1. 前面一堆invariant只是确保把container转换成DOM元素,router注册好没有,至于invariant, 放上官方描述,慢慢体会
    A mirror of Facebook's `invariant` (e.g. React, flux).
    A way to provide descriptive errors in development but generic errors in production.
    
    1. oldAppStart.call(app);这步其实很重要,因为首次进来肯定没有app._store的,这个oldAppStart其实是dva-core里面的start函数,用来调用reduxcreateStore, 放上源码,有点多啊,不想看怎么办,没事,我来给你细细讲解
      function start() {
        // Global error handler
        var onError = function onError(err, extension) {
          if (err) {
            if (typeof err === 'string') err = new Error(err);
    
            err.preventDefault = function () {
              err._dontReject = true;
            };
    
            plugin.apply('onError', function (err) {
              throw new Error(err.stack || err);
            })(err, app._store.dispatch, extension);
          }
        };
    
        var sagaMiddleware = (0, _middleware.default)();
        var promiseMiddleware = (0, _createPromiseMiddleware.default)(app);
        app._getSaga = _getSaga.default.bind(null);
        var sagas = [];
        var reducers = (0, _objectSpread2.default)({}, initialReducer);
        var _iteratorNormalCompletion = true;
        var _didIteratorError = false;
        var _iteratorError = undefined;
    
        try {
          for (var _iterator = (0, _getIterator2.default)(app._models), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
            var m = _step.value;
            reducers[m.namespace] = (0, _getReducer.default)(m.reducers, m.state, plugin._handleActions);
            if (m.effects) sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));
          }
        } catch (err) {
          _didIteratorError = true;
          _iteratorError = err;
        } finally {
          try {
            if (!_iteratorNormalCompletion && _iterator.return != null) {
              _iterator.return();
            }
          } finally {
            if (_didIteratorError) {
              throw _iteratorError;
            }
          }
        }
    
        var reducerEnhancer = plugin.get('onReducer');
        var extraReducers = plugin.get('extraReducers');
        (0, _invariant.default)((0, _keys.default)(extraReducers).every(function (key) {
          return !(key in reducers);
        }), "[app.start] extraReducers is conflict with other reducers, reducers list: ".concat((0, _keys.default)(reducers).join(', '))); // Create store
    
        var store = app._store = (0, _createStore.default)({
          // eslint-disable-line
          reducers: createReducer(),
          initialState: hooksAndOpts.initialState || {},
          plugin: plugin,
          createOpts: createOpts,
          sagaMiddleware: sagaMiddleware,
          promiseMiddleware: promiseMiddleware
        }); // Extend store
    
        store.runSaga = sagaMiddleware.run;
        store.asyncReducers = {}; // Execute listeners when state is changed
    
        var listeners = plugin.get('onStateChange');
        var _iteratorNormalCompletion2 = true;
        var _didIteratorError2 = false;
        var _iteratorError2 = undefined;
    
        try {
          var _loop = function _loop() {
            var listener = _step2.value;
            store.subscribe(function () {
              listener(store.getState());
            });
          };
    
          for (var _iterator2 = (0, _getIterator2.default)(listeners), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
            _loop();
          } // Run sagas
    
        } catch (err) {
          _didIteratorError2 = true;
          _iteratorError2 = err;
        } finally {
          try {
            if (!_iteratorNormalCompletion2 && _iterator2.return != null) {
              _iterator2.return();
            }
          } finally {
            if (_didIteratorError2) {
              throw _iteratorError2;
            }
          }
        }
    
        sagas.forEach(sagaMiddleware.run); // Setup app
    
        setupApp(app); // Run subscriptions
    
        var unlisteners = {};
        var _iteratorNormalCompletion3 = true;
        var _didIteratorError3 = false;
        var _iteratorError3 = undefined;
    
        try {
          for (var _iterator3 = (0, _getIterator2.default)(this._models), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
            var _model = _step3.value;
    
            if (_model.subscriptions) {
              unlisteners[_model.namespace] = (0, _subscription.run)(_model.subscriptions, _model, app, onError);
            }
          } // Setup app.model and app.unmodel
    
        } catch (err) {
          _didIteratorError3 = true;
          _iteratorError3 = err;
        } finally {
          try {
            if (!_iteratorNormalCompletion3 && _iterator3.return != null) {
              _iterator3.return();
            }
          } finally {
            if (_didIteratorError3) {
              throw _iteratorError3;
            }
          }
        }
    
        app.model = injectModel.bind(app, createReducer, onError, unlisteners);
        app.unmodel = unmodel.bind(app, createReducer, reducers, unlisteners);
        app.replaceModel = replaceModel.bind(app, createReducer, reducers, unlisteners, onError);
        /**
         * Create global reducer for redux.
         *
         * @returns {Object}
         */
    
        function createReducer() {
          return reducerEnhancer((0, _redux.combineReducers)((0, _objectSpread2.default)({}, reducers, extraReducers, app._store ? app._store.asyncReducers : {})));
        }
      }
    

    首先我们来看第一行onError,描述是全局错误处理,还记得Plugins插件里面的onError吗,没错,这里正是学习插件运行的最好机会呀,这个onError肯定在报错的时候会调用的

    // Global error handler
        var onError = function onError(err, extension) {
          if (err) {
            if (typeof err === 'string') err = new Error(err);
    
            err.preventDefault = function () {
              err._dontReject = true;
            };
    
            plugin.apply('onError', function (err) {
              throw new Error(err.stack || err);
            })(err, app._store.dispatch, extension);
          }
        };
    

    你看里面重要的plugin.apply这里执行插件的onError, 这个apply可不是js原生的apply来绑定this的呀,而是自己实现的方法,找到Plugins.js插件下面的源码如下, 果然没错,把hooks里面的onError数组取出来叫做fns, 然后返回一个函数return (...args)的目的是为了获取实时调用的参数,然后遍历fns分别传入参数一一调用,同时也支持传入一个默认的defaultHandler,在没有注册插件的时候采用默认处理方式

      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);
          }
        };
      }
    

    这样一个插件的注册和调用的流程我们都理解了,是不是很像自己实现一套插件机制,这就是所有类库宣传的可插拔的高级特性,代码就这么回事,但是这种思想要建立起来,还有封装方式,这些都是宝贵的经验呢!!!

    相关文章

      网友评论

        本文标题:dva.js 源码理解

        本文链接:https://www.haomeiwen.com/subject/almhiftx.html