Skip to content

使用发布订阅模式实现 Event 类

在腾讯面试中遇到的这个问题,参考文章

用 JavaScript 实现一个类,包含绑定事件on、解绑事件off、只监听一次事件once和派发事件 emit

  • on(eventName, func): 监听 eventName事件,事件触发的时候调用 func函数
  • emit(eventName, arg1, arg2, arg3,arg4...) : 触发eventName 事件, 并且把参数 arg1, arg2, arg3, arg4...传给事件处理函数
  • off(eventName, func) : 停止监听某个事件
  • once(eventName, func) : 监听 eventName事件,事件触发的时候调用 func函数,随后立即删除该绑定

在组件化开发的页面中,也就是同一个页面由两三个人来开发的情况下,为了保证组件的独立性,往往使用订阅发布模式,即组件间通信使用事件监听和派发的方式,而不是直接相互调用组件,这就是题目中要求实现的类。

代码一览:

js
class Event {
	constructor() {
		// 存储事件的数据对象
		// 为了迅速查找,使用字典
		this.events = {};
	}
	// 绑定事件
	on(eventName, callBack) {
		if (!this.events[eventName]) {
			this.events[eventName] = [];
		}
		this.events[eventName].push(callBack);
	}

	// 派发事件:遍历事件列表,对每个事件执行相应的回调函数
	emit(eventName, ...args) {
		if (this.events[eventName]) {
			this.events[eventName].forEach((callBack) => callBack(...args)); 
		}
	}

	// 取消订阅事件,从事件列表中删除指定的回调函数
	off(eventName, callBack) {
		if (this.events[eventName]) {
			if (callBack) {
				// 如果指定了回调函数,就删除该函数
				this.events[eventName] = this.events[eventName].filter(
					(cb) => cb != callBack
				);
			} else {
				// 如果没指定回调函数,就删除所有绑定的函数
				this.events[eventName] = [];
			}
		}
	}

	// 为事件绑定一个只执行一次的回调函数
	once(eventName, callBack) {
		// 将回调函数与取消订阅事件封装起来,成为一个新的函数
		const func = () => {
			callBack();
			this.off(eventName, func);// 注意这里传入的参数是新定义的 func 而不是 callBack
		};
		this.on(eventName, func);
	}
}

function test() {
	console.log("in test");
}

const test2 = function () {
	console.log("in test2");
};

myEvent = new Event();

myEvent.on("test", test);
myEvent.once("test2", test2);
myEvent.emit("test2");

LazyMan 实现

参考 多种方式实现 LazyMan - 掘金

先来了解一下什么是 LazyManLazyMan方法可以按照以下方式调用:

js
LazyMan('Hank');
// 输出:
// Hi! This is Hank!

LazyMan('Hank').sleep(3).eat('dinner')
// 输出:
// Hi! This is Hank!
// //等待3秒..
// Wake up after 3
// Eat dinner~

LazyMan('Hank').eat('dinner').eat('supper')
// 输出:
// Hi This is Hank!
// Eat dinner~
// Eat supper~

LazyMan('Hank').sleepFirst(2).eat('dinner').sleep(3).eat('supper')
// 输出:
// //等待2秒..
// Wake up after 2
// Hi This is Hank!
// Eat dinner~
// //等待3秒..
// Wake up after 2
// Eat supper~

// 以此类推

分析一下本题的核心:链式调用异步问题

方法一:任务队列

这个方法的核心是 next() 方法以及 return this

js
class LazyMan {
	taskQueue = [];
	constructor(name) {
		this.name = name;
		setTimeout(() => {
			// 创造一个异步
			this.next();
		});
	}

	sayName() {
		const fn = ()=>{
			console.log(`我叫 ${this.name}`);
			this.next();
		}
		this.taskQueue.push(fn);
		return this;
	}
	next() {
		const fn = this.taskQueue.shift();
		if (fn) {
			fn(); // 如果 fn 存在,则执行
		}
	}
	holdon(sec) {
		return () => {
			setTimeout(() => {
				console.log(`${sec} 秒后醒来啦`);
				this.next();
			}, sec * 1000);
		};
	}
	sleep(sec) {
		this.taskQueue.push(this.holdon(sec));
		return this;
	}
	eat(food){
		const fn = ()=>{
			console.log(`吃了 ${food}`);
			this.next();
		}
		this.taskQueue.push(fn);
		return this;
	}
	sleepFirst(sec){
		this.taskQueue.unshift(this.holdon(sec));
		return this;
	}
}

const zrz = new LazyMan('zrz');

zrz.sayName().eat('黄焖鸡').sleepFirst(2).eat('烤肉').sleep('3').eat('黄焖鸡again!');

结果:

bash
(base) jiangjiaqi@MacBook-Air-3 lazyman % node lazyman.js
2 秒后醒来啦
我叫 zrz
吃了 黄焖鸡
吃了 烤肉
3 秒后醒来啦
吃了 黄焖鸡again!

任务队列 + Promise 实现

TIP

Promise 是一个对象,表示异步操作的结果。