DVA 是如何实现 Store 的动态更新的?

2,620 阅读3分钟

一般我们提到store的更新都是指的state的更新,即通过dispach发送一个action,再由reducer进行处理并返回一个新的state

但是,当我们需要在运行时动态修改state的数据结构,以及动态修改reducer时,应该怎么办呢?

事实上这样的场景并不少见,例如:在代码分离的场景下,如果需要在懒加载时更新store,就会涉及到上述问题。下面将结合dva的实现方式进行一番阐述。

dva/dynamic

dva通过dynamic方法实现懒加载,使用方法如下:

import dynamic from 'dva/dynamic';

const UserPageComponent = dynamic({
  app,
  models: () => [
    import('./models/users'),
  ],
  component: () => import('./routes/UserPage'),
});

内部实现原理就不细说了,可以跟代码分离中所述实现方式类似。主要关注其中的models字段,该字段即运行时需要动态注入storemodel模块。dva实例是通过model()来实现statereducer的注入,而dva实例在start()执行前后,其model方法大的实现是不同的.

start()之前的model()方法

这时,由于store还未初始化,因此model()所做的仅仅只是将传入的参数pushapp._models中,其中appdva实例,而_model则是存储各模块model的列表。具体实现如下:

function model(m) {
    if (process.env.NODE_ENV !== 'production') {
      checkModel(m, app._models); // 对传入的model进行校验
    }
    const prefixedModel = prefixNamespace({ ...m }); // 将reducer,effect等方法重命名到对应namespace下
    app._models.push(prefixedModel);
    return prefixedModel;
}

start()之后的model()方法

start()中有如下一段代码:

app.model = injectModel.bind(app, createReducer, onError, unlisteners);

即对model方法进行替换,再看一下injectModel的实现:

function injectModel(createReducer, onError, unlisteners, m) {
    m = model(m);  // 执行此前的mode方法,将新的model参数push到__models中

    const store = app._store;
    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'))
      );
    }
    if (m.subscriptions) {
      unlisteners[m.namespace] = runSubscription(
        m.subscriptions,
        m,
        app,
        onError
      );
    }
}

那么我们新创建的reducer在哪儿呢?如何将其添加到reducers中呢?最后如何用新的new reducer替换原有的store中的old reducer呢?

首先,通过getReducer创建新的reducer并保存到store.asyncReducers对应的namespace下:

store.asyncReducers[m.namespace] = getReducer(
    m.reducers,
    m.state,
    plugin._handleActions
);

然后通过createReducer()获取最新的reducers,具体实现为:

function createReducer() {
    return reducerEnhancer(
        combineReducers({
            ...reducers,
            ...extraReducers,
            ...(app._store ? app._store.asyncReducers : {}), // 这里!!!
        })
    );
}

最后通过store.replaceReducer(createReducer())实现reducer的动态替换。至此,基本的model动态注入就完成了。

unmodel方法

我们观察源码时还会发现start()中还为app增加了unmodel()方法。具体实现如下:

function unmodel(createReducer, reducers, unlisteners, namespace) {
    const store = app._store;

    // Delete reducers
    delete store.asyncReducers[namespace];
    delete reducers[namespace];
    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);
}

首先通过delete直接进行删除store.asyncReducersstore.asyncReducers下对应namespacekey,然后通过replaceReducer更新reducer

接下来比较巧妙,通过store.dispatch({ type: '@@dva/UPDATE' });实现整个state的更新。事实上dva在初始化时,默认会传入一个model:

const dvaModel = {
  namespace: '@@dva',
  state: 0,
  reducers: {
    UPDATE(state) {
      return state + 1;
    },
  },
};

这里只是简单的对@@dva下的state加一处理,但由于dispatch(action)时,该action会在新的reducer中依次执行,从而实现整个store中的state树实现自动更新,是不是非常精巧呢~

最后别忘了将app._models下对应的model删除,大功告成!