Dva 源码解析

作者:杨光

隐藏在 package.json 里的秘密

随便哪个 dva 的项目,只要敲入 npm start 就可以运行启动。之前敲了无数次我都没有在意,直到我准备研究源码的时候才意识到:在敲下这行命令的时候,到底发生了什么呢?

答案要去 package.json 里去寻找。

有位技术大牛曾经告诉过我:看源码之前,先去看 package.json 。看看项目的入口文件,翻翻它用了哪些依赖,对项目便有了大致的概念。

package.json 里是这么写的:

  1. "scripts": {
  2. "start": "roadhog server"
  3. },

翻翻依赖,"roadhog": "^0.5.2"

既然能在 devDependencies 找到,那么肯定也能在 npm 上找到。原来是个和 webpack 相似的库,而且作者看着有点眼熟…

如果说 dva 是亲女儿,那 roadhog 就是亲哥哥了,起的是 webpack 自动打包和热更替的作用。

在 roadhog 的默认配置里有这么一条信息:

  1. {
  2. "entry": "src/index.js",
  3. }

后转了一圈,启动的入口回到了 src/index.js

src/index.js

src/index.js 里,dva 一共做了这么几件事:

  • 从 'dva' 依赖中引入 dva :import dva from 'dva';

  • 通过函数生成一个 app 对象:const app = dva();

  • 加载插件:app.use({});

  • 注入 model:app.model(require('./models/example'));

  • 添加路由:app.router(require('./routes/indexAnother'));

  • 启动:app.start('#root');

在这 6 步当中,dva 完成了 使用 React 解决 view 层redux 管理 modelsaga 解决异步的主要功能。事实上在我查阅资料以及回忆用过的脚手架时,发现目前端框架之所以被称为“框架”也就是解决了这些事情。前端工程师至今所做的事情都是在 分离动态的 data 和静态的 view ,只不过侧重点和实现方式也不同。

至今为止出了这么多框架,但是前端 MVX 的思想一直都没有改变。

dva

寻找 “dva”

既然 dva 是来自于 dva,那么 dva 是什么这个问题自然要去 dva 的源码中寻找了。

剧透:dva 是个函数,返回一了个 app 的对象。

剧透2:目前 dva 的源码核心部分包含两部分,dvadva-core。前者用高阶组件 React-redux 实现了 view 层,后者是用 redux-saga 解决了 model 层。

老规矩,还是先翻 package.json 。

引用依赖很好的说明了 dva 的功能:统一 view 层。

  1. // dva 使用的依赖如下:
  2. "babel-runtime": "^6.26.0", // 一个编译后文件引用的公共库,可以有效减少编译后的文件体积
  3. "dva-core": "^1.1.0", // dva 另一个核心,用于处理数据层
  4. "global": "^4.3.2", // 用于提供全局函数的引用
  5. "history": "^4.6.3", // browserHistory 或者 hashHistory
  6. "invariant": "^2.2.2", // 一个有趣的断言库
  7. "isomorphic-fetch": "^2.2.1", // 方便请求异步的函数,dva 中的 fetch 来源
  8. "react-async-component": "^1.0.0-beta.3", // 组件懒加载
  9. "react-redux": "^5.0.5", // 提供了一个高阶组件,方便在各处调用 store
  10. "react-router-dom": "^4.1.2", // router4,终于可以像写组件一样写 router 了
  11. "react-router-redux": "5.0.0-alpha.6",// redux 的中间件,在 provider 里可以嵌套 router
  12. "redux": "^3.7.2" // 提供了 store、dispatch、reducer

不过 script 没有给太多有用的信息,因为 ruban build 中的 ruban 显然是个私人库(虽然在 tnpm 上可以查到但是也是私人库)。但根据惯例,应该是 dva 包下的 index.js 文件提供了对外调用:

  1. Object.defineProperty(exports, "__esModule", {
  2. value: true
  3. });
  4. exports.default = require('./lib');
  5. exports.connect = require('react-redux').connect;

显然这个 exports.default 就是我们要找的 dva,但是源码中没有 ./lib 文件夹。当然直接看也应该看不懂,因为一般都是使用 babel 的命令 babel src -d libs 进行编译后生成的,所以直接去看 src/index.js 文件。

src/index.js

src/index.js在此

在这里,dva 做了三件比较重要的事情:

  • 使用 call 给 dva-core 实例化的 app(这个时候还只有数据层) 的 start 方法增加了一些新功能(或者说,通过代理模式给 model 层增加了 view 层)。
  • 使用 react-redux 完成了 react 到 redux 的连接。
  • 添加了 redux 的中间件 react-redux-router,强化了 history 对象的功能。

使用 call 方法实现代理模式

dva 中实现代理模式的方式如下:

1. 新建 function ,函数内实例化一个 app 对象。**2. 新建变量指向该对象希望代理的方法, oldStart = app.start3. 新建同名方法 start,在其中使用 call,指定 oldStart 的调用者为 app。4. 令 app.start = start,完成对 app 对象的 start 方法的代理。**

上代码:

  1. export default function(opts = {}) {
  2. // ...初始化 route ,和添加 route 中间件的方法。
  3. /**
  4. * 1. 新建 function ,函数内实例化一个 app 对象。
  5. *
  6. */
  7. const app = core.create(opts, createOpts);
  8. /**
  9. * 2. 新建变量指向该对象希望代理的方法
  10. *
  11. */
  12. const oldAppStart = app.start;
  13. app.router = router;
  14. /**
  15. * 4. 令 app.start = start,完成对 app 对象的 start 方法的代理。
  16. * @type {[type]}
  17. */
  18. app.start = start;
  19. return app;
  20. // router 赋值
  21. /**
  22. * 3.1 新建同名方法 start,
  23. *
  24. */
  25. function start(container) {
  26. // 合法性检测代码
  27. /**
  28. * 3.2 在其中使用 call,指定 oldStart 的调用者为 app。
  29. */
  30. oldAppStart.call(app);
  31. // 因为有 3.2 的执行才有现在的 store
  32. const store = app._store;
  33. // 使用高阶组件创建视图
  34. }
  35. }

为什么不直接在 start 方式中 oldAppStart ?

  • 因为 dva-core 的 start 方法里有用到 this,不用 call 指定调用者为 app 的话,oldAppStart() 会找错对象。

实现代理模式一定要用到 call 吗?

  • 不一定,看有没有 使用 this 或者代理的函数是不是箭头函数。从另一个角度来说,如果使用了 function 关键字又在内部使用了 this,那么一定要用 call/apply/bind 指定 this。

前端还有那里会用到 call ?

  • 就实际开发来讲,因为已经使用了 es6 标准,基本和 this 没什么打交道的机会。使用 class 类型的组件中偶尔还会用到 this.xxx.bind(this),stateless 组件就洗洗睡吧(因为压根没有 this)。如果实现代理,可以使用继承/反向继承的方法 —— 比如高阶组件。

使用 react-redux 的高阶组件传递 store

经过 call 代理后的 start 方法的主要作用,便是使用 react-redux 的 provider 组件将数据与视图联系了起来,生成 React 元素呈现给使用者。

不多说,上代码。

  1. // 使用 querySelector 获得 dom
  2. if (isString(container)) {
  3. container = document.querySelector(container);
  4. invariant(
  5. container,
  6. `[app.start] container ${container} not found`,
  7. );
  8. }
  9. // 其他代码
  10. // 实例化 store
  11. oldAppStart.call(app);
  12. const store = app._store;
  13. // export _getProvider for HMR
  14. // ref: https://github.com/dvajs/dva/issues/469
  15. app._getProvider = getProvider.bind(null, store, app);
  16. // If has container, render; else, return react component
  17. // 如果有真实的 dom 对象就把 react 拍进去
  18. if (container) {
  19. render(container, store, app, app._router);
  20. // 热加载在这里
  21. app._plugin.apply('onHmr')(render.bind(null, container, store, app));
  22. } else {
  23. // 否则就生成一个 react ,供外界调用
  24. return getProvider(store, this, this._router);
  25. }
  26. // 使用高阶组件包裹组件
  27. function getProvider(store, app, router) {
  28. return extraProps => (
  29. <Provider store={store}>
  30. { router({ app, history: app._history, ...extraProps }) }
  31. </Provider>
  32. );
  33. }
  34. // 真正的 react 在这里
  35. function render(container, store, app, router) {
  36. const ReactDOM = require('react-dom'); // eslint-disable-line
  37. ReactDOM.render(React.createElement(getProvider(store, app, router)), container);
  38. }

React.createElement(getProvider(store, app, router)) 怎么理解?

  • getProvider 实际上返回的不单纯是函数,而是一个无状态的 React 组件。从这个角度理解的话,ReactElement.createElement(string/ReactClass type,[object props],[children …]) 是可以这么写的。

怎么理解 React 的 stateless 组件和 class 组件?

  • 你猜猜?
  1. JavaScript 并不存在 class 这个东西,即便是 es6 引入了以后经过 babel 编译也会转换成函数。因此直接使用无状态组件,省去了将 class 实例化再调用 render 函数的过程,有效的加快了渲染速度。
  2. 即便是 class 组件,React.createElement 最终调用的也是 render 函数。不过这个目前只是我的推论,没有代码证据的证明。

react-redux 与 provider

provider 是个什么东西?

本质上是个高阶组件,也是代理模式的一种实践方式。接收 redux 生成的 store 做参数后,通过上下文 context 将 store 传递进被代理组件。在保留原组件的功能不变的同时,增加了 store 的 dispatch 等方法。

connect 是个什么东西?

connect 也是一个代理模式实现的高阶组件,为被代理的组件实现了从 context 中获得 store 的方法。

connect()(MyComponent) 时发生了什么?

只放关键部分代码,因为我也只看懂了关键部分(捂脸跑):

  1. import connectAdvanced from '../components/connectAdvanced'
  2. export function createConnect({
  3. connectHOC = connectAdvanced,
  4. .... 其他初始值
  5. } = {}) {
  6. return function connect( { // 0 号 connnect
  7. mapStateToProps,
  8. mapDispatchToProps,
  9. ... 其他初始值
  10. } = {}
  11. ) {
  12. ....其他逻辑
  13. return connectHOC(selectorFactory, {// 1号 connect
  14. .... 默认参数
  15. selectorFactory 也是个默认参数
  16. })
  17. }
  18. }
  19. export default createConnect() // 这是 connect 的本体,导出时即生成 connect 0
  1. // hoist-non-react-statics,会自动把所有绑定在对象上的非React方法都绑定到新的对象上
  2. import hoistStatics from 'hoist-non-react-statics'
  3. // 1号 connect 的本体
  4. export default function connectAdvanced() {
  5. // 逻辑处理
  6. // 1 号 connect 调用时生成 2 号 connect
  7. return function wrapWithConnect(WrappedComponent) {
  8. // ... 逻辑处理
  9. // 在函数内定义了一个可以拿到上下文对象中 store 的组件
  10. class Connect extends Component {
  11. getChildContext() {
  12. // 上下文对象中获得 store
  13. const subscription = this.propsMode ? null : this.subscription
  14. return { [subscriptionKey]: subscription || this.context[subscriptionKey] }
  15. }
  16. // 逻辑处理
  17. render() {
  18. // 最终生成了新的 react 元素,并添加了新属性
  19. return createElement(WrappedComponent, this.addExtraProps(selector.props))
  20. }
  21. }
  22. // 逻辑处理
  23. // 最后用定义的 class 和 被代理的组件生成新的 react 组件
  24. return hoistStatics(Connect, WrappedComponent) // 2 号函数调用后生成的对象是组件
  25. }
  26. }

结论:对于 connect()(MyComponent)

  • connect 调用时生成 0 号 connect
  • connect() 0 号 connect 调用,返回 1 号 connect 的调用 connectHOC() ,生成 2 号 connect(也是个函数) 。
  • connect()(MyComponent) 等价于 connect2(MyComponent),返回值是一个新的组件

redux 与 router

redux 是状态管理的库,router 是(唯一)控制页面跳转的库。两者都很美好,但是不美好的是两者无法协同工作。换句话说,当路由变化以后,store 无法感知到。

于是便有了 react-router-redux

react-router-redux 是 redux 的一个中间件(中间件:JavaScript 代理模式的另一种实践 针对 dispatch 实现了方法的代理,在 dispatch action 的时候增加或者修改) ,主要作用是:

加强了React Router库中history这个实例,以允许将history中接受到的变化反应到state中去。

github 在此

从代码上讲,主要是监听了 history 的变化:

history.listen(location => analyticsService.track(location.pathname))

dva 在此基础上又进行了一层代理,把代理后的对象当作初始值传递给了 dva-core,方便其在 model 的subscriptions 中监听 router 变化。

看看 index.js 里 router 的实现:

1.在 createOpts 中初始化了添加 react-router-redux 中间件的方法和其 reducer ,方便 dva-core 在创建 store 的时候直接调用。

  • 使用 patchHistory 函数代理 history.linsten,增加了一个回调函数的做参数(也就是订阅)。

subscriptions 的东西可以放在 dva-core 里再说,

  1. import createHashHistory from 'history/createHashHistory';
  2. import {
  3. routerMiddleware,
  4. routerReducer as routing,
  5. } from 'react-router-redux';
  6. import * as core from 'dva-core';
  7. export default function (opts = {}) {
  8. const history = opts.history || createHashHistory();
  9. const createOpts = {
  10. // 初始化 react-router-redux 的 router
  11. initialReducer: {
  12. routing,
  13. },
  14. // 初始化 react-router-redux 添加中间件的方法,放在所有中间件最前面
  15. setupMiddlewares(middlewares) {
  16. return [
  17. routerMiddleware(history),
  18. ...middlewares,
  19. ];
  20. },
  21. // 使用代理模式为 history 对象增加新功能,并赋给 app
  22. setupApp(app) {
  23. app._history = patchHistory(history);
  24. },
  25. };
  26. const app = core.create(opts, createOpts);
  27. const oldAppStart = app.start;
  28. app.router = router;
  29. app.start = start;
  30. return app;
  31. function router(router) {
  32. invariant(
  33. isFunction(router),
  34. `[app.router] router should be function, but got ${typeof router}`,
  35. );
  36. app._router = router;
  37. }
  38. }
  39. // 使用代理模式扩展 history 对象的 listen 方法,添加了一个回调函数做参数并在路由变化是主动调用
  40. function patchHistory(history) {
  41. const oldListen = history.listen;
  42. history.listen = (callback) => {
  43. callback(history.location);
  44. return oldListen.call(history, callback);
  45. };
  46. return history;
  47. }

剧透:redux 中创建 store 的方法为:

  1. // combineReducers 接收的参数是对象
  2. // 所以 initialReducer 的类型是对象
  3. // 作用:将对象中所有的 reducer 组合成一个大的 reducer
  4. const reducers = {};
  5. // applyMiddleware 接收的参数是可变参数
  6. // 所以 middleware 是数组
  7. // 作用:将所有中间件组成一个数组,依次执行
  8. const middleware = [];
  9. const store = createStore(
  10. combineReducers(reducers),
  11. initial_state, // 设置 state 的初始值
  12. applyMiddleware(...middleware)
  13. );

视图与数据(上)

src/index.js 主要实现了 dva 的 view 层,同时传递了一些初始化数据到 dva-core 所实现的 model 层。当然,还提供了一些 dva 中常用的方法函数:

  • dynamic 动态加载(2.0 以后官方提供 1.x 自己手动实现吧)
  • fetch 请求方法(其实 dva 只是做了一把搬运工)
  • saga(数据层处理异步的方法)。这么看 dva 真的是很薄的一层封装。

而 dva-core 主要解决了 model 的问题,包括 state 管理、数据的异步加载、订阅-发布模式的实现,可以作为数据层在别处使用(看 2.0 更新也确实是作者的意图)。使用的状体啊管理库还是 redux,异步加载的解决方案是 saga。当然,一切也都写在 index.js 和 package.json 里。

视图与数据(下)

处理 React 的 model 层问题有很多种办法,比如状态管理就不一定要用 Redux,也可以使用 Mobx(写法会更有 MVX 框架的感觉);异步数据流也未必使用 redux-saga,redux-thunk 或者 redux-promise 的解决方式也可以(不过目前看来 saga 是相对更优雅的)。

放两篇个人感觉比较全面的技术文档:

package.json

dva-corepackage.json 中依赖包如下:

  1. "babel-runtime": "^6.26.0", // 一个编译后文件引用的公共库,可以有效减少编译后的文件体积
  2. "flatten": "^1.0.2", // 一个将多个数组值合并成一个数组的库
  3. "global": "^4.3.2",// 用于提供全局函数比如 document 的引用
  4. "invariant": "^2.2.1",// 一个有趣的断言库
  5. "is-plain-object": "^2.0.3", // 判断是否是一个对象
  6. "redux": "^3.7.1", // redux ,管理 react 状态的库
  7. "redux-saga": "^0.15.4", // 处理异步数据流
  8. "warning": "^3.0.0" // 同样是个断言库,不过输出的是警告

当然因为打包还是用的 ruban,script 里没有什么太多有用的东西。继续依循惯例,去翻 src/index.js

src/index.js

src/index 的源码在这里

dvasrc/index.js 里,通过传递 2 个变量 optscreateOpts 并调用 core.createdva 创建了一个 app 对象。其中 opts 是使用者添加的控制选项,createOpts 则是初始化了 reducer 与 redux 的中间件。

dva-coresrc/index.js 里便是这个 app 对象的具体创建过程以及包含的方法:

  1. export function create(hooksAndOpts = {}, createOpts = {}) {
  2. const {
  3. initialReducer,
  4. setupApp = noop,
  5. } = createOpts;
  6. const plugin = new Plugin();
  7. plugin.use(filterHooks(hooksAndOpts));
  8. const app = {
  9. _models: [
  10. prefixNamespace({ ...dvaModel }),
  11. ],
  12. _store: null,
  13. _plugin: plugin,
  14. use: plugin.use.bind(plugin),
  15. model,
  16. start,
  17. };
  18. return app;
  19. // .... 方法的实现
  20. function model(){
  21. // model 方法
  22. }
  23. functoin start(){
  24. // Start 方法
  25. }
  26. }

我最开始很不习惯 JavaScript 就是因为 JavaScript 还是一个函数向的编程语言,也就是函数里可以定义函数,返回值也可以是函数,class 最后也是被解释成函数。在 dva-core 里创建了 app 对象,但是把 model 和 start 的定义放在了后面。一开始对这种简写没看懂,后来熟悉了以后发现确实好理解。一眼就可以看到 app 所包含的方法,如果需要研究具体方法的话才需要向后看。

Plugin 是作者设置的一堆钩子性监听函数——即是在符合某些条件的情况下下(dva 作者)进行手动调用。这样使用者只要按照作者设定过的关键词传递回调函数,在这些条件下便会自动触发。

有趣的是,我最初理解钩子的概念是在 Angular 里。为了能像 React 一样优雅的控制组件的生命周期,Angular 设置了一堆接口(因为使用的是 ts,所以 Angular 里有类和接口的区分)。只要组件实现(implements)对应的接口————或者称生命周期钩子,在对应的条件下就会运行接口的方法。

Plugin 与 plugin.use

Plugin 与 plugin.use 都有使用数组的 reduce 方法的行为:

  1. const hooks = [
  2. 'onError',
  3. 'onStateChange',
  4. 'onAction',
  5. 'onHmr',
  6. 'onReducer',
  7. 'onEffect',
  8. 'extraReducers',
  9. 'extraEnhancers',
  10. ];
  11. export function filterHooks(obj) {
  12. return Object.keys(obj).reduce((memo, key) => {
  13. // 如果对象的 key 在 hooks 数组中
  14. // 为 memo 对象添加新的 key,值为 obj 对应 key 的值
  15. if (hooks.indexOf(key) > -1) {
  16. memo[key] = obj[key];
  17. }
  18. return memo;
  19. }, {});
  20. }
  21. export default class Plugin {
  22. constructor() {
  23. this.hooks = hooks.reduce((memo, key) => {
  24. memo[key] = [];
  25. return memo;
  26. }, {});
  27. /*
  28. 等同于
  29. this.hooks = {
  30. onError: [],
  31. onStateChange:[],
  32. ....
  33. extraEnhancers: []
  34. }
  35. */
  36. }
  37. use(plugin) {
  38. invariant(isPlainObject(plugin), 'plugin.use: plugin should be plain object');
  39. const hooks = this.hooks;
  40. for (const key in plugin) {
  41. if (Object.prototype.hasOwnProperty.call(plugin, key)) {
  42. invariant(hooks[key], `plugin.use: unknown plugin property: ${key}`);
  43. if (key === 'extraEnhancers') {
  44. hooks[key] = plugin[key];
  45. } else {
  46. hooks[key].push(plugin[key]);
  47. }
  48. }
  49. }
  50. }
  51. // 其他方法
  52. }
  • 构造器中的 reduce 初始化了一个以 hooks 数组所有元素为 key,值为空数组的对象,并赋给了 class 的私有变量 this.hooks

  • filterHooks 通过 reduce 过滤了 hooks 数组以外的钩子。

  • use 中使用 hasOwnProperty 判断 keyplugin 的自身属性还是继承属性,使用原型链调用而不是 plugin.hasOwnProperty() 是防止使用者故意捣乱在 plugin 自己写一个 hasOwnProperty = () => false // 这样无论如何调用 plugin.hasOwnProperty() 返回值都是 false

  • use 中使用 reducethis.hooks 添加了 plugin[key]

model 方法

model 是 app 添加 model 的方法,在 dva 项目 的 index.js 是这么用的。

app.model(require('./models/example'));

dva 中没对 model 做任何处理,所以 dva-core 中的 model 就是 dva 项目 里调用的 model。

  1. function model(m) {
  2. if (process.env.NODE_ENV !== 'production') {
  3. checkModel(m, app._models);
  4. }
  5. app._models.push(prefixNamespace(m));
  6. }
  • checkModel 主要是用 invariant 对传入的 model 进行了合法性检查。

  • prefixNamespace 又使用 reduce 对每一个 model 做处理,为 model 的 reducers 和 effects 中的方法添加了 ${namespace}/ 的前缀。

Ever wonder why we dispatch the action like this in dva ? dispatch({type: 'example/loadDashboard'

start 方法

start 方法是 dva-core 的核心,在 start 方法里,dva 完成了 store 初始化 以及 redux-saga 的调用。比起 dvastart,它引入了更多的调用方式。

一步一步分析:

onError

  1. const onError = (err) => {
  2. if (err) {
  3. if (typeof err === 'string') err = new Error(err);
  4. err.preventDefault = () => {
  5. err._dontReject = true;
  6. };
  7. plugin.apply('onError', (err) => {
  8. throw new Error(err.stack || err);
  9. })(err, app._store.dispatch);
  10. }
  11. };

这是一个全局错误处理,返回了一个接收错误并处理的函数,并以 errapp._store.dispatch 为参数执行调用。

看一下 plugin.apply 的实现:

  1. apply(key, defaultHandler) {
  2. const hooks = this.hooks;
  3. /* 通过 validApplyHooks 进行过滤, apply 方法只能应用在全局报错或者热更替上 */
  4. const validApplyHooks = ['onError', 'onHmr'];
  5. invariant(validApplyHooks.indexOf(key) > -1, `plugin.apply: hook ${key} cannot be applied`);
  6. /* 从钩子中拿出挂载的回调函数 ,挂载动作见 use 部分*/
  7. const fns = hooks[key];
  8. return (...args) => {
  9. // 如果有回调执行回调
  10. if (fns.length) {
  11. for (const fn of fns) {
  12. fn(...args);
  13. }
  14. // 没有回调直接抛出错误
  15. } else if (defaultHandler) {
  16. defaultHandler(...args);
  17. /*
  18. 这里 defaultHandler 为 (err) => {
  19. throw new Error(err.stack || err);
  20. }
  21. */
  22. }
  23. };
  24. }

sagaMiddleware

下一行代码是:

const sagaMiddleware = createSagaMiddleware();

redux-sagas 的入门教程有点差异,因为正统的教程上添加 sagas 中间件的方法是: createSagaMiddleware(…sagas)

sagas 为含有 saga 方法的 generator 函数数组。

但是 api 里确实还提到,还有一~招从天而降的掌法~种动态调用的方式:

const task = sagaMiddleware.run(dynamicSaga)

于是:

  1. const sagaMiddleware = createSagaMiddleware();
  2. // ...
  3. const sagas = [];
  4. const reducers = {...initialReducer
  5. };
  6. for (const m of app._models) {
  7. reducers[m.namespace] = getReducer(m.reducers, m.state);
  8. if (m.effects) sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));
  9. }
  10. // ....
  11. store.runSaga = sagaMiddleware.run;
  12. // Run sagas
  13. sagas.forEach(sagaMiddleware.run);

sagas

那么 sagas 是什么呢?

  1. const {
  2. middleware: promiseMiddleware,
  3. resolve,
  4. reject,
  5. } = createPromiseMiddleware(app);
  6. app._getSaga = getSaga.bind(null, resolve, reject);
  7. const sagas = [];
  8. for (const m of app._models) {
  9. if (m.effects) sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));
  10. }

显然,sagas 是一个数组,里面的元素是用 app._getSaga 处理后的返回结果,而 app._getSaga 又和上面 createPromiseMiddleware 代理 app 后返回的对象有很大关系。

createPromiseMiddleware

createPromiseMiddleware 的代码在此

如果看着觉得眼熟,那肯定不是因为看过 redux-promise 源码的缘故,:-p。

middleware

middleware 是一个 redux 的中间件,即在不影响 redux 本身功能的情况下为其添加了新特性的代码。redux 的中间件通过拦截 action 来实现其作用的。

  1. const middleware = () => next => (action) => {
  2. const { type } = action;
  3. if (isEffect(type)) {
  4. return new Promise((resolve, reject) => {
  5. // .... resolve ,reject
  6. });
  7. } else {
  8. return next(action);
  9. }
  10. };
  11. function isEffect(type) {
  12. // dva 里 action 的 type 有固定格式: model.namespace/model.effects
  13. // const [namespace] = type.split(NAMESPACE_SEP); 是 es6 解构的写法
  14. // 等同于 const namespace = type.split(NAMESPACE_SEP)[0];
  15. // NAMESPACE_SEP 的值是 `/`
  16. const [namespace] = type.split(NAMESPACE_SEP);
  17. // 根据 namespace 过滤出对应的 model
  18. const model = app._models.filter(m => m.namespace === namespace)[0];
  19. // 如果 model 存在并且 model.effects[type] 也存在,那必然是 effects
  20. if (model) {
  21. if (model.effects && model.effects[type]) {
  22. return true;
  23. }
  24. }
  25. return false;
  26. }

const middleware = ({dispatch}) => next => (action) => {… return next(action)} 基本上是一个标准的中间件写法。在 return next(action) 之前可以对 action 做各种各样的操作。因为此中间件没用到 dispatch 方法,所以省略了。

本段代码的意思是,如果 dispatch 的 action 指向的是 model 里的 effects,那么返回一个 Promise 对象。此 Promise 的对象的解决( resolve )或者驳回方法 ( reject ) 放在 map 对象中。如果是非 effects (那就是 action 了),放行。

换句话说,middleware 拦截了指向 effects 的 action。

神奇的 bind

bind 的作用是绑定新的对象,生成新函数是大家都知道概念。但是 bind 也可以提前设定好函数的某些参数生成新函数,等到最后一个参数确定时直接调用。

JavaScript 的参数是怎么被调用的?JavaScript 专题之函数柯里化。作者:冴羽。文章来源:掘金

这段代码恰好就是 bind 的一种实践方式。

  1. const map = {};
  2. const middleware = () => next => (action) => {
  3. const { type } = action;
  4. // ...
  5. return new Promise((resolve, reject) => {
  6. map[type] = {
  7. resolve: wrapped.bind(null, type, resolve),
  8. reject: wrapped.bind(null, type, reject),
  9. };
  10. });
  11. // ....
  12. };
  13. function wrapped(type, fn, args) {
  14. if (map[type]) delete map[type];
  15. fn(args);
  16. }
  17. function resolve(type, args) {
  18. if (map[type]) {
  19. map[type].resolve(args);
  20. }
  21. }
  22. function reject(type, args) {
  23. if (map[type]) {
  24. map[type].reject(args);
  25. }
  26. }
  27. return {
  28. middleware,
  29. resolve,
  30. reject,
  31. };

分析这段代码,dva 是这样做的:

  • 通过 wrapped.bind(null, type, resolve) 产生了一个新函数,并且赋值给匿名对象的 resolve 属性(reject 同理)。

1.1 wrap 接收三个参数,通过 bind 已经设定好了两个。wrapped.bind(null, type, resolve) 等同于 wrap(type, resolve, xxx)此处 resolve 是 Promise 对象中的)。

1.2 通过 bind 赋给匿名对象的 resolve 属性后,匿名对象.resolve(xxxx) 等同于 wrap(type, resolve, xxx),即 reslove(xxx)。

  • 使用 type 在 map 对象中保存此匿名对象,而 type 是 action 的 type,即 namespace/effects 的形式,方便之后进行调用。

  • return 出的 resolve 接收 type 和 args 两个参数。type 用来在 map 中寻找 1 里的匿名函数,args 用来像 1.2 里那样执行。

这样做的作用是:分离了 promise 与 promise 的执行。在函数的作用域外依然可以访问到函数的内部变量,换言之:闭包。

getSaga

导出的 resolvereject 方法,通过 bind 先设置进了 getSaga (同时也赋给了 app._getSaga),sagas 最终也将 getSaga 的返回值放入了数组。

getSaga 源码

  1. export default function getSaga(resolve, reject, effects, model, onError, onEffect) {
  2. return function *() {
  3. for (const key in effects) {
  4. if (Object.prototype.hasOwnProperty.call(effects, key)) {
  5. const watcher = getWatcher(resolve, reject, key, effects[key], model, onError, onEffect);
  6. // 将 watcher 分离到另一个线程去执行
  7. const task = yield sagaEffects.fork(watcher);
  8. // 同时 fork 了一个线程,用于在 model 卸载后取消正在进行中的 task
  9. // `${model.namespace}/@@CANCEL_EFFECTS` 的发出动作在 index.js 的 start 方法中,unmodel 方法里。
  10. yield sagaEffects.fork(function *() {
  11. yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
  12. yield sagaEffects.cancel(task);
  13. });
  14. }
  15. }
  16. };
  17. }

可以看到,getSaga 最终返回了一个 generator 函数

在该函数遍历了 model 中 effects 属性 的所有方法(注:同样是 generator 函数)。结合 index.js 里的 for (const m of app._models),该遍历针对所有的 model。

对于每一个 effect,getSaga 生成了一个 watcher ,并使用 saga 函数的 fork 将该函数切分到另一个单独的线程中去(生成了一个 task 对象)。同时为了方便对该线程进行控制,在此 fork 了一个 generator 函数。在该函数中拦截了取消 effect 的 action(事实上,应该是卸载effect 所在 model 的 action),一旦监听到则立刻取消分出去的 task 线程。

getWatcher
  1. function getWatcher(resolve, reject, key, _effect, model, onError, onEffect) {
  2. let effect = _effect;
  3. let type = 'takeEvery';
  4. let ms;
  5. if (Array.isArray(_effect)) {
  6. // effect 是数组而不是函数的情况下暂不考虑
  7. }
  8. function *sagaWithCatch(...args) {
  9. // .... sagaWithCatch 的逻辑
  10. }
  11. const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);
  12. switch (type) {
  13. case 'watcher':
  14. return sagaWithCatch;
  15. case 'takeLatest':
  16. return function*() {
  17. yield takeLatest(key, sagaWithOnEffect);
  18. };
  19. case 'throttle':
  20. return function*() {
  21. yield throttle(ms, key, sagaWithOnEffect);
  22. };
  23. default:
  24. return function*() {
  25. yield takeEvery(key, sagaWithOnEffect);
  26. };
  27. }
  28. }
  29. function createEffects(model) {
  30. // createEffects(model) 的逻辑
  31. }
  32. function applyOnEffect(fns, effect, model, key) {
  33. for (const fn of fns) {
  34. effect = fn(effect, sagaEffects, model, key);
  35. }
  36. return effect;
  37. }

先不考虑 effect 的属性是数组而不是方法的情况。

getWatcher 接收六个参数:

  • resolve/reject: 中间件 middleware 的 res 和 rej 方法。
  • key:经过 prefixNamespace 转义后的 effect 方法名,namespace/effect(也是调用 action 时的 type)。-_effect:effects 中 key 属性所指向的 generator 函数。
  • model: model
  • onError: 之前定义过的捕获全局错误的方法
  • onEffect:plugin.use 中传入的在触发 effect 时执行的回调函数(钩子函数)applyOnEffect 对 effect 进行了动态代理,在保证 effect (即 _effect)正常调用的情况下,为期添加了 fns 的回调函数数组(即 onEffect)。使得在 effect 执行时, onEffect 内的每一个回调函数都可以被触发。

因为没有经过 effects 的属性是数组的情况,所以 type 的值是 takeEvery,也就是监听每一个发出的 action ,即 getWatcher 的返回值最终走的是 switch 的 default 选项:

  1. function*() {
  2. yield takeEvery(key, sagaWithOnEffect);
  3. };

换句话说,每次发出指向 effects 的函数都会调用 sagaWithOnEffect

根据 const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key); 的执行情况,如果 onEffect 的插件为空的情况下,sagaWithOnEffect 的值为 sagaWithCatch

  1. function *sagaWithCatch(...args) {
  2. try {
  3. yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` });
  4. const ret = yield effect(...args.concat(createEffects(model)));
  5. yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@end` });
  6. resolve(key, ret);
  7. } catch (e) {
  8. onError(e);
  9. if (!e._dontReject) {
  10. reject(key, e);
  11. }
  12. }
  13. }

sagaWithOnEffect 函数中,sagas 使用传入的参数(也就是 action)执行了对应的 model 中 对应的 effect 方法,同时将返回值使用之前保存在 map 里的 resolve 返回了其返回值。同时在执行 effect 方法的时候,将 saga 本身的所有方法(put、call、fork 等等)作为第二个参数,使用 concat 拼接在 action 的后面。在执行 effect 方法前,又发出了 start 和 end 两个 action,方便 onEffect 的插件进行拦截和调用。

因此,对于 if (m.effects) sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));

  • dva 通过 app._getSaga(m.effects, m, onError, plugin.get('onEffect')) 返回了一个 genenrator 函数。
  • 在 genenrator 函数中手动 fork 出一个 watcher 函数的监听线程(当然也 fork 了取消线程的功能)。
  • 该函数(在普通状态下)是一个 takeEvery 的阻塞是线程,接收 2 个参数。第一个参数为监听的 action,第二个参数为监听到 action 后的回调函数。
  • (普通状态下)的回调函数,就是手动调用了 model 里 effects 中对应属性的函数。在此之前之后发出了 startend 的 action,同时用之前 promise 中间件保存在 map 中的 resolve 方法返回了值。
  • 最后使用 sagas.forEach(sagaMiddleware.run) 启动了 watcher 的监听。

store

现在已经有了针对异步数据流的解决办法,那么该创建 store 了。

正常情况的 redux 的 createStore 接收三个参数 reducer, initState,applyMiddleware(middlewares)。

不过 dva 提供了自己的 createStore 方法,用来组织一系列自己创建的参数。

  1. // Create store
  2. const store = app._store = createStore({ // eslint-disable-line
  3. reducers: createReducer(),
  4. initialState: hooksAndOpts.initialState || {},
  5. plugin,
  6. createOpts,
  7. sagaMiddleware,
  8. promiseMiddleware,
  9. });

createReducer

  1. function createReducer() {
  2. return reducerEnhancer(combineReducers({
  3. ...reducers,
  4. ...extraReducers,
  5. ...(app._store ? app._store.asyncReducers : {}),
  6. }));
  7. }

createReducer 实际上是用 plugin 里的 onReducer (如果有)扩展了 reducer 功能,对于 const reducerEnhancer = plugin.get('onReducer');,plugin 里的相关代码为:

  1. function getOnReducer(hook) {
  2. return function (reducer) {
  3. for (const reducerEnhancer of hook) {
  4. reducer = reducerEnhancer(reducer);
  5. }
  6. return reducer;
  7. };
  8. }

如果有 onReducer 的插件,那么用 reducer 的插件扩展 reducer;否则直接返回 reducer。

combineReducers 中:

  • 第一个 …reducers 是从 dva 里传入的 historyReducer,以及通过 reducers[m.namespace] = getReducer(m.reducers, m.state); 剥离出的 model 中的 reducer
  • 第二个参数为手动在 plugin 里添加的 extraReducers;
  • 第三个参数为异步 reducer,主要是用于在 dva 运行以后动态加载 model 里的 reducer。

createStore

现在我们有了一个 combine 过的 reducer,有了 core 中创建的 sagaMiddleware 和 promiseMiddleware,还有了从 dva 中传入的 createOpts,现在可以正式创建 store 了。

从 dva 中传入的 createOpts 为

  1. setupMiddlewares(middlewares) {
  2. return [
  3. routerMiddleware(history),
  4. ...middlewares,
  5. ];
  6. },

用与把 redux-router 的中间件排在中间件的第一个。

虽然看起来很长,但是对于大多数普通用户来说,在未开启 redux 的调试插件,未传入额外的 onAction 以及 extraEnhancers 的情况下,上面的代码等价于:

  1. import { createStore, applyMiddleware, compose } from 'redux';
  2. import flatten from 'flatten';
  3. import invariant from 'invariant';
  4. import window from 'global/window';
  5. import { returnSelf, isArray } from './utils';
  6. export default function ({
  7. reducers,
  8. initialState,
  9. plugin,
  10. sagaMiddleware,
  11. promiseMiddleware,
  12. createOpts: {
  13. setupMiddlewares = returnSelf,
  14. },
  15. }) {
  16. const middlewares = setupMiddlewares([
  17. sagaMiddleware,
  18. promiseMiddleware
  19. ]);
  20. const enhancers = [
  21. applyMiddleware(...middlewares)
  22. ];
  23. return createStore(reducers, initialState, compose(...enhancers));
  24. // 对于 redux 中 的 compose 函数,在数组长度为 1 的情况下返回第一个元素。
  25. // compose(...enhancers) 等同于 applyMiddleware(...middlewares)
  26. }

订阅

现在 dva 已经创建了 store,有了异步数据流加载方案,并且又做了一些其他的事情:

  1. // Extend store
  2. store.runSaga = sagaMiddleware.run;
  3. store.asyncReducers = {};
  4. // Execute listeners when state is changed
  5. const listeners = plugin.get('onStateChange');
  6. for (const listener of listeners) {
  7. store.subscribe(() => {
  8. listener(store.getState());
  9. });
  10. }
  11. // Run sagas
  12. sagas.forEach(sagaMiddleware.run);
  • 手动运行 getSaga 里返回的 watcer 函数。
  • 判断如果有 onStateChange 的 plugin 也手动运行一下。model 里的 state、effect、reducer 已经实现了,就缺最后的订阅 subscription 部分。
  1. // Setup app
  2. setupApp(app);
  3. // Run subscriptions
  4. const unlisteners = {};
  5. for (const model of this._models) {
  6. if (model.subscriptions) {
  7. unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError);
  8. }
  9. }

setupApp(app) 是从 dva 里传过来的,主要是使用 patchHistory 函数代理 history.linsten,即强化了 redux 和 router 的联系,是的路径变化可以引起 state 的变化,进而听过监听 state 的变化来触发回调。

这也是 core 中唯一使用 this 的地方,逼得 dva 中必须使用 oldStart.call(app) 来进行调用。

runSubscription

这是 runSubscription 的代码

  1. export function run(subs, model, app, onError) {
  2. const funcs = [];
  3. const nonFuncs = [];
  4. for (const key in subs) {
  5. if (Object.prototype.hasOwnProperty.call(subs, key)) {
  6. const sub = subs[key];
  7. const unlistener = sub({
  8. dispatch: prefixedDispatch(app._store.dispatch, model),
  9. history: app._history,
  10. }, onError);
  11. if (isFunction(unlistener)) {
  12. funcs.push(unlistener);
  13. } else {
  14. nonFuncs.push(key);
  15. }
  16. }
  17. }
  18. return { funcs, nonFuncs };
  19. }
  • 第一个参数为 model 中的 subscription 对象。
  • 第二个参数为对应的 model
  • 第三个参数为 core 里创建的 app
  • 第四个参数为全局异常捕获的 onError
  • Object.prototype.hasOwnProperty.call(subs, key)还是使用原型方法判断 key 是不是 subs 的自有属性。

  • 如果是自由属性,那么拿到属性对应的值(是一个 function)

  • 调用该 function,传入 dispatch 和 history 属性。history 就是经过 redux-router 强化过的 history,而 dispatch,也就是 prefixedDispatch(app._store.dispatch, model)

  1. export default function prefixedDispatch(dispatch, model) {
  2. return (action) => {
  3. // 断言检测
  4. return dispatch({ ...action, type: prefixType(type, model) });
  5. };
  6. }

实际上是用将 action 里的 type 添加了 ${model.namespance}/ 的前缀。

自此,model 中的四大组件全部完毕,完成了 dva 的数据层处理。