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