【源码共读】| 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...
相关推荐
Warren983 小时前
Lua 脚本在 Redis 中的应用
java·前端·网络·vue.js·redis·junit·lua
mCell4 小时前
JavaScript 运行机制详解:再谈 Event Loop
前端·javascript·浏览器
帧栈8 小时前
开发避坑指南(27):Vue3中高效安全修改列表元素属性的方法
前端·vue.js
max5006008 小时前
基于桥梁三维模型的无人机检测路径规划系统设计与实现
前端·javascript·python·算法·无人机·easyui
excel8 小时前
使用函数式封装绘制科赫雪花(Koch Snowflake)
前端
萌萌哒草头将军9 小时前
Node.js v24.6.0 新功能速览 🚀🚀🚀
前端·javascript·node.js
持久的棒棒君10 小时前
启动electron桌面项目控制台输出中文时乱码解决
前端·javascript·electron
小离a_a11 小时前
使用原生css实现word目录样式,标题后面的...动态长度并始终在标题后方(生成点线)
前端·css
郭优秀的笔记11 小时前
抽奖程序web程序
前端·css·css3
布兰妮甜12 小时前
CSS Houdini 与 React 19 调度器:打造极致流畅的网页体验
前端·css·react.js·houdini