【源码共读】| dva源码解析

dva 是一个基于 reduxredux-saga 的数据流方案,在umi 中也能看到它的身影。
dvajs.com/guide/

简单使用

  • 定义路由
javascript 复制代码
// router.js
function RouterConfig({ history }) {
  return (
    <Router history={history}>
      <Switch>
        <Route path="/" exact component={IndexPage} />
        <Route path="/user" exact component={UserPage} />
      </Switch>
    </Router>
  );
}
  • 定义Model层
javascript 复制代码
// example
import request from "../utils/request";

export function queryUser() {
  // return request('/api/users');
  return request("https://randomuser.me/api");
}
////////////////////////////////////////////////////

import { queryUser } from "../services/example";

export default {
  namespace: "user",

  state: { name: { first: "匿名", age: "10" } },

  subscriptions: {
    setup({ dispatch, history }) {
      // eslint-disable-line
    },
  },

  effects: {
    *fetchUser({ payload }, { call, put }) {
      const res = yield call(queryUser);
      console.log(res);
      const user = res.data.results[0];
      // eslint-disable-line
      yield put({ type: "save", payload: user });
    },
  },

  reducers: {
    save(state, action) {
      return { ...state, ...action.payload };
    },
  },
};
  • view 层使用
javascript 复制代码
import { connect } from "dva";
import React from "react";

const UserPage = ({ user, fetchUser }) => {
  const refreshUser = () => {
    fetchUser();
  };

  return (
    <div>
      <h3>用户信息</h3>
      <div>姓名:{user.name.first}</div>
      <button onClick={refreshUser}>刷新</button>
    </div>
  );
};

export default connect(
  (state) => {
    return { user: state.user };
  },
  {
    fetchUser: () => ({ type: "user/fetchUser" }),
  }
)(UserPage);

至此,这就是dva的使用了,非常简洁。

源码解析

那么,这个流程是怎么实现的呢,dva是怎么将react,redux-saga串起来的呢,让我们来看下源码的实现
github.com/dvajs/dva

首先,我们先来看下入口文件

这个是生成npm包的工具,找到对应的.fatherrc看下

dva()函数的作用是

  • 返回一个app 对象
  • 将路由挂载到对象上
  • 功能拆分为
    • create
    • router
    • start
javascript 复制代码
// packages\dva\src\index.js
export default function(opts = {}) {
  // 默认路由为HashRouter
  const history = opts.history || createHashHistory();
  // 初始化配置
  const createOpts = {
    initialReducer: {
      router: connectRouter(history),
    },
    setupMiddlewares(middlewares) {
      return [routerMiddleware(history), ...middlewares];
    },
    setupApp(app) {
      app._history = patchHistory(history);
    },
  };
  // 返回一个app对象
  const app = create(opts, createOpts);
  const oldAppStart = app.start;
  app.router = router;
  app.start = start;
  return app;
  // 省略...
}

我们先来看create函数

这个函数的做了几件事

  • 获取初始化的参数
  • 创建了app对象
  • start函数中使用了saga来处理异步逻辑
  • 为app对象添加model、unmodel、replaceModel方法

github.com/dvajs/dva/b...

javascript 复制代码
export function create(hooksAndOpts = {}, createOpts = {}) {
  // 获取参数
  const { initialReducer, setupApp = noop } = createOpts

  // 加载插件
  const plugin = new Plugin()
  plugin.use(filterHooks(hooksAndOpts))

  const app = {
    // 解构 reducers 和 effects
    // 注入 model 层
    _models: [prefixNamespace({ ...dvaModel })],
    _store: null,
    _plugin: plugin,
    use: plugin.use.bind(plugin),
    model,
    start
  }
  return app

  /**
   * 应用启动前,注册 model
   */
  function model(m) {
    // 给 model 增加 namespace
    // 省略代码
    return prefixedModel
  }

  // 处理异步加载的文件注入,即在app 启动后懒加载
  // 如果模型中定义了effects,那么运行这些effects
  // 如果模型中定义了subscriptions,那么运行这些subscriptions,并将返回的取消订阅函数存储到unlisteners对象中
  function injectModel(createReducer, onError, unlisteners, m) {
    // 省略代码
  }

  /**
   * 注销 model
   * 移除 model 中的 reducers
   * 取消订阅
   * 删除 app._models 中的 model
   *
   */
  function unmodel(createReducer, reducers, unlisteners, namespace) {
    // 省略代码
  }

  /**
   * 替换 model 或者添加 model
   */
  function replaceModel(createReducer, reducers, unlisteners, onError, m) {
    // 省略代码
  }

  /**
   * 启动应用
   */
  function start() {
    // Global error handler
    // 处理全局错误
    const onError = (err, extension) => {
      // 省略代码
    }
    // 用于处理异步操作和处理Promise。然后,将sagaMiddleware的run方法绑定到应用程序的store上,以便运行saga
    const sagaMiddleware = createSagaMiddleware()
    const promiseMiddleware = createPromiseMiddleware(app)
    app._getSaga = getSaga.bind(null)

    // 遍历Model,为每个模型创建对应的reducer,并将其添加到reducers对象中
    // 如果模型有effects,则将其对应的saga添加到sagas数组中
    const sagas = []
    const reducers = { ...initialReducer }
    for (const m of app._models) {
      // 省略代码
    }
    const reducerEnhancer = plugin.get('onReducer')
    const extraReducers = plugin.get('extraReducers')
    // 检测 extraReducers 是否和 dva 内部的 reducers 冲突
    invariant(
      Object.keys(extraReducers).every((key) => !(key in reducers)),
      `[app.start] extraReducers is conflict with other reducers, reducers list: ${Object.keys(
        reducers
      ).join(', ')}`
    )

    // Create store
    app._store = createStore({
      reducers: createReducer(),
      initialState: hooksAndOpts.initialState || {},
      plugin,
      createOpts,
      sagaMiddleware,
      promiseMiddleware
    })

    const store = app._store

    // 将sagaMiddleware的run方法赋值给store的runSaga属性,将空对象赋值给store的asyncReducers属性。
    // Extend store
    store.runSaga = sagaMiddleware.run
    store.asyncReducers = {}

    // Execute listeners when state is changed
    const listeners = plugin.get('onStateChange')
    for (const listener of listeners) {
      store.subscribe(() => {
        listener(store.getState())
      })
    }

    // Run sagas
    sagas.forEach(sagaMiddleware.run)

    // Setup app
    setupApp(app)

    // Run subscriptions
    // 运行订阅,遍历应用程序的模型,如果模型有subscriptions,则运行相应的订阅函数。
    const unlisteners = {}
    for (const model of this._models) {
      // 省略代码
    }

    // Setup app.model and app.unmodel
    // 为app对象添加model、unmodel、replaceModel方法
    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的作用是将sage(Model层)注入到app中,并挂载到store中。

那么,这个store是如何与React结合并进行注入的呢?

我们回到dva函数
github.com/dvajs/dva/b...

  • 使用connectRouter包裹 root reducer 并且提供我们创建的history对象,获得新的 root reducer
  • 在 provider 里可以嵌套 router


可以看到,在start函数中,在最后一步加载元素时,做了数据拦截,将store和router传进去

这样可以确保所有的子组件都可以通过 context 访问到 Redux 的 store。

总结

经过对核心功能源码的深入分析,我们了解到dva的主要工作是将Model层抽离出来进行数据管理。其内部实现主要通过redux-saga来处理数据和异步action,最后利用connectRouter进行封装,从而实现在全局范围内都能通过context访问数据,大大简化了数据管理流程。

参考:

  1. dvajs.com/guide/sourc...
相关推荐
一生为追梦1 小时前
Linux 内存管理机制概述
前端·chrome
喝旺仔la1 小时前
使用vue创建项目
前端·javascript·vue.js
心.c1 小时前
植物大战僵尸【源代码分享+核心思路讲解】
前端·javascript·css·数据结构·游戏·html
喝旺仔la1 小时前
Element Plus中button按钮相关大全
前端·javascript·vue.js
柒@宝儿姐2 小时前
Git的下载与安装
前端·javascript·vue.js·git·elementui·visual studio
Hiweir ·2 小时前
机器翻译之数据处理
前端·人工智能·python·rnn·自然语言处理·nlp·机器翻译
曈欣3 小时前
vue 中属性值上变量和字符串怎么拼接
前端·javascript·vue.js
QGC二次开发3 小时前
Vue3:v-model实现组件通信
前端·javascript·vue.js·前端框架·vue·html
努力的小雨4 小时前
从设计到代码:探索高效的前端开发工具与实践
前端
小鼠米奇5 小时前
详解Ajax与axios的区别
前端·javascript·ajax