面试官问发布订阅模式是在问什么?
大家好,我是若川。最近組織了源碼共讀活動,感興趣的可以加我微信 ruochuan12 參與,已進行了三個多月,大家一起交流學習,共同進步。
本文來自 @simonezhou 小姐姐投稿的第八期筆記。面試官常問發布訂閱、觀察者模式,我們日常開發也很常用。文章講述了 mitt、tiny-emitter、Vue eventBus這三個發布訂閱、觀察者模式相關的源碼。
源碼地址
mitt:https://github.com/developit/mitt
tiny-emitter:https://github.com/scottcorgan/tiny-emitter
1. mitt 源碼解讀
1.1 package.json 項目 build 打包(運用到包暫不深究,保留個印象即可)
執行 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) 命令執行
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) 進行打包
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 如何調試查看分析?
使用 microbundle watch 命令,新增 script,執行 npm run dev:
"dev":?"microbundle?watch?-f?es,cjs,umd"對應目錄新增入口,比如 test.js,執行 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?});對應源碼 src/index.js 也依然可以加相關的 log 進行查看,代碼變動后會觸發重新打包
1.3. TS 聲明
使用上可以(官方給的例子),比如定義 foo 事件,回調函數里面的參數要求是 string 類型,可以想象一下源碼 TS 是怎么定義的:
import?mitt?from?'mitt';//?key?為事件名,key?對應屬性為回調函數的參數類型? type?Events?=?{foo:?string;bar?:?number;?//?對應事件允許不傳參數 };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)?)源碼內關于 TS 定義(關鍵幾句):
export?type?EventType?=?string?|?symbol;//?Handler?為事件(除了*事件)回調函數定義 export?type?Handler<T?=?unknown>?=?(event:?T)?=>?void;//?WildcardHandler?為事件?*?回調函數定義 export?type?WildcardHandler<T?=?Record<string,?unknown>>?=?(type:?keyof?T,???//?keyof?T,事件名event:?T[keyof?T]??//?T[keyof?T],?事件名對應的回調函數入參類型 )?=>?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;//?這句主要兼容無參數類型的事件,如果說事件對應回調必須傳參,使用中如果未傳,那么會命中?never,如下圖emit<Key?extends?keyof?Events>(type:?undefined?extends?Events[Key]???Key?:?never):?void; }以下是會報 TS 錯誤:
以下是正確的:
1.4 主邏輯
整體就是一個 function,輸入為事件 Map,輸出為 all 所有事件 Map,還有 on,emit,off 幾個關于事件方法:
on 為【事件訂閱】,push 對應 Handler 到對應事件 Map 的 Handler 回調函數數組內(可熟悉下 Map 相關API https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Map):
off 為【事件注銷】,從對應事件 Map 的 Handlers 中,splice 掉:
emit 為【事件觸發】,讀取事件 Map 的 Handlers,循環逐一觸發,如果訂閱了 * 全事件,則讀取 * 的 Handlers 逐一觸發:
為什么是使用 slice().map() ,而不是直接使用 forEach() 進行觸發?具體可查看:https://github.com/developit/mitt/pull/109,具體可以拷貝相關代碼進行調試,直接更換成 forEach 的話,針對以下例子所觸發的 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)?//?觸發?A,C?事件,B?會被漏掉 Emitter.emit('test',?32432)?//?觸發?B,C,這個是正確的//?原因解釋: //?forEach?時,在?Handlers?循環過程中,同時觸發了?off?操作 //?按這個例子的話,A?是第一個被注冊的,所以第一個會被?slice?掉 //?因為?array?是引用類型,slice?之后,那么?B?函數就會變成第一個 //?但此時遍歷已經到第二個了,所以?B?函數就會被漏掉執行//?解決方案: //?所以對數組進行?[].slice()?做一個淺拷貝,off?的?Handlers?與?當前循環中的?Handlers?處理成不同一個 //?[].slice.forEach()?效果其實也是一樣的,用?map?的話個人感覺不是很語義化1.5 小結
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,當我們想要編譯器不捕獲當前值或者類型時,我們可以返回 never類型。never 表示永遠不存在的值的類型
mitt 的事件回調函數參數,只會有一個,而不是多個,如何兼容多個參數的情況,官方推薦是使用 object 的(object is recommended and powerful),這種設計擴展性更高,更值得推薦。
2. tiny-emitter 源碼解讀
2.1 主邏輯
所有方法都是掛載在 E 的 prototype 內的,總共暴露了 once,emit,off,on 四個事件的方法:
once 訂閱一次事件,當被觸發一次后,就會被銷毀:
on 事件訂閱
off 事件銷毀
emit 事件觸發
2.2 小結
return this,支持鏈式調用
emit 事件觸發時,[].slice.call(arguments, 1) 剔除第一個參數,獲取到剩余的參數列表,再使用 apply 來調用
on 事件訂閱時,記錄的是 { fn, ctx },fn 為回調函數,ctx 支持綁定上下文
3. mitt 與 tiny-emitter 對比
TS 靜態類型校驗上 mitt > tiny-emitter,開發更友好,對于回調函數參數的管理,tiny-emitter 支持多參數調用的,但是 mitt 提倡使用 object 管理,設計上感覺 mitt 更加友好以及規范
在 off 事件銷毀中,tiny-emitter 與 mitt 處理方式不同,tiny-emitter 會一次性銷毀所有相同的 callback,而 mitt 則只是銷毀第一個
mitt 不支持 once 方法,tiny-emitter 支持 once 方法
mitt 支持 * 全事件訂閱,tiny-emitter 則不支持
4. Vue eventBus 事件總線(3.x 已廢除,2.x 依然存在)
關于 events 的處理:https://github.com/vuejs/vue/blob/dev/src/core/instance/events.js
事件相關初始化:https://github.com/vuejs/vue/blob/dev/src/core/instance/index.js
初始化過程
$on 事件訂閱
$once 事件訂閱&執行一次
$off 事件退訂
$emit 事件觸發
實現邏輯大致和 mitt,tiny-emitter 一致,也是 pubsub,整體思路都是維護一個 object 或者 Map,on 則是放到數組內,emit 則是循環遍歷逐一觸發,off 則是查找到對應的 handler 移除數組
TODO:
Vue 中對于方法調用錯誤異常的處理方案: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?私信 江西?拉你進群。
推薦閱讀
1個月,200+人,一起讀了4周源碼
我歷時3年才寫了10余篇源碼文章,但收獲了100w+閱讀
老姚淺談:怎么學JavaScript?
我在阿里招前端,該怎么幫你(可進面試群)
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》10余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助1000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。
識別上方二維碼加我微信、拉你進源碼共讀群
今日話題
略。歡迎分享、收藏、點贊、在看我的公眾號文章~
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的面试官问发布订阅模式是在问什么?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MK60单片机开发环境-IAR Embe
- 下一篇: [html] 在head标签中必不少的