Dva源码解读系列之dva-core

1,039 阅读10分钟

image.png

(图片来自Dva官网)

dva 的源码核心部分包含 dva 和 dva-core 两部分,在 Dva源码解读系列之app对象 一文中我们对 dva 源码的 dva 部分作了解析,dva/src/index.js 负责处理对外的逻辑,使用 react-redux 的Provider实现了View层。它创建了一个挂载了model、router、start等dva所有属性和方法的 app 对象,在最后通过 start 方法启动了一个React应用。

在 dva/src/index.js 中,调用了 dva-core 的 create 方法 创建了一个 app 对象:

// 调用 dva-core 的 create 方法创建一个 app 对象
const app = create(opts, createOpts);

接下来,我们来看看 dva-core 中的 create 方法都做了什么。

create 方法

create 方法是被默认导出的。它接受两个参数,第一个参数 hooksAndOpts 是使用者添加的控制选项,第二个参数createOpts是初始化了 reducer 和 redux 的中间件。方法内部创建了一个app对象并挂载了_models、_store、_plugin、use、model、start 等属性。

其中 _models 包含dvaModel和所有的用户model,_store默认为null,plugin属性挂载各种插件,这些插件是基于dva生命周期函数的,plugin.use方法被plugin对象代理,以免找错this,model方法用于注册model,start方法用于启动程序,最终create方法返回了这个app对象。

export function create(hooksAndOpts = {}, createOpts = {}) {
  const { initialReducer, setupApp = noop } = createOpts;

  const plugin = new Plugin();
  plugin.use(filterHooks(hooksAndOpts));

  const app = {
    _models: [prefixNamespace({ ...dvaModel })],
    _store: null,
    _plugin: plugin,
    use: plugin.use.bind(plugin),
    model,
    start,
  };
  return app;
  
  // ...
  
}

plugin 对象

创建 plugin 对象

  const plugin = new Plugin();
  plugin.use(filterHooks(hooksAndOpts));

在create方法中,首先创建了一个 plugin 对象,并用filterHooks方法过滤掉不合法的插件,然后对过滤后的插件进行了注册。plugin对象负责管理和使用中间件。

export default class Plugin {
  constructor() {
    // 将 this.hooks 初始化为一个对象
  }

  // 注册中间件
  use(plugin) {
    // ...
  }

  // 全局错误处理函数
  apply(key, defaultHandler) {
    // ...
  }

  // 获取生命周期处理函数
  get(key) {
    // ...
  }
}

// 将某一个 key 对应的生命周期函数的回调函数数组组合起来,生成一个对象
function getExtraReducers(hook) {
    // ...
}

// 将用户在项目中定义的 reducer 经过每一个reducer 中间件处理
function getOnReducer(hook) {
  // ...
}

在 plugin 对象中,主要做了以下几件事:

定义生命周期函数

const hooks = [
  'onError',
  'onStateChange',
  'onAction',
  'onHmr',
  'onReducer',
  'onEffect',
  'extraReducers',
  'extraEnhancers',
  '_handleActions',
];

在 Plugin.js 中,首先定义一个hooks生命周期钩子函数数组,并按照这个数组,在 plugin 对象中初始化一个hooks对象,每一个属性是一个生命周期函数,被初始化为一个数组,挂载用户注册的插件。实际上dva的中间件或者说插件就是在这里被使用的。

创建 hooks 对象

constructor() {
    this._handleActions = null;
    // 将this.hooks初始化为一个对象,
    // 它包括上面hooks数组的所有元素作为属性名,每个值都是一个空数组
    // 这个空数组用来存放用户注册的中间件
    this.hooks = hooks.reduce((memo, key) => {
      memo[key] = [];
      return memo;
    }, {});
  }

在 Plugin 的构造函数中,使用 reduce 方法将创建一个 hooks 对象,它包括上面 hooks 生命周期钩子函数数组的所有元素作为属性名,每个值都是一个空数组,用户注册的中间件就放在这个数组里。

注册中间件

use(plugin) {
  // 数据格式校验,要求 plugin 必须是一个 纯对象
  invariant(isPlainObject(plugin), 'plugin.use: plugin should be plain object');
  const { hooks } = this;
  for (const key in plugin) {
    // 使用 Object.prototype.hasOwnProperty 是为了防止用户在 plugin 对象上自定义 hasOwnProperty 覆盖掉原函数,进行一些不恰当的操作
    if (Object.prototype.hasOwnProperty.call(plugin, key)) {
      // 生命周期钩子函数名校验,要求传入的 key,即生命周期函数名是存在的
      invariant(hooks[key], `plugin.use: unknown plugin property: ${key}`);
      // 对生命周期钩子 _handleActions 和 extraEnhancers 作特殊处理
      if (key === '_handleActions') {
        this._handleActions = plugin[key];
      } else if (key === 'extraEnhancers') {
        hooks[key] = plugin[key];
      } else {
        // 给生命周期钩子添加中间件,也包括 onReducers 
        hooks[key].push(plugin[key]);
      }
    }
  }
}

plugin.use() 方法用来注册中间件,将用户定义的函数 push 进对应生命周期钩子的数组中,为了防止用户在 plugin 对象上自定义 hasOwnProperty 覆盖掉原函数,进行一些不恰当的操作,源码中使用了 Object.prototype.hasOwnProperty.call() 来检测用户是否在 plugin 对象上自定义了 hasOwnProperty 。

全局错误处理函数

apply(key, defaultHandler) {
  const { hooks } = this;
  // 通过 validApplyHooks 进行过滤,apply 方法只能应用在全局报错或者热更替上
  const validApplyHooks = ['onError', 'onHmr'];
  invariant(validApplyHooks.indexOf(key) > -1, `plugin.apply: hook ${key} cannot be applied`);
  // 从 plugin 实例的 hooks 中取出挂载的 onError、onHmr 回调函数
  const fns = hooks[key];

  return (...args) => {
    // hooks 中挂载了onError、onHmr 的回调函数,则以依次执行所有的回调函数
    if (fns.length) {
      for (const fn of fns) {
        fn(...args);
      }
    } else if (defaultHandler) {
      // hooks 中没有挂载的 onError、onHmr 的回调函数,则直接执行传递进来的处理函数,抛出错误
      defaultHandler(...args);
    }
  };
}

在 plugin.apply() 方法中,对 hooks 生命周期钩子进行了过滤,使 apply 方法只能应用在全局报错(onError) 或 热更替(onHmr) 上。

获取生命周期处理函数

get(key) {
  const { hooks } = this;
  // 对 生命周期钩子进行校验,检测当前的生命周期钩子是否是在 hooks 中定义的
  invariant(key in hooks, `plugin.get: hook ${key} cannot be got`);
  // 当前 key  为在 hooks 中定义的 extraReducers 钩子,将 hooks 上挂载的所有函数组成一个对象然后返回
  if (key === 'extraReducers') {
    return getExtraReducers(hooks[key]);
  } 
  // 当前 key 是 hooks 中定义的 onReducer 钩子,将用户定义的 reducer 用对应钩子的所有中间件进行处理
  else if (key === 'onReducer') {
    return getOnReducer(hooks[key]);
  } else {
    // 直接返回对应钩子的处理函数
    return hooks[key];
  }
}

get() 方法用户获取对应生命周期钩子的中间件。在 get 方法中对钩子的类型作了判断,如果当前 key 为 hooks 中定义的 extraReducers 钩子,则调用 getExtraReducers 方法,将 hooks 上挂载所有函数组成一个对象然后返回,如果当前 key 为 hooks 中定义的 onReducer 钩子,则调用 getOnReducer 方法,将用户项目中定义的 reducer 用对应钩子的所有中间件处理一遍然后返回。如果是其它的钩子,则直接返回对应钩子的中间件。

下面,我们分别来看看 getExtraReducers 方法和 getOnReducer 方法。

getExtraReducers

// 将某一个 key 对应的生命周期函数的回调函数数组组合起来,生成一个对象
function getExtraReducers(hook) {
  let ret = {};
  for (const reducerObj of hook) {
    ret = { ...ret, ...reducerObj };
  }
  return ret;
}

getOnReducer

// 将用户在项目中定义的 reducer 经过每一个reducer 中间件处理
// 最终返回一个经过所有中间件处理的 recuer 函数
function getOnReducer(hook) {
  return function(reducer) {
    for (const reducerEnhancer of hook) {
      reducer = reducerEnhancer(reducer);
    }
    return reducer;
  };
}

过滤不合法的 hooks

export function filterHooks(obj) {
  // 数组的 reduce 方法遍历 obj,将符合要求的的 hooks 钩子函数放进一个新对象中,最后将obj过滤为只包括hooks的对象
  return Object.keys(obj).reduce((memo, key) => {
    if (hooks.indexOf(key) > -1) {
      memo[key] = obj[key];
    }
    return memo;
  }, {});
}

在 Plugin.js 中还导出了一个 filterHooks 方法,该方法用于过滤不合法的 hooks,最终过滤出只包括在 hooks 中定义的生命周期钩子的对象。

对 plugin 对象的解读就到此结束了,下面介绍在创建 app 对象时给model添加前缀的 prefixNamespace 方法。

prefixNamespace 方法

app对象的 _model 属性,它包括了 dvaModel 和所有用户在dva项目中定义的model,在创建 app对象的时候,使用 prefixNamespace 对所有 model 的 reducers 和 effects 都加上了 namespace 的前缀。

import warning from 'warning';
import { isArray } from './utils';
import { NAMESPACE_SEP } from './constants';

function prefix(obj, namespace, type) {
  return Object.keys(obj).reduce((memo, key) => {
    warning(
      key.indexOf(`${namespace}${NAMESPACE_SEP}`) !== 0,
      `[prefixNamespace]: ${type} ${key} should not be prefixed with namespace ${namespace}`,
    );
    const newKey = `${namespace}${NAMESPACE_SEP}${key}`; // 
    memo[newKey] = obj[key];
    return memo;
  }, {});
}

export default function prefixNamespace(model) {
  const { namespace, reducers, effects } = model;

  // 给每个 reducer 添加前缀  //  ${namespace}/${reducer}
  if (reducers) {
    if (isArray(reducers)) {
      // 需要复制一份,不能直接修改 model.reducers[0], 会导致微前端场景下,重复添加前缀
      const [reducer, ...rest] = reducers;
      model.reducers = [prefix(reducer, namespace, 'reducer'), ...rest];
    } else {
      model.reducers = prefix(reducers, namespace, 'reducer');
    }
  }
  // 给每个 effect 添加前缀  ${namespace}/${effect}
  if (effects) {
    model.effects = prefix(effects, namespace, 'effect');
  }
  return model;
}

接下来介绍app 对象的 model 属性。

model

注册 model

function model(m) {
  if (process.env.NODE_ENV !== 'production') {
    // 使用 invariant 对 传入的model 进行合法性检查
    checkModel(m, app._models);
  }
  // 给传入的 model 添加前缀
  const prefixedModel = prefixNamespace({ ...m });
  // 将添加了前缀的 model push 进 _models 中
  app._models.push(prefixedModel);
  return prefixedModel;
}

app 对象中的 model,是通过 model 方法注册的。在 model 方法中,首先用 checkModel 方法对传入的 model 进行合法性检查,然后使用 prefixNamespace 方法给所有的 model 添加前缀,最后返回加了前缀的 model 对象。在 model 方法中,仅仅只是对传入的model添加了前缀,并未做其它处理,因此这个model 就是 dva 项目里用户定义的 model。

检查model

export default function checkModel(model, existModels) {
  const { namespace, reducers, effects, subscriptions } = model;

  // namespace 必须被定义
  invariant(namespace, `[app.model] namespace should be defined`);
  // 并且是字符串
  invariant(
    typeof namespace === 'string',
    `[app.model] namespace should be string, but got ${typeof namespace}`,
  );
  // 并且唯一
  invariant(
    !existModels.some(model => model.namespace === namespace),
    `[app.model] namespace should be unique`,
  );

  // state 可以为任意值

  // reducers 可以为空,PlainObject 或者数组
  if (reducers) {
    invariant(
      isPlainObject(reducers) || isArray(reducers),
      `[app.model] reducers should be plain object or array, but got ${typeof reducers}`,
    );
    // 数组的 reducers 必须是 [Object, Function] 的格式
    invariant(
      !isArray(reducers) || (isPlainObject(reducers[0]) && isFunction(reducers[1])),
      `[app.model] reducers with array should be [Object, Function]`,
    );
  }

  // effects 可以为空,PlainObject
  if (effects) {
    invariant(
      isPlainObject(effects),
      `[app.model] effects should be plain object, but got ${typeof effects}`,
    );
  }

  if (subscriptions) {
    // subscriptions 可以为空,PlainObject
    invariant(
      isPlainObject(subscriptions),
      `[app.model] subscriptions should be plain object, but got ${typeof subscriptions}`,
    );

    // subscription 必须为函数
    invariant(isAllFunction(subscriptions), `[app.model] subscription should be function`);
  }
}

checkModel 方法检查 model 的合法性。一个合法的model 应该满足以下几个要求:

  1. namespace必须被定义为字符串,不可为空,并且是唯一的。

  2. reducers是一个数组或一个纯对象,如果reducers是一个数组,那么第一个元素必须是一个对象,第二个要求是一个函数,这在后面添加前缀会用上。

  3. effects必须是一个纯对象。

  4. subscriptions必须是一个纯对象,每一个属性必须是一个函数。

添加前缀

使用 checkModel 检查完 model 的合法性后,使用了 prefixNamespace 方法给 model 添加前缀。prefixNamespace 方法在上文已有介绍,在此不再赘述。

至此,model已经合法了,中间件也已经注册了,reducer和effects也已经有了调用的方式和依据,接下来,dva 要做的就是启动应用程序了。

app 对象的最后一个属性,start 方法,就是用于启动应用程序的。

start

start 方法是 dva-core 的核心,它用于启动应用程序,被 dva/index 中的app对象的 start 代理。在 start 中做了以下几件事:

  1. 定义全局错误处理
  2. 将 sagas 连接到 Redux Store
  3. 拦截指向 effects 的action
  4. 初始化化 _getSaga 处理异步数据
  5. 挂载 reducers 和 effects
  6. 创建 store
  7. 订阅 history 改变
  8. 挂载 model

接下来,我们一一解读 start 方法所做的事情。

定义全局错误处理

const onError = (err, extension) => {
  if (err) {
    if (typeof err === 'string') err = new Error(err);
    err.preventDefault = () => {
      err._dontReject = true;
    };
    // 调用 plugin 的 apply 方法对错误进行处理,apply方法只能在应用在全局报错或者热更替上
    plugin.apply('onError', err => {
      // 全局报错
      throw new Error(err.stack || err);
    })(err, app._store.dispatch, extension);
  }
};

在 start 方法中,首先定义了一个 onError 函数,用于全局错误处理。以 err、app._store.dispatch 和 extension 为参数执行 plugin 的 apply 方法,指定 onError 只能用在全局报错上。plugin.apply 方法请参考前文 plugin 对象的全局错误处理函数

将 sagas 连接到 Redux Store

start 方法做的第二件事,是调用 redux-saga 的 createSagaMiddleware 方法创建一个 Redux middleware,将 Sagas 连接到了 Redux Store。

const sagaMiddleware = createSagaMiddleware();

拦截指向 effects 的action

const promiseMiddleware = createPromiseMiddleware(app);

start 方法接下来做的事情,是调用 createPromiseMiddleware 方法来拦截指向 effects 的 action,并检查 action 的 type 指向的方法是否属于 effects,如果是,就返回一个挂载了 Promise 状态方法 resolve 和 reject 的 promise 中间件,否则就将 action 对象返回,不作处理。

import { NAMESPACE_SEP } from './constants';

export default function createPromiseMiddleware(app) {
  // const middleware = ({dispatch}) => next => (action) => {... return next(action)} 是一个中间件的标准写法,在 return next(action) 之前可以对 action 做各种各样的操作
  // 中间件拦截了指向 effects 的 action
  return () => next => action => {
    const { type } = action;
    // isEffect 检查 action 的type,是否指向 effect,也就是在 model 的 effects 中定义的异步方法
    // 如果 type 指向 effect,则返回一个 Promise 对象,并在 action 对象上挂载两个Promise的状态方法 resolve 和 reject
    if (isEffect(type)) {
      return new Promise((resolve, reject) => {
        next({
          __dva_resolve: resolve,
          __dva_reject: reject,
          ...action,
        });
      });
    } else {
      // type 指向的不是 effect,则直接将 action 对象返回,不做处理
      return next(action);
    }
  };

  // isEffect 检查 type 是否指向 model 的 effects 里的方法
  function isEffect(type) {
    if (!type || typeof type !== 'string') return false;
    // dva 里 action 的 type 有固定格式: model.namespace/model.effects
    const [namespace] = type.split(NAMESPACE_SEP);
    // 根据 namespace 过滤出对应的 model
    const model = app._models.filter(m => m.namespace === namespace)[0];
    // 如果 model 存在并且 model.effects[type] 也存在,那必定是 effects
    if (model) {
      // type 指向 model 的 effects 中定义的异步方法
      if (model.effects && model.effects[type]) {
        return true;
      }
    }

    return false;
  }
}

初始化 _getSaga 处理异步数据

接下来,start 方法调用 getSaga 方法来初始化 app 对象的 _getSaga 方法,这个函数借助了 redux-saga 的能力来处理数据。

app._getSaga = getSaga.bind(null);

getSaga 调用 bind 来初始化 _getSaga,使 _getSaga 拥有 getSaga 预设预设的初始参数,这些参数(如果有的话)会作为 bind() 的第二个参数跟在 this 后面,之后它们会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们的后面。

getSaga

getSaga 方法接受5个参数,第一个参数 effects 是 model 中定义的 effects,第二个参数 model 就是在项目中定义的 model 对象,第三个参数 onError 是在 dva-core/index 中定义的全局错误处理函数 onError,第四个参数 onEffect 则是在 Plugin.js 中定义的生命周期钩子,而最后有一个函数 opts,是在调用 create 方法时传递进来的 用户添加的控制选项。getSaga 方法最终返回了一个 generator 函数。

// getSaga 方法借助 redux-saga 的能力来处理异步数据
export default function getSaga(effects, model, onError, onEffect, opts = {}) {
  // 返回一个 generator 函数
  return function*() {
    for (const key in effects) {
      if (Object.prototype.hasOwnProperty.call(effects, key)) {
        //  getWatcher 返回一个 generator 函数,调用 redux-saga 提供 的 takeEvery 来实时监听 action 和 effect
        const watcher = getWatcher(key, effects[key], model, onError, onEffect, opts);
        // 使用 redux-saga 提供的 fock 方法将 watcher 分离到一个单独的线程中去执行(即生成了一个新的 task 对象)
        const task = yield sagaEffects.fork(watcher);
        // 为了方便对 task 的控制,开辟一个新的进程,执行一个 generator 函数,在卸载 effect 所在 model 的action 时取消正在执行的 task 线程
        yield sagaEffects.fork(function*() {
          yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
          yield sagaEffects.cancel(task);
        });
      }
    }
  };
}

在 getSaga 返回的 generator 函数中,遍历 model 的 effects 属性,给每一个 effect (注:同样是 generator 函数) 添加一个 watcher,然后使用 redux-saga 的 fork 函数单独开辟一个线程监听 action,并执行 effect(生成了一个 task 对象)。同时为了方便控制该线程,又另外开辟了一个新的线程执行一个 generator 函数,拦截取消 effect 的action,一旦监听到则立刻取消已经分出去的 task 线程。

getWatcher

getWatcher 方法返回一个 generator 函数,给传递进去的_effect (model 中的effect) 添加一个 watcher,用来监听匹配的 action。getWatcher 接收 6 个参数:

  • key 经过 prefixNamespace 转义后的 effect 方法名,(在 model 中定义的 generator 函数名称)
  • _effect 在 model 的 effects 中定义的异步方法
  • model model 对象本身
  • onError 在dva-core/index里面定义的全局错误处理函数onError
  • onEffect 在 plugin 定义的生命周期函数
  • opts 用户添加的控制选项
function getWatcher(key, _effect, model, onError, onEffect, opts) {
  let effect = _effect;
  // 定义 type 的默认值
  let type = 'takeEvery';
  let ms;
  let delayMs;

  // _effect 是数组则执行相关的操作
  if (Array.isArray(_effect)) {
    [effect] = _effect;
    const opts = _effect[1];
    if (opts && opts.type) {
      ({ type } = opts);
      if (type === 'throttle') {
        invariant(opts.ms, 'app.start: opts.ms should be defined if type is throttle');
        ({ ms } = opts);
      }
      if (type === 'poll') {
        invariant(opts.delay, 'app.start: opts.delay should be defined if type is poll');
        ({ delay: delayMs } = opts);
      }
    }
    invariant(
      ['watcher', 'takeEvery', 'takeLatest', 'throttle', 'poll'].indexOf(type) > -1,
      'app.start: effect type should be takeEvery, takeLatest, throttle, poll or watcher',
    );
  }

  function noop() {}

  // sagaWithCatch 实际上执行用户定义的effect方法,执行前后分别通知redux-saga
  function* sagaWithCatch(...args) {
    // 在 createPromiseMiddleware 中添加了两个 Promise 状态方法
    const { __dva_resolve: resolve = noop, __dva_reject: reject = noop } =
      args.length > 0 ? args[0] : {};
    try {
      yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` });
      // 执行用户在model的effects 中定义的 effect 方法
      const ret = yield effect(...args.concat(createEffects(model, opts)));
      yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@end` });
      resolve(ret);
    } catch (e) {
      onError(e, {
        key,
        effectArgs: args,
      });
      if (!e._dontReject) {
        reject(e);
      }
    }
  }

  // 使用 plugin 中定义的 hooks 中的回调函数依次处理 effect
  const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);

  switch (type) {
    case 'watcher':
      return sagaWithCatch;
    case 'takeLatest':
      return function*() {
        yield sagaEffects.takeLatest(key, sagaWithOnEffect);
      };
    case 'throttle':
      return function*() {
        yield sagaEffects.throttle(ms, key, sagaWithOnEffect);
      };
    case 'poll':
      return function*() {
        function delay(timeout) {
          return new Promise(resolve => setTimeout(resolve, timeout));
        }
        function* pollSagaWorker(sagaEffects, action) {
          const { call } = sagaEffects;
          while (true) {
            yield call(sagaWithOnEffect, action);
            yield call(delay, delayMs);
          }
        }
        const { call, take, race } = sagaEffects;
        while (true) {
          const action = yield take(`${key}-start`);
          yield race([call(pollSagaWorker, sagaEffects, action), take(`${key}-stop`)]);
        }
      };

    // 在effect不是数组的情况下,
    // 监听每一个发出的action,getWatcher的返回值走的都是default
    // 即,每次发出指向effect的action时都会调用sagaWithOnEffect
    //
    // takeEvery()方法接受两个参数,
    // 要匹配的action和一个saga(一个saga就是一个generator函数)
    // takeEvery监听action,在每次这个action被发起时,创建一个新的saga任务
    // 因此,dva项目中,所有的指向effect的action的派发,都会在这里创建一个实时任务
    default:
      return function*() {
        yield sagaEffects.takeEvery(key, sagaWithOnEffect);
      };
  }
}

在 getWatcher 内部,主要做了以下几件事(不考虑数组的情况):

  1. 定义 sagaWithCatch 方法在内部执行用户定义的 effect 方法,并在执行前后通知 redux-saga。由于在 createPromiseMiddleware 中已经给 action 添加了 Promise 状态方法 resolve 和 reject,因此在 effect 执行结束后调用 Promise 相应的状态方法返回对应的执行结果。

  2. 在执行用户定义的 effect 时,调用 createEffects 方法对数据进行校验。该方法重新封装了 redux-saga 的 put 和 take 方法,对 action 的 type 添加了数据类型及格式的校验。

  3. 执行 applyOnEffect 方法,这个方法将用户提供的 onEffect 生命周期处理函数注册到 effect 上,在不影响 effect 运行的同时,依次执行用户添加的中间件。

  4. 最后,用 switch 判断用户定义的代理类型,默认情况下,返回一个generator 函,用 redux-saga 的 takeEvery 方法,监听对应的 action,并在匹配到的时候自动创建一个异步任务,执行用户定义的 effect 函数。

createEffects

createEffects 方法重新封装了 redux-saga 的 put、take 方法,并对 action 的 type 添加了数据类型及格式的校验。

// 封装redux-saga提供的put、take方法,添加数据校验,主要是type的检查
function createEffects(model, opts) {

  // 给数据添加校验
  function assertAction(type, name) {
    invariant(type, 'dispatch: action should be a plain Object with type');

    const { namespacePrefixWarning = true } = opts;

    if (namespacePrefixWarning) {
      warning(
        type.indexOf(`${model.namespace}${NAMESPACE_SEP}`) !== 0,
        `[${name}] ${type} should not be prefixed with namespace ${model.namespace}`,
      );
    }
  }

  // 重新封装 redux-saga 的 put 方法
  function put(action) {
    const { type } = action;
    assertAction(type, 'sagaEffects.put');
    return sagaEffects.put({ ...action, type: prefixType(type, model) });
  }

  // The operator `put` doesn't block waiting the returned promise to resolve.
  // Using `put.resolve` will wait until the promsie resolve/reject before resuming.
  // It will be helpful to organize multi-effects in order,
  // and increase the reusability by seperate the effect in stand-alone pieces.
  // https://github.com/redux-saga/redux-saga/issues/336
  function putResolve(action) {
    const { type } = action;
    assertAction(type, 'sagaEffects.put.resolve');
    return sagaEffects.put.resolve({
      ...action,
      type: prefixType(type, model),
    });
  }
  put.resolve = putResolve;

  // 重新封装 redux-saga 的 take 方法
  function take(type) {
    if (typeof type === 'string') {
      assertAction(type, 'sagaEffects.take');
      return sagaEffects.take(prefixType(type, model));
    } else if (Array.isArray(type)) {
      return sagaEffects.take(
        type.map(t => {
          if (typeof t === 'string') {
            assertAction(t, 'sagaEffects.take');
            return prefixType(t, model);
          }
          return t;
        }),
      );
    } else {
      return sagaEffects.take(type);
    }
  }
  return { ...sagaEffects, put, take };
}

applyOnEffect

applyOnEffect 方法,将用户提供的 onEffect 生命周期处理函数注册到 effect 上,在不影响 effect 运行的同时,依次执行用户添加的中间件。

/**
 * 函数的作用,是将所有运行的 effect 按照 plugin 中定义的hooks的回调函数的顺序,并依次执行回调函数处理 effect
 * 最终得到一个被所有回调函数处理(或称为代理)后的 effect,并返回
 * @param {*} fns 在 plugin 定义的生命周期函数
 * @param {*} effect 用户在 model 中定义的 effect
 * @param {*} model model 对象本身
 * @param {*} key 经过 prefixNamespace 转义后的 effect 方法名,(在 model 中定义的 generator 函数名称)
 */
function applyOnEffect(fns, effect, model, key) {
  for (const fn of fns) {
    effect = fn(effect, sagaEffects, model, key);
  }
  return effect;
}

组合 reducers 和 effects

reducer、effect 的规则都已经确定了,但还是存储在各自的model上,接下来 start 要做的事情,就是把各个model 的 reducer、effect 组合在一起。在这里使用一个 for 循环,将各个 model 的 reducer 和 effects 统一组合到一个总的 reducers 和 sagas 上。

getReducer 方法返回对应类型的 reducer (区别是 reducer 有可能是数组或对象)。

在上面初始化 _getSaga 时,getSaga 调用 bind 方法提前设定好了 _getSaga 的预设参数,在这里组合 effects 时,便可以传入完整的参数,执行 _getSaga 方法,将effect 放进 sagas 数组里。

// 将各个 model 的 reducer 和 effects 统一挂载到一个总的 reducers 和 sagas 上
    const sagas = [];
    const reducers = { ...initialReducer };
    for (const m of app._models) {
      // getReducer 返回对应类型的 reducer(区别是 reducer有可能是数组或对象)
      reducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
      if (m.effects) {
        // 传入完整的参数,执行 _getSaga 方法,执行后的结果放进 sagas 数组里
        sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));
      }
    }
    // onReducer、extraReducers 在 plugin 中有定义,从 plugin 拿到 reducer 插件代理过的 reducer
    const reducerEnhancer = plugin.get('onReducer');
    const extraReducers = plugin.get('extraReducers');
    invariant(
      Object.keys(extraReducers).every(key => !(key in reducers)),
      `[app.start] extraReducers is conflict with other reducers, reducers list: ${Object.keys(
        reducers,
      ).join(', ')}`,
    );

创建 store

同步数据流和异步数据流的解决方案都已经有了,那么接下来该创建 store 了。

createStore(reducer, [preloadedState], enhancer)

正常情况下 redux 的 createStore 接收三个参数,即 reducer、initState、applyMiddleware(middlewares)(注:对createStore 的三个参数进行了转义处理)。

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

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

const store = app._store;

// Extend store
store.runSaga = sagaMiddleware.run;
store.asyncReducers = {};

store 有 reducers、state、dva 中间件 plugin,create 方法调用时传递进来的初始化配置,saga 中间件,promise 中间件等属性。其中 reducers 是由 dva 管理的 routerReducer (connected-react-router 将 react-router 绑定到了 redux),用户提供的 reducer,extractReducer插件,异步 reducer 组成的。

createStore

dva 提供的 crateStore 方法,扩展了 redux 的 createStore 方法,以便于组织一系列自己创建的参数

import { createStore, applyMiddleware, compose } from 'redux';
import flatten from 'flatten';
import invariant from 'invariant';
import win from 'global/window';
import { returnSelf, isArray } from './utils';

export default function({
  reducers,
  initialState,
  plugin,
  sagaMiddleware,
  promiseMiddleware,
  createOpts: { setupMiddlewares = returnSelf },
}) {
  // extra enhancers
  const extraEnhancers = plugin.get('extraEnhancers');
  invariant(
    isArray(extraEnhancers),
    `[app.start] extraEnhancers should be array, but got ${typeof extraEnhancers}`,
  );

  const extraMiddlewares = plugin.get('onAction');
  const middlewares = setupMiddlewares([
    promiseMiddleware,
    sagaMiddleware,
    ...flatten(extraMiddlewares),
  ]);

  const composeEnhancers =
    process.env.NODE_ENV !== 'production' && win.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
      ? win.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true, maxAge: 30 })
      : compose;

  const enhancers = [applyMiddleware(...middlewares), ...extraEnhancers];

  return createStore(reducers, initialState, composeEnhancers(...enhancers));
}

createReducer

function createReducer() {
  // 使用 plugin 里的 onReducer 扩展 reducer 的功能,在上面引入了 onReducer:  const reducerEnhancer = plugin.get('onReducer');
  return reducerEnhancer(
    // 使用 redux 提供 combineReducers 接口组合 reducer
    combineReducers({
      ...reducers, // 从 dva 里传入的 historyReducer,以及通过 reducers[m.namespace] = getReducer(m.reducers, m.state); 剥离出的 model 中的 reducer (用户提供的 reducer)
      ...extraReducers, // 手动在 plugin 里添加的 extraReducers
      ...(app._store ? app._store.asyncReducers : {}), // 异步 reducer,主要是用于在 dva 运行以后动态加载 model 里的 reducer
    }),
  );
}

createReducer 实际上是用 plugin 里的 onReducer 扩展了 reducer 的功能:

const reducerEnhancer = plugin.get('onReducer');

在 reducerEnhancer 里,借助了 redux 的 combineReducers 来组合 historyReducer、extraReducers 和 异步 reducer。

而在 combineReducers 中:

  • 第一个 ...reducers 是从 dva 里传入的 historyReducer,以及通过 reducers[m.namespace] = getReducer(m.reducers, m.state); 剥离出的 model 中的 reducer

  • 第二个extraReducers 为手动在 plugin 里添加的 extraReducers

  • 第三个参数为异步 reducer,主要是用于在 dva 运行以后动态加载 model 里的 reducer

下面我们来看看 Plugin 里的 onReducer:

onReducer

// 将用户在项目中定义的 reducer 经过每一个reducer 中间件处理
// 最终返回一个经过所有中间件处理的 redcuer 函数
function getOnReducer(hook) {
  return function(reducer) {
    // 如果有 onReducer 的插件,则用 reducer 的插件扩展 reducer
    for (const reducerEnhancer of hook) {
      reducer = reducerEnhancer(reducer);
    }
    return reducer;
  };
}

onReducer 将用户在项目中定义的 reducer 使用 reducer 的中间件处理,最终返回一个经过所有中间处理的 reducer 函数。

订阅 history 改变

监听 onStateChange

// 当 状态发生变化时,执行挂载在 onStateChange 上的监听器,并在 store 上添加一个监听
const listeners = plugin.get('onStateChange');
for (const listener of listeners) {
  store.subscribe(() => {
    listener(store.getState());
  });
}

从 plugin 对象中获取挂载到 onStateChange 上的监听器,当状态发生了改变时,执行监听器,并在 store 上添加一个监听。

运行所有sagas

// Run sagas
// 动态加载saga,也就是运行sagaMiddleware.run()返回的watcher函数
sagas.forEach(sagaMiddleware.run);

动态加载 saga,运行 sagaMiddleware.run 返回的 watcher 函数。

监听 history 变化

// Setup app
// setupApp 是从 dva/index 中传进来的,内部调用了 patchHistory 方法监听 history变化,触发回调,并将监听的 history 添加到 app 对象上
setupApp(app);

setupApp 是从 dva/index 中传进来的,在 dav/index 中将 history 代理到了app._history 上,并且调用了patchHistory 方法监听 history,因此每次 history 发生变化,都会通知到 redux,触发 state 的更新。

运行 subscriptions

// Run subscriptions
const unlisteners = {};
for (const model of this._models) {
  if (model.subscriptions) {
    // subscription中的函数接受app对象,可以订阅到history的变化。
    unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError);
  }
}

在运行 model 的 subscriptions 时,调用了 runSubscription 方法:

/**
 * 首先判断 key 是否是 subs 的自有属性,如果是,就运行这个函数(在 model 的 subscription 中定义的 function),将 dispatch 和 history 传入这个 function 中,最后返回订阅对象
 * @param {*} subs model 中的 subscription 对象
 * @param {*} model 对应的 model 对象
 * @param {*} app 在 dva-core 中创建的 app 对象
 * @param {*} onError 全局异常捕获额 onError
 */
export function run(subs, model, app, onError) {
  const funcs = [];
  const nonFuncs = [];
  for (const key in subs) {
    // 使用 原型方法判断 key 是否是 subs (model 中的 subscription) 的自有属性
    if (Object.prototype.hasOwnProperty.call(subs, key)) {
      // 拿到属性对应的值,即在 model 的 subscription 中定义的 function
      const sub = subs[key];
      // 执行该 function,分别传入 dispatch 和 history 属性
      const unlistener = sub(
        {
          // prefixedDispatch 给 action 里的 type 添加 ${model.namespance}/ 的前缀
          dispatch: prefixedDispatch(app._store.dispatch, model),
          // 在 dav/index 中使用 patchHistory 扩展过的 history
          history: app._history,
        },
        onError,
      );
      if (isFunction(unlistener)) {
        funcs.push(unlistener);
      } else {
        nonFuncs.push(key);
      }
    }
  }
  // 返回订阅对象
  return { funcs, nonFuncs };
}

run 方法接受四个参数:

  • 第一个参数是 model 中的 subscription 对象。

  • 第二个参数是对应的 model

  • 第三个参数是 dva-core 中创建的 app

  • 第四个参数是全局异常捕获的 onError

在 run 方法中,首先判断 key 是否是 subs (model 中的 subscription 对象) 的自有属性,如果是,就运行这个函数(在 model 的 subscription 中定义的 function),将 dispatch 和 history 传入这个 function 中,最后返回订阅对象。

挂载 model

start 方法做的最后一件事,就是将 model 到 app 对象上。

// Setup app.model and app.unmodel
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);

injectModel

在注册 model 时,首先给 model 添加了前缀,然后在 store 上挂载当前 model 的 reducers、effects 以及 subscriptions。

/**
 * Inject model after app is started.
 *
 * @param createReducer
 * @param onError
 * @param unlisteners
 * @param m
 */
function injectModel(createReducer, onError, unlisteners, m) {
  // 注册 model ,实际上是给传入的model添加前缀
  m = model(m);

  const store = app._store;
  // 挂载 reducers、effects(异步操作)、subscriptions(订阅)
  store.asyncReducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
  store.replaceReducer(createReducer());
  if (m.effects) {
    store.runSaga(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));
  }
  if (m.subscriptions) {
    unlisteners[m.namespace] = runSubscription(m.subscriptions, m, app, onError);
  }
}

unModel

unModel 卸载 app 对象上的 model,并同时清除 app 对象的 store 上的 reducers、effects 以及订阅(subscrioptions)。

/**
 * Unregister model.  卸载 model,将存储在 app 对象 _store 上的 reducers删除,并取消相关的 effects 及订阅(subscrioptions)
 *
 * @param createReducer
 * @param reducers
 * @param unlisteners
 * @param namespace
 *
 * Unexpected key warn problem:
 * https://github.com/reactjs/redux/issues/1636
 */
function unmodel(createReducer, reducers, unlisteners, namespace) {
  const store = app._store;

  // 删除存储在 app 对象 _store  上的 reducers
  // Delete reducers
  delete store.asyncReducers[namespace];
  delete reducers[namespace];
  // 将 app 对象 _sotre 上的 reducer 替换为初始化时创建的全局 reducer
  store.replaceReducer(createReducer());
  store.dispatch({ type: '@@dva/UPDATE' });

  // 取消 异步操作
  // Cancel effects
  store.dispatch({ type: `${namespace}/@@CANCEL_EFFECTS` });

  // 取消相关事件的监听
  // Unlisten subscrioptions
  unlistenSubscription(unlisteners, namespace);

  // Delete model from app._models
  app._models = app._models.filter(model => model.namespace !== namespace);
}

replaceModel

在热更新的时候,检查 app 对象上是否已经有 model,如果已经存在,先将旧的 model 清除,并同时清除该 model 上的 reducer、effects 及 subscrioptions,然后再给 app 对象添加一个新的 model。如果 app 对象上没有 model,则直接给 app 对象添加 model。

// 如果 model 已经存在,先将 model 清除,然后再给 app 对象添加新的 model
// 如果 app 对象没有 model,则直接给 app 对象 添加 model
/**
 * Replace a model if it exsits, if not, add it to app
 * Attention:
 * - Only available after dva.start gets called
 * - Will not check origin m is strict equal to the new one
 * Useful for HMR
 * @param createReducer
 * @param reducers
 * @param unlisteners
 * @param onError
 * @param m
 */
function replaceModel(createReducer, reducers, unlisteners, onError, m) {
  const store = app._store;
  const { namespace } = m;
  const oldModelIdx = findIndex(app._models, model => model.namespace === namespace);

  // model 已经存在,将 model 清除
  if (~oldModelIdx) {
    // Cancel effects
    store.dispatch({ type: `${namespace}/@@CANCEL_EFFECTS` });

    // Delete reducers
    delete store.asyncReducers[namespace];
    delete reducers[namespace];

    // Unlisten subscrioptions
    unlistenSubscription(unlisteners, namespace);

    // Delete model from app._models
    app._models.splice(oldModelIdx, 1);
  }

  // 给 app 对象添加新的 model
  // add new version model to store
  app.model(m);

  store.dispatch({ type: '@@dva/UPDATE' });
}

小结

dva-core 是 dva 核心功能的实现部分,它通过 create 方法创建并返回一个 dva 对象 给 dva/src/index。在create 方法内部,使用内部的 start 方法初始化各种被 dva/src/index 配置过的属性。而 start 方法是 dva-core 的核心,它用于启动应用程序,被 dva/src/index 中的 app 对象的 start 代理。在 start 方法中,完成了 store 的初始化以及 redux-saga 的调用。