侧边栏

dva源码分析

发布于 | 分类于 源码分析

dva是基于react的二次封装,在工作中经常用到,因此有必要了解一下其中的实现。本文将从源码层面分析dva是如何将reduxredux-sagareact-router等进行封装的。

本文使用的dva版本为2.6.0-beta.20,参考

dva构建流程

下面是一个简单的dva应用

js
// 初始化应用
const app = dva({
    onError(error) {
        console.log(error)
    }
})
app.use(createLoading()) // 使用插件
app.model(model) // 注册model
app.router(App) // 注册路由
app.start('#app') // 渲染应用

我们从这几个api开始,了解dva是如何封装react应用的。

整个dva项目使用lerna管理的,在每个package的package.json中找到模块对应的入口文件,然后查看对应源码。

dva start

js
// dva/src/index.js
export default function(opts = {}) {
  const history = opts.history || createHashHistory();
  const createOpts = {
    initialReducer: {
      router: connectRouter(history),
    },
    setupMiddlewares(middlewares) {
      return [routerMiddleware(history), ...middlewares];
    },
    setupApp(app) {
      app._history = patchHistory(history);
    },
  };

  const app = create(opts, createOpts);
  const oldAppStart = app.start;
  app.router = router; 
  app.start = start;
  return app;
  // 为app暴露的router接口,主要是更新app._router 
  function router(router) {
    app._router = router;
  }
  // 为app暴露的start接口,主要渲染整个应用
  function start(container) {
    // 根据container找到对应的DOM节点
    if (!app._store) oldAppStart.call(app); // 执行原本的app.start
    const store = app._store;

    // 为HMR暴露_getProvider接口
    app._getProvider = getProvider.bind(null, store, app);
    // If has container, render; else, return react component
    if (container) {
      render(container, store, app, app._router);
      app._plugin.apply('onHmr')(render.bind(null, container, store, app));
    } else {
      return getProvider(store, this, this._router);
    }
  }
}

// 渲染逻辑
function getProvider(store, app, router) {
  const DvaRoot = extraProps => (
    // app.router传入的组件在此处被渲染,并传入app,history等prop
    <Provider store={store}>{router({ app, history: app._history, ...extraProps })}</Provider>
  );
  return DvaRoot;
}
function render(container, store, app, router) {
  const ReactDOM = require('react-dom');
  ReactDOM.render(React.createElement(getProvider(store, app, router)), container);
}

可以看见app是通过create(opts, createOpts)进行初始化的,其中opts是暴露给使用者的配置,createOpts是暴露给开发者的配置,真实的create方法在dva-core中实现。

dva-core craete

下面是create方法的大致实现

js
// dva-core/src/index.js
const dvaModel = {
  namespace: '@@dva',
  state: 0,
  reducers: {
    UPDATE(state) {
      return state + 1;
    },
  },
};
export function create(hooksAndOpts = {}, createOpts = {}) {
  const { initialReducer, setupApp = noop } = createOpts; // 在dva/index.js中构造了createOpts对象
  const plugin = new Plugin(); // dva-core中的插件机制,每个实例化的dva对象都包含一个plugin对象
  plugin.use(filterHooks(hooksAndOpts)); // 将dva(opts)构造参数opts上与hooks相关的属性转换成一个插件

  const app = {
    _models: [prefixNamespace({ ...dvaModel })],
    _store: null,
    _plugin: plugin,
    use: plugin.use.bind(plugin), // 暴露的use方法,方便编写自定义插件
    model, // 暴露的model方法,用于注册model
    start, // 原本的start方法,在应用渲染到DOM节点时通过oldStart调用
  };
  return app;
}

可以看见上面代码主要暴露了usemodelstart这三个接口。

插件系统

dva实现高度依赖内置的Plugin系统,为了便于理解后续代码,我们暂时跳出start方法,去探究Plugin原理。

dva的插件时一个包含下面部分属性的JS对象,每个属性对应一个函数或配置项

js
// plugin对象只暴露了下面这几个属性,顾名思义,以on开头的方法会在整个应用的某些时刻触发,类似于生命周期钩子函数
const hooks = [
  'onError',
  'onStateChange',
  'onAction',
  'onHmr',
  'onReducer',
  'onEffect',
  'extraReducers',
  'extraEnhancers',
  '_handleActions',
];

一个大概的插件结构类似于

js
const testPlugin = {
  onStateChange(newState){
    console.log(newState)
  }
  // ...其他需要监听的hook
}
app.use(testPlugin)

来看看在start初始化时构造的Plugin管理器

js
// Plugin管理对象
class Plugin {
  constructor() {
    this._handleActions = null;
    // 每个钩子对应的处理函数默认都是空数组
    this.hooks = hooks.reduce((memo, key) => {
      memo[key] = [];
      return memo;
    }, {});
  }
  // 接收一个plugin对象
  use(plugin) {
    const { hooks } = this;
    for (const key in plugin) {
      if (Object.prototype.hasOwnProperty.call(plugin, key)) {
        // ...检测use的plugin上的key,并处理特定形式的key
        if (key === '_handleActions') {
          this._handleActions = plugin[key]; // 如果定义了插件的_handleActions方法,会覆盖Plugin对象本身的_handleActions
        } else if (key === 'extraEnhancers') {
          hooks[key] = plugin[key]; // 如果定义了插件的extraEnhancers方法,会覆盖this.hooks本身的extraEnhancers
        } else {
          hooks[key].push(plugin[key]); // 其他key对应的方法会追加到hooks对应key数组中
        }
      }
    }
  }
  // 返回一个通过key执行hooks上注册的所有方法的函数
  apply(key, defaultHandler) {
    const { hooks } = this;
    const fns = hooks[key];
    // 遍历fns并以此执行
    return (...args) => {};
  }

  // 根据key找到hooks上对应的方法
  get(key) {
    const { hooks } = this;
    if (key === 'extraReducers') {
      // 将hooks[key]都合并到一个对象中,如将[{ x: 1 }, { y: 2 }]转换为{x:1,y:2}的对象
      return getExtraReducers(hooks[key]);
    } else if (key === 'onReducer') {
      // 返回一个接受reducer并依次运行hooks.onReducer的函数,每次循环都会将前一个hooks.onReducer元素方法返回值作为新的reducer
      return getOnReducer(hooks[key]);
    } else {
      return hooks[key];
    }
  }
}
function getOnReducer(hook) {
  return function(reducer) {
    for (const reducerEnhancer of hook) {
      reducer = reducerEnhancer(reducer);
    }
    return reducer;
  };
}

可以看见,整个插件系统的实现还是比较简单的,主要暴露了use注册插件、get根据key获取插件等接口。

js
app.use = plugin.use.bind(plugin)

初始化Store

回到前面章节,继续查看start方法的执行流程,我们可以看见完成的store构建流程

js
// oldStart方法
function start() {
  // 全局错误处理函数,如果某个插件注册了onError钩子,将在此处通过plugin.apply('onError')调用
  const onError = (err, extension) => {};
  // 遍历app._models,收集saga
  app._getSaga = getSaga.bind(null);
  const sagas = [];
  const reducers = { ...initialReducer };
  for (const m of app._models) {
    reducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
    if (m.effects) {
      sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));
    }
  }
  // 创建 app._store,具体参看下面章节的createStore
	// app._store = craeteStore()... 
  const store = app._store;

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

  // Execute listeners when state is changed
  // 获取所有监听了onStateChange的插件,通过store.subscribe注册到redux中
  const listeners = plugin.get('onStateChange');
  for (const listener of listeners) {
    store.subscribe(() => {
      listener(store.getState());
    });
  }

  // 运行所有sagas
  sagas.forEach(sagaMiddleware.run);
  setupApp(app);
  // 运行所有model的subscriptions方法
  const unlisteners = {};
  for (const model of this._models) {
    if (model.subscriptions) {
      unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError);
    }
  }

  // 在执行start时注册model、unmodel和replaceModel三个接口,具体参看下面章节
  // ...
}

可见在整个start方法中

  • 遍历app._models,收集并运行sagas
  • 初始化app._store,使用store.subscribe注册所有插件的onStateChange回调
  • 运行所有的model.subscriptions
  • 暴露app.modelapp.unmodelapp.replaceModel三个接口

createStore

createStore方法是封装的redux createStore,传入了一些中间件和reducers

js
const sagaMiddleware = createSagaMiddleware(); // 初始化saga中间件
const promiseMiddleware = createPromiseMiddleware(app);
app._store = createStore({
  reducers: createReducer(),
  initialState: hooksAndOpts.initialState || {},
  plugin,
  createOpts,
  sagaMiddleware,
  promiseMiddleware,
});

function createStore({
    reducers, initialState, plugin, sagaMiddleware, promiseMiddleware, 
    createOpts: { setupMiddlewares = returnSelf },
}) {
  // extra enhancers
  const extraEnhancers = plugin.get('extraEnhancers');
  const extraMiddlewares = plugin.get('onAction');
  const middlewares = setupMiddlewares([
    promiseMiddleware,
    sagaMiddleware,
    ...flatten(extraMiddlewares),
  ]);

  const enhancers = [applyMiddleware(...middlewares), ...extraEnhancers];
  // redux.createStore
  return createStore(reducers, initialState, compose(...enhancers));
}

其中的reducers参数由createReducer方法构建,该方法返回一个增强版的reducer

js
const reducerEnhancer = plugin.get('onReducer'); // 在插件系统中提到,传入onReducer时实际返回所有插件组合后的onReducer方法
const extraReducers = plugin.get('extraReducers'); // 传入extraReducers实际返回的是所有插件合并后的reducerObj对象
function createReducer() {
    return reducerEnhancer(
    // redux.combineReducers
    combineReducers({
        ...reducers, // start Opts参数上的默认reducer
        ...extraReducers,
        ...(app._store ? app._store.asyncReducers : {}),
    }),
    );
}

其中promiseMiddleware中间件由createPromiseMiddleware创建,是一个当isEffect(action.type)时返回Promise的中间件,并在原本的action上挂载该Promise的{__dva_resolve: resolve, __dva_reject: reject}

js
export default function createPromiseMiddleware(app) {
  return () => next => action => {
    const { type } = action;
    // isEffect的实现为:从app._models中找到namespace与action.type.split('/')[0]相同的model,然后根据model.effects[type]是否存在判断当前action是否需要返回Promise
    if (isEffect(type)) {
      return new Promise((resolve, reject) => {
        next({
          __dva_resolve: resolve,
          __dva_reject: reject,
          ...action,
        });
      });
    } else {
      return next(action);
    }
  };
}

model三接口

一个model大概是下面的结构

js
{
    namespace: 'index_model',
    state: {
        text: 'hello'
    },
    effects: {
        * asyncGetInfo({payload = {}}, {call, put}) {
        }
    },
    reducers: {
        updateData: (state, {payload}) => {
            return {...state, ...payload}
        }
    }
}

create方法中,app.model方法实现如下,其本质是

js
function model(m) {
  // 将model上的reducers和effects各个key处理为`${namespace}${NAMESPACE_SEP}${key}`形式
  const prefixedModel = prefixNamespace({ ...m });
  app._models.push(prefixedModel);
  return prefixedModel;
}

在执行start方法后,会覆盖app.model方法为injectModel,并新增unmodelreplaceModel

js
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);  

// 动态添加model,这样就可以实现model与route组件类似的异步加载方案
function injectModel(createReducer, onError, unlisteners, m) {
  m = model(m);

  const store = app._store;
  store.asyncReducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
  // 重新调用createReducer
  store.replaceReducer(createReducer());
  if (m.effects) {
    store.runSaga(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));
  }
  // 更新unlisteners方便后面移除
  if (m.subscriptions) {
    unlisteners[m.namespace] = runSubscription(m.subscriptions, m, app, onError);
  }
}
// 根据namespace移除对应model,需要移除store.asyncReducers并调用store.replaceReducer(createReducer())重新生成reducer
function unmodel(createReducer, reducers, unlisteners, namespace) {}
// 找到namespace相同的model并替换,如果不存在,则直接添加
function replaceModel(createReducer, reducers, unlisteners, onError, m) {}

通过此处了解到,dva中的model分为两类

  • 在调用start之前注册的model
  • 在调用start之后动态注册的model

路由

在前面的dva.start方法中我们看到了createOpts,并了解到在dva-corestart中的不同时机调用了对应方法

js
import * as routerRedux from 'connected-react-router';
const { connectRouter, routerMiddleware } = routerRedux;

const createOpts = {
  initialReducer: {
    router: connectRouter(history),
  },
  setupMiddlewares(middlewares) {
    return [routerMiddleware(history), ...middlewares];
  },
  setupApp(app) {
    app._history = patchHistory(history);
  },
};

其中initialReducersetupMiddlewares在初始化store时调用,然后才调用setupApp

可以看见针对router相关的reducer和中间件配置,其中connectRouterrouterMiddleware均使用了connected-react-router这个库,其主要思路是:把路由跳转也当做了一种特殊的action。

首先来看看connectRouter方法的实现,该方法会返回关于router的reducer

js
// connected-react-router/src/reducer.js
export const LOCATION_CHANGE = '@@router/LOCATION_CHANGE'
const createConnectRouter = (structure) => {
  const { fromJS, merge } = structure // 复制和合并JS对象的两个工具方法
  const createRouterReducer = (history) => {
    const initialRouterState = fromJS({
      location: injectQuery(history.location),
      action: history.action,
    })
    return (state = initialRouterState, { type, payload } = {}) => {
      if (type === LOCATION_CHANGE) {
        const { location, action, isFirstRendering } = payload
        // 首次渲染时不更新state
        return isFirstRendering
          ? state
          : merge(state, { location: fromJS(injectQuery(location)), action })
      }
      return state
    }
  }

  return createRouterReducer
}

然后是routerMiddleware,该方法返回关于router的中间件

js
export const CALL_HISTORY_METHOD = '@@router/CALL_HISTORY_METHOD'
const routerMiddleware = (history) => store => next => action => {
  if (action.type !== CALL_HISTORY_METHOD) {
    return next(action)
  }
  // 当action为路由跳转时,调用history方法而不是执行next
  const { payload: { method, args } } = action
  history[method](...args)
}

最后回到dva中,查看patchHistory方法

js
function patchHistory(history) {
  // 劫持了history.listen方法
  const oldListen = history.listen;
  history.listen = callback => {
    const cbStr = callback.toString();
    // 当使用了dva.routerRedux.ConnectedRouter路由组件时,其构造函数会执行history.listen,此时isConnectedRouterHandler将为true
    const isConnectedRouterHandler =
      (callback.name === 'handleLocationChange' && cbStr.indexOf('onLocationChanged') > -1) ||
      (cbStr.indexOf('.inTimeTravelling') > -1 && cbStr.indexOf('arguments[2]') > -1);
    // 在app.start之后的其他地方如model.subscriptions中使用history时,会接收到location和action两个参数
    callback(history.location, history.action);
    return oldListen.call(history, (...args) => {
      if (isConnectedRouterHandler) {
        callback(...args);
      } else {
        // Delay all listeners besides ConnectedRouter
        setTimeout(() => {
          callback(...args);
        });
      }
    });
  };
  return history;
}

可以看见,dva对router并未做过多封装,只是通过connected-react-router提供了一个reducer和一个中间件,并将该库暴露为routerRedux

小结

从源码可以看见,dva主要是对reduxredux-saga进行封装,简化并对外暴露了几个有限接口。除此之外,还内置了react-router和其他一些库,因此也可以看做是一个轻量级应用框架

  • dva主要对外提供了相关的api
  • dva-corereduxredux-saga进行封装,并实现了一个简单的插件系统

学习dva源码的一个好处可以让你去了解整个React的生态,并学会使用一种还不错的开发方案。了解dva的封装实现只是学习的第一步,要想编写高效可维护的代码,还需要深入reduxredux-sagareact-router等库的使用和实现。

你要请我喝一杯奶茶?

版权声明:自由转载-非商用-保持署名和原文链接。

本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。