【源码共读】| 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...
相关推荐
m0_471199636 分钟前
【小程序】订单数据缓存 以及针对海量库存数据的 懒加载+数据分片 的具体实现方式
前端·vue.js·小程序
编程大师哥7 分钟前
Java web
java·开发语言·前端
A小码哥8 分钟前
Vibe Coding 提示词优化的四个实战策略
前端
Murrays8 分钟前
【React】01 初识 React
前端·javascript·react.js
大喜xi12 分钟前
ReactNative 使用百分比宽度时,aspectRatio 在某些情况下无法正确推断出高度,导致图片高度为 0,从而无法显示
前端
helloCat12 分钟前
你的前端代码应该怎么写
前端·javascript·架构
电商API_1800790524713 分钟前
大麦网API实战指南:关键字搜索与详情数据获取全解析
java·大数据·前端·人工智能·spring·网络爬虫
康一夏14 分钟前
CSS盒模型(Box Model) 原理
前端·css
web前端12314 分钟前
React Hooks 介绍与实践要点
前端·react.js
我是小疯子6615 分钟前
JavaScriptWebAPI核心操作全解析
前端