持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第10天,点击查看活动详情
1. 问题背景
有这么一个需求:在同一个页面中,可以点击 提交按钮,进入下一个 表单,点击 表单 中的 提交按钮,又会出现一个 表单,点击这个 表单 中的 提交按钮 又会出现一个 表单 。。。现在为了用户体验,要求 按下回车 也可以触发按钮
如果我们对全局进行多个 回车事件 的绑定,这时候就会带来一个问题,如果用户按下回车,会 触发多个回车事件,这时候我们要怎么区分呢?
2. 问题模型
为了简化这个过程我们将这个问题转化成如下模型
const fn1 = () => {
console.log(1);
};
const fn2 = () => {
console.log(2);
};
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.keyCode === 13) {
fn1();
}
});
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.keyCode === 13) {
fn2();
}
});
这里在 document 绑定了两个回车事件,当按下回车的时候,依次输入 1 2,但我们希望第一次按下回车输出 1,第二次回车输出 2
3. 第一种解决方案 - 异步等待
思路:我们可以设置一个变量,这个变量专门 记录回车事件的次数,这样就可以 区分 两个回车事件
let count = 0;
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.keyCode === 13 && !(count % 2)) {
fn1();
count++;
}
});
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.keyCode === 13 && count % 2) {
fn2();
count++;
}
});
当我们按下回车,依然会同属输出 1 2,这是为什么呢?
原因:当我们按下回车的时候,判断是否是偶数,如果是的话,执行 fn1,然后增加计数器,这时候立刻进行第二次判断,是奇数,执行 fn2
解决办法:在很多的实际的开发场景中,按下回车,往往会引起视图的变化,这个过程是异步的,所以我们将这种 异步 的思想用来解决这个问题了,当输入回车的时候,对计数器进行判断,然后执行对应的处理函数,异步地增加 计数器,也就是把计数器增加的过程放置在进行其余分支判断之后,这是一种简单 异步等待 的思想
let count = 0;
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.keyCode === 13 && !(count % 2)) {
fn1();
setTimeout(() => {
count++;
});
}
});
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.keyCode === 13 && count % 2) {
fn2();
setTimeout(() => {
count++;
});
}
});
我们只要在对 增加计数器 这个过程包裹一个异步任务就可以实现这个需求了,每当执行完处理函数,等到执行完所有其余的同步代码,异步等待 计数器增加
4. 第二种解决方案 - 策略模式
如果我们想要还增加一个回车事件呢?
我们可以对判断计数器的任务多增加一个 判断分支
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.keyCode === 13) {
if (count % 3 === 0) {
fn1();
setTimeout(() => {
count++;
});
} else if (count % 3 === 1) {
fn2();
setTimeout(() => {
count++;
});
} else if (count % 3 === 2) {
fn3();
setTimeout(() => {
count++;
});
}
}
});
在这里我们将多个回车事件合并在一个事件处理函数中,每当我们多增加一个回车事件,我们可以多增加一个分支,既然如此,不妨我们把这种方式 封装 起来
const enterHandlers: Function[] = [];
const addEnterListener = (fn: Function) => {
enterHandlers.push(fn);
};
let count = 0;
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.keyCode === 13) {
const len = enterHandlers.length;
enterHandlers[count++ % len]();
}
});
// 绑定回车事件
addEnterListener(() => {
console.log(1);
});
addEnterListener(() => {
console.log(2);
});
addEnterListener(() => {
console.log(3);
});
在这里我们首先定义了一个专门 收集回车事件的处理函数 的 数组,当触发回车事件的时候会自动分发到对应的处理函数,如果我们想要增加一个全新的回车事件,只需要往这个数组中追加 新的事件处理函数
这个时候不设置定时器函数,依然可以区分多个回车事件,这样的做法 摆脱 了 异步
在很多实际开发的时候,每当我们执行 回车事件 触发按钮,进入下一个表单的时候,就应该立即删除之前的回车事件,上面一种做法很难做到 删除事件
const docKeydownHandler = (e: KeyboardEvent) => {
if (e.keyCode === 13) {
const len = enterHandlers.length;
enterHandlers[count++ % len]();
if (count === len) {
document.removeEventListener('keydown', docKeydownClick);
}
}
};
document.addEventListener('keydown', docKeydownHandler);
即使这样,也执行 最后一次 的回车处理函数 一起 删除 整个回车事件
5. 第三种解决方案 - Promise 与 发布订阅
我们的目的是:每执行完一个事件处理函数立即删除当前处理函数,如果我们用一个变量记录当前的回车事件,执行完处理函数,更新这个变量
let cb: Function;
const docKeydownHandler = (e: KeyboardEvent) => {
if (e.keyCode === 13) {
cb();
}
};
document.addEventListener('keydown', docKeydownHandler);
const addEnterLister = (fn: Function) => {
cb = fn;
};
addEnterLister(() => {
console.log(1);
addEnterLister(() => {
console.log(2);
addEnterLister(() => {
console.log(3);
document.removeEventListener('keydown', docKeydownHandler);
});
});
});
这样整个代码就陷入 回调地狱,我们可以使用 Promise 优化这个过程
Promise.resolve()
.then(() => {
return new Promise(resolve => {
addEnterLister(() => {
console.log(1);
resolve(0);
});
});
})
.then(() => {
return new Promise(resolve => {
addEnterLister(() => {
console.log(2);
resolve(0);
});
});
})
.then(() => {
return new Promise(resolve => {
addEnterLister(() => {
console.log(3);
resolve(0);
document.removeEventListener('keydown', docKeydownHandler);
});
});
});
我们依然对这种 Promise 方法处理进行封装
let cb: Function;
const docKeydownHandler = (e: KeyboardEvent) => {
if (e.keyCode === 13) {
cb();
}
};
document.addEventListener('keydown', docKeydownHandler);
// 订阅函数
const addEnterLister = (fn: Function) => {
thenCbs.push(() => {
return new Promise(resolve => {
cb = () => {
fn();
resolve(0);
};
});
});
};
const thenCbs: (() => Promise<0>)[] = [];
// 订阅
addEnterLister(() => {
console.log(1);
});
addEnterLister(() => {
console.log(2);
});
addEnterLister(() => {
console.log(3);
document.removeEventListener('keydown', docKeydownHandler);
});
// 发布
let res: Promise<0> = Promise.resolve(0);
for (let i = 0; i < thenCbs.length; i++) {
res = res.then(thenCbs[i]);
}
在这个过程中,我们定义了 thenCbs 专门用于收集 Promise 创建器(函数返回值为 Promise) 的函数,在 发布 的时候构造出 Promise 链条,当执行每一次 回车事件 的时候,回车事件处理函数执行完,立即修改 cb,然后传递 Promise
6. 第四种解决方案 - 立即删除事件
除了上述三种方法,还有一种笨方法:每当执行完 回车事件,立即删除回车事件
const fn1 = () => {
console.log(1);
};
const fn2 = () => {
console.log(2);
};
const cb1 = (e: KeyboardEvent) => {
if (e.keyCode === 13) {
fn1();
document.removeEventListener('keydown', cb1);
document.addEventListener('keydown', cb2);
}
};
const cb2 = (e: KeyboardEvent) => {
if (e.keyCode === 13) {
fn2();
document.removeEventListener('keydown', cb2);
document.addEventListener('keydown', cb1);
}
};
document.addEventListener('keydown', cb1);
优点:
- 直观易懂
缺点:
- 极其难扩展
- 程序耦合性强
- 不容易观察执行顺序
7. 总结
对于解决页面中 多个回车事件,我们提供了四种解决方案,对于这个需求,其实可以细分为 三种 更具体的情况:
- 循环多个回车事件,输出
1 2 3 4 1 2 3 4 ...的效果,也就是不removeEventListener回车事件 - 只循环一次回车事件,输出
1 2 3 4 4 4 ...,不removeEventListener回车事件 - 只循环一次回车事件,输出
1 2 3 4之后立即removeEventListener回车事件
如果是第一种情况,完全可以使用 策略模式
如果是第二种和第三种情况,使用 Promise 结合发布订阅 具有更好的扩展性
有这样一个情景,实现注册页面的时候,在这个页面有多个表单,填写完一个表单,点击提交按钮之后出现一个新的表单。。。
就像 JD 注册的过程
为了更好的适配性,我们将 Promise 封装成面向对象的形式
class EnterHandlers {
thenCbs: (() => Promise<0>)[] = [];
res = Promise.resolve(0);
cb: Function = () => {};
docKeydownHandler: (e: KeyboardEvent) => void;
constructor() {
this.docKeydownHandler = (e: KeyboardEvent) => {
if (e.keyCode === 13) {
this.cb();
}
};
document.addEventListener('keydown', this.docKeydownHandler);
}
addEnterListener(fn: Function) { // 绑定回车事件
this.thenCbs.push(() => {
return new Promise(resolve => {
this.cb = () => {
fn();
resolve(0);
};
});
});
return this;
}
addLastEnterListener(fn: Function) { // 执行完立即删除整个回车事件
this.addEnterListener(() => {
fn();
document.removeEventListener('keydown', this.docKeydownHandler);
});
return this;
}
notify() { // 发布
for (let i = 0; i < this.thenCbs.length; i++) {
this.res = this.res.then(this.thenCbs[i]);
}
return this;
}
}
使用:
new EnterHandlers()
.addEnterListener(() => {
console.log(1);
})
.addEnterListener(() => {
console.log(2);
})
.addEnterListener(() => {
console.log(3);
})
.addLastEnterListener(() => {
console.log(4);
})
.notify();
结语
如果觉得本文不错的话,可以给作者点一个小小的赞,你的鼓励将是我前进的动力
如果本文对你有帮助或者你有不同的解决方案,欢迎在评论区留下你的足迹
Promise 相关文章推荐
- promise 中的细节:
《其实你不知道 Promise.then 》
《你真的明白 promise 的 then 吗?》
《await 中那些不为人知的细节》 - 专栏:
《从 v8 看 promise》