Dva 源碼解析
作者:楊光
dva官網(wǎng)源碼解析
隱藏在 package.json 里的秘密
隨便哪個 dva 的項目并淋,只要敲入 npm start 就可以運行啟動。之前敲了無數(shù)次我都沒有在意绳军,直到我準備研究源碼的時候才意識到:在敲下這行命令的時候锨推,到底發(fā)生了什么呢?
答案要去 package.json 里去尋找余佃。
有位技術(shù)大牛曾經(jīng)告訴過我:看源碼之前蔽氨,先去看 package.json 藐唠。看看項目的入口文件孵滞,翻翻它用了哪些依賴中捆,對項目便有了大致的概念。
package.json 里是這么寫的:
"scripts": {
"start": "roadhog server"
},
翻翻依賴坊饶,"roadhog": "^0.5.2"
。
既然能在 devDependencies 找到蟋滴,那么肯定也能在 npm 上找到津函。原來是個和 webpack 相似的庫尔苦,而且作者看著有點眼熟...
如果說 dva 是親女兒,那 roadhog 就是親哥哥了允坚,起的是 webpack 自動打包和熱更替的作用魂那。
在 roadhog 的默認配置里有這么一條信息:
{
"entry": "src/index.js",
}
后轉(zhuǎn)了一圈,啟動的入口回到了 src/index.js
稠项。
src/index.js
在 src/index.js
里涯雅,dva 一共做了這么幾件事:
從 'dva' 依賴中引入 dva :
import dva from 'dva'
;通過函數(shù)生成一個 app 對象:
const app = dva()
;加載插件:
app.use({})
;注入 model:
app.model(require('./models/example'))
;添加路由:
app.router(require('./routes/indexAnother'))
;啟動:app.start('#root');
在這 6 步當中,dva 完成了 使用 React 解決 view 層
展运、redux 管理 model
活逆、saga 解決異步
的主要功能。事實上在我查閱資料以及回憶用過的腳手架時拗胜,發(fā)現(xiàn)目前端框架之所以被稱為“框架”也就是解決了這些事情蔗候。前端工程師至今所做的事情都是在 分離動態(tài)的 data 和靜態(tài)的 view ,只不過側(cè)重點和實現(xiàn)方式也不同埂软。
至今為止出了這么多框架琴庵,但是前端 MVX 的思想一直都沒有改變。
dva
尋找 “dva”
既然 dva 是來自于 dva
仰美,那么 dva 是什么這個問題自然要去 dva 的源碼中尋找了。
劇透:dva 是個函數(shù)诉字,返回一了個 app 的對象琅轧。
劇透2:目前 dva 的源碼核心部分包含兩部分,
dva
和dva-core
乍桂。前者用高階組件 React-redux 實現(xiàn)了 view 層睹酌,后者是用 redux-saga 解決了 model 層权谁。
老規(guī)矩,還是先翻 package.json 旺芽。
引用依賴很好的說明了 dva 的功能:統(tǒng)一 view 層洗出。
// dva 使用的依賴如下:
"babel-runtime": "^6.26.0", // 一個編譯后文件引用的公共庫菠镇,可以有效減少編譯后的文件體積
"dva-core": "^1.1.0", // dva 另一個核心隘梨,用于處理數(shù)據(jù)層
"global": "^4.3.2", // 用于提供全局函數(shù)的引用
"history": "^4.6.3", // browserHistory 或者 hashHistory
"invariant": "^2.2.2", // 一個有趣的斷言庫
"isomorphic-fetch": "^2.2.1", // 方便請求異步的函數(shù)捻脖,dva 中的 fetch 來源
"react-async-component": "^1.0.0-beta.3", // 組件懶加載
"react-redux": "^5.0.5", // 提供了一個高階組件矛渴,方便在各處調(diào)用 store
"react-router-dom": "^4.1.2", // router4桂躏,終于可以像寫組件一樣寫 router 了
"react-router-redux": "5.0.0-alpha.6",// redux 的中間件,在 provider 里可以嵌套 router
"redux": "^3.7.2" // 提供了 store川陆、dispatch、reducer
不過 script 沒有給太多有用的信息,因為 ruban build
中的 ruban
顯然是個私人庫(雖然在 tnpm 上可以查到但是也是私人庫)拂封。但根據(jù)慣例,應(yīng)該是 dva 包下的 index.js
文件提供了對外調(diào)用:
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = require('./lib');
exports.connect = require('react-redux').connect;
顯然這個 exports.default
就是我們要找的 dva,但是源碼中沒有 ./lib
文件夾缘回。當然直接看也應(yīng)該看不懂,因為一般都是使用 babel 的命令 babel src -d libs
進行編譯后生成的在孝,所以直接去看 src/index.js
文件魔招。
src/index.js
src/index.js
在此 :
在這里,dva 做了三件比較重要的事情:
- 使用 call 給 dva-core 實例化的 app(這個時候還只有數(shù)據(jù)層) 的 start 方法增加了一些新功能(或者說髓迎,通過代理模式給 model 層增加了 view 層)。
- 使用 react-redux 完成了 react 到 redux 的連接。
- 添加了 redux 的中間件 react-redux-router床牧,強化了 history 對象的功能。
使用 call 方法實現(xiàn)代理模式
dva 中實現(xiàn)代理模式的方式如下:
1. 新建 function 暂吉,函數(shù)內(nèi)實例化一個 app 對象。 2. 新建變量指向該對象希望代理的方法绕辖, oldStart = app.start
围小。 3. 新建同名方法 start刘绣,在其中使用 call绢馍,指定 oldStart 的調(diào)用者為 app。 4. 令 app.start = start源请,完成對 app 對象的 start 方法的代理决瞳。
上代碼:
export default function(opts = {}) {
// ...初始化 route 胸囱,和添加 route 中間件的方法允蜈。
/**
* 1\. 新建 function 蛤克,函數(shù)內(nèi)實例化一個 app 對象彻犁。
*
*/
const app = core.create(opts, createOpts);
/**
* 2\. 新建變量指向該對象希望代理的方法
*
*/
const oldAppStart = app.start;
app.router = router;
/**
* 4\. 令 app.start = start豺型,完成對 app 對象的 start 方法的代理仲智。
* @type {[type]}
*/
app.start = start;
return app;
// router 賦值
/**
* 3.1 新建同名方法 start,
*
*/
function start(container) {
// 合法性檢測代碼
/**
* 3.2 在其中使用 call姻氨,指定 oldStart 的調(diào)用者為 app钓辆。
*/
oldAppStart.call(app);
// 因為有 3.2 的執(zhí)行才有現(xiàn)在的 store
const store = app._store;
// 使用高階組件創(chuàng)建視圖
}
}
為什么不直接在 start 方式中 oldAppStart ?
- 因為 dva-core 的 start 方法里有用到 this肴焊,不用 call 指定調(diào)用者為 app 的話似嗤,oldAppStart() 會找錯對象。
實現(xiàn)代理模式一定要用到 call 嗎届宠?
- 不一定烁落,看有沒有 使用 this 或者代理的函數(shù)是不是箭頭函數(shù)。從另一個角度來說豌注,如果使用了 function 關(guān)鍵字又在內(nèi)部使用了 this伤塌,那么一定要用 call/apply/bind 指定 this。
前端還有那里會用到 call 幌羞?
- 就實際開發(fā)來講寸谜,因為已經(jīng)使用了 es6 標準竟稳,基本和 this 沒什么打交道的機會属桦。使用 class 類型的組件中偶爾還會用到 this.xxx.bind(this)熊痴,stateless 組件就洗洗睡吧(因為壓根沒有 this)。如果實現(xiàn)代理聂宾,可以使用繼承/反向繼承的方法 —— 比如高階組件果善。
使用 react-redux 的高階組件傳遞 store
經(jīng)過 call 代理后的 start 方法的主要作用,便是使用 react-redux 的 provider 組件將數(shù)據(jù)與視圖聯(lián)系了起來系谐,生成 React 元素呈現(xiàn)給使用者巾陕。
不多說,上代碼纪他。
// 使用 querySelector 獲得 dom
if (isString(container)) {
container = document.querySelector(container);
invariant(
container,
`[app.start] container ${container} not found`,
);
}
// 其他代碼
// 實例化 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
// 如果有真實的 dom 對象就把 react 拍進去
if (container) {
render(container, store, app, app._router);
// 熱加載在這里
app._plugin.apply('onHmr')(render.bind(null, container, store, app));
} else {
// 否則就生成一個 react 鄙煤,供外界調(diào)用
return getProvider(store, this, this._router);
}
// 使用高階組件包裹組件
function getProvider(store, app, router) {
return extraProps => (
<Provider store={store}>
{ router({ app, history: app._history, ...extraProps }) }
</Provider>
);
}
// 真正的 react 在這里
function render(container, store, app, router) {
const ReactDOM = require('react-dom'); // eslint-disable-line
ReactDOM.render(React.createElement(getProvider(store, app, router)), container);
}
React.createElement(getProvider(store, app, router)) 怎么理解?
- getProvider 實際上返回的不單純是函數(shù)茶袒,而是一個無狀態(tài)的 React 組件梯刚。從這個角度理解的話,ReactElement.createElement(string/ReactClass type,[object props],[children ...]) 是可以這么寫的薪寓。
怎么理解 React 的 stateless 組件和 class 組件亡资?
- 你猜猜?
JavaScript 并不存在 class 這個東西向叉,即便是 es6 引入了以后經(jīng)過 babel 編譯也會轉(zhuǎn)換成函數(shù)锥腻。因此直接使用無狀態(tài)組件,省去了將 class 實例化再調(diào)用 render 函數(shù)的過程母谎,有效的加快了渲染速度瘦黑。
即便是 class 組件,React.createElement 最終調(diào)用的也是 render 函數(shù)销睁。不過這個目前只是我的推論供璧,沒有代碼證據(jù)的證明。
react-redux 與 provider
provider 是個什么東西冻记?
本質(zhì)上是個高階組件睡毒,也是代理模式的一種實踐方式。接收 redux 生成的 store 做參數(shù)后冗栗,通過上下文 context 將 store 傳遞進被代理組件演顾。在保留原組件的功能不變的同時,增加了 store 的 dispatch 等方法隅居。
connect 是個什么東西钠至?
connect 也是一個代理模式實現(xiàn)的高階組件,為被代理的組件實現(xiàn)了從 context 中獲得 store 的方法胎源。
connect()(MyComponent) 時發(fā)生了什么棉钧?
只放關(guān)鍵部分代碼,因為我也只看懂了關(guān)鍵部分(捂臉跑):
import connectAdvanced from '../components/connectAdvanced'
export function createConnect({
connectHOC = connectAdvanced,
.... 其他初始值
} = {}) {
return function connect( { // 0 號 connnect
mapStateToProps,
mapDispatchToProps,
... 其他初始值
} = {}
) {
....其他邏輯
return connectHOC(selectorFactory, {// 1號 connect
.... 默認參數(shù)
selectorFactory 也是個默認參數(shù)
})
}
}
export default createConnect() // 這是 connect 的本體涕蚤,導(dǎo)出時即生成 connect 0
// hoist-non-react-statics宪卿,會自動把所有綁定在對象上的非React方法都綁定到新的對象上
import hoistStatics from 'hoist-non-react-statics'
// 1號 connect 的本體
export default function connectAdvanced() {
// 邏輯處理
// 1 號 connect 調(diào)用時生成 2 號 connect
return function wrapWithConnect(WrappedComponent) {
// ... 邏輯處理
// 在函數(shù)內(nèi)定義了一個可以拿到上下文對象中 store 的組件
class Connect extends Component {
getChildContext() {
// 上下文對象中獲得 store
const subscription = this.propsMode ? null : this.subscription
return { [subscriptionKey]: subscription || this.context[subscriptionKey] }
}
// 邏輯處理
render() {
// 最終生成了新的 react 元素的诵,并添加了新屬性
return createElement(WrappedComponent, this.addExtraProps(selector.props))
}
}
// 邏輯處理
// 最后用定義的 class 和 被代理的組件生成新的 react 組件
return hoistStatics(Connect, WrappedComponent) // 2 號函數(shù)調(diào)用后生成的對象是組件
}
}
結(jié)論:對于 connect()(MyComponent)
- connect 調(diào)用時生成 0 號 connect
- connect() 0 號 connect 調(diào)用,返回 1 號 connect 的調(diào)用
connectHOC()
佑钾,生成 2 號 connect(也是個函數(shù)) 西疤。 - connect()(MyComponent) 等價于 connect2(MyComponent),返回值是一個新的組件
redux 與 router
redux 是狀態(tài)管理的庫休溶,router 是(唯一)控制頁面跳轉(zhuǎn)的庫代赁。兩者都很美好,但是不美好的是兩者無法協(xié)同工作兽掰。換句話說芭碍,當路由變化以后,store 無法感知到孽尽。
于是便有了 react-router-redux
豁跑。
react-router-redux
是 redux 的一個中間件(中間件:JavaScript 代理模式的另一種實踐 針對 dispatch 實現(xiàn)了方法的代理,在 dispatch action 的時候增加或者修改) 泻云,主要作用是:
加強了React Router庫中history這個實例艇拍,以允許將history中接受到的變化反應(yīng)到state中去。
從代碼上講宠纯,主要是監(jiān)聽了 history 的變化:
history.listen(location => analyticsService.track(location.pathname))
dva 在此基礎(chǔ)上又進行了一層代理卸夕,把代理后的對象當作初始值傳遞給了 dva-core,方便其在 model 的 subscriptions 中監(jiān)聽 router 變化婆瓜。
看看 index.js
里 router 的實現(xiàn):
1.在 createOpts 中初始化了添加 react-router-redux 中間件的方法和其 reducer 快集,方便 dva-core 在創(chuàng)建 store 的時候直接調(diào)用。
- 使用 patchHistory 函數(shù)代理 history.linsten廉白,增加了一個回調(diào)函數(shù)的做參數(shù)(也就是訂閱)个初。
subscriptions 的東西可以放在 dva-core 里再說,
import createHashHistory from 'history/createHashHistory';
import {
routerMiddleware,
routerReducer as routing,
} from 'react-router-redux';
import * as core from 'dva-core';
export default function (opts = {}) {
const history = opts.history || createHashHistory();
const createOpts = {
// 初始化 react-router-redux 的 router
initialReducer: {
routing,
},
// 初始化 react-router-redux 添加中間件的方法猴蹂,放在所有中間件最前面
setupMiddlewares(middlewares) {
return [
routerMiddleware(history),
...middlewares,
];
},
// 使用代理模式為 history 對象增加新功能院溺,并賦給 app
setupApp(app) {
app._history = patchHistory(history);
},
};
const app = core.create(opts, createOpts);
const oldAppStart = app.start;
app.router = router;
app.start = start;
return app;
function router(router) {
invariant(
isFunction(router),
`[app.router] router should be function, but got ${typeof router}`,
);
app._router = router;
}
}
// 使用代理模式擴展 history 對象的 listen 方法,添加了一個回調(diào)函數(shù)做參數(shù)并在路由變化是主動調(diào)用
function patchHistory(history) {
const oldListen = history.listen;
history.listen = (callback) => {
callback(history.location);
return oldListen.call(history, callback);
};
return history;
}
劇透:redux 中創(chuàng)建 store 的方法為:
// combineReducers 接收的參數(shù)是對象
// 所以 initialReducer 的類型是對象
// 作用:將對象中所有的 reducer 組合成一個大的 reducer
const reducers = {};
// applyMiddleware 接收的參數(shù)是可變參數(shù)
// 所以 middleware 是數(shù)組
// 作用:將所有中間件組成一個數(shù)組磅轻,依次執(zhí)行
const middleware = [];
const store = createStore(
combineReducers(reducers),
initial_state, // 設(shè)置 state 的初始值
applyMiddleware(...middleware)
);
視圖與數(shù)據(jù)(上)
src/index.js
主要實現(xiàn)了 dva 的 view 層珍逸,同時傳遞了一些初始化數(shù)據(jù)到 dva-core 所實現(xiàn)的 model 層。當然聋溜,還提供了一些 dva 中常用的方法函數(shù):
-
dynamic
動態(tài)加載(2.0 以后官方提供 1.x 自己手動實現(xiàn)吧) -
fetch
請求方法(其實 dva 只是做了一把搬運工) -
saga
(數(shù)據(jù)層處理異步的方法)谆膳。
這么看 dva 真的是很薄的一層封裝。
而 dva-core 主要解決了 model 的問題撮躁,包括 state 管理漱病、數(shù)據(jù)的異步加載、訂閱-發(fā)布模式的實現(xiàn),可以作為數(shù)據(jù)層在別處使用(看 2.0 更新也確實是作者的意圖)杨帽。使用的狀體啊管理庫還是 redux凝果,異步加載的解決方案是 saga。當然睦尽,一切也都寫在 index.js 和 package.json 里。
視圖與數(shù)據(jù)(下)
處理 React 的 model 層問題有很多種辦法型雳,比如狀態(tài)管理就不一定要用 Redux当凡,也可以使用 Mobx(寫法會更有 MVX 框架的感覺);異步數(shù)據(jù)流也未必使用 redux-saga纠俭,redux-thunk 或者 redux-promise 的解決方式也可以(不過目前看來 saga 是相對更優(yōu)雅的)沿量。
放兩篇個人感覺比較全面的技術(shù)文檔:
以及兩者的 github:
然后繼續(xù)深扒 dva-core
朴则,還是先從 package.json
扒起。
package.json
dva-core
的 package.json
中依賴包如下:
"babel-runtime": "^6.26.0", // 一個編譯后文件引用的公共庫钓简,可以有效減少編譯后的文件體積
"flatten": "^1.0.2", // 一個將多個數(shù)組值合并成一個數(shù)組的庫
"global": "^4.3.2",// 用于提供全局函數(shù)比如 document 的引用
"invariant": "^2.2.1",// 一個有趣的斷言庫
"is-plain-object": "^2.0.3", // 判斷是否是一個對象
"redux": "^3.7.1", // redux 乌妒,管理 react 狀態(tài)的庫
"redux-saga": "^0.15.4", // 處理異步數(shù)據(jù)流
"warning": "^3.0.0" // 同樣是個斷言庫,不過輸出的是警告
當然因為打包還是用的 ruban
外邓,script 里沒有什么太多有用的東西撤蚊。繼續(xù)依循慣例,去翻 src/index.js
损话。
src/index.js
src/index
的源碼在這里
在 dva
的 src/index.js
里侦啸,通過傳遞 2 個變量 opts
和 createOpts
并調(diào)用 core.create
,dva
創(chuàng)建了一個 app 對象丧枪。其中 opts
是使用者添加的控制選項光涂,createOpts
則是初始化了 reducer 與 redux 的中間件。
dva-core
的 src/index.js
里便是這個 app 對象的具體創(chuàng)建過程以及包含的方法:
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;
// .... 方法的實現(xiàn)
function model(){
// model 方法
}
functoin start(){
// Start 方法
}
}
我最開始很不習(xí)慣 JavaScript 就是因為 JavaScript 還是一個函數(shù)向的編程語言拧烦,也就是函數(shù)里可以定義函數(shù)忘闻,返回值也可以是函數(shù),class 最后也是被解釋成函數(shù)恋博。在 dva-core 里創(chuàng)建了 app 對象服赎,但是把 model 和 start 的定義放在了后面。一開始對這種簡寫沒看懂交播,后來熟悉了以后發(fā)現(xiàn)確實好理解重虑。一眼就可以看到 app 所包含的方法,如果需要研究具體方法的話才需要向后看秦士。
Plugin 是作者設(shè)置的一堆鉤子性監(jiān)聽函數(shù)——即是在符合某些條件的情況下下(dva 作者)進行手動調(diào)用缺厉。這樣使用者只要按照作者設(shè)定過的關(guān)鍵詞傳遞回調(diào)函數(shù),在這些條件下便會自動觸發(fā)。
有趣的是提针,我最初理解鉤子的概念是在 Angular 里命爬。為了能像 React 一樣優(yōu)雅的控制組件的生命周期,Angular 設(shè)置了一堆接口(因為使用的是 ts辐脖,所以 Angular 里有類和接口的區(qū)分)饲宛。只要組件實現(xiàn)(implements)對應(yīng)的接口————或者稱生命周期鉤子,在對應(yīng)的條件下就會運行接口的方法嗜价。
Plugin 與 plugin.use
Plugin 與 plugin.use 都有使用數(shù)組的 reduce 方法的行為:
const hooks = [
'onError',
'onStateChange',
'onAction',
'onHmr',
'onReducer',
'onEffect',
'extraReducers',
'extraEnhancers',
];
export function filterHooks(obj) {
return Object.keys(obj).reduce((memo, key) => {
// 如果對象的 key 在 hooks 數(shù)組中
// 為 memo 對象添加新的 key艇抠,值為 obj 對應(yīng) key 的值
if (hooks.indexOf(key) > -1) {
memo[key] = obj[key];
}
return memo;
}, {});
}
export default class Plugin {
constructor() {
this.hooks = hooks.reduce((memo, key) => {
memo[key] = [];
return memo;
}, {});
/*
等同于
this.hooks = {
onError: [],
onStateChange:[],
....
extraEnhancers: []
}
*/
}
use(plugin) {
invariant(isPlainObject(plugin), 'plugin.use: plugin should be plain object');
const hooks = this.hooks;
for (const key in plugin) {
if (Object.prototype.hasOwnProperty.call(plugin, key)) {
invariant(hooks[key], `plugin.use: unknown plugin property: ${key}`);
if (key === 'extraEnhancers') {
hooks[key] = plugin[key];
} else {
hooks[key].push(plugin[key]);
}
}
}
}
// 其他方法
}
構(gòu)造器中的
reduce
初始化了一個以hooks
數(shù)組所有元素為 key,值為空數(shù)組的對象久锥,并賦給了 class 的私有變量this.hooks
家淤。filterHooks
通過reduce
過濾了hooks
數(shù)組以外的鉤子。use
中使用hasOwnProperty
判斷key
是plugin
的自身屬性還是繼承屬性瑟由,使用原型鏈調(diào)用而不是plugin.hasOwnProperty()
是防止使用者故意搗亂在plugin
自己寫一個hasOwnProperty = () => false // 這樣無論如何調(diào)用 plugin.hasOwnProperty() 返回值都是 false
絮重。use
中使用reduce
為this.hooks
添加了plugin[key]
。
model 方法
model
是 app 添加 model 的方法歹苦,在 dva 項目 的 index.js 是這么用的青伤。
app.model(require('./models/example'));
在 dva
中沒對 model 做任何處理,所以 dva-core
中的 model 就是 dva 項目 里調(diào)用的 model殴瘦。
function model(m) {
if (process.env.NODE_ENV !== 'production') {
checkModel(m, app._models);
}
app._models.push(prefixNamespace(m));
}
checkModel
主要是用invariant
對傳入的 model 進行了合法性檢查潮模。prefixNamespace
又使用 reduce 對每一個 model 做處理,為 model 的 reducers 和 effects 中的方法添加了${namespace}/
的前綴痴施。
Ever wonder why we dispatch the action like this in dva ?
dispatch({type: 'example/loadDashboard'
start 方法
start
方法是 dva-core
的核心擎厢,在 start
方法里,dva 完成了 store
初始化 以及 redux-saga
的調(diào)用辣吃。比起 dva
的 start
动遭,它引入了更多的調(diào)用方式。
一步一步分析:
onError
const onError = (err) => {
if (err) {
if (typeof err === 'string') err = new Error(err);
err.preventDefault = () => {
err._dontReject = true;
};
plugin.apply('onError', (err) => {
throw new Error(err.stack || err);
})(err, app._store.dispatch);
}
};
這是一個全局錯誤處理神得,返回了一個接收錯誤并處理的函數(shù)厘惦,并以 err
和 app._store.dispatch
為參數(shù)執(zhí)行調(diào)用。
看一下 plugin.apply
的實現(xiàn):
apply(key, defaultHandler) {
const hooks = this.hooks;
/* 通過 validApplyHooks 進行過濾哩簿, apply 方法只能應(yīng)用在全局報錯或者熱更替上 */
const validApplyHooks = ['onError', 'onHmr'];
invariant(validApplyHooks.indexOf(key) > -1, `plugin.apply: hook ${key} cannot be applied`);
/* 從鉤子中拿出掛載的回調(diào)函數(shù) 宵蕉,掛載動作見 use 部分*/
const fns = hooks[key];
return (...args) => {
// 如果有回調(diào)執(zhí)行回調(diào)
if (fns.length) {
for (const fn of fns) {
fn(...args);
}
// 沒有回調(diào)直接拋出錯誤
} else if (defaultHandler) {
defaultHandler(...args);
/*
這里 defaultHandler 為 (err) => {
throw new Error(err.stack || err);
}
*/
}
};
}
sagaMiddleware
下一行代碼是:
const sagaMiddleware = createSagaMiddleware();
和 redux-sagas
的入門教程有點差異,因為正統(tǒng)的教程上添加 sagas 中間件的方法是: createSagaMiddleware(...sagas)
sagas 為含有 saga 方法的 generator 函數(shù)數(shù)組节榜。
但是 api 里確實還提到羡玛,還有一~~招從天而降的掌法~~種動態(tài)調(diào)用的方式:
const task = sagaMiddleware.run(dynamicSaga)
于是:
const sagaMiddleware = createSagaMiddleware();
// ...
const sagas = [];
const reducers = {...initialReducer
};
for (const m of app._models) {
reducers[m.namespace] = getReducer(m.reducers, m.state);
if (m.effects) sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));
}
// ....
store.runSaga = sagaMiddleware.run;
// Run sagas
sagas.forEach(sagaMiddleware.run);
sagas
那么 sagas 是什么呢?
const {
middleware: promiseMiddleware,
resolve,
reject,
} = createPromiseMiddleware(app);
app._getSaga = getSaga.bind(null, resolve, reject);
const sagas = [];
for (const m of app._models) {
if (m.effects) sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));
}
顯然宗苍,sagas 是一個數(shù)組稼稿,里面的元素是用 app._getSaga
處理后的返回結(jié)果薄榛,而 app._getSaga
又和上面 createPromiseMiddleware 代理 app 后返回的對象有很大關(guān)系。
createPromiseMiddleware
createPromiseMiddleware 的代碼在此让歼。
如果看著覺得眼熟敞恋,那肯定不是因為看過 redux-promise 源碼的緣故,:-p谋右。
middleware
middleware
是一個 redux 的中間件硬猫,即在不影響 redux 本身功能的情況下為其添加了新特性的代碼。redux 的中間件通過攔截 action 來實現(xiàn)其作用的改执。
const middleware = () => next => (action) => {
const { type } = action;
if (isEffect(type)) {
return new Promise((resolve, reject) => {
// .... resolve ,reject
});
} else {
return next(action);
}
};
function isEffect(type) {
// dva 里 action 的 type 有固定格式: model.namespace/model.effects
// const [namespace] = type.split(NAMESPACE_SEP); 是 es6 解構(gòu)的寫法
// 等同于 const namespace = type.split(NAMESPACE_SEP)[0];
// NAMESPACE_SEP 的值是 `/`
const [namespace] = type.split(NAMESPACE_SEP);
// 根據(jù) namespace 過濾出對應(yīng)的 model
const model = app._models.filter(m => m.namespace === namespace)[0];
// 如果 model 存在并且 model.effects[type] 也存在恍箭,那必然是 effects
if (model) {
if (model.effects && model.effects[type]) {
return true;
}
}
return false;
}
const middleware = ({dispatch}) => next => (action) => {... return next(action)} 基本上是一個標準的中間件寫法唱较。在 return next(action) 之前可以對 action 做各種各樣的操作雳刺。因為此中間件沒用到 dispatch 方法仲锄,所以省略了霞丧。
本段代碼的意思是呢岗,如果 dispatch 的 action 指向的是 model 里的 effects,那么返回一個 Promise 對象蛹尝。此 Promise 的對象的解決( resolve )或者駁回方法 ( reject ) 放在 map 對象中后豫。如果是非 effects (那就是 action 了),放行突那。
換句話說挫酿,middleware 攔截了指向 effects 的 action。
神奇的 bind
bind 的作用是綁定新的對象愕难,生成新函數(shù)是大家都知道概念早龟。但是 bind 也可以提前設(shè)定好函數(shù)的某些參數(shù)生成新函數(shù),等到最后一個參數(shù)確定時直接調(diào)用猫缭。
JavaScript 的參數(shù)是怎么被調(diào)用的葱弟?JavaScript 專題之函數(shù)柯里化。作者:冴羽猜丹。文章來源:掘金
這段代碼恰好就是 bind 的一種實踐方式芝加。
const map = {};
const middleware = () => next => (action) => {
const { type } = action;
// ...
return new Promise((resolve, reject) => {
map[type] = {
resolve: wrapped.bind(null, type, resolve),
reject: wrapped.bind(null, type, reject),
};
});
// ....
};
function wrapped(type, fn, args) {
if (map[type]) delete map[type];
fn(args);
}
function resolve(type, args) {
if (map[type]) {
map[type].resolve(args);
}
}
function reject(type, args) {
if (map[type]) {
map[type].reject(args);
}
}
return {
middleware,
resolve,
reject,
};
分析這段代碼,dva 是這樣做的:
- 通過
wrapped.bind(null, type, resolve)
產(chǎn)生了一個新函數(shù)射窒,并且賦值給匿名對象的 resolve 屬性(reject 同理)藏杖。
1.1 wrap 接收三個參數(shù),通過 bind 已經(jīng)設(shè)定好了兩個脉顿。
wrapped.bind(null, type, resolve)
等同于wrap(type, resolve, xxx)
(此處resolve
是 Promise 對象中的)蝌麸。
1.2 通過 bind 賦給匿名對象的 resolve 屬性后,匿名對象.resolve(xxxx) 等同于 wrap(type, resolve, xxx)艾疟,即 reslove(xxx)祥楣。
使用 type 在 map 對象中保存此匿名對象开财,而 type 是 action 的 type,即 namespace/effects 的形式误褪,方便之后進行調(diào)用责鳍。
return 出的 resolve 接收 type 和 args 兩個參數(shù)。type 用來在 map 中尋找 1 里的匿名函數(shù)兽间,args 用來像 1.2 里那樣執(zhí)行历葛。
這樣做的作用是:分離了 promise 與 promise 的執(zhí)行。在函數(shù)的作用域外依然可以訪問到函數(shù)的內(nèi)部變量嘀略,換言之:閉包恤溶。
getSaga
導(dǎo)出的 resolve
與 reject
方法,通過 bind 先設(shè)置進了 getSaga
(同時也賦給了 app._getSaga
)帜羊,sagas 最終也將 getSaga
的返回值放入了數(shù)組咒程。
export default function getSaga(resolve, reject, effects, model, onError, onEffect) {
return function *() {
for (const key in effects) {
if (Object.prototype.hasOwnProperty.call(effects, key)) {
const watcher = getWatcher(resolve, reject, key, effects[key], model, onError, onEffect);
// 將 watcher 分離到另一個線程去執(zhí)行
const task = yield sagaEffects.fork(watcher);
// 同時 fork 了一個線程,用于在 model 卸載后取消正在進行中的 task
// `${model.namespace}/@@CANCEL_EFFECTS` 的發(fā)出動作在 index.js 的 start 方法中讼育,unmodel 方法里帐姻。
yield sagaEffects.fork(function *() {
yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
yield sagaEffects.cancel(task);
});
}
}
};
}
可以看到,getSaga
最終返回了一個 generator 函數(shù)奶段。
在該函數(shù)遍歷了 model 中 effects 屬性 的所有方法(注:同樣是 generator 函數(shù))饥瓷。結(jié)合 index.js
里的 for (const m of app._models)
,該遍歷針對所有的 model痹籍。
對于每一個 effect呢铆,getSaga 生成了一個 watcher ,并使用 saga 函數(shù)的 fork 將該函數(shù)切分到另一個單獨的線程中去(生成了一個 task 對象)蹲缠。同時為了方便對該線程進行控制棺克,在此 fork 了一個 generator 函數(shù)。在該函數(shù)中攔截了取消 effect 的 action(事實上线定,應(yīng)該是卸載effect 所在 model 的 action)逆航,一旦監(jiān)聽到則立刻取消分出去的 task 線程。
getWatcher
function getWatcher(resolve, reject, key, _effect, model, onError, onEffect) {
let effect = _effect;
let type = 'takeEvery';
let ms;
if (Array.isArray(_effect)) {
// effect 是數(shù)組而不是函數(shù)的情況下暫不考慮
}
function *sagaWithCatch(...args) {
// .... sagaWithCatch 的邏輯
}
const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);
switch (type) {
case 'watcher':
return sagaWithCatch;
case 'takeLatest':
return function*() {
yield takeLatest(key, sagaWithOnEffect);
};
case 'throttle':
return function*() {
yield throttle(ms, key, sagaWithOnEffect);
};
default:
return function*() {
yield takeEvery(key, sagaWithOnEffect);
};
}
}
function createEffects(model) {
// createEffects(model) 的邏輯
}
function applyOnEffect(fns, effect, model, key) {
for (const fn of fns) {
effect = fn(effect, sagaEffects, model, key);
}
return effect;
}
先不考慮 effect 的屬性是數(shù)組而不是方法的情況渔肩。
getWatcher
接收六個參數(shù):
-
resolve/reject
: 中間件middleware
的 res 和 rej 方法因俐。 -
key
:經(jīng)過 prefixNamespace 轉(zhuǎn)義后的 effect 方法名,namespace/effect(也是調(diào)用 action 時的 type)周偎。 -_effect
:effects 中 key 屬性所指向的 generator 函數(shù)抹剩。 -
model
: model -
onError
: 之前定義過的捕獲全局錯誤的方法 -
onEffect
:plugin.use 中傳入的在觸發(fā) effect 時執(zhí)行的回調(diào)函數(shù)(鉤子函數(shù))
applyOnEffect
對 effect 進行了動態(tài)代理,在保證 effect (即 _effect
)正常調(diào)用的情況下蓉坎,為期添加了 fns 的回調(diào)函數(shù)數(shù)組(即 onEffect
)澳眷。使得在 effect 執(zhí)行時, onEffect
內(nèi)的每一個回調(diào)函數(shù)都可以被觸發(fā)蛉艾。
因為沒有經(jīng)過 effects 的屬性是數(shù)組的情況钳踊,所以 type
的值是 takeEvery
衷敌,也就是監(jiān)聽每一個發(fā)出的 action ,即 getWatcher
的返回值最終走的是 switch 的 default 選項:
function*() {
yield takeEvery(key, sagaWithOnEffect);
};
換句話說拓瞪,每次發(fā)出指向 effects 的函數(shù)都會調(diào)用 sagaWithOnEffect
缴罗。
根據(jù) const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);
的執(zhí)行情況,如果 onEffect 的插件為空的情況下祭埂,sagaWithOnEffect
的值為 sagaWithCatch
面氓。
function *sagaWithCatch(...args) {
try {
yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` });
const ret = yield effect(...args.concat(createEffects(model)));
yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@end` });
resolve(key, ret);
} catch (e) {
onError(e);
if (!e._dontReject) {
reject(key, e);
}
}
}
在 sagaWithOnEffect
函數(shù)中,sagas 使用傳入的參數(shù)(也就是 action)執(zhí)行了對應(yīng)的 model 中 對應(yīng)的 effect 方法蛆橡,同時將返回值使用之前保存在 map 里的 resolve 返回了其返回值舌界。同時在執(zhí)行 effect 方法的時候,將 saga 本身的所有方法(put泰演、call呻拌、fork 等等)作為第二個參數(shù),使用 concat
拼接在 action 的后面睦焕。在執(zhí)行 effect 方法前藐握,又發(fā)出了 start 和 end 兩個 action,方便 onEffect 的插件進行攔截和調(diào)用复亏。
因此趾娃,對于 if (m.effects) sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));
缭嫡。
- dva 通過
app._getSaga(m.effects, m, onError, plugin.get('onEffect'))
返回了一個 genenrator 函數(shù)缔御。 - 在 genenrator 函數(shù)中手動 fork 出一個 watcher 函數(shù)的監(jiān)聽線程(當然也 fork 了取消線程的功能)。
- 該函數(shù)(在普通狀態(tài)下)是一個 takeEvery 的阻塞是線程妇蛀,接收 2 個參數(shù)耕突。第一個參數(shù)為監(jiān)聽的 action,第二個參數(shù)為監(jiān)聽到 action 后的回調(diào)函數(shù)评架。
- (普通狀態(tài)下)的回調(diào)函數(shù)眷茁,就是手動調(diào)用了 model 里 effects 中對應(yīng)屬性的函數(shù)。在此之前之后發(fā)出了
start
和end
的 action纵诞,同時用之前 promise 中間件保存在 map 中的 resolve 方法返回了值上祈。 - 最后使用 sagas.forEach(sagaMiddleware.run) 啟動了 watcher 的監(jiān)聽。
store
現(xiàn)在已經(jīng)有了針對異步數(shù)據(jù)流的解決辦法浙芙,那么該創(chuàng)建 store 了登刺。
正常情況的 redux 的 createStore 接收三個參數(shù) reducer, initState,applyMiddleware(middlewares)。
不過 dva 提供了自己的 createStore
方法嗡呼,用來組織一系列自己創(chuàng)建的參數(shù)纸俭。
// Create store
const store = app._store = createStore({ // eslint-disable-line
reducers: createReducer(),
initialState: hooksAndOpts.initialState || {},
plugin,
createOpts,
sagaMiddleware,
promiseMiddleware,
});
createReducer
function createReducer() {
return reducerEnhancer(combineReducers({
...reducers,
...extraReducers,
...(app._store ? app._store.asyncReducers : {}),
}));
}
createReducer
實際上是用 plugin 里的 onReducer (如果有)擴展了 reducer 功能,對于 const reducerEnhancer = plugin.get('onReducer');
南窗,plugin 里的相關(guān)代碼為:
function getOnReducer(hook) {
return function (reducer) {
for (const reducerEnhancer of hook) {
reducer = reducerEnhancer(reducer);
}
return reducer;
};
}
如果有 onReducer 的插件揍很,那么用 reducer 的插件擴展 reducer郎楼;否則直接返回 reducer。
combineReducers 中:
- 第一個
...reducers
是從 dva 里傳入的 historyReducer窒悔,以及通過reducers[m.namespace] = getReducer(m.reducers, m.state);
剝離出的 model 中的 reducer - 第二個參數(shù)為手動在 plugin 里添加的 extraReducers呜袁;
- 第三個參數(shù)為異步 reducer,主要是用于在 dva 運行以后動態(tài)加載 model 里的 reducer蛉迹。
createStore
現(xiàn)在我們有了一個 combine 過的 reducer傅寡,有了 core 中創(chuàng)建的 sagaMiddleware 和 promiseMiddleware,還有了從 dva 中傳入的 createOpts北救,現(xiàn)在可以正式創(chuàng)建 store 了荐操。
從 dva 中傳入的 createOpts 為
setupMiddlewares(middlewares) {
return [
routerMiddleware(history),
...middlewares,
];
},
用與把 redux-router 的中間件排在中間件的第一個。
雖然看起來很長珍策,但是對于大多數(shù)普通用戶來說托启,在未開啟 redux 的調(diào)試插件,未傳入額外的 onAction 以及 extraEnhancers 的情況下攘宙,上面的代碼等價于:
import { createStore, applyMiddleware, compose } from 'redux';
import flatten from 'flatten';
import invariant from 'invariant';
import window from 'global/window';
import { returnSelf, isArray } from './utils';
export default function ({
reducers,
initialState,
plugin,
sagaMiddleware,
promiseMiddleware,
createOpts: {
setupMiddlewares = returnSelf,
},
}) {
const middlewares = setupMiddlewares([
sagaMiddleware,
promiseMiddleware
]);
const enhancers = [
applyMiddleware(...middlewares)
];
return createStore(reducers, initialState, compose(...enhancers));
// 對于 redux 中 的 compose 函數(shù)屯耸,在數(shù)組長度為 1 的情況下返回第一個元素。
// compose(...enhancers) 等同于 applyMiddleware(...middlewares)
}
訂閱
現(xiàn)在 dva 已經(jīng)創(chuàng)建了 store蹭劈,有了異步數(shù)據(jù)流加載方案疗绣,并且又做了一些其他的事情:
// 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
sagas.forEach(sagaMiddleware.run);
- 手動運行 getSaga 里返回的 watcer 函數(shù)。
- 判斷如果有 onStateChange 的 plugin 也手動運行一下铺韧。
model 里的 state多矮、effect、reducer 已經(jīng)實現(xiàn)了哈打,就缺最后的訂閱 subscription 部分塔逃。
// Setup app
setupApp(app);
// Run subscriptions
const unlisteners = {};
for (const model of this._models) {
if (model.subscriptions) {
unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError);
}
}
setupApp(app) 是從 dva 里傳過來的,主要是使用 patchHistory 函數(shù)代理 history.linsten料仗,即強化了 redux 和 router 的聯(lián)系湾盗,是的路徑變化可以引起 state 的變化,進而聽過監(jiān)聽 state 的變化來觸發(fā)回調(diào)立轧。
這也是 core 中唯一使用 this 的地方格粪,逼得 dva 中必須使用 oldStart.call(app) 來進行調(diào)用。
runSubscription
這是 runSubscription 的代碼
export function run(subs, model, app, onError) {
const funcs = [];
const nonFuncs = [];
for (const key in subs) {
if (Object.prototype.hasOwnProperty.call(subs, key)) {
const sub = subs[key];
const unlistener = sub({
dispatch: prefixedDispatch(app._store.dispatch, model),
history: app._history,
}, onError);
if (isFunction(unlistener)) {
funcs.push(unlistener);
} else {
nonFuncs.push(key);
}
}
}
return { funcs, nonFuncs };
}
- 第一個參數(shù)為 model 中的 subscription 對象氛改。
- 第二個參數(shù)為對應(yīng)的 model
- 第三個參數(shù)為 core 里創(chuàng)建的 app
- 第四個參數(shù)為全局異常捕獲的 onError
Object.prototype.hasOwnProperty.call(subs, key)
還是使用原型方法判斷 key 是不是 subs 的自有屬性帐萎。如果是自由屬性,那么拿到屬性對應(yīng)的值(是一個 function)
調(diào)用該 function平窘,傳入 dispatch 和 history 屬性吓肋。history 就是經(jīng)過 redux-router 強化過的 history,而 dispatch瑰艘,也就是
prefixedDispatch(app._store.dispatch, model)
export default function prefixedDispatch(dispatch, model) {
return (action) => {
// 斷言檢測
return dispatch({ ...action, type: prefixType(type, model) });
};
}
實際上是用將 action 里的 type 添加了 ${model.namespance}/
的前綴是鬼。
自此肤舞,model 中的四大組件全部完畢,完成了 dva 的數(shù)據(jù)層處理均蜜。