我正在参与「启航方案」
前言
在上篇文章一盏茶的时刻,聊聊 js 事情循环(Event loop)中,笔者大概叙述了一下 js 的履行机制-事情循环,此篇文章笔者迁就此论题,继续聊聊 js 的异步处理方案有哪些,以及各个方案的优缺陷是什么……
回调函数(callback)
回调函数 简略理解便是一个函数被作为参数传递给另一个函数。回调是前期最常用的一种异步处理方案。
回调并不一定便是异步,并没有直接关系。
举个简略的比如:
function f1(cb) {
setTimeout(() => {
cb && cb();
}, 2000);
}
f1(() => {
console.log("1");
});
如上,咱们运用 setTimeout
在函数 f1
中模仿了一个耗时 2s
的使命,耗时使命完毕后会抛出一个回调,那么咱们在调用时就能够做到在函数 f1
的耗时使命完毕后履行回调函数了。
选用这种方法,咱们把同步操作变成了异步操作,f1
不会堵塞程序运转,相当于先履行程序的主要逻辑,将耗时的操作推延履行。
回调优缺陷
长处:简略、容易理解
缺陷:代码不高雅,可读性差,不易保护,高度耦合,层层嵌套造成回调地狱
事情监听(发布订阅形式)
发布订阅形式 定义了目标间的一种一对多的依赖关系,当一个目标的状况发生改动时,所有依赖于它的目标都将会得到通知。
其实咱们都用过发布订阅形式,比如咱们在 DOM
节点上绑定一个事情函数:
document.body.addEventListener('click', function () {
console.log('点击');
})
但这仅仅对发布订阅形式最简略的运用,在许多场景下咱们常常会运用一些自定义事情来满意咱们的需求。
发布订阅形式有许多种完成方法,下面咱们用 class
来简略完成下:
class Emitter {
constructor() {
// _listener数组,key为自定义事情名,value为履行回调数组-由于可能有多个
this._listener = []
}
// 订阅 监听事情
on(type, fn) {
// 判别_listener数组中是否存在该事情命
// 存在将回调push到事情名对应的value数组中,不存在直接新增
this._listener[type]
? this._listener[type].push(fn)
: (this._listener[type] = [fn])
}
// 发布 触发事情
trigger(type, ...rest) {
// 判别该触发事情是否存在
if (!this._listener[type]) return
// 遍历履行该事情回调数组并传递参数
this._listener[type].forEach(callback => callback(...rest))
}
}
如上所示,咱们创立了一个 Emitter
类,并且添加了两个原型办法 on
和 trigger
,运用如下:
// 创立一个emitter实例
const emitter = new Emitter()
emitter.on("done", function(arg1, arg2) {
console.log(arg1, arg2)
})
emitter.on("done", function(arg1, arg2) {
console.log(arg2, arg1)
})
function fn1() {
console.log('我是主程序')
setTimeout(() => {
emitter.trigger("done", "异步参数一", "异步参数二")
}, 1000)
}
fn1()
咱们先创立一个 emitter
实例,接着注册事情,再触发事情,也处理了异步问题。
事情监听优缺陷
长处:比较符合模块化思维,咱们自写监听器时能够做许多优化从而更好地监控程序运转。
缺陷:整个程序变成了事情驱动,流程上或多或少都会有点影响,每次运用还得注册事情监听再进行触发挺费事的,代码也不太高雅。
Promise
ES2015(ES6)规范化和引入了 Promise
目标,它是异步编程的一种处理方案。
简略来说便是用同步的方法写异步的代码,可用来处理回调地狱问题。
Promise
目标状况一旦改动,就不会再变,只要两种改变可能:
- 从
Pending
变为Resolved
- 从
Pending
变为Rejected
咱们用 setTimeout
模仿异步操作:
function analogAsync(n) {
return new Promise((resolve) => {
setTimeout(() => resolve(n + 500), n);
});
}
function fn1(n) {
console.log(`step1 with ${n}`);
return analogAsync(n);
}
function fn2(n) {
console.log(`step2 with ${n}`);
return analogAsync(n);
}
function fn3(n) {
console.log(`step3 with ${n}`);
return analogAsync(n);
}
用 Promise
来完成:
function fn() {
let time1 = 0;
fn1(time1)
.then((time2) => fn2(time2))
.then((time3) => fn3(time3))
.then((res) => {
console.log(`result is ${res}`);
});
}
fn();
Promise 优缺陷
长处:Promise
用同步的方法写异步的代码,避免了层层嵌套的回调函数,可读性更好。链式操作,能够在 then
中继续写 Promise
目标并回来,然后继续调用 then
来进行回调操作。
缺陷:Promise
目标一旦新建就会立即履行,无法半途取消。若不设置回调函数,Promise
内部会抛出过错,不会流到外部。
Generator
Generator
其实是一个函数,只不过是一个特殊的函数。一般函数,你履行了这个函数,函数内部不会停,直到这个函数完毕。Generator
这个函数特殊之处便是中心能够停。
示例:
function *generatorFn() {
console.log("a");
yield '1';
console.log("b");
yield '2';
console.log("c");
return '3';
}
let it = generatorFn();
it.next();
it.next();
it.next();
it.next();
上面这个示例便是一个 Generator
函数,它有如下特点:
- 不同于一般函数,
Generator
函数在function
后边,函数名之前有个*
- 函数内部有
yield
字段- 调用后其函数回来值运用了
next
办法
Generator 优缺陷
长处:高雅的流程操控方法,能够让函数可中止履行
缺陷:Generator
函数的履行必须靠履行器,只针对异步处理来说,仍是不太方便
async/await
ES2017 规范引入了async
函数,使得异步操作变得更加方便。async
是异步的意思,而await
是async wait
的简写,即异步等待,async/await
的出现,被许多人认为是 js 异步操作的终究且最高雅的处理方案。
async 在做什么
async
函数回来的是一个 Promise
目标,假如在async
函数中直接 return
一个直接量,async
会把这个直接量通过Promise.resolve()
封装成 Promise
目标回来。
await 在等待什么
await
等待的是一个表达式,这个表达式的核算成果是 Promise
目标或者其它值(换句话说,便是没有特殊限制,啥都行)。
- 假如
await
后边不是Promise
目标,直接履行- 假如
await
后边是Promise
目标会堵塞后边的代码,Promise
目标resolve
,然后得到resolve
的值,作为await
表达式的运算成果await
只能在async
函数中运用
上述用 setTimeout
模仿异步操作,咱们用 async/await
来完成:
async function fn() {
let time1 = 0;
let time2 = await fn1(time1);
let time3 = await fn2(time2);
let res = await fn3(time3);
console.log(`result is ${res}`);
}
fn();
输出成果和上面用 Promise
完成是相同的,但这个async/await
代码结构看起来清晰得多,几乎跟同步写法相同,非常高雅。
async/await 优缺陷
长处:内置履行器,更好的语义,更广的适用性
缺陷:乱用await
可能会导致性能问题,由于await
会堵塞代码
参考:「硬核JS」深化了解异步处理方案
总结
以上便是笔者关于 js 异步处理方案的概述,以及对它们的优缺陷的分析,如有不足欢迎我们指出,假如我们觉得还不错的话,不要忘了点赞呦~