北京市专业网站建设,广州安全教育平台登录账号登录入口,通信部门网站备案证明,罗村网站开发大家好#xff0c;我是若川。最近组织了源码共读活动#xff0c;感兴趣的可以加我微信 ruochuan12 参与#xff0c;已进行了三个多月#xff0c;大家一起交流学习#xff0c;共同进步。本文来自 simonezhou 小姐姐投稿的第八期笔记。面试官常问发布订阅、观察者模式#… 大家好我是若川。最近组织了源码共读活动感兴趣的可以加我微信 ruochuan12 参与已进行了三个多月大家一起交流学习共同进步。本文来自 simonezhou 小姐姐投稿的第八期笔记。面试官常问发布订阅、观察者模式我们日常开发也很常用。文章讲述了 mitt、tiny-emitter、Vue eventBus这三个发布订阅、观察者模式相关的源码。源码地址mitthttps://github.com/developit/mitttiny-emitterhttps://github.com/scottcorgan/tiny-emitter1. 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-allA CLI tool to run multiple npm-scripts in parallel or sequentialhttps://www.npmjs.com/package/npm-run-all 命令执行clean 命令使用 rimrafThe UNIX command rm -rf for node. https://www.npmjs.com/package/rimraf删除 dist 文件路径bundle 命令使用 microbundleThe 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 bundlemain: dist/mitt.js, // CommonJS output bundlejsnext:main: dist/mitt.mjs, // ES Modules output bundleumd:main: dist/mitt.umd.js, // UMD output bundlesource: src/index.ts, // inputtypings: index.d.ts, // TypeScript typings directoryexports: {import: ./dist/mitt.mjs, // ES Modules output bundlerequire: ./dist/mitt.js, // CommonJS output bundledefault: ./dist/mitt.mjs // Modern ES Modules output bundle},...
}1.2 如何调试查看分析使用 microbundle watch 命令新增 script执行 npm run devdev: 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 mittEvents(); // inferred as EmitterEventsemitter.on(foo, (e) {}); // e has inferred type stringemitter.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 HandlerT unknown (event: T) void;// WildcardHandler 为事件 * 回调函数定义
export type WildcardHandlerT Recordstring, unknown (type: keyof T, // keyof T事件名event: T[keyof T] // T[keyof T], 事件名对应的回调函数入参类型
) void;export interface EmitterEvents extends RecordEventType, unknown {// ...onKey extends keyof Events(type: Key, handler: HandlerEvents[Key]): void;on(type: *, handler: WildcardHandlerEvents): void;// ...emitKey extends keyof Events(type: Key, event: Events[Key]): void;// 这句主要兼容无参数类型的事件如果说事件对应回调必须传参使用中如果未传那么会命中 never如下图emitKey extends keyof Events(type: undefined extends Events[Key] ? Key : never): void;
}以下是会报 TS 错误以下是正确的1.4 主逻辑整体就是一个 function输入为事件 Map输出为 all 所有事件 Map还有 onemitoff 几个关于事件方法export default function mittEvents extends RecordEventType, unknown(// 支持 all 初始化all?: EventHandlerMapEvents
): EmitterEvents {// 内部维护了一个 MapallKey 为事件名Value 为 Handler 回调函数数组all all || new Map();return {all, // 所有事件 事件对应方法emit, // 触发事件on, // 订阅事件off // 注销事件}
}on 为【事件订阅】push 对应 Handler 到对应事件 Map 的 Handler 回调函数数组内可熟悉下 Map 相关API https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/MaponKey extends keyof Events(type: Key, handler: GenericEventHandler) {// Map get 获取const handlers: ArrayGenericEventHandler | undefined all!.get(type);// 如果已经初始化过的话是个数组直接 push 即可if (handlers) {handlers.push(handler);}// 如果第一次注册事件则 set 新的数组else {all!.set(type, [handler] as EventHandlerListEvents[keyof Events]);}
}off 为【事件注销】从对应事件 Map 的 Handlers 中splice 掉offKey extends keyof Events(type: Key, handler?: GenericEventHandler) {// Map get 获取const handlers: ArrayGenericEventHandler | undefined all!.get(type);// 如果有事件列表则进入没有则忽略if (handlers) {// 对 handler 事件进行 splice 移出数组// 这里是对找到的第一个 handler 进行移出所以如果订阅了多次只会去除第一个// handlers.indexOf(handler) 0 为无符号位移// 关于网上对 用法说明It doesnt 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) 0, 1);}// 如果不传对应的 Handler则为清空事件对应的所有订阅else {all!.set(type, []);}}
}emit 为【事件触发】读取事件 Map 的 Handlers循环逐一触发如果订阅了 * 全事件则读取 * 的 Handlers 逐一触发emitKey extends keyof Events(type: Key, evt?: Events[Key]) {// 获取对应 type 的 Handlerslet handlers all!.get(type);if (handlers) {(handlers as EventHandlerListEvents[keyof Events]).slice().map((handler) {handler(evt!);});}// 获取 * 对应的 Handlershandlers all!.get(*);if (handlers) {(handlers as WildCardEventHandlerListEvents).slice().map((handler) {handler(type, evt!);});}
}为什么是使用 slice().map() 而不是直接使用 forEach() 进行触发具体可查看https://github.com/developit/mitt/pull/109具体可以拷贝相关代码进行调试直接更换成 forEach 的话针对以下例子所触发的 emit 是错误的import mitt from ./mitttype Events {test: number
}const Emitter mittEvents()
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) // 触发 AC 事件B 会被漏掉
Emitter.emit(test, 32432) // 触发 BC这个是正确的// 原因解释
// 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.htmlundefined extends Events[Key] ? Key : never当我们想要编译器不捕获当前值或者类型时我们可以返回 never类型。never 表示永远不存在的值的类型// 来自 typescript 中的 lib.es5.d.ts 定义/*** Exclude null and undefined from T*/
type NonNullableT T extends null | undefined ? never : T;// 如果 T 的值包含 null 或者 undefined则会 never 表示不允许走到此逻辑否则返回 T 本身的类型mitt 的事件回调函数参数只会有一个而不是多个如何兼容多个参数的情况官方推荐是使用 object 的object is recommended and powerful这种设计扩展性更高更值得推荐。2. tiny-emitter 源码解读2.1 主逻辑所有方法都是挂载在 E 的 prototype 内的总共暴露了 onceemitoffon 四个事件的方法function E () {// Keep this empty so its easier to inherit from// (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
}// 所有事件都挂载在 this.e 上是个 object
E.prototype {on: function (name, callback, ctx) {},once: function (name, callback, ctx) {},emit: function (name) {},off: function (name, callback) {}
}module.exports E;
module.exports.TinyEmitter E;once 订阅一次事件当被触发一次后就会被销毁once: function (name, callback, ctx) {var self this;// 构造另一个回调函数调用完之后销毁该 callbackfunction listener () {self.off(name, listener); // 销毁callback.apply(ctx, arguments); // 执行};listener._ callback// on 函数返回 this所以可以链式调用return this.on(name, listener, ctx); // 订阅这个构造的回调函数
}on 事件订阅on: function (name, callback, ctx) {var e this.e || (this.e {});// 单纯 push 进去这里也没有做去重所以同一个回调函数可以被订阅多次(e[name] || (e[name] [])).push({fn: callback,ctx: ctx});// 返回 this可以链式调用return this;
}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对应的事件 name 也可以被 delete(liveEvents.length)? e[name] liveEvents: delete e[name];// 返回 this可以链式调用return this;
}emit 事件触发emit: function (name) {// 取除了第一位的剩余所有参数var data [].slice.call(arguments, 1);// slice() 浅拷贝var evtArr ((this.e || (this.e {}))[name] || []).slice();var i 0;var len evtArr.length;// 循环逐个触发 handler把 data 传入其中for (i; i len; i) {evtArr[i].fn.apply(evtArr[i].ctx, data);}// 返回 this可以链式调用return this;
}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初始化过程// index.js 调用 initMixin 方法初始化 _events object
initMixin(Vue)// event.js 定义 initEvents 方法
// vm._events 保存所有事件 事件回调函数是个 object
export function initEvents (vm: Component) {vm._events Object.create(null)// ...
}// index.js 调用 eventsMixin往 Vue.prototype 挂载相关事件方法
eventsMixin(Vue)// event.js 定义了 eventsMixin 方法
export function eventsMixin (Vue: ClassComponent) {// 事件订阅Vue.prototype.$on function (event: string | Arraystring, fn: Function): Component {}// 事件订阅执行一次Vue.prototype.$once function (event: string, fn: Function): Component {}// 事件退订Vue.prototype.$off function (event?: string | Arraystring, fn?: Function): Component {}// 事件触发Vue.prototype.$emit function (event: string): Component {}
}$on 事件订阅// event 是个 string也可以是个 string 数组
// 说明可以一次性对多个事件订阅同一个回调函数
Vue.prototype.$on function (event: string | Arraystring, fn: Function): Component {const vm: Component thisif (Array.isArray(event)) {for (let i 0, l event.length; i l; i) {vm.$on(event[i], fn)}} else {// 本质是就是对应 eventpush 对应的 fn(vm._events[event] || (vm._events[event] [])).push(fn)// 以下先不展开关于 hookEvent 的调用说明// optimize hook:event cost by using a boolean flag marked at registration// instead of a hash lookupif (hookRE.test(event)) {vm._hasHookEvent true}}return vm
}$once 事件订阅执行一次// 包装一层 on内包含退订操作以及调用操作
// 订阅的是包装后的 on 回调函数
Vue.prototype.$once function (event: string, fn: Function): Component {const vm: Component thisfunction on () {vm.$off(event, on)fn.apply(vm, arguments)}on.fn fnvm.$on(event, on)return vm
}$off 事件退订Vue.prototype.$off function (event?: string | Arraystring, fn?: Function): Component {const vm: Component this// 没有传参数说明全部事件退订直接清空if (!arguments.length) {vm._events Object.create(null)return vm}// 存在 event 数组遍历逐一调用自己if (Array.isArray(event)) {for (let i 0, l event.length; i l; i) {vm.$off(event[i], fn)}return vm}// 以下情况为非数组事件名为单一事件则获取该事件对应订阅的 callbacksconst cbs vm._events[event]// 若 callbacks 为空什么都不用做if (!cbs) {return vm}// 如果传入的 fn 为空说明退订这个事件的所有 callbacksif (!fn) {vm._events[event] nullreturn vm}// callbacks 不为空并且 fn 不为空则为退订某个 callbacklet cblet i cbs.lengthwhile (i--) {cb cbs[i]// 订阅多次的 callback都会被退订一次退订所有相同的 callbackif (cb fn || cb.fn fn) {cbs.splice(i, 1)break}}return vm
}$emit 事件触发Vue.prototype.$emit function (event: string): Component {const vm: Component thisif (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// 获取除了第一位剩余的其他所有参数const args toArray(arguments, 1)const info event handler for ${event}// 遍历逐一触发for (let i 0, l cbs.length; i l; i) {// 以下暂不展开这是 Vue 中对于方法调用错误异常的处理方案invokeWithErrorHandling(cbs[i], vm, args, vm, info)}}return vm
}实现逻辑大致和 mitttiny-emitter 一致也是 pubsub整体思路都是维护一个 object 或者 Mapon 则是放到数组内emit 则是循环遍历逐一触发off 则是查找到对应的 handler 移除数组TODOVue 中对于方法调用错误异常的处理方案invokeWithErrorHandlinghookEvent 的使用原理5. 附录rimrafhttps://www.npmjs.com/package/rimrafmicrobundlehttps://www.npmjs.com/package/microbundlepackage.json exports 字段https://nodejs.org/api/packages.html#packages_conditional_exportsMaphttps://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/MapTS 条件类型https://www.typescriptlang.org/docs/handbook/2/conditional-types.htmlTS Neverhttps://www.typescriptlang.org/docs/handbook/basic-types.html#neverTS keyof: https://www.typescriptlang.org/docs/handbook/2/keyof-types.html#the-keyof-type-operatorWhat 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年内前端人走向前列。识别上方二维码加我微信、拉你进源码共读群今日话题略。欢迎分享、收藏、点赞、在看我的公众号文章~