一文彻底搞懂 DvaJS 原理

yield call(delay, 1000);

yield put({ type: ‘add’ });

},

},

});

// 注册视图

app.router(() => );

// 启动应用

app.start(‘#root’);

Dva底层原理和部分关键实现


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

  2. dva 是个函数,返回一了个 app 的对象。

  3. 目前 dva 的源码核心部分包含两部分,dva 和 dva-core。前者用高阶组件 React-redux 实现了 view 层,后者是用 redux-saga 解决了 model 层。

dva[15]

dva 做了三件比较重要的事情:

  1. 代理 router 和 start 方法,实例化 app 对象

  2. 调用 dva-core 的 start 方法,同时渲染视图

  3. 使用 react-redux 完成了 react 到 redux 的连接。

// dva/src/index.js

export default function (opts = {}) {

// 1. 使用 connect-react-router 和 history 初始化 router 和 history

// 通过添加 redux 的中间件 react-redux-router,强化了 history 对象的功能

const history = opts.history || createHashHistory();

const createOpts = {

initialReducer: {

router: connectRouter(history),

},

setupMiddlewares(middlewares) {

return [routerMiddleware(history), …middlewares];

},

setupApp(app) {

app._history = patchHistory(history);

},

};

// 2. 调用 dva-core 里的 create 方法 ,函数内实例化一个 app 对象。

const app = create(opts, createOpts);

const oldAppStart = app.start;

// 3. 用自定义的 router 和 start 方法代理

app.router = router;

app.start = start;

return app;

// 3.1 绑定用户传递的 router 到 app._router

function router(router) {

invariant(

isFunction(router),

[app.router] router should be function, but got ${typeof router},

);

app._router = router;

}

// 3.2 调用 dva-core 的 start 方法,并渲染视图

function start(container) {

// 对 container 做一系列检查,并根据 container 找到对应的DOM节点

if (!app._store) {

oldAppStart.call(app);

}

const store = app._store;

// 为HMR暴露_getProvider接口

// ref: https://github.com/dvajs/dva/issues/469

app._getProvider = getProvider.bind(null, store, app);

// 渲染视图

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 => (

{router({ app, history: app._history, …extraProps })}

);

return DvaRoot;

}

function render(container, store, app, router) {

const ReactDOM = require(‘react-dom’); // eslint-disable-line

ReactDOM.render(React.createElement(getProvider(store, app, router)), container);

}

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

dva-core[16]

dva-core 则完成了核心功能:

  1. 通过 create 方法完成 app 实例的构造,并暴露 use、model 和 start 三个接口

  2. 通过 start 方法完成

  • store 的初始化

  • models 和 effects 的封装,收集并运行 sagas

  • 运行所有的 model.subscriptions

  • 暴露 app.model、app.unmodel、app.replaceModel 三个接口

dva-core create

作用: 完成 app 实例的构造,并暴露 use、model 和 start 三个接口

// 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;

}

dva-core start

作用:

  1. 封装models 和 effects ,收集并运行 sagas

  2. 完成store 的初始化

  3. 运行所有的model.subscriptions

  4. 暴露app.model、app.unmodel、app.replaceModel三个接口

function start() {

const sagaMiddleware = createSagaMiddleware();

const promiseMiddleware = createPromiseMiddleware(app);

app._getSaga = getSaga.bind(null);

const sagas = [];

const reducers = { …initialReducer };

for (const m of app._models) {

// 把每个 model 合并为一个reducer,key 是 namespace 的值,value 是 reducer 函数

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

if (m.effects) {

// 收集每个 effects 到 sagas 数组

sagas.push(app._getSaga(m.effects, m, onError, plugin.get(‘onEffect’), hooksAndOpts));

}

}

// 初始化 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 = {};

// Execute listeners when state is changed

const listeners = plugin.get(‘onStateChange’);

for (const listener of listeners) {

store.subscribe(() => {

listener(store.getState());

});

}

// Run sagas, 调用 Redux-Saga 的 createSagaMiddleware 创建 saga中间件,调用中间件的 run 方法所有收集起来的异步方法

// run方法监听每一个副作用action,当action发生的时候,执行对应的 saga

sagas.forEach(sagaMiddleware.run);

// Setup app

setupApp(app);

// 运行 subscriptions

const unlisteners = {};

for (const model of this._models) {

if (model.subscriptions) {

unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError);

}

}

// 暴露三个 Model 相关的接口,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);

/**

* Create global reducer for redux.

* @returns {Object}

*/

function createReducer() {

return reducerEnhancer(

combineReducers({

…reducers,

…extraReducers,

…(app._store ? app._store.asyncReducers : {}),

}),

);

}

}

}

路由

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

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

},

};

其中 initialReducer 和 setupMiddlewares 在初始化 store 时调用,然后才调用 setupApp

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

Dva 与 React、React-Redux、Redux-Saga 之间的差异


原生 React

按照 React 官方指导意见, 如果多个 Component 之间要发生交互, 那么状态(即: 数据)就维护在这些 Component 的最小公约父节点上, 也即是

以及 本身不维持任何 state, 完全由父节点 传入 props 以决定其展现, 是一个纯函数的存在形式, 即: Pure Component

React-Redux

与上图相比, 几个明显的改进点:

  1. 状态及页面逻辑从 里面抽取出来, 成为独立的 store, 页面逻辑就是 reducer

  2. 及都是 Pure Component, 通过 connect 方法可以很方便地给它俩加一层 wrapper 从而建立起与 store 的联系: 可以通过 dispatch 向 store 注入 action, 促使 store 的状态进行变化, 同时又订阅了 store 的状态变化, 一旦状态变化, 被 connect 的组件也随之刷新

  3. 使用 dispatch 往 store 发送 action 的这个过程是可以被拦截的, 自然而然地就可以在这里增加各种 Middleware, 实现各种自定义功能, eg: logging

这样一来, 各个部分各司其职, 耦合度更低, 复用度更高, 扩展性更好。

Redux-Saga

因为我们可以使用 Middleware 拦截 action, 这样一来异步的网络操作也就很方便了, 做成一个 Middleware 就行了, 这里使用 redux-saga 这个类库, 举个栗子:

  1. 点击创建 Todo 的按钮, 发起一个 type == addTodo 的 action

  2. saga 拦截这个 action, 发起 http 请求, 如果请求成功, 则继续向 reducer 发一个 type == addTodoSucc 的 action, 提示创建成功, 反之则发送 type == addTodoFail 的 action 即可

Dva

有了前面三步的铺垫, Dva 的出现也就水到渠成了, 正如 Dva 官网所言, Dva 是基于 React + Redux + Saga 的最佳实践, 对于提升编码体验有三点贡献:

  1. 把 store 及 saga 统一为一个 model 的概念, 写在一个 js 文件里面

  2. 增加了一个 Subscriptions, 用于收集其他来源的 action, 比如键盘操作等

  3. model 写法很简约, 类似于 DSL(领域特定语言),可以提升编程的沉浸感,进而提升效率

约定大于配置

app.model({

namespace: ‘count’,

state: {

record: 0,

current: 0,

},

reducers: {

add(state) {

const newCurrent = state.current + 1;

return { …state,

record: newCurrent > state.record ? newCurrent : state.record,

current: newCurrent,

};

},

minus(state) {

return { …state, current: state.current - 1};

},

},

effects: {

*add(action, { call, put }) {

yield call(delay, 1000);

yield put({ type: ‘minus’ });

},

},

subscriptions: {

keyboardWatcher({ dispatch }) {

key(‘⌘+up, ctrl+up’, () => { dispatch({type:‘add’}) });

},

},

});

Dva 背后值得学习的思想


Dva 的 api 参考了choo[17],概念来自于 elm。

  1. Choo 的理念:编程应该是有趣且轻松的,API 要看上去简单易用。

We believe programming should be fun and light, not stern and stressful. It’s cool to be cute; using serious words without explaining them doesn’t make for better results - if anything it scares people off. We don’t want to be scary, we want to be nice and fun, and then_casually_be the best choice around._Real casually._

We believe frameworks should be disposable, and components recyclable. We don’t want a web where walled gardens jealously compete with one another. By making the DOM the lowest common denominator, switching from one framework to another becomes frictionless. Choo is modest in its design; we don’t believe it will be top of the class forever, so we’ve made it as easy to toss out as it is to pick up.

We don’t believe that bigger is better. Big APIs, large complexities, long files - we see them as omens of impending userland complexity. We want everyone on a team, no matter the size, to fully understand how an application is laid out. And once an application is built, we want it to be small, performant and easy to reason about. All of which makes for easy to debug code, better results and super smiley faces.

  1. 来自 Elm 的概念:
  • Subscription,订阅,从源头获取数据,数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。

参考资料

[1]

redux: https://github.com/reduxjs/redux

[2]

redux-saga: https://github.com/redux-saga/redux-saga

[3]

react-router: https://github.com/ReactTraining/react-router

[4]

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

最后:

总结来说,面试成功=基础知识+项目经验+表达技巧+运气。我们无法控制运气,但是我们可以在别的地方花更多时间,每个环节都提前做好准备。

面试一方面是为了找到工作,升职加薪,另一方面也是对于自我能力的考察。能够面试成功不仅仅是来自面试前的临时抱佛脚,更重要的是在平时学习和工作中不断积累和坚持,把每个知识点、每一次项目开发、每次遇到的难点知识,做好积累,实践和总结。

点击这里领取Web前端开发经典面试题

往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-IqV96s7n-1713637911409)]

[外链图片转存中…(img-73a0DI74-1713637911410)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

[外链图片转存中…(img-oHDwVurr-1713637911410)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

[外链图片转存中…(img-kNpETZQK-1713637911410)]

最后:

总结来说,面试成功=基础知识+项目经验+表达技巧+运气。我们无法控制运气,但是我们可以在别的地方花更多时间,每个环节都提前做好准备。

面试一方面是为了找到工作,升职加薪,另一方面也是对于自我能力的考察。能够面试成功不仅仅是来自面试前的临时抱佛脚,更重要的是在平时学习和工作中不断积累和坚持,把每个知识点、每一次项目开发、每次遇到的难点知识,做好积累,实践和总结。

点击这里领取Web前端开发经典面试题

  • 25
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值