XXHolic / blog

I wonder how~, I wonder why~

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Read Dva

XXHolic opened this issue · comments

目录

引子

看了下 Dva 源码,整理一下个人理解。

源码版本 dva@2.6.0-beta.20

简介

dva 是一个基于 reduxredux-saga 的数据流方案,为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。更多信息见这里

了解 redux 和 redux-saga 原理有助于更快理解其中的逻辑。相关解读可见 Read ReduxRead Redux-Saga

下面以计数器的例子进行相关介绍。

源码

计数器示例
import React from 'react';
import dva, { connect } from 'dva';

// 1. Initialize
const app = dva();

// 2. Model
app.model({
  namespace: 'count',
  state: 0,
  reducers: {
    add  (count) { return count + 1 },
    minus(count) { return count - 1 },
  },
});

// 3. View
const App = connect(({ count }) => ({
  count
}))(function(props) {
  return (
    <div>
      <h2>{ props.count }</h2>
      <button key="add" onClick={() => { props.dispatch({type: 'count/add'})}}>+</button>
      <button key="minus" onClick={() => { props.dispatch({type: 'count/minus'})}}>-</button>
    </div>
  );
});

// 4. Router
app.router(() => <App />);

// 5. Start
app.start('#root');

接下来按照计数器示例中的步骤分别介绍。

Initialize

初始化时执行的 dva 方法,所在源文件为 index.js 。主要逻辑如下:

相关主要源码
import React from 'react';
import { createHashHistory } from 'history';
import { create } from 'dva-core';
import * as routerRedux from 'connected-react-router';

const { connectRouter, routerMiddleware } = routerRedux;

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;

  function router(router) {} // 路由注册

  function start(container) {} // 开始渲染
}

从代码中可以发现:

  1. 创建了路由对象 history ,并将 historyrouter 结合,并重写了 listen 方法。
  2. 创建的 app 对象主要方法 create ,提供了初始化的 _model 数据,_store 默认为 null ,并定义了主要方法 modalstart
  3. 创建了 app 对象后,添加了新的 router 方法,并重新定义了 create() 返回的 start 方法。

Model

执行对象 app 所拥有的 model 方法,所在源文件为 index.js

相关主要源码
  function model(m) {
    const prefixedModel = prefixNamespace({ ...m });
    app._models.push(prefixedModel);
    return prefixedModel;
  }

prefixNamespace 方法:

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;

  if (reducers) {
    if (isArray(reducers)) {
      model.reducers[0] = prefix(reducers[0], namespace, 'reducer');
    } else {
      model.reducers = prefix(reducers, namespace, 'reducer');
    }
  }
  if (effects) {
    model.effects = prefix(effects, namespace, 'effect');
  }
  return model;
}

从代码中可以发现:

  1. model 方法将传入的对象,根据特定的字段 namespacereducerseffects 分别进行归类,并放入到统一的字段 _modal 中。
  2. 注意这里重新组装的时候,reducerseffects 都在原有的基础上,添加了 namespace/ 的格式前缀。

View

这步是主要的显示逻辑,计数器示例中使用了 connect 方法,该方法从全局的 store 中匹配对应的 state 数据,然后传入到组件中。

Router

router 方法在第一步 Initialize 中就已经定义好了:

  function router(router) {
    invariant(
      isFunction(router),
      `[app.router] router should be function, but got ${typeof router}`,
    );
    app._router = router;
  }

计数器示例中传入了一个函数,作用是进行路由注册,并放入到另外一个变量 _router 中,在后面就会用到。

Start

start 方法也在第一步 Initialize 中就已经定义好了:

  function start(container) {
    // 允许 container 是字符串,然后用 querySelector 找元素
    if (isString(container)) {
      container = document.querySelector(container);
    }

    // 并且是 HTMLElement
    invariant(
      !container || isHTMLElement(container),
      `[app.start] container should be HTMLElement`,
    );

    // 路由必须提前注册
    invariant(app._router, `[app.start] router must be registered before app.start()`);

    if (!app._store) {
      oldAppStart.call(app);
    }
    const store = app._store;

    // export _getProvider for HMR
    // ref: https://github.com/dvajs/dva/issues/469
    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);
    }
  }

从代码中可以发现:

  1. 首先是找到渲染的容器。
  2. 在前面初始化中 _storenull ,于是第一次会执行 create() 返回对象中的 start 方法,该方法将 redux 和 redux-saga 进行了初始化并关联,强化了原有的 model 方法,添加了 unmodelreplaceModel 方法,为后续做好了数据和方法准备。
  3. 最后使用了 Provider 进行包裹渲染,这个是跟前面 connect 方法相呼应。

参考资料

🗑️

DVA 的本质实际上是这个啊!

55-poster