dva源码解析(一)

栏目: 服务器 · 发布时间: 7年前

内容简介:dva源码解析(一)

写在前面

dva 是蚂蚁金服推出的一个单页应用框架,对 reduxreact-routerredux-saga 进行了上层封装,没有引入新的概念,但是极大的程度上提升了开发效率;下面内容为本人理解,如有错误,还请指出,不胜感激。

redux的痛苦

redux 的优点很多,痛点也有,比如异步控制, redux-saga 的出现使得异步操作变得优雅,但是基于 redux-saga 不得不承认的一点就是开发过程实在是太麻烦了,假若增加一个操作,不得不操作 actionsreducerssagas ,对于 sagas 可以还需要进行 watch ,而后还要进行 fork ;(PS: 本来就够麻烦了,再加上一个 sagas );在添加一个操作时,不得不操作这么多的文件,实在是麻烦,而 dva 的出现在一定程度上解决了这个问题。

dva基本概念

未使用 dva 下的目录经常是这样的:

actions
   --/ user.js
   --/ team.js
reducers
   --/ user.js
   --/ team.js
sagas/
   --/ user.js
   --/ team.js

dva 将其合并:

models
   --/ user.js
   --/ team.js

dva 中有着几个概念:

namespace       =>  combineReducers中对应的key值
state           =>  对应初始的state,也就是initialState
effects         =>  saga的处理函数
reducers        =>  对应reducers,不同的是,写法上将switch...case转化为对象

除了这些以外, dva 中还有 subscriptions ,这一概念来源于 elm

dva的实现

初始化

const app = dva({
    history: browserHistory
});

上面的过程发生了什么?

dva 本质上调用了下面函数:

function dva(hooks = {}) {
    const history = hooks.history || defaultHistory;
    const initialState = hooks.initialState || {};
    delete hooks.history;
    delete hooks.initialState;

    const plugin = new Plugin();
    plugin.use(hooks);

    const app = {
      // properties
      _models: [],
      _router: null,
      _store: null,
      _history: null,
      _plugin: plugin,
      _getProvider: null,
      // methods
      use,
      model,
      router,
      start,
    };
    return app;
}

hooks 为传入的一些配置,例如可以通过传入 history 来改变路由的实现, dva 默认采用的是 hashHistory ;从这里可以看出 dva 暴露出来的方法:

  • app.router() :指定路由,需要传入一个函数,一般类似于 ({ history }) => (<Router>...</Router>)

  • app.use() :添加插件,这个稍后来看~

  • app.model() :添加 model ,也就是对应的添加一个 store 下的数据,该方法做的就是对传入的 model 进行检查,对 reducers 添加命名空间,而后将其 push_models 中。

    • namespace 必须且唯一,因为内置了 react-redux-router ,所以 namespace 也不能为 routing

    • subscriptionseffects 均为可选参数,传入的话必须为对象

    • reducers 为可选,支持对象和数组两种传入方式(传入数组的方式,往往伴随着高阶 reducer 的应用,具体稍后再看~)

  • app.start() :初始化应用,接受参数为选择器或者 DOM 节点

需要注意的是:

  • reducerseffectskey 不需要用 namespace/action 的形式了,因为 dva 会自动将其加上, dispatch 的时候, saga 需要加上 namespace ,而 saga 中的 put 不需要加入 namespace ,原因是 dvaput进行了重载

  • dva 同时支持rn应用,引入 dva/mobile 即可,这时 react-router 不在需要,利用rn中的 Navigator 即可,不会引用 react-routerreact-redux-routernamespace 可以命名为 routing ;正是由于这点差异,作者将路由相关的内容作为参数传入了进去,具体可以参见这个文件。

创建

将一些配置项初始化好后,就可以 app.start 就是来创建一个应用,下面就一点点的看看 start 的过程(以下基于默认情况,也就是使用了 react-router ):

  • 参数校验,是否为 DOM 元素或者检查是否可以根据传入的选择器字符串找到对应的 DOM ,这个 DOM 对应的就是 ReactDOM.render 的第二个参数。

  • 错误处理,使得发生错误时,不至于应用奔溃,当然需要传入自定义 hooks.onError 来处理:

// 传入hooks.onError则调用,反之调用默认函数处理,抛出异常即可
  const onError = plugin.apply('onError', (err) => {
    throw new Error(err.stack || err);
  });
  // 目的是出现错误时,也可以进行dispatch操作
  const onErrorWrapper = (err) => {
    if (err) {
      if (typeof err === 'string') err = new Error(err);
      onError(err, app._store.dispatch);
    }
  };
  • 遍历 _models ,初始化 reducers,sagas

const sagas = [];
// initalReducer为{ routing: routerReducer }
const reducers = { ...initialReducer };  // 为rootReducer
for (const m of this._models) {
    // 得到默认的state
    reducers[m.namespace] = getReducer(m.reducers, m.state);
    if (m.effects) sagas.push(getSaga(m.effects, m, onErrorWrapper));
}

处理reducers

对于 reduxreducers 最常见的是基于 switch..case 的,而 dva 做出了一些改变,将每一个 case 分支变作了一个函数:

dva源码解析(一)

(PS: 本人认为,这个可以块可以更改,利用 some 操作来尽可能少的调用无意义的 reducer ,于是我提了一个pr)

每一个 reducer 的实现如下:

// actionType对应的是dva的reducers中的key值
(state, action) => {
    const { type } = action;
    if (type && actionType !== type) {
        return state;
    }
    return reducer(state, action);
};

处理sagas

看完了对于 reducers 的处理,下面来看一下对于 sagas 的处理:

function getSaga(effects, model, onError) {
  return function *() {
    for (const key in effects) {
      if (Object.prototype.hasOwnProperty.call(effects, key)) {
        const watcher = getWatcher(key, effects[key], model, onError);
        const task = yield sagaEffects.fork(watcher);
        // 为了移除时可以将saga任务注销
        yield sagaEffects.fork(function *() {
          yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
          yield sagaEffects.cancel(task);
        });
      }
    }
  };
}

getWatcher 返回一个 saga 监听函数,也就是通常写的 watchXXXmodel.effects[key] 可以是一个任务函数;也可以是个数组,第一个参数为任务函数,第二为配置对象,可以传入 typetype 有4个可选值, takeEvery (默认), takeLatestthrottlewatcher 四种, dvaeffects 做了一个错误处理:

effect => function *(...args) {
  try {
    yield effect(...args.concat(createEffects(model)));
  } catch (e) {
    onError(e);   // 为之前的onErrorWrapper
  }
}

注意:

  • watcher 是指传入的任务函数就是一个 watcher 直接 fork 就好

  • throttle 还要传入一个 ms 配置,这个 ms 代表着在多少毫秒内只触发一次同一类型 saga 任务,而 takeEvery 是不会限制同一类型执行次数, takeLatest 只能执行一个同一类型任务,有执行中的再次执行就会取消

  • getSaga 可以看出, ${namespace}/@@CANCEL_EFFECTS 可以取消对应的任务监听

  • 可以通过配置 hooks.onEffect 来增加 sagawatcher

增强 redux

  • redux 中间件,由 sagaMiddwarerouterMiddware (启用 react-router 时), hooks.onAction 传入的其它中间件,如 redux-logger

  • 其它增强,如 redux-devtools ,内置了 redux-devtools ,另需的话在 hooks.extraReducers 传入

const enhancers = [
    applyMiddleware(...middlewares),
    devtools(),
    ...extraEnhancers,
  ];
  const store = this._store = createStore(  // eslint-disable-line
    createReducer(),
    initialState,
    compose(...enhancers),
  );

设置redux的回调函数

通过配置 hooks.onStateChange 可以指定 reduxstate 改变后所触发的回调函数:

const listeners = plugin.get('onStateChange');
  for (const listener of listeners) {
    store.subscribe(() => {
      listener(store.getState());
    });
  }
}

新概念subscriptions

subscriptions 是一个新概念,会在 dom ready 之后执行,在这里面可以做一些基础数据的获取:

一般会将初始数据的获取放在 react 的生命周期中,比如 componentWillMount ,但是假设我们做了代码分割,实现了按需加载,那么我们开始获取数据的时间为:获取相应的 js +解析 js +执行 react 生命周期,但是 redux 的数据加载和 ui 组件没有太大关系,可以将数据获取的时间点提前, subscriptions 提供了解决方法,其意义为订阅,对于上面的场景,我们可以订阅路由,到了执行的路由执行相应的 dispatch() ,如:

setup({ dispatch, history }) {
  return history.listen(({ pathname, query }) => {
    if (pathname === '/users') {
      dispatch({ type: 'fetch', payload: query });
    }
  });
}

(PS: 对于这个新概念,我也不是很清楚,后面的文章会有专门的描述,大家先有一个概念就好)

挂载

上述过程均为初始化的过程,就是获取到需要的 reducerssagas 以及对于一些中间件和插件的配置,下面要进行的就是挂载了,也就熟悉的 render(<Provider>, container)

动态处理model

dva.modeldva.unmodel ,封装了在运行时的 store 进行一类增加和删除的操作,例如可以再切换到某一路由时动态的加入一个 model (个人猜测,热更新很有可能也利用了这个两个 apihooks.onHmr )。

未完结

关于 redux 还有一个利器,那就是高阶 reduce ,当然在 dva 中也有体现,这篇文章已经很长了,这些内容留在下一篇中介绍。以上是本人对于 dva 的粗略的理解,内容如有错误,还请大家指出。 dva 的确简化了开发的流程,而且在蚂蚁金服的很多业务线也有着应用,是一个很值得大家一试!


以上所述就是小编给大家介绍的《dva源码解析(一)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Seasoned Schemer

The Seasoned Schemer

Daniel P. Friedman、Matthias Felleisen / The MIT Press / 1995-12-21 / USD 38.00

drawings by Duane Bibbyforeword and afterword by Guy L. Steele Jr.The notion that "thinking about computing is one of the most exciting things the human mind can do" sets both The Little Schemer (form......一起来看看 《The Seasoned Schemer》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具