资讯专栏INFORMATION COLUMN

dva源码解析(一)

bladefury / 932人阅读

摘要:动态处理与,封装了在运行时的进行一类增加和删除的操作,例如可以再切换到某一路由时动态的加入一个个人猜测,热更新很有可能也利用了这个两个与。以上是本人对于的粗略的理解,内容如有错误,还请大家指出。

写在前面

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 }) => (...)

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分支变作了一个函数:

(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(, container)

动态处理model

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

未完结

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/83474.html

相关文章

  • dva系列源码解读

    摘要:介绍概述本次对源码的解读除了传统的从入手外还将引入带入问题读源码的理念,因为只有这样当读完源码之后才会有切身的收获。 介绍 概述 本次对 dva 源码的解读除了传统的从 api 入手外还将引入带入问题读源码的理念,因为只有这样当读完源码之后才会有切身的收获。另外除了 dva 的源码外还会解读一些常用的 dva 插件的源码。 电子书 https://dva-source-docs.net...

    focusj 评论0 收藏0
  • React生态,dva源码阅读

    摘要:下面会从浅到深,淡淡在阅读源码过程中自己的理解。分拆子页面后,每一个子页面对应一个文件。总结上面就是最早版本的源码,很简洁的使用了等其目的也很简单简化相关生态的繁琐逻辑参考源码地址   dva的思想还是很不错的,大大提升了开发效率,dva集成了Redux以及Redux的中间件Redux-saga,以及React-router等等。得益于Redux的状态管理,以及Redux-saga中...

    bergwhite 评论0 收藏0
  • React生态,dva源码阅读

    摘要:下面会从浅到深,淡淡在阅读源码过程中自己的理解。分拆子页面后,每一个子页面对应一个文件。总结上面就是最早版本的源码,很简洁的使用了等其目的也很简单简化相关生态的繁琐逻辑参考源码地址   dva的思想还是很不错的,大大提升了开发效率,dva集成了Redux以及Redux的中间件Redux-saga,以及React-router等等。得益于Redux的状态管理,以及Redux-saga中...

    txgcwm 评论0 收藏0
  • React生态,dva源码阅读

    摘要:下面会从浅到深,淡淡在阅读源码过程中自己的理解。分拆子页面后,每一个子页面对应一个文件。总结上面就是最早版本的源码,很简洁的使用了等其目的也很简单简化相关生态的繁琐逻辑参考源码地址   dva的思想还是很不错的,大大提升了开发效率,dva集成了Redux以及Redux的中间件Redux-saga,以及React-router等等。得益于Redux的状态管理,以及Redux-saga中...

    harryhappy 评论0 收藏0
  • Taro 优秀学习资源汇总

    摘要:多端统一开发框架优秀学习资源汇总官方资源项目仓库官方文档项目仓库官方文档微信小程序官方文档百度智能小程序官方文档支付宝小程序官方文档字节跳动小程序官方文档文章教程不敢阅读包源码带你揭秘背后的哲学从到构建适配不同端微信小程序等的应用小程序最 Awesome Taro 多端统一开发框架 Taro 优秀学习资源汇总 showImg(https://segmentfault.com/img/r...

    toddmark 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<