美文网首页
dva源码随记

dva源码随记

作者: Gary嘉骏 | 来源:发表于2019-03-03 23:22 被阅读0次

    前言

    在学习antd的UI框架时,了解到了dva这个轻量级的应用框架,集成了react,redux,redux-saga,react-router 。视图,数据流,路由都有。开发起来还是比较简洁的。学习难度不大,主要还是一些约定开发,因为核心还是上面的4个库。

    源码主要分成两大块,dva是建立路由,视图与数据层的关系,dva-core是数据层。

    开始

    版本 : 2.5.0-beta.2

    dva入口

    src/index.js

    只有100多行代码。实际是把配置传入dva-core中生成app实例,然后绑定视图,而视图就是路由组件,并且使用Provider 组件(参考官网高级用法context,有点像Vue的provide和inject)包裹,共享了store。
    有些小细节。

    1. app.model是dva-core实例的方法,后面再看。
    2. app.router方法是为了检测参数是否为function。因为后期要传入对象。只接受function类型。
    3. patchHistory是为了装饰一下history.listen,使得能在监听设置时就能获得当前的location值。对于框架来说,是为了获得应用启动时的location。
    4. 除router方法外,初始化dva应用时使用的方法都是dva-core也就是数据层的方法。不过start就作了装饰代理,使数据与视图绑定,并传入history等参数给用户去绑定路由。
    import React from 'react';
    import invariant from 'invariant';
    import createHashHistory from 'history/createHashHistory';
    import {
      routerMiddleware,
      routerReducer as routing,
    } from 'react-router-redux';
    import document from 'global/document';
    import { Provider } from 'react-redux';
    import * as core from 'dva-core';
    import { isFunction } from 'dva-core/lib/utils';
    
    export default function (opts = {}) {
      const history = opts.history || createHashHistory();
    
      //  传给core初始化数据层。
      const createOpts = {
        initialReducer: {
          routing,
        },
        setupMiddlewares(middlewares) {
          return [
            routerMiddleware(history),// 路由的中间件
            ...middlewares,
          ];
        },
        setupApp(app) {
          app._history = patchHistory(history);// 为了能通过listen获得初始的location数据
        },
      };
    
      const app = core.create(opts, createOpts); // 创建数据层实例
      const oldAppStart = app.start;
      app.router = router; // router组件
      app.start = start; // 主入口
      return app;
    
      // 为了断言router须为function
      function router(router) {
        invariant(
          isFunction(router),
          `[app.router] router should be function, but got ${typeof router}`,
        );
        app._router = router;
      }
    
      //  
      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);
        }
      }
    }
    
    function isHTMLElement(node) {
      return typeof node === 'object' && node !== null && node.nodeType && node.nodeName;
    }
    
    function isString(str) {
      return typeof str === 'string';
    }
    
    function getProvider(store, app, router) {
      const DvaRoot = extraProps => (
        <Provider store={store}>
          { router({ app, history: app._history, ...extraProps }) }
        </Provider>
      );
      return DvaRoot;
    }
    
    function render(container, store, app, router) {
      const ReactDOM = require('react-dom');  // eslint-disable-line
      ReactDOM.render(React.createElement(getProvider(store, app, router)), container);
    }
    
    //  装饰一下history.listen,使得能在监听设置时能获得当前的location值
    function patchHistory(history) {
      const oldListen = history.listen;
      history.listen = (callback) => {
        callback(history.location); // 初始值
        return oldListen.call(history, callback);
      };
      return history;
    }
    
    

    接下来就要进入dva-core.create方法里。

    dva-core/src/index.js

    代码的编写好清晰,主要的都在前面,主到次的写法。

    1. 第一个参数是可传入钩子函数
    2. new Plugin() 是保存及应用钩子的对象
    3. dvaModel的是在model更新时触发reducer。
    4. prefixNamespace方法是为reducers和effects的对象添加namespace前缀,并提醒开发者代码中不需要加前缀
    5. 能发现app.use 能设置钩子函数。
    //  第一个参数是可传入钩子函数
    export function create(hooksAndOpts = {}, createOpts = {}) {
    const { initialReducer, setupApp = noop } = createOpts;
    
      const plugin = new Plugin();// 保存及应用钩子的对象
      plugin.use(filterHooks(hooksAndOpts)); // 筛选及保存钩子函数
    
      // dvaModel好似是unmodel时值加一。
      // prefixNamespace是为reducers和effects的对象添加namespace前缀,并提醒开发者代码中不需要加前缀
      // 能发现app.use 能设置钩子函数。
      const app = {
        _models: [prefixNamespace({ ...dvaModel })],
        _store: null,
        _plugin: plugin,
        use: plugin.use.bind(plugin),
        model,
        start,
      };
      return app;
    

    接着看app.model

    1. checkModel函数:开发环境检查model对象的格式是否正确,并且namespace不能有重复
    2. 在checkModel这里再次发现reduces是可以传入数组,格式是[object,function],初步了解得知object是平时的写法,而function是store enhancer,在这function里可对store对象进行扩展
    3. 所有的model保存在了app._models中
    4. 函数返回的就是格式化后的model
    // 所有的model保存在了app._models中
       // 函数返回的就是格式化后的model
      function model(m) {
        if (process.env.NODE_ENV !== 'production') {
          checkModel(m, app._models);/* 开发环境检查model对象的格式是否正确,并且namespace不能有重复
          在这里再次发现reduces是可以传入数组,格式是[object,function],
          初步了解得知object是平时的写法,而function是store enhancer,在这function里可对store对象进行扩展
          */
        }
        const prefixedModel = prefixNamespace({ ...m });
        app._models.push(prefixedModel);
        return prefixedModel;
      }
    

    然后到最重点处原始的start方法。

    1. 原来的start函数是没有参数传入的

    首先定义可触发onError钩子的函数

    在全局错误处理函数中,能发现之前plugin.apply的写法的意义,调用时可设置默认的函数,并且apply后返回的是一个函数,可传入任何参数去触发,很灵活。

    const onError = (err, extension) => {
          if (err) {
            if (typeof err === 'string') err = new Error(err);
            err.preventDefault = () => {
              err._dontReject = true;
            };
            plugin.apply('onError', err => {
              throw new Error(err.stack || err);
            })(err, app._store.dispatch, extension);
          }
        };
    

    然后是获得初始化store时传入的中间件

    1. createPromiseMiddleware: 为了dispatch时找到effect的话,返回Promise,后面的处理model.effects能看到
    const sagaMiddleware = createSagaMiddleware(); // saga的提供store的中间件。
        const promiseMiddleware = createPromiseMiddleware(app); 
    // 为了dispatch时找到effect的话,返回Promise
    
    export default function createPromiseMiddleware(app) {
      return () => next => action => {
        const { type } = action;
        if (isEffect(type)) {
          return new Promise((resolve, reject) => {
            next({
              __dva_resolve: resolve,
              __dva_reject: reject,
              ...action,
            });
          });
        } else {
          return next(action);
        }
      };
    }
    

    然后开始循环app._model去收集reduces与saga

    1. getReducer 合并所有的reducers成一个函数。
      可看出高阶的用法是把reduce的结果返回给数组第二个位置的function。
      有默认的defaultHandleActions,它会对所有的reducers的key分别生成key与action.type进行对比,
      相同则调用value函数,否则直接返回state的函数,最后再reduce前面所有的函数
      。有一个想法,可app.use{_handleActions:fn(用some先找出来,
      这样执行一次函数就好)}
    for (const m of app._models) {
          /* getReducer 合并所有的reducers成一个函数。
          可看出高阶的用法是把reduce的结果返回给数组第二个位置的function。
          有默认的defaultHandleActions,它会对所有的reducers的key分别生成key与action.type进行对比,
          相同则调用value函数,否则直接返回state的函数,最后再reduce前面所有的函数
          。有一个想法,可app.use{_handleActions:fn(用some先找出来,
            这样执行一次函数就好)}*/
          reducers[m.namespace] = getReducer(
            m.reducers,
            m.state,
            plugin._handleActions
          );
          if (m.effects)
            // 初始化saga传入effects对象,model,全局错误处理函数,onEffect的钩子数组
            sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));
        }
    

    我们重点看看getSaga函数。

    1. 这是初始化saga,传入effects对象,model,全局错误处理函数,onEffect的钩子数组
    2. 对每一个watcher开出一个并行任务
    3. 同时对外设置了cancel上面的任务的方法。dispatch({type:${model.namespace}/@@CANCEL_EFFECTS})就cancel该model的所有effects。(主要用于给app.unmodel与app.replaceModel)
     function getSaga(effects, model, onError, onEffect) {
      return function*() {
        for (const key in effects) {
          // 保证是原始的hasOwnProperty条用
          if (Object.prototype.hasOwnProperty.call(effects, key)) {
            // 生成一个观察者。
            const watcher = getWatcher(key, effects[key], model, onError, onEffect);
            // 对每一个watcher开出一个并行任务
            const task = yield sagaEffects.fork(watcher);
            // 同时对外设置了cancel上面的任务的方法。put(`${model.namespace}/@@CANCEL_EFFECTS`)就cancel
            // 该model的所有effects。
            yield sagaEffects.fork(function*() {
              yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
              yield sagaEffects.cancel(task);
            });
          }
        }
      };
    }
    

    明显重点在getWatcher。

    1. 默认type类型是takeEvery
    2. effect写成数组形式可以在arr[0]中传入opt,可以设置观察的type,
      只能是'watcher', 'takeEvery', 'takeLatest', 'throttle'的之一

    直接看默认的takeEvery

    最后执行的是

    return function*() {
            yield takeEvery(key, sagaWithOnEffect);
          }
    

    为什么要是sagaWithOnEffect呢,因为有onEffect钩子,这是提供修改effect的钩子。

    1. onEffect钩子可获取的参数是effect,saga的操作集合,该model对象,effect的key
    // 触发onEffect的钩子,这个可以修改初始化的saga
      const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);
    
    ....
    ....
    
    function applyOnEffect(fns, effect, model, key) {
      for (const fn of fns) {
        // 传入的参数为包装好的effect,saga的操作集合,该model对象,effect的key
        effect = fn(effect, sagaEffects, model, key);
      }
      return effect;
    }
    

    所以重点的转到了sagaWithCatch。

    1. 还记得createPromiseMiddleware中间件吗,是effect的action会next({ __dva_resolve: resolve, __dva_reject: reject, ...action, }),所以这里能取得中间件返回promise的resolve和reject。由此,对于effect的action,我们可以用dispatch({ type: 'any/any', payload: xxx, }).then(() => ...);去在effect结束或者报错时作一些操作
    2. 可在reducers中设置开始${key}${NAMESPACE_SEP}@@start与结束的钩子${key}${NAMESPACE_SEP}@@end。如果没报错的话,确实end可以当做resolve去使用。
    3. 报错的话肯定会触发包装过的全局公用onError,但如果设置钩子时执行了err.preventDefault(),则不再抛出错误,也就是dispatch().catch()无效
    function* sagaWithCatch(...args) {
        const { __dva_resolve: resolve = noop, __dva_reject: reject = noop } =
          args.length > 0 ? args[0] : {};
        try {
          // effect开始钩子
          yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` });
          // createEffects: 把加工过的操作符集合加在参数的最后一个。
          // 并对put,put.resolve,take做了包装,使得不需要传type时不需要加namespace
          const ret = yield effect(...args.concat(createEffects(model)));
          // effect结束钩子
          yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@end` });
          resolve(ret);
        } catch (e) {
          onError(e, {
            key,
            effectArgs: args,
          });
          // 如果在onError钩子中执行了err.preventDefault(),则不再抛出错误
          if (!e._dontReject) {
            reject(e);
          }
        }
      }
    

    saga与reducer收集好后,就可以创建store了

    1. createReducer是把对combineReducer(还传入了非model里的reducersplugin.get('extraReducers'))后reducer传入onReducer钩子组合成的reducerEnhancer函数(plugin.get('onReducer'))
    2. plugin.get('extraEnhancers')获得用户设置的store增强工具
    3. plugin.get('onAction')获得用户设置的store中间件。
    4. 然后收集所有框架内中间键,dva-core中有promiseMiddleware与sagaMiddleware,dva中有routerMiddleware(history),从createOpts传入setupMiddlewares 函数setupMiddlewares(middlewares) { return [ routerMiddleware(history), ...middlewares, ]; },
    5. 对于 redux 中 的 compose 函数,在数组长度为 1 的情况下返回第一个元素。compose(...enhancers) 等同于 applyMiddleware(...middlewares)
    const store = (app._store = createStore({
          // eslint-disable-line
          reducers: createReducer(),
          initialState: hooksAndOpts.initialState || {},
          plugin,
          createOpts,
          sagaMiddleware,
          promiseMiddleware,
        }));
    
    function({
      reducers,
      initialState,
      plugin,
      sagaMiddleware,
      promiseMiddleware,
      createOpts: { setupMiddlewares = returnSelf },
    }) {
      // extra enhancers
      const extraEnhancers = plugin.get('extraEnhancers');
      invariant(
        isArray(extraEnhancers),
        `[app.start] extraEnhancers should be array, but got ${typeof extraEnhancers}`
      );
    
      // 由这个初始化可以知道,onAction钩子必须在app.start前设置
      const extraMiddlewares = plugin.get('onAction');
      const middlewares = setupMiddlewares([
        promiseMiddleware,
        sagaMiddleware,
        ...flatten(extraMiddlewares),
      ]);
    
      const composeEnhancers =
      process.env.NODE_ENV !== "production" &&
      window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
        ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
        : compose;
    
      const enhancers = [applyMiddleware(...middlewares), ...extraEnhancers];
    
      return createStore(reducers, initialState, composeEnhancers(...enhancers));
    }
    
    1. 把saga中间件的监听Saga的函数赋值给store.runSaga
    2. 把记录运行中传入的reducers的store.asyncReducers设为空对象
    store.runSaga = sagaMiddleware.run; // 开始监听Saga的函数
        store.asyncReducers = {};
    
    1. 登记onStateChange钩子,其实就是调用store.subscribe去触发
    // 其实就是调用store.subscribe去触发onStateChange钩子
        const listeners = plugin.get('onStateChange');
        for (const listener of listeners) {
          store.subscribe(() => {
            listener(store.getState());
          });
        }
    

    监听所有的Saga

    // 循环监听Saga
        sagas.forEach(sagaMiddleware.run);
    

    执行了dva传入的setupApp(app) {app._history = patchHistory(history)} 给app赋值了_history

    setupApp(app) {
          app._history = patchHistory(history);
        },  给app赋值了_history*/ 
        setupApp(app);
    

    接着处理subscriptions,遍历models去订阅

    1. 订阅所有model的subscriptions,并记录返回取消订阅的方法
    const unlisteners = {};
        for (const model of this._models) {
          if (model.subscriptions) {
            // 订阅所有model的subscriptions,并返回取消订阅的方法
            unlisteners[model.namespace] = runSubscription(
              model.subscriptions,
              model,
              app,
              onError
            );
          }
        }
    

    我们暂停往下,关注runSubscription

    1. subscriptions中参数了dispatch只能触发当前model的action,因为会自动加prefix
    2. 有history对象,就是说可以切换路由
    3. 可触发onError钩子。
    4. 有可能移除或覆盖model的话,用户必须返回取消订阅的方法。不返回的话,下面unlisten方法(遍历运行而已)会发出警告
    function run(subs, model, app, onError) {
      const funcs = [];
      const nonFuncs = [];
      for (const key in subs) {
        if (Object.prototype.hasOwnProperty.call(subs, key)) {
          const sub = subs[key];
          const unlistener = sub({
            dispatch: prefixedDispatch(app._store.dispatch, model),
            history: app._history,
          }, onError);
          if (isFunction(unlistener)) {
            funcs.push(unlistener);
          } else {
            nonFuncs.push(key);
          }
        }
      }
      return { funcs, nonFuncs };
    }
    

    回到主线。基本到最后了。添加工具函数app.model,app.unmodel,app.replaceModel。

    先看app.model

    1. 此函数已bind了前3个参数:初始化合并reducers的函数,处理错误函数,记录取消订阅的对象。
    app.model = injectModel.bind(app, createReducer, onError, unlisteners);
    
    1. model(m)之前讲过,是检查及保存model。
    2. 把此模块的reducers合成后赋值给store.asyncReducers[m.namespace] 
    3. 调用原始的store. replaceReducer,会与store.asyncReducers作比较合并,createReducer就是过得当前所有reducers的combine。
    4. 监听effect与订阅subscriptions与之前一致
    function injectModel(createReducer, onError, unlisteners, m) {
        m = model(m);
    
        const store = app._store;
        store.asyncReducers[m.namespace] = getReducer(
          m.reducers,
          m.state,
          plugin._handleActions
        );
        // 调用原始的replaceReducer,会与store.asyncReducers作比较合并
        store.replaceReducer(createReducer());
        if (m.effects) {
          store.runSaga(
            app._getSaga(m.effects, m, onError, plugin.get('onEffect'))
          );
        }
        if (m.subscriptions) {
          unlisteners[m.namespace] = runSubscription(
            m.subscriptions,
            m,
            app,
            onError
          );
        }
      }
    

    我们再看看 app.unmodel,移除model

    1. 删除reducers是直接把store.asyncReducers与reducers里的key删除,简单粗暴。然后再次执行store.replaceReducer(createReducer())
    2. cancel effects前面一分析了,整个model的effect tasks移除
    3. unlisteners在之前订阅时已经收集过了,所以直接根据namespace取消就好
    4. 最后记得移除app._models里对应的model
    function unmodel(createReducer, reducers, unlisteners, namespace) {
        const store = app._store;
    
        // Delete reducers
        delete store.asyncReducers[namespace];
        delete reducers[namespace];
    
        store.replaceReducer(createReducer());
        store.dispatch({ type: '@@dva/UPDATE' });
    
        // Cancel effects
        store.dispatch({ type: `${namespace}/@@CANCEL_EFFECTS` });
    
        // Unlisten subscrioptions
        unlistenSubscription(unlisteners, namespace);
    
        // Delete model from app._models
        app._models = app._models.filter(model => model.namespace !== namespace);
      }
    

    最后app.replaceModel逻辑其实就是unmodel后model。

    还有就是,内部@@dva的model,是会在replaceModel与unmodel中进行update的action:state自增1。

    这样整个dva框架的流程就走完了。挺轻量巧妙的。

    编外:

    dva还提供有一些工具函数:fetch,dynamic

    fetch只是export了isomorphic-fetch

    dynamic动态加载model与视图

    用法:
    app: dva 实例,加载 models 时需要
    models: 返回 Promise 数组的函数,Promise 返回 dva model
    component:返回 Promise 的函数,Promise 返回 React Component

    const UserPageComponent = dynamic({
      app,
      models: () => [
        import('./models/users'),
      ],
      component: () => import('./routes/UserPage'),
    });
    
    1. 传入resolve返回一个async组件。获取所有的models和component,model可为空。
    2. 组件中有一个AsyncComponent的state,render函数是根据AsyncComponent是否为空去渲染的,所以只要resolve后更新state就好了,这里也了解到一点,组件挂载前不需要使用setState去更新state。
    3. 可使用dynamic.setDefaultLoadingComponent去设置加载时的过度组件。
    function dynamic(config) {
      const { app, models: resolveModels, component: resolveComponent } = config;
      return asyncComponent({
        resolve: config.resolve || function () {
          const models = typeof resolveModels === 'function' ? resolveModels() : [];
          const component = resolveComponent();
          return new Promise((resolve) => {
            Promise.all([...models, component]).then((ret) => {
              if (!models || !models.length) {
                return resolve(ret[0]);
              } else {
                const len = models.length;
                ret.slice(0, len).forEach((m) => {
                  m = m.default || m;
                  if (!Array.isArray(m)) {
                    m = [m];
                  }
                  m.map(_ => registerModel(app, _));
                });
                resolve(ret[len]);
              }
            });
          });
        },
        ...config,
      });
    }
    
    function asyncComponent(config) {
      const { resolve } = config;
    
      return class DynamicComponent extends Component {
        constructor(...args) {
          super(...args);
          this.LoadingComponent =
            config.LoadingComponent || defaultLoadingComponent;
          this.state = {
            AsyncComponent: null,
          };
          this.load();
        }
    
        componentDidMount() {
          this.mounted = true;
        }
    
        componentWillUnmount() {
          this.mounted = false;
        }
    
        load() {
          resolve().then((m) => {
            const AsyncComponent = m.default || m;
            if (this.mounted) {
              this.setState({ AsyncComponent });
            } else {
              this.state.AsyncComponent = AsyncComponent; // eslint-disable-line
            }
          });
        }
    
        render() {
          const { AsyncComponent } = this.state;
          const { LoadingComponent } = this;
          if (AsyncComponent) return <AsyncComponent {...this.props} />;
    
          return <LoadingComponent {...this.props} />;
        }
      };
    }
    

    到此,对dva源码分析完成。接着下篇学习及分析umi

    相关文章

      网友评论

          本文标题:dva源码随记

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