学习链接
第 8 题:setTimeout、Promise、Async/Await 的区别
异步操作
所谓”异步”,简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
callback
回调函数是 JavaScript 语言对异步编程的一种实现。
所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。
优点:简单、容易理解和实现
缺点:不利于代码的阅读和维护(尤其是多个回调函数嵌套的情况,即回调地狱)
- 各个部分之间高度耦合,使得程序结构混乱,不易阅读和调试
- 错误处理不方便
执行分成两段,第一段执行完以后,任务所在的上下文环境就已经结束了。在这以后抛出的错误,原来的上下文环境已经无法捕捉 - 每个任务只能指定一个回调函数
Promise
Promise
是异步编程的一种解决方案,就是为了解决 callback 的问题而提出的。
它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise
对象。
- 它不是新的语法功能,而是一种新的写法
允许将回调函数的嵌套,改成链式调用,使得异步任务的两段执行看得更清楚 - 也就是说,
Promise
相当于提供了一个容器,里面保存了某个未来才会结束的事件(通常为异步操作),
然后将异步操作的结果用Promise
包装后传递给后面的方法(回调) Promise
提供了一个统一的接口,使得控制异步操作变得更加容易
缺点:
- 无法取消
Promise
,一旦新建它就会立即执行,无法中途取消 - 如果不设置回调函数,
Promise
内部抛出的错误,不会反应到外部 - 当处于
pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成) Promise
自身的 API 会使得代码产生一些冗余,让原来的语义变得不清楚
Generator
异步编程(多任务)的一种解决方案——“协程”(coroutine),意思是多个线程互相协作,完成异步任务,是协作式多任务的轻量级线程。
协程有点像函数(子例程),又有点像线程。它的运行流程大致如下。
- 第一步,协程
A
开始执行。 - 第二步,协程
A
执行到一半,进入暂停,执行权转移到协程B
。 - 第三步,(一段时间后)协程
B
交还执行权。 - 第四步,协程
A
恢复执行。
上面流程的协程 A
,就是异步任务,因为它分成两段(或多段)执行。
同子例程的比较
说与子例程像,主要是指在转移执行权方面的关系。
- 子例程可以调用其他的子例程,这样的子例程之间存在调用的父子关系
- 协程之间可以通过
yield
(让步),即暂时让出执行权来调用其他协程,
这种方式转移执行权的协程之间不是调用的父子关系,而是彼此对称、平等的
- 子例程的生命期遵循后进先出,子例程调用其他子例程,调用者等待被调用者结束后继续执行
协程的生命期完全由对它们的使用需要来决定 - 子例程的起始处是惟一的入口点,每当子例程被调用时,执行都从被调用子例程的起始处开始
协程可以有多个入口点,协程的起始处是第一个入口点,每个**yield**
返回出口点都是再次被调用执行时的入口点 - 子例程只在结束时一次性返回全部结果值
协程可以在**yield**
时不调用其他协程,而是每次返回一部分的结果值,这种协程常称为生成器或迭代器
子例程可以看作是特定状况的协程,任何子例程都可转写为不调用 yield
的协程。
同线程的比较
说与线程像,主要是指在处理多任务方面的关系。
协程是协作式 多任务的,而线程是抢占式 多任务的。
即资源分配时
- 哪个线程先得到资源,由运行环境决定
- 协程的执行权由协程自行分配
这也意味着协程提供并发性而非并行性。
即在同一时间
- 可有多个线程处于运行状态
- 只有一个协程处于运行态,其他都是暂停状态
在协程之间的切换非常方便,不需要涉及任何的系统调用或者任何阻塞调用,这使得协程比线程更加适合实时运算的场景。
协程是完全由程序所控制,也就是在用户态执行,这样的好处就是性能得到了很大的提升,不会像切换线程那样消耗资源。
**Generator**
函数,是 ES6 对协程的不完全实现,也称为 “半协程” ,是协程的子集。
- 生成器只能把控制权转交给调用生成器的调用者
即只有生成器的调用者,能将程序执行权还给生成器 - 完全实现的协程,有能力控制将控制权转交给哪个协程
即若实现的是协程而非半协程,那么任何函数都可让暂停的Generator
函数继续运行 - 在
Generator
函数中的yield
语句不指定要跳转到的协程,而是向父例程传递返回值
优点:
Generator
函数是可以暂停执行和恢复执行的
在生成器函数内部执行一段代码,遇到yield
关键字,js引擎会返回关键字后面的内容给外部,并暂停该函数的执行。外部函数可以通过next
这类方法恢复函数的执行。
缺点:
- 流程管理不方便(即何时执行第一阶段、何时执行第二阶段)
async/await
异步的角度来看,async
算是 Generator + Promise
的语法糖。
async
函数对 Generator
函数的改进,体现在以下四点。
- 内置执行器
Generator
函数的执行需要调用next
方法,或者用co
模块,也就是需要依靠执行器,才能真正执行,得到最后结果async
函数自带执行器。也就是说,async
函数的执行,与普通函数一模一样,只要一行
- 更好的语义
async
和await
,比起星号和yield
,语义更清楚了async
表示函数里有异步操作,await
表示紧跟在后面的表达式需要等待结果
- 更广的适用性
co
模块约定,yield
命令后面只能是Thunk
函数或Promise
对象- 而
async
函数的await
命令后面,可以是Promise
对象和任意类型的值
(数值、字符串和布尔值,但这时会自动转成立即 resolved 的Promise
对象)
- 返回值是
**Promise**
async
函数的返回值是Promise
对象,这比Generator
函数的返回值是 Iterator 对象方便多了。可以用then
方法指定下一步的操作- 进一步说,
async
函数完全可以看作多个异步操作,包装成的一个Promise
对象,而await
命令就是内部then
命令的语法糖
与微任务的关系
async
函数中,可将 await
当作 then
来看待,以此作为分界线。
本次 await
后跟着的同步语句(或者说本轮宏任务),加上与上一个 await
之间的宏任务,
即为本轮的微任务。(暂未想到合适的表述,具体效果见 async 例)
Promise
new Promise(resolve => {
console.log(1, 1);
resolve();
console.log(1, 2);
}).then(() => {
console.log(1, 3);
}).then(() => {
console.log(1, 4);
})
new Promise(resolve => {
console.log(2, 1);
resolve();
console.log(2, 2);
}).then(() => {
console.log(2, 3);
}).then(() => {
console.log(2, 4);
})
// 1 1
// 1 2
// 2 1
// 2 2
// 1 3
// 2 3
// 1 4
// 2 4
async
setTimeout(() => console.log('setTimeout'));
async function test() {
console.log('async', 1);
await new Promise((resolve, reject) => {
console.log('async', 2);
resolve();
})
.then(() => console.log('async', 3))
.then(() => console.log('async', 4));
console.log('async', 5);
await console.log('async', 6);
await console.log('async', 7);
}
test();
console.log(3);
new Promise(resolve => {
console.log('Promise', 1);
resolve();
console.log('Promise', 2);
})
.then(() => {
console.log('Promise', 3);
})
.then(() => {
console.log('Promise', 4);
})
.then(() => {
console.log('Promise', 5);
})
.then(() => {
console.log('Promise', 6);
});
// async 1
// async 2
// 3
// Promise 1
// Promise 2
// async 3
// Promise 3
// async 4
// Promise 4
// async 5
// async 6
// Promise 5
// async 7
// Promise 6
// setTimeout
Generator
function* test() {
console.log('Generator', 1);
yield console.log('Generator', 2);
yield new Promise(resolve => {
console.log('Generator', 3);
resolve();
}).then(() => {
console.log('Generator', 4);
});
yield console.log('Generator', 5);
}
const temp = test();
temp.next();
temp.next();
temp.next();
temp.next();
new Promise(resolve => {
console.log('Promise', 1);
resolve();
})
.then(() => {
console.log('Promise', 2);
})
.then(() => {
console.log('Promise', 3);
})
// Generator 1
// Generator 2
// Generator 3
// Generator 5
// Promise 1
// Generator 4
// Promise 2
// Promise 3