面试官问发布订阅模式是在问什么?
大家好,我是若川。最近组织了源码共读活动,感兴趣的可以加我微信 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标签中必不少的