(Image from Dva official website)

Dva source code core part includes DVA and DVA-core two parts, in the DVA source code interpretation series app object article we have analyzed the DVA part of dVA source code, DVA/SRC /index.js is responsible for processing external logic, The View layer is implemented using the React-Redux Provider. It creates an app object that mounts all dVA properties and methods, such as Model, Router, and Start. Finally, it starts a React application using the Start method.

In dva/ SRC /index.js, we call the create method of dva-core to create an app object:

Const app = create(opts, createOpts);Copy the code

Next, let’s look at what the create method in DVA-core does.

The create method

The create method is exported by default. It accepts two parameters, the first parameter hooksAndOpts is the control option added by the consumer, and the second parameter createOpts is the middleware that has initialized the Reducer and redux. Method creates an app object and mounts the attributes _models, _store, _plugin, use, model, start, etc.

_models contains dvaModel and all user models, _store is null by default, plugin property mounts various plug-ins, these plug-ins are based on dVA life cycle function, plugin. Use method is proxy by plugin object, so as not to find the wrong this. The Model method is used to register the Model, the start method is used to start the program, and finally the Create method returns the app object.

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;
  
  // ...
  
}
Copy the code

The plugin object

Create a Plugin object

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

In the Create method, a Plugin object is created, illegal plug-ins are filtered out using the filterHooks method, and the filtered plug-ins are registered. Plugin objects are responsible for managing and consuming middleware.

Export default class Plugin {constructor() {// use(Plugin) {// use(Plugin)... } // global error handlers apply(key, defaultHandler) {//... } // get(key) {//... }} function getExtraReducers(hook) {//... } function getOnReducer(hook) {//... }Copy the code

In the Plugin object, the following things are done:

Define life cycle functions

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

In plugin.js, we first define an array of hooks life cycle hook functions. From this array, we initialize an hooks object in the Plugin object. Each property is a life cycle function, initialized as an array to mount the plugins registered by the user. This is actually where dVA middleware or plug-ins are used.

Create hooks object

constructor() { this._handleActions = null; // Initialize this.hooks as an object, which includes all the elements of the above hooks array as attribute names, This.hooks = links.reduce ((memo, key) => {memo[key] = []; this.hooks = links.reduce ((memo, key) => {memo[key] = []; return memo; }, {}); }Copy the code

In the Plugin constructor, use the Reduce method to create a hooks object that contains all the elements of the above hooks lifecycle hook function array as attribute names, each value being an empty array that holds the middleware that the user is registered with.

Registered middleware

Use (plugin) {// Random (isPlainObject(plugin), 'plugin. Use: plugin should be plain object'); const { hooks } = this; For (const key in the plugin) {/ / use the Object. The prototype. HasOwnProperty is to prevent the user custom hasOwnProperty override on the plugin Object function, For some improper operation if (Object. The prototype. The hasOwnProperty. Call (plugin, key)) {/ / life cycle hook function name check, the key of the incoming request Invariant (hooks[key], 'plugin. Use: unknown plugin property: ${key}'); If (key === '_handleActions') {this._handleActions = plugin[key]; } else if (key === 'extraEnhancers') { hooks[key] = plugin[key]; } else {// Add middleware to lifecycle hooks, including onReducers hooks[key].push(plugin[key]); }}}}Copy the code

The plugin.use() method is used to register middleware and push user-defined functions into the array of corresponding lifecycle hooks. In order to prevent users from overwriting the original functions and doing improper operations on the plugin object, the plugin.use() method is used to register middleware and push user-defined functions into the array of corresponding lifecycle hooks. Source is used in the Object. The prototype. The hasOwnProperty. Call on the plugin Object () to test whether the user to customize the hasOwnProperty.

Global error handlers

apply(key, defaultHandler) { const { hooks } = this; Const validApplyHooks = ['onError', 'onHmr']; invariant(validApplyHooks.indexOf(key) > -1, `plugin.apply: hook ${key} cannot be applied`); Const FNS = hooks[key]; // Remove onError from hooks of plugin instance const FNS = hooks[key]; return (... If (FNS. Length) {for (const fn of FNS) {fn(const fn of FNS) {if (const fn of FNS) {if (const fn of FNS) {fn(... args); }} else if (defaultHandler) {// hooks do not mount onError; args); }}; }Copy the code

In plugin.apply(), hooks for the hooks lifecycle are filtered so that apply only applies to global errors (onError) or hot replacements (onHmr).

Gets the life cycle handler

get(key) { const { hooks } = this; Invariant (key in hooks, 'plugin.get: hook ${key} cannot be got'); // The current key is the extraReducers hook defined in hooks, Make an object out of all functions mounted on hooks and return if (key === 'extraReducers') {return getExtraReducers(hooks[key]); } // The current key is an onReducer hook defined in hooks, Else if (key === 'onReducer') {return getOnReducer(hooks[key]); } else {// Return hooks[key]; }}Copy the code

The get() method gets the middleware corresponding to the lifecycle hooks. If the current key is an extraReducers hook defined in hooks, call getExtraReducers, mount all functions from hooks into an object and return, If the current key is the onReducer hook defined in hooks, call the getOnReducer method, process the Reducer defined in the user project with all the middleware of the corresponding hook and return. If it is another hook, the middleware of the corresponding hook is returned directly.

Next, we will look at the getExtraReducers method and the getOnReducer method respectively.

getExtraReducers

Function getExtraReducers(hook) {let ret = {}; function getExtraReducers(hook) {let ret = {}; for (const reducerObj of hook) { ret = { ... ret, ... reducerObj }; } return ret; }Copy the code

getOnReducer

Function getOnReducer(hook) {return The reducer function defined by the user in the project goes through each reducer middleware process function(reducer) { for (const reducerEnhancer of hook) { reducer = reducerEnhancer(reducer); } return reducer; }; }Copy the code

Filter for illegal hooks

Export function filterHooks(obj) {export function filterHooks(obj) {// The reduce method of the array traverses obj, putting the appropriate hooks functions into a new object, Return object.keys (obj).reduce((Memo, key) => { if (hooks.indexOf(key) > -1) { memo[key] = obj[key]; } return memo; }, {}); }Copy the code

Also exported in plugin.js is a filterHooks method that filters out illegal hooks, eventually filtering out objects that include only the lifecycle hooks defined in hooks.

This concludes our interpretation of the Plugin object and introduces the prefixNamespace method that prefixes the Model when creating the app object.

PrefixNamespace method

The _model property of the app object, which contains dvaModel and all models defined by the user in the DVA project, PrefixNamespace prefixes all model reducers and effects with 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; ${namespace}/${reducer} if (reducers) {if (isArray(reducers)) { Reducers [0] cannot be directly modified, which results in repeated addition of const [Reducer,...rest] = reducers in micro-front-end scenarios. model.reducers = [prefix(reducer, namespace, 'reducer'), ...rest]; } else { model.reducers = prefix(reducers, namespace, 'reducer'); ${namespace}/${effect} if (effects) {model. Effects = prefix(effects, namespace, 'effect'); } return model; }Copy the code

Next, the Model properties of the App object.

model

The registration model

function model(m) { if (process.env.NODE_ENV ! == 'production') {// Use invariant to legalize the incoming model checkModel(m, app._models); } const prefixedModel = prefixNamespace({... m }); // push the prefixedModel into _models app._models. Push (prefixedModel); return prefixedModel; }Copy the code

The model in the app object is registered through the Model method. In the Model method, the checkModel method is used to check the validity of incoming Models, then the prefixNamespace method is used to prefix all models, and finally the prefixed Model object is returned. In the Model method, we just prefix the model passed in and do nothing else, so the model is the user-defined model in the DVA project.

Check the model

export default function checkModel(model, existModels) { const { namespace, reducers, effects, subscriptions } = model; Invariant (' [app.model] namespace should be defined '); Invariant (typeof namespace === 'string', '[app.model] namespace should be string, but got ${typeof namespace}`, ); // And only remeber (! existModels.some(model => model.namespace === namespace), `[app.model] namespace should be unique`, ); // State can be arbitrary // reducers can be null, PlainObject or array if (reducers) {invariant (isPlainObject (reducers) | | isArray (reducers), `[app.model] reducers should be plain object or array, but got ${typeof reducers}`, ); // The reducers of arrays must be format invariant(! isArray(reducers) || (isPlainObject(reducers[0]) && isFunction(reducers[1])), `[app.model] reducers with array should be [Object, Function]`, ); } // Effects can be null, PlainObject if (effects) { invariant( isPlainObject(effects), `[app.model] effects should be plain object, but got ${typeof effects}`, ); } if (subscriptions) {// Subscriptions can be null, PlainObject invariant( isPlainObject(subscriptions), `[app.model] subscriptions should be plain object, but got ${typeof subscriptions}`, ); // subscription must be method invariant(isAllFunction(subscriptions), '[app.model] subscription should be function'); }}Copy the code

The checkModel method checks the validity of model. A legitimate model should meet the following requirements:

  1. Namespace must be defined as a string, non-null, and unique.

  2. Reducers is an array or a pure object. If reducers is an array, then the first element must be an object and the second requirement is a function, which will be used by adding a prefix later.

  3. Effects must be a pure object.

  4. Subscriptions must be a pure object and each property must be a function.

Add a prefix

After checking the validity of the Model using checkModel, prefixNamespace is used to prefix the Model. The prefixNamespace method was introduced earlier and won’t be repeated here.

At this point, the model is legal, the middleware has been registered, and the reducer and effects have been called. Next, dVA needs to start the application.

The last property of the app object, the start method, is used to start the application.

start

The start method is the core of dvA-core, which is used to start the application and is proxyed by the START of the APP object in DVA /index. The following things are done in start:

  1. Define global error handling
  2. Connect Sagas to the Redux Store
  3. Intercept action to Effects
  4. Initializers _getSaga handle asynchronous data
  5. Mount reducers and Effects
  6. Create the store
  7. Subscription History changes
  8. Mount model

Next, let’s take a look at what the start method does.

Define global error handling

const onError = (err, extension) => { if (err) { if (typeof err === 'string') err = new Error(err); err.preventDefault = () => { err._dontReject = true; }; // Call plugin's apply method to handle the error. The apply method can only be applied in global Error or hot replacement plugin. Apply (' onError 'err = > {/ / global Error throw new Error (err) stack | | err); })(err, app._store.dispatch, extension); }};Copy the code

In the start method, we first define an onError function for global error handling. Execute plugin apply with err, app._store.dispatch, and extension parameters, specifying that onError can only be used for global error reporting. For the plugin.apply method, refer to the global error handler for the Plugin object in the previous section.

Connect Sagas to the Redux Store

The second thing the start method does is call redux-Saga’s createSagaMiddleware method to create a Redux Middleware that connects Sagas to the Redux Store.

const sagaMiddleware = createSagaMiddleware();
Copy the code

Intercept action to Effects

const promiseMiddleware = createPromiseMiddleware(app);
Copy the code

The next thing the start method does is call the createPromiseMiddleware method to intercept the action pointing to Effects and check whether the action type points to a method that belongs to Effects. If so, A Promise middleware with the resolve and Reject Promise state methods mounted is returned, otherwise the Action object is returned unprocessed.

import { NAMESPACE_SEP } from './constants'; export default function createPromiseMiddleware(app) { // const middleware = ({dispatch}) => next => (action) => {... Return Next (action)} is a standard way of writing middleware, // Middleware intercepts action return () to effects => next => action => {const {type}  = action; // isEffect checks if the action type refers to effect, that is, the asynchronous method defined in model effects. // If type refers to effect, return a Promise object. Resolve (isEffect(type)) {return new Promise((resolve, reject)); reject) => { next({ __dva_resolve: resolve, __dva_reject: reject, ... action, }); }); } else {return next(action); }}; Function isEffect(type) {if (! type || typeof type ! == 'string') return false; / / in the dva action type has the fixed format: model. The namespace/model. The effects const [namespace] = type. The split (NAMESPACE_SEP); Const model = app._models. Filter (m => m.namespace === namespace)[0]; // If model exists and model.effects[type] exists, Effects if (model. Effects && model. Effects [type]) {return true; } } return false; }}Copy the code

Initialization _getSaga handles asynchronous data

Next, the start method calls the getSaga method to initialize the _getSaga method of the app object, which takes advantage of redux-Saga’s ability to process data.

app._getSaga = getSaga.bind(null);
Copy the code

GetSaga calls bind to initialize _getSaga so that _getSaga has getSaga’s default initial arguments, which (if any) will follow this as the second argument to bind(), They are then inserted at the beginning of the argument list of the target function, followed by the arguments passed to the binding function.

getSaga

The getSaga method takes five parameters. The first parameter is the effects defined in model, and the second parameter model is the Model object defined in the project. The third argument onError is the global error handler onError defined in dvA-core /index, the fourth argument onEffect is the lifecycle hook defined in plugin.js, and finally there is the function opts, Is the user-added control option passed in when the create method is called. The getSaga method finally returns a generator function.

Export default function getSaga(effects, model, onError, onEffect, Opts = {}) {// Return a generator function return function*() {for (const key in effects) {if (Object. The prototype. HasOwnProperty. Call (effects, the key)) {/ / getWatcher returns a generator function, Const watcher = getWatcher(key, effects[key], model, onError, onEffect, opts); // Use the fock method provided by Redux-saga to separate the watcher into a separate thread (i.e. generate a new task) const task = yield sagaeffces.fork (watcher); // To facilitate task control, start a new process, execute a generator function, Yield sagaeffects. fork(function*() {yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`); yield sagaEffects.cancel(task); }); }}}; }Copy the code

In the generator returned by getSaga, the Effects properties of the model are iterated over, giving each effect (note: Also generator) add a watcher, and then use redux-Saga’s fork to open a separate thread to listen for action and effect (a task is generated). At the same time, in order to facilitate the control of this thread, a new thread is created to execute a generator function, which intercepts the effect cancellation action and immediately cancellations the task thread once it is monitored.

getWatcher

The getWatcher method returns a generator function that adds a Watcher to the _effect (effect in model) passed in to listen for matching actions. GetWatcher accepts 6 parameters:

  • Effect method name of key escaped from prefixNamespace (name of generator function defined in model)
  • _effect The asynchronous method defined in the effects of the model
  • Model The model object itself
  • OnError Global error handler onError defined in dVA-core /index
  • OnEffect life cycle function defined in plugin
  • Opts Control options added by the user
function getWatcher(key, _effect, model, onError, onEffect, opts) { let effect = _effect; // define the default value of type let type = 'takeEvery'; let ms; let delayMs; 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() {} function noop() {} function noop() {} function noop() {} function noop() {} function noop() {} function noop() {} function noop() {} Const {__dvA_resolve: resolve = noop, __dva_reject: reject = noop } = args.length > 0 ? args[0] : {}; try { yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` }); // Execute the effect method defined by the user in the effects of model 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); 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`)]); }}; // If effect is not an array, getWatcher returns default for every action emitted. The sagaWithOnEffect // // takeEvery() method is called every time an action pointing to effect is issued, // takeEvery listens to the action and creates a new saga task each time this action is initiated. // therefore, in the dva project, Default: return function*() {yield sagaeffects. takeEvery(key, sagaWithOnEffect); }; }}Copy the code

Inside getWatcher, the following things are done (regardless of arrays) :

  1. Define the sagaWithCatch method to execute the user-defined effect method internally and notify Redux-Saga before and after execution. Since the Promise state methods resolve and Reject have been added to the action in createPromiseMiddleware, invoking the Promise state method after effect execution returns the corresponding execution result.

  2. When a user-defined effect is executed, the createEffects method is called to validate the data. This method rewraps the PUT and take methods of Redux-Saga, and adds data type and format validation to action type.

  3. Execute the applyOnEffect method, which registers the user-provided onEffect lifecycle handler with Effect and executes the user-added middleware in turn without affecting effect.

  4. Finally, use switch to determine the type of user-defined agent. By default, return a generator. Use redux-Saga’s takeEvery method to listen for the corresponding action and automatically create an asynchronous task when a match is found. Execute the user-defined effect function.

createEffects

CreateEffects reencapsulates the put and take methods of Redux-Saga, and validates the type and format of the action type.

Redux-saga redux-saga redux-saga redux-Saga redux-Saga Function createEffects(model, opts) {function assertAction(type, name) {random (type, invariant) '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}`, ); 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; 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 }; }Copy the code

applyOnEffect

The applyOnEffect method registers the user-provided onEffect lifecycle processing function with Effect, and executes the middleware added by the user in turn without affecting the operation of Effect.

/** * the effect function executes on the order of the hooks callback functions defined in plugin. The result is an effect that has been handled by the hooks callback functions. And returns @param {*} FNS's life cycle function defined in plugin * @param {*} effect Effect defined by the user in model * @param {*} model model object itself * @param {*} key effect method name after prefixNamespace escape, */ function applyOnEffect(FNS, effect, model, key) { for (const fn of fns) { effect = fn(effect, sagaEffects, model, key); } return effect; }Copy the code

Combine reducers and effects

Reducer and effect rules have been determined, but they are still stored in their respective models. The next thing that Start needs to do is to combine reducer and effect of each model. Here, a for loop is used to unify the reducer and effects of each model into a total reducers and SAGAs.

The getReducer method returns a Reducer of the corresponding type (reducer may be an array or object).

When you initialize _getSaga above, getSaga calls bind to set the default parameters for _getSaga. When you combine effects, you can pass in the full parameters and execute the _getSaga method. Put Effect into the Sagas array.

// Mount the reducer and effects of each model onto a total reducers and sagas const sagas = []; const reducers = { ... initialReducer }; For (const m of app._models) {// getReducer Returns a reducer (reducer may be an array or object) reducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions); If (m.ffects) {// Pass in the full parameter, execute the _getSaga method, Sagas.push (app._getsaga (m.ffects, m, onError, plugin.get('onEffect'), hooksAndOpts)); } // onReducer and extraReducers are defined in plugin. Get (' onreducerenhancer '); 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(', ')}`, );Copy the code

Create the store

With both synchronous and asynchronous data flow solutions already in place, it’s time to create a Store.

createStore(reducer, [preloadedState], enhancer)
Copy the code

Normally, redux’s createStore receives three parameters (Reducer, initState, applyMiddleware(middlewares)).

However, DVA extends Redux’s createStore to provide its own createStore method that organizes a set of parameters it creates.

// 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 = {};
Copy the code

Store has attributes such as reducers, State, DVA middleware Plugin, initial configuration passed in when the CREATE method is called, Saga middleware, Promise middleware and so on. Reducers are routerReducer managed by DVA (connected-react-router binds react-router to Redux), reducer and extractReducer plug-ins provided by users, Formed by the asynchronous Reducer.

createStore

The crateStore method provided by DVA extends Redux’s createStore method to organize a set of self-created parameters

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)); }Copy the code

createReducer

Function createReducer() {// Use the onReducer extension reducer function in the plugin. const reducerEnhancer = plugin.get('onReducer'); Return reducerEnhancer(// Reduce combineReducers using Redux combineReducers({... Reducers, // ReducyReducer passed from DVA, and reducers[m.mapespace] = getReducer(M.reducers, M.state); Reducer in the removed model (reducer provided by users)... ExtraReducers, // Manually added extraReducers in plugin... (app._store ? App._store. AsyncReducers: {}), // asynchronous reducer, mainly used to dynamically load the reducer in the model after DVA),); }Copy the code

CreateReducer extends the reducer function with onReducer:

const reducerEnhancer = plugin.get('onReducer');
Copy the code

In reducerEnhancer, the combineReducers of Redux are used to combine historyReducer, extraReducers and asynchronous Reducer.

While in combineReducers:

  • The first… Reducers are historyReducer imported from DVA, and reducers[M.naiespace] = getReducer(M.rut, M.state); Reducer in the extracted Model

  • The second extraReducers are the ones that were added manually in the plugin

  • The third parameter is asynchronous Reducer, which is mainly used to dynamically load reducer in the model after DVA runs

Let’s look at onReducer in the Plugin:

onReducer

Function getOnReducer(hook) {return A redcuer function that has been processed by all the middleware Function (reducer) {// If there are onReducer plug-ins, Then extend reducer for (const reducerEnhancer of hook) {reducer = reducerEnhancer(reducer); } return reducer; }; }Copy the code

OnReducer processes the reducer defined by the user in the project using the middleware of reducer, and finally returns a Reducer function after all intermediate processing.

Subscription History changes

To monitor the onStateChange

// Whenever the status changes, execute the listeners mounted on onStateChange and add a const listeners = plugin.get('onStateChange') to the store; for (const listener of listeners) { store.subscribe(() => { listener(store.getState()); }); }Copy the code

Gets the listener mounted to onStateChange from the Plugin object, executes the listener when the state changes, and adds a listener to the store.

Run all sagAs

Sagas.foreach (sagamiddleware.run); sagas.foreach (sagamiddleware.run);Copy the code

To load saga dynamically, run the watcher function returned by sagamiddleware. run.

Listen for history changes

// Setup app // setupApp is passed from dVA /index. The patchHistory method is called internally to listen for changes in history and trigger a callback. Add listening history to app object setupApp(app);Copy the code

SetupApp is passed in from DVA /index. In daV /index, the history is proxy to app._history, and the patchHistory method is called to listen for history, so every time history changes, Will be notified to Redux, triggering an update of state.

Run the subscriptions

// Run subscriptions const unlisteners = {}; For (const model of this._models) {if (model.Subscriptions) {// The subscription function accepts APP objects and can subscribe to changes in history. unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError); }}Copy the code

When running subscriptions to the Model, the runSubscription method was called:

/** * First check if the key is a property of the subs. If so, run the function (function defined in Model Subscription) and pass dispatch and history into the function. The subscription object in @param {*} subs model * @param {*} model * @param {*} app The app created in DVA-core * @param {*} onError onError */ export function run(subs, model, app, onError) {const funcs = []; const nonFuncs = []; For (const key in subs) {// Use the stereotype method to determine if key is the own property of subs (model subscription) if (Object. The prototype. HasOwnProperty. Call (subs, key)) {/ / get the corresponding attribute values, Function const sub = subs[key]; // Execute this function, Const unlistener = sub({// prefixedDispatch) prefixes type in the action with ${model.namespance}/ Dispatch: prefixedDispatch(app._store.dispatch, model), // patchHistory in dav/index app._history, }, onError, ); if (isFunction(unlistener)) { funcs.push(unlistener); } else { nonFuncs.push(key); Return {funcs, nonFuncs}; }Copy the code

The run method takes four arguments:

  • The first parameter is the Subscription object in the Model.

  • The second parameter is the corresponding Model

  • The third parameter is the app created in DVA-core

  • The fourth parameter is onError for global exception capture

In the run method, we first determine if the key is a property of the subs (the Subscription object in model), and if so, we run the function (function defined in Model Subscription). Pass Dispatch and History into this function, and finally return the subscription object.

Mount model

The last thing that the start method does, is to apply the Model to the app object.

// 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);
Copy the code

injectModel

When registering the Model, the model was prefixed and reducers, Effects and Subscriptions of the current model were then mounted on the store.

/** * Inject model after app is started. * * @param createReducer * @param onError * @param unlisteners * @param m */ Function injectModel(createReducer, onError, unlisteners, m) {function injectModel(onError, unlisteners, m) { const store = app._store; // Mount reducers, effects(asynchronous operations), subscriptions(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); }}Copy the code

unModel

UnModel unloads the Model on the APP object and simultaneously clears the reducers, Effects, and subscriptions on the Store of the App object.

/** * Unregister model. Remove the reducers stored in the app object _store, The effects and subscrioptions are cancelled * * @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; // Delete reducers stored on app object _store // Delete reducers Delete store.asyncReducers[namespace]; delete reducers[namespace]; // Replace the reducer on app object _sotre with the global reducer store.replacereducer (createReducer()) created during initialization. store.dispatch({ type: '@@dva/UPDATE' }); // Cancel the asynchronous operation // Cancel effects store.dispatch({type: '${namespace}/ @@cancel_effects'}); // Unlisteners subscription (unlisteners, namespace); // Delete model from app._models app._models = app._models.filter(model => model.namespace ! == namespace); }Copy the code

replaceModel

During hot update, check whether there are models on the APP object. If there are, delete the old model first and clear reducer, Effects and Subscrioptions on the model. Then add a new Model to the app object. If there is no Model on the app object, add model directly to the app object.

// If the model already exists, delete the model first, and then add a new model to the app object. 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); 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); } // Add new version model to store app.model(m); store.dispatch({ type: '@@dva/UPDATE' }); }Copy the code

summary

Dva-core is the implementation part of the dVA core function. It creates and returns a DVA object to DVA/SRC /index through the create method. Inside the create method, the internal start method is used to initialize various properties configured by DVA/SRC /index. The start method is the core of dvA-core, which is used to start the application and is proxyed by the app object in DVA/SRC /index. In the start method, the initialization of store and the call to redux-saga are completed.