面试官问发布订阅模式是在问什么?
大家好,我是若川。最近組織了源碼共讀活動,感興趣的可以加我微信 ruochuan12 參與,已進(jìn)行了三個多月,大家一起交流學(xué)習(xí),共同進(jìn)步。
本文來自 @simonezhou 小姐姐投稿的第八期筆記。面試官常問發(fā)布訂閱、觀察者模式,我們?nèi)粘i_發(fā)也很常用。文章講述了 mitt、tiny-emitter、Vue eventBus這三個發(fā)布訂閱、觀察者模式相關(guān)的源碼。
源碼地址
mitt:https://github.com/developit/mitt
tiny-emitter:https://github.com/scottcorgan/tiny-emitter
1. mitt 源碼解讀
1.1 package.json 項目 build 打包(運用到包暫不深究,保留個印象即可)
執(zhí)行 npm run build:
//? "scripts":?{..."bundle":?"microbundle?-f?es,cjs,umd","build":?"npm-run-all?--silent?clean?-p?bundle?-s?docs","clean":?"rimraf?dist","docs":?"documentation?readme?src/index.ts?--section?API?-q?--parse-extension?ts",...},使用 npm-run-all(A CLI tool to run multiple npm-scripts in parallel or sequential:https://www.npmjs.com/package/npm-run-all) 命令執(zhí)行
clean 命令,使用 rimraf(The UNIX command rm -rf for node. https://www.npmjs.com/package/rimraf)刪除 dist 文件路徑
bundle 命令,使用 microbundle(The zero-configuration bundler for tiny modules, powered by Rollup. https://www.npmjs.com/package/microbundle) 進(jìn)行打包
microbundle 命令指定 format: es, cjs, umd, ?package.json 指定 soucre 字段為打包入口 js:
{"name":?"mitt",??????????//?package?name......"module":?"dist/mitt.mjs",????//?ES?Modules?output?bundle"main":?"dist/mitt.js",??????//?CommonJS?output?bundle"jsnext:main":?"dist/mitt.mjs",???//?ES?Modules?output?bundle"umd:main":?"dist/mitt.umd.js",??//?UMD?output?bundle"source":?"src/index.ts",?????//?input"typings":?"index.d.ts",?????//?TypeScript?typings?directory"exports":?{"import":?"./dist/mitt.mjs",????//?ES?Modules?output?bundle"require":?"./dist/mitt.js",??//?CommonJS?output?bundle"default":?"./dist/mitt.mjs"??//?Modern?ES?Modules?output?bundle},... }
1.2 如何調(diào)試查看分析?
使用 microbundle watch 命令,新增 script,執(zhí)行 npm run dev:
"dev":?"microbundle?watch?-f?es,cjs,umd"對應(yīng)目錄新增入口,比如 test.js,執(zhí)行 node test.js 測試功能:
const?mitt?=?require('./dist/mitt');const?Emitter?=?mitt();Emitter.on('test',?(e,?t)?=>?console.log(e,?t));Emitter.emit('test',?{?a:?12321?});對應(yīng)源碼 src/index.js 也依然可以加相關(guān)的 log 進(jìn)行查看,代碼變動后會觸發(fā)重新打包
1.3. TS 聲明
使用上可以(官方給的例子),比如定義 foo 事件,回調(diào)函數(shù)里面的參數(shù)要求是 string 類型,可以想象一下源碼 TS 是怎么定義的:
import?mitt?from?'mitt';//?key?為事件名,key?對應(yīng)屬性為回調(diào)函數(shù)的參數(shù)類型? type?Events?=?{foo:?string;bar?:?number;?//?對應(yīng)事件允許不傳參數(shù) };const?emitter?=?mitt<Events>();?//?inferred?as?Emitter<Events>emitter.on('foo',?(e)?=>?{});?//?'e'?has?inferred?type?'string'emitter.emit('foo',?42);?//?Error:?Argument?of?type?'number'?is?not?assignable?to?parameter?of?type?'string'.?(2345)emitter.on('*',?(type,?e)?=>?console.log(type,?e)?)源碼內(nèi)關(guān)于 TS 定義(關(guān)鍵幾句):
export?type?EventType?=?string?|?symbol;//?Handler?為事件(除了*事件)回調(diào)函數(shù)定義 export?type?Handler<T?=?unknown>?=?(event:?T)?=>?void;//?WildcardHandler?為事件?*?回調(diào)函數(shù)定義 export?type?WildcardHandler<T?=?Record<string,?unknown>>?=?(type:?keyof?T,???//?keyof?T,事件名event:?T[keyof?T]??//?T[keyof?T],?事件名對應(yīng)的回調(diào)函數(shù)入?yún)㈩愋?)?=>?void;export?interface?Emitter<Events?extends?Record<EventType,?unknown>>?{//?...on<Key?extends?keyof?Events>(type:?Key,?handler:?Handler<Events[Key]>):?void;on(type:?'*',?handler:?WildcardHandler<Events>):?void;//?...emit<Key?extends?keyof?Events>(type:?Key,?event:?Events[Key]):?void;//?這句主要兼容無參數(shù)類型的事件,如果說事件對應(yīng)回調(diào)必須傳參,使用中如果未傳,那么會命中?never,如下圖emit<Key?extends?keyof?Events>(type:?undefined?extends?Events[Key]???Key?:?never):?void; }以下是會報 TS 錯誤:
以下是正確的:
1.4 主邏輯
整體就是一個 function,輸入為事件 Map,輸出為 all 所有事件 Map,還有 on,emit,off 幾個關(guān)于事件方法:
on 為【事件訂閱】,push 對應(yīng) Handler 到對應(yīng)事件 Map 的 Handler 回調(diào)函數(shù)數(shù)組內(nèi)(可熟悉下 Map 相關(guān)API https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Map):
off 為【事件注銷】,從對應(yīng)事件 Map 的 Handlers 中,splice 掉:
emit 為【事件觸發(fā)】,讀取事件 Map 的 Handlers,循環(huán)逐一觸發(fā),如果訂閱了 * 全事件,則讀取 * 的 Handlers 逐一觸發(fā):
為什么是使用 slice().map() ,而不是直接使用 forEach() 進(jìn)行觸發(fā)?具體可查看:https://github.com/developit/mitt/pull/109,具體可以拷貝相關(guān)代碼進(jìn)行調(diào)試,直接更換成 forEach 的話,針對以下例子所觸發(fā)的 emit 是錯誤的:
import?mitt?from?'./mitt'type?Events?=?{test:?number }const?Emitter?=?mitt<Events>() Emitter.on('test',?function?A(num)?{console.log('A',?num)Emitter.off('test',?A) }) Emitter.on('test',?function?B()?{console.log('B') }) Emitter.on('test',?function?C()?{console.log('C') })Emitter.emit('test',?32432)?//?觸發(fā)?A,C?事件,B?會被漏掉 Emitter.emit('test',?32432)?//?觸發(fā)?B,C,這個是正確的//?原因解釋: //?forEach?時,在?Handlers?循環(huán)過程中,同時觸發(fā)了?off?操作 //?按這個例子的話,A?是第一個被注冊的,所以第一個會被?slice?掉 //?因為?array?是引用類型,slice?之后,那么?B?函數(shù)就會變成第一個 //?但此時遍歷已經(jīng)到第二個了,所以?B?函數(shù)就會被漏掉執(zhí)行//?解決方案: //?所以對數(shù)組進(jìn)行?[].slice()?做一個淺拷貝,off?的?Handlers?與?當(dāng)前循環(huán)中的?Handlers?處理成不同一個 //?[].slice.forEach()?效果其實也是一樣的,用?map?的話個人感覺不是很語義化1.5 小結(jié)
TS keyof 的靈活運用
undefined extends Events[Key] ? Key : never,為 TS 的條件類型(https://www.typescriptlang.org/docs/handbook/2/conditional-types.html)
undefined extends Events[Key] ? Key : never,當(dāng)我們想要編譯器不捕獲當(dāng)前值或者類型時,我們可以返回 never類型。never 表示永遠(yuǎn)不存在的值的類型
mitt 的事件回調(diào)函數(shù)參數(shù),只會有一個,而不是多個,如何兼容多個參數(shù)的情況,官方推薦是使用 object 的(object is recommended and powerful),這種設(shè)計擴展性更高,更值得推薦。
2. tiny-emitter 源碼解讀
2.1 主邏輯
所有方法都是掛載在 E 的 prototype 內(nèi)的,總共暴露了 once,emit,off,on 四個事件的方法:
once 訂閱一次事件,當(dāng)被觸發(fā)一次后,就會被銷毀:
on 事件訂閱
off 事件銷毀
emit 事件觸發(fā)
2.2 小結(jié)
return this,支持鏈?zhǔn)秸{(diào)用
emit 事件觸發(fā)時,[].slice.call(arguments, 1) 剔除第一個參數(shù),獲取到剩余的參數(shù)列表,再使用 apply 來調(diào)用
on 事件訂閱時,記錄的是 { fn, ctx },fn 為回調(diào)函數(shù),ctx 支持綁定上下文
3. mitt 與 tiny-emitter 對比
TS 靜態(tài)類型校驗上 mitt > tiny-emitter,開發(fā)更友好,對于回調(diào)函數(shù)參數(shù)的管理,tiny-emitter 支持多參數(shù)調(diào)用的,但是 mitt 提倡使用 object 管理,設(shè)計上感覺 mitt 更加友好以及規(guī)范
在 off 事件銷毀中,tiny-emitter 與 mitt 處理方式不同,tiny-emitter 會一次性銷毀所有相同的 callback,而 mitt 則只是銷毀第一個
mitt 不支持 once 方法,tiny-emitter 支持 once 方法
mitt 支持 * 全事件訂閱,tiny-emitter 則不支持
4. Vue eventBus 事件總線(3.x 已廢除,2.x 依然存在)
關(guān)于 events 的處理:https://github.com/vuejs/vue/blob/dev/src/core/instance/events.js
事件相關(guān)初始化:https://github.com/vuejs/vue/blob/dev/src/core/instance/index.js
初始化過程
$on 事件訂閱
$once 事件訂閱&執(zhí)行一次
$off 事件退訂
$emit 事件觸發(fā)
實現(xiàn)邏輯大致和 mitt,tiny-emitter 一致,也是 pubsub,整體思路都是維護(hù)一個 object 或者 Map,on 則是放到數(shù)組內(nèi),emit 則是循環(huán)遍歷逐一觸發(fā),off 則是查找到對應(yīng)的 handler 移除數(shù)組
TODO:
Vue 中對于方法調(diào)用錯誤異常的處理方案:invokeWithErrorHandling
hookEvent 的使用&原理
5. 附錄
rimraf:https://www.npmjs.com/package/rimraf
microbundle:https://www.npmjs.com/package/microbundle
package.json exports 字段:https://nodejs.org/api/packages.html#packages_conditional_exports
Map:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Map
TS 條件類型:https://www.typescriptlang.org/docs/handbook/2/conditional-types.html
TS Never:https://www.typescriptlang.org/docs/handbook/basic-types.html#never
TS keyof: https://www.typescriptlang.org/docs/handbook/2/keyof-types.html#the-keyof-type-operator
What is the JavaScript >>> operator and how do you use it? https://stackoverflow.com/questions/1822350/what-is-the-javascript-operator-and-how-do-you-use-it
最近組建了一個江西人的前端交流群,如果你是江西人可以加我微信?ruochuan12?私信 江西?拉你進(jìn)群。
推薦閱讀
1個月,200+人,一起讀了4周源碼
我歷時3年才寫了10余篇源碼文章,但收獲了100w+閱讀
老姚淺談:怎么學(xué)JavaScript?
我在阿里招前端,該怎么幫你(可進(jìn)面試群)
·················?若川簡介?·················
你好,我是若川,畢業(yè)于江西高校。現(xiàn)在是一名前端開發(fā)“工程師”。寫有《學(xué)習(xí)源碼整體架構(gòu)系列》10余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結(jié),已經(jīng)寫了7篇,點擊查看年度總結(jié)。
同時,最近組織了源碼共讀活動,幫助1000+前端人學(xué)會看源碼。公眾號愿景:幫助5年內(nèi)前端人走向前列。
識別上方二維碼加我微信、拉你進(jìn)源碼共讀群
今日話題
略。歡迎分享、收藏、點贊、在看我的公眾號文章~
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的面试官问发布订阅模式是在问什么?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MK60单片机开发环境-IAR Embe
- 下一篇: [html] 在head标签中必不少的