<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          發(fā)布訂閱模式這個面試題該咋答

          共 24387字,需瀏覽 49分鐘

           ·

          2023-08-16 20:42

          本文來自 @simonezhou 小姐姐投稿的第八期筆記。面試官常問發(fā)布訂閱觀察者模式,我們?nèi)粘i_發(fā)也很常用。文章講述了 mitt、tiny-emitter、Vue eventBus這三個發(fā)布訂閱、觀察者模式相關(guān)的源碼。

          源碼地址

          1. mitt:https://github.com/developit/mitt
          2. tiny-emitter:https://github.com/scottcorgan/tiny-emitter

          1. mitt 源碼解讀

          1.1 package.json 項目 build 打包(運(yùn)用到包暫不深究,保留個印象即可)

          執(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', { a12321 });

          對應(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)㈩愋?/span>
          ) => 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 主邏輯


          1. 整體就是一個 function,輸入為事件 Map,輸出為 all 所有事件 Map,還有 on,emit,off 幾個關(guān)于事件方法:
          export default function mitt<Events extends Record<EventTypeunknown>>(
            // 支持 all 初始化
           all?: EventHandlerMap<Events>
          ): Emitter<Events
          {
            // 內(nèi)部維護(hù)了一個 Map(all),Key 為事件名,Value 為 Handler 回調(diào)函數(shù)數(shù)組
            all = all || new Map();
            return {
             all,   // 所有事件 & 事件對應(yīng)方法
             emit,  // 觸發(fā)事件
             on,   // 訂閱事件
             off   // 注銷事件
            }
          }
          1. 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):
          on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
            // Map get 獲取
           const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
            // 如果已經(jīng)初始化過的話,是個數(shù)組,直接 push 即可
           if (handlers) {
            handlers.push(handler);
           }
            // 如果第一次注冊事件,則 set 新的數(shù)組
           else {
            all!.set(type, [handler] as EventHandlerList<Events[keyof Events]>);
           }
          }
          1. off 為【事件注銷】,從對應(yīng)事件 Map 的 Handlers 中,splice 掉:
          off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) {
            // Map get 獲取
           const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
            // 如果有事件列表,則進(jìn)入,沒有則忽略
           if (handlers) {
              // 對 handler 事件進(jìn)行 splice 移出數(shù)組
              // 這里是對找到的第一個 handler 進(jìn)行移出,所以如果訂閱了多次,只會去除第一個
              // handlers.indexOf(handler) >>> 0,>>> 為無符號位移
              // 關(guān)于網(wǎng)上對 >>> 用法說明:It doesn't just convert non-Numbers to Number, it converts them to Numbers that can be expressed as 32-bit unsigned ints.
            if (handler) {
             handlers.splice(handlers.indexOf(handler) >>> 01);
            }
              // 如果不傳對應(yīng)的 Handler,則為清空事件對應(yīng)的所有訂閱
            else {
             all!.set(type, []);
            }
           }
          }
          1. emit 為【事件觸發(fā)】,讀取事件 Map 的 Handlers,循環(huán)逐一觸發(fā),如果訂閱了 * 全事件,則讀取 * 的 Handlers 逐一觸發(fā):
          emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
            // 獲取對應(yīng) type 的 Handlers
           let handlers = all!.get(type);
           if (handlers) {
            (handlers as EventHandlerList<Events[keyof Events]>)
             .slice()
             .map((handler) => {
              handler(evt!);
             });
           }

            // 獲取 * 對應(yīng)的 Handlers
           handlers = all!.get('*');
           if (handlers) {
            (handlers as WildCardEventHandlerList<Events>)
             .slice()
             .map((handler) => {
              handler(type, evt!);
             });
           }
          }

          為什么是使用 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 的靈活運(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)前值或者類型時,我們可以返回 never類型。never 表示永遠(yuǎn)不存在的值的類型
          // 來自 typescript 中的 lib.es5.d.ts 定義

          /**
           * Exclude null and undefined from T
           */

          type NonNullable<T> = T extends null | undefined ? never : T;

          // 如果 T 的值包含 null 或者 undefined,則會 never 表示不允許走到此邏輯,否則返回 T 本身的類型
          • mitt 的事件回調(diào)函數(shù)參數(shù),只會有一個,而不是多個,如何兼容多個參數(shù)的情況,官方推薦是使用 object 的(object is recommended and powerful),這種設(shè)計擴(kuò)展性更高,更值得推薦。

          2. tiny-emitter 源碼解讀

          2.1 主邏輯

          1. 所有方法都是掛載在 E 的 prototype 內(nèi)的,總共暴露了 once,emit,off,on 四個事件的方法:
          function E ({
            // Keep this empty so it's easier to inherit from
            // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
          }

          // 所有事件都掛載在 this.e 上,是個 object
          E.prototype = {
           onfunction (name, callback, ctx{},
            oncefunction (name, callback, ctx{},
           emitfunction (name{},
            offfunction (name, callback{}
          }

          module.exports = E;
          module.exports.TinyEmitter = E;
          1. once 訂閱一次事件,當(dāng)被觸發(fā)一次后,就會被銷毀:
          once: function (name, callback, ctx{
            var self = this;
            // 構(gòu)造另一個回調(diào)函數(shù),調(diào)用完之后,銷毀該 callback
            function listener ({
              self.off(name, listener);     // 銷毀
              callback.apply(ctx, arguments);  // 執(zhí)行
            };

            listener._ = callback
            
            // on 函數(shù)返回 this,所以可以鏈?zhǔn)秸{(diào)用
            return this.on(name, listener, ctx); // 訂閱這個構(gòu)造的回調(diào)函數(shù)
          }
          1. on 事件訂閱
          on: function (name, callback, ctx{
            var e = this.e || (this.e = {});

            // 單純 push 進(jìn)去,這里也沒有做去重,所以同一個回調(diào)函數(shù)可以被訂閱多次
            (e[name] || (e[name] = [])).push({
              fn: callback,
              ctx: ctx
            });

            // 返回 this,可以鏈?zhǔn)秸{(diào)用
            return this;
          }
          1. off 事件銷毀
          off: function (name, callback{
            var e = this.e || (this.e = {});
            var evts = e[name];
            var liveEvents = []; // 保存還有效的 hanlder

           // 傳遞的 callback,如果命中,就不會被放到 liveEvents 里面
            // 所以這里的銷毀是一次性銷毀全部相同的 callback,與 mitt 不一樣
            if (evts && callback) {
              for (var i = 0, len = evts.length; i < len; i++) {
                if (evts[i].fn !== callback && evts[i].fn._ !== callback)
                  liveEvents.push(evts[i]);
              }
            }

            // Remove event from queue to prevent memory leak
            // Suggested by https://github.com/lazd
            // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910

            // 如果沒有任何 handler,對應(yīng)的事件 name 也可以被 delete
            (liveEvents.length)
              ? e[name] = liveEvents
              : delete e[name];

            // 返回 this,可以鏈?zhǔn)秸{(diào)用
            return this;
          }
          1. emit 事件觸發(fā)
          emit: function (name{
            // 取除了第一位的剩余所有參數(shù)
            var data = [].slice.call(arguments1);
            
            // slice() 淺拷貝
            var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
            var i = 0;
            var len = evtArr.length;

            // 循環(huán)逐個觸發(fā) handler,把 data 傳入其中
            for (i; i < len; i++) {
              evtArr[i].fn.apply(evtArr[i].ctx, data);
            }

            // 返回 this,可以鏈?zhǔn)秸{(diào)用
            return this;
          }

          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
          1. 初始化過程
          // index.js 調(diào)用 initMixin 方法,初始化 _events object
          initMixin(Vue)

          // event.js 定義 initEvents 方法
          // vm._events 保存所有事件 & 事件回調(diào)函數(shù),是個 object
          export function initEvents (vm: Component{
            vm._events = Object.create(null)
            // ...
          }

          // index.js 調(diào)用 eventsMixin,往 Vue.prototype 掛載相關(guān)事件方法
          eventsMixin(Vue)

          // event.js 定義了 eventsMixin 方法
          export function eventsMixin (Vue: Class<Component>{
            // 事件訂閱
           Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {}
            // 事件訂閱執(zhí)行一次
            Vue.prototype.$once = function (event: string, fn: Function): Component {}
            // 事件退訂
            Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {}
            // 事件觸發(fā)
            Vue.prototype.$emit = function (event: string): Component {}
          }
          1. $on 事件訂閱
          // event 是個 string,也可以是個 string 數(shù)組
          // 說明可以一次性對多個事件,訂閱同一個回調(diào)函數(shù)
          Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
            const vm: Component = this
            if (Array.isArray(event)) {
              for (let i = 0, l = event.length; i < l; i++) {
                vm.$on(event[i], fn)
              }
            } else {
              // 本質(zhì)是就是對應(yīng) event,push 對應(yīng)的 fn
              (vm._events[event] || (vm._events[event] = [])).push(fn)
              
              // 以下先不展開,關(guān)于 hookEvent 的調(diào)用說明
              // optimize hook:event cost by using a boolean flag marked at registration
              // instead of a hash lookup
              if (hookRE.test(event)) {
                vm._hasHookEvent = true
              }
            }
            return vm
          }
          1. $once 事件訂閱&執(zhí)行一次
          // 包裝一層 on,內(nèi)包含退訂操作以及調(diào)用操作
          // 訂閱的是包裝后的 on 回調(diào)函數(shù)
          Vue.prototype.$once = function (event: string, fn: Function): Component {
            const vm: Component = this
            function on ({
              vm.$off(event, on)
              fn.apply(vm, arguments)
            }
            on.fn = fn
            vm.$on(event, on)
            return vm
          }
          1. $off 事件退訂
          Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
            const vm: Component = this
            
            // 沒有傳參數(shù),說明全部事件退訂,直接清空
            if (!arguments.length) {
              vm._events = Object.create(null)
              return vm
            }
            
            // 存在 event 數(shù)組,遍歷逐一調(diào)用自己
            if (Array.isArray(event)) {
              for (let i = 0, l = event.length; i < l; i++) {
                vm.$off(event[i], fn)
              }
              return vm
            }
            
            // 以下情況為非數(shù)組事件名,為單一事件,則獲取該事件對應(yīng)訂閱的 callbacks
            const cbs = vm._events[event]
            // 若 callbacks 為空,什么都不用做
            if (!cbs) {
              return vm
            }
            // 如果傳入的 fn 為空,說明退訂這個事件的所有 callbacks
            if (!fn) {
              vm._events[event] = null
              return vm
            }
            // callbacks 不為空,并且 fn 不為空,則為退訂某個 callback
            let cb
            let i = cbs.length
            while (i--) {
              cb = cbs[i]
              // 訂閱多次的 callback,都會被退訂,一次退訂所有相同的 callback
              if (cb === fn || cb.fn === fn) {
                cbs.splice(i, 1)
                break
              }
            }
            return vm
          }
          1. $emit 事件觸發(fā)
          Vue.prototype.$emit = function (event: string): Component {
            const vm: Component = this
            if (process.env.NODE_ENV !== 'production') {
              const lowerCaseEvent = event.toLowerCase()
              if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
                tip(
                  `Event "${lowerCaseEvent}" is emitted in component ` +
                  `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
                  `Note that HTML attributes are case-insensitive and you cannot use ` +
                  `v-on to listen to camelCase events when using in-DOM templates. ` +
                  `You should probably use "${hyphenate(event)}" instead of "${event}".`
                )
              }
            }
            
            // 獲取這個 event 的 callbacks 出來
            let cbs = vm._events[event]
            if (cbs) {
              cbs = cbs.length > 1 ? toArray(cbs) : cbs
              // 獲取除了第一位,剩余的其他所有參數(shù)
              const args = toArray(arguments1)
              const info = `event handler for "${event}"`
              // 遍歷逐一觸發(fā)
              for (let i = 0, l = cbs.length; i < l; i++) {
                // 以下暫不展開,這是 Vue 中對于方法調(diào)用錯誤異常的處理方案
                invokeWithErrorHandling(cbs[i], vm, args, vm, info)
              }
            }
            return vm
          }

          實現(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

          瀏覽 691
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  波多野结衣无码一区二区 | 亚測免费一级 | 人妻熟女一二三区夜夜爱 | 苍井空免费一级A片 | 国产乱婬A∨片免费观看 |