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

dva 是一个基于 redux 和 redux-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...
相关推荐
lpfasd1237 小时前
node-sass 停止维护!老项目迁移 Dart Sass 完整教程
前端·rust·sass
Data analyse45610 小时前
rnweb 回放数据量太大怎么办?分片存储与断点续传的实现思路
前端·网络·算法·数据分析
xiaogai_gai10 小时前
金蝶收料通知单查询策略实战教程:从源系统拉取到中间层落地的完整路径
java·前端·数据库
环境栈笔记11 小时前
指纹浏览器 WebRTC 怎么设置?从 ICE 候选、代理出口到功能验证
前端·人工智能·后端·自动化
赵大仁11 小时前
官网性能优化实战:LCP 3.2s → 2.1s、CLS 0.107 → 0,以及被我推翻的 6 个判断
前端·性能优化·cls·lcp·core web vitals
xiancai_xianyu11 小时前
Text2SQL落地生产库前,SQL要过哪三道闸
前端·数据库·sql·sql注入·text2sql·ai agent开发·企业级ai框架
小羊没烦恼!11 小时前
T-SQL查询进阶--理解SQL SERVER中的分区表
服务器·开发语言·前端·javascript·html
史瓦辛格11 小时前
给 DeepSeek Harness 写费用插件:把「这次会话花了多少钱」算准
前端·deepseek
星光不问赶路人11 小时前
Vue 3 + TypeScript 中 enum 的使用实践
前端·vue.js
鸥梨菌Honevid12 小时前
UVM 中 REQ 和 RSP 类型详解:从通信模型到 item_done 与 put_response
java·前端·servlet