同步和异步
- 同步(Synchronous)
定义:任务按顺序依次执行,前一个任务完成前,后续任务必须等待。
特点:阻塞性执行,程序逻辑直观,但效率较低
- 异步(Asynchronous)
定义:任务发起后无需等待结果,程序继续执行其他操作,待任务完成后通过回调或事件通知处理结果。
特点:非阻塞性执行,支持并发,资源利用率高
简单来说,同步就是刷牙然后煮面,异步就是让面在一边煮着一边跑去刷牙。
期约
期约是对尚不存在结果的一个替身。
在ES6中,期约是一种引用类型(Promise),使用new操作符实例化。
期约状态机
期约对象有三种状态,这些状态是期约对象内置的,除了调用相应的API,否则不能对其进行更改。
pending(待定)
fulfilled(兑现)
rejected(拒绝)
新建一个期约对象,并且期约对象还没进行任何操作时,期约对象的状态为pending
,当期约对象已经被成功解决后,则转为fulfilled
,而解决失败则转为rejected
,具体让期约状态转换的函数后面介绍。
待定(pending)是期约的最初始状态。在待定状态下,期约可以落定(settled)为代表成功的兑现(fulfilled)状态,或者代表失败的拒绝(rejected)状态。
无论落定为哪种状态都是不可逆的。只要从待定转换为兑现或拒绝,期约的状态就不再改变。而且,也不能保证期约必然会脱离待定状态。因此,组织合理的代码无论期约解决(resolve)
还是拒绝(reject),甚至永远处于待定(pending)状态,都应该具有恰当的行为。
重要的是,期约的状态是私有的,不能直接通过JavaScript检测到。这主要是为了避免根据读取到的期约状态,以同步方式处理期约对象。
另外,期约的状态也不能被外部JavaScript代码修改。这与不能读取该状态的原因一样:【期约故意将异步行为封装起来,从而隔离外部的同步代码】《JavaScript高级程序设计第四版》
如何理解期约对象
正如前面介绍的,期约对象是专门为异步编程而设计的,期约对象是对尚不存在结果的一个替身。
举个例子,你参加了一场考试,试卷由他人进行批改(异步操作),而你在试卷批改的过程中是自由的,你可以吃饭睡觉,你也可以尝试查询试卷批改的状态。
如果试卷还在批改当中,则返回pending
,如果已经批改结束,并且你已经通过了考试,返回fulfilled
,如果未通过考试,则返回rejected
。
理解了上述的场景,我们就能够理解期约对象和期约对象的状态机了。
期约对象代表了某一个异步操作。
期约状态机代表了异步操作的完成状态。
如何控制期约对象的状态机
期约对象的状态是私有的,只能通过期约对象内部的API进行操作。
向期约对象中传入一个执行器函数(回调函数),期约对象会向执行器函数传入两个参数resolve
和reject
,调用resolve()
使期约状态变为fulfilled
,调用reject()
使期约状态变为rejected
const waiter1 = new Promise((resolve, reject) => { });
const waiter2 = new Promise((resolve, reject) => resolve());
const waiter3 = new Promise((resolve, reject) => reject());
// 其中undefined表示期约对象在完成后期待一个返回值,但这里没有给返回值
console.log('waiter1:', waiter1); // waiter1: Promise {<pending>}
console.log('waiter2:', waiter2); // waiter2: Promise {<fulfilled>:undefined}
console.log('waiter3:', waiter3); // waiter1: Promise {<rejected>:undefined}
期约对象的静态方法
Promise.resolve()
这个方法可以直接创建一个
fulfilled
状态的期约对象,这个期约对象的值由传入的参数指定。const settled1 = Promise.resolve(3) const settled2 = Promise.resolve('我是字符串') const settled3 = Promise.resolve(new Promise(() => {})) console.log(settled1) // Promise {<fulfilled>: 3} console.log(settled2) // Promise {<fulfilled>: '我是字符串'} console.log(settled3) // Promise {<pending>}
可以看到,我们传入什么值,期约对象就会返回什么值。
但是如果我们传入的是另一个期约对象,则会直接返回传入的期约对象。
Promise.reject()
这个方法可以直接创建一个
rejected
状态的期约对象,这个期约对象的值由传入的参数指定。const settled1 = Promise.reject(3) const settled2 = Promise.reject('我是字符串') const settled3 = Promise.reject(new Promise(() => {})) console.log(settled1) // Promise {<rejected>: 3} console.log(settled2) // Promise {<rejected>: '我是字符串'} console.log(settled3) // Promise {<rejected>: Promise {<pending>}}
这个方法和
Promise.resolve()
类似,也会将传入的值作为期约对象的值返回。但不同的是,如果传入一个期约对象,那么这个期约对象也会作为期约对象的值返回。
调用reject()或者
Promise.reject()
都会抛出一个异步错误。同步代码块中的trycatch结构无法捕获到异步错误,只有异步结构中才能捕获异步错误期约的实例方法
实现Thenable方法
在ECMAScript暴露的异步结构中,任何对象都有一个then()方法。
Promise.prototype.then()
then()方法挂载在Promise的原型上,所以被所有Promise实例对象共享。
then()方法接收两个回调函数参数,第一个参数在Promise对象的状态落定为
fulfilled
时执行,第二个参数在Promise对象的状态落定为rejected
时执行。如何理解then()方法
在平时写js方法时,我们都是使用的同步代码块,也就是说,写在后面的代码一定后执行,比如我们在前面一行计算
let sum = 10 + 1
,那么我们就可以在这一行后面的任意位置输出sum
,因为sum
的计算是写在前面的,在同步代码块中,他已经被计算完毕了。现在我们使用了异步编程,我们已经知道Promise对象内置了一个状态机,它用于通知自己是否执行完毕。
由于Promise是异步执行的,假如我们在同步代码块中读取Promise对象,我们有可能获取3种结果。如果我们需要打印Promise的返回值,我们不可能在同步代码块中不停的检测Promise的状态,这样会导致后面的代码无法执行,这就违背了异步编程的初衷。
所以我们使用then()方法,then()方法可以想象为一个触发器,设置好then()方法后,只要Promise对象
settled
到了任意一种状态,就会触发then()方法中设定好的函数,这样我们就可以异步的处理Promise的返回值而无需再在同步代码块中处理Promise的返回值了。const p1 = new Promise((resolve, reject) => { // 1秒后返回10 + 1的计算结果 setTimeout(() => resolve(10 + 1), 1000); }); p1.then(() => { console.log('我计算完成,被触发了'); console.log('我是then中的p1:',p1) }, () => { console.log('我计算失败,被触发了'); }); console.log('我是同步代码块中的p1:',p1)
可以看到,同步代码块由于执行的比较快,已经运行到输出Promise对象的值了,但是此时Promise对象还没执行完,状态为
pending
,而then中却可以正常输出Promise的返回值11,这是因为then()中的第一个参数只有在Promise对象状态为fulfilled时才被调用。我们将then()方法的第一个参数称为onResolved处理程序,第二个参数称为onRejected处理程序。
.then()
返回的 Promise 状态如何确定?回调返回值类型决定状态
返回普通值(非 Promise 对象):新 Promise 会被
Promise.resolve()
包装为 fulfilled 状态p.then(() => 42); // 新 Promise 状态:fulfilled,值:42
抛出异常:新 Promise 变为 rejected 状态,异常对象作为拒绝原因
p.then(() => { throw new Error("fail"); }); // 状态:rejected,原因:Error对象
返回 Promise 对象:新 Promise 将 继承该 Promise 的状态和值
p.then(() => Promise.reject("error")); // 新 Promise 状态:rejected,原因:"error"
因此通过.then()方法返回的也是Promise对象,所以也有.then()方法,.then()方法可以进行链式调用
const p1 = new Promise((resolve, reject) => { // 3秒后返回10 + 1的计算结果 setTimeout(() => resolve(double(1)), 1000); }); p1.then(value => { return value }) .then(value => { return double(value) }) .then(value => { return double(value) }) .then(value => console.log(value)) // 8
Promise.prototype.catch()
等于
then(null,() => {})
,也就是onRejected处理程序。Promise.prototype.finally()
传入finally()的回调函数能保证一定被执行,和try-catch-finally中的finally用法一致。
Promise.all()和Promise.race()
这两个方法都可以传入一个包含多个期约的可迭代对象,常见方法是传入一个包含多个期约的数组。
Promise.all()
:会等待传入的期约全部兑现后才兑现,如果有一个期约待定或者拒绝,则返回待定或者拒绝Promise.race()
:会返回根据第一个兑现或者拒绝的期约决定状态的新期约对象。
异步函数
通过刚刚期约对象的学习我们了解到,期约对象是异步执行的,因此如果想操作期约对象的流程必须要使用then()
方法。
但是这样同样导致了一个问题,同步代码块和异步代码块被完全的区分开了,从使用了期约对象开始,所有和这个期约对象有关的流程都要在then中实现,这会使得then()方法的函数体变得很大,并不好维护。
于是,从ES8开始引入了一组新的关键字async/await
用于解决这个问题。
基本概念
async
函数声明方式:在函数前添加
async
关键字,如async function fetchData() {}
。返回值:始终返回一个
Promise
对象。若函数返回非Promise
值(如字符串、数值),该值会被自动包装为resolve
状态的Promise
async function example() { return "Hello"; } example().then(console.log); // 输出 "Hello"
await
关键字使用范围:仅能在
async
函数内部使用。功能:暂停当前
async
函数的执行,等待右侧的Promise
完成(resolve
或reject
),并返回解析后的值async function fetchUser() { const response = await fetch('/api/user'); // 等待请求完成 return response.json(); }
简单来说,关键字
async
声明了这个函数应该被异步执行,关键字await
表明被async
声明的函数应该被停止执行,等到await
右侧的表达式返回值后才被继续执行。
async
被async
关键字标记的函数会返回一个Promise对象,如果返回的值不是Promise对象,则会使用Promise.resolve()对返回的值进行包装。
async
函数的执行流程
- 同步代码的立即执行
未遇到
await
时:async
函数内部的代码会按照同步顺序立即执行,与普通函数的行为完全一致。例如:async function demo() { console.log("A"); // 同步执行 console.log("B"); // 同步执行 } demo(); console.log("C");
输出顺序为:
A → B → C
本质:
async
函数被调用时,其函数体内的同步代码会直接进入主线程的同步任务队列,立即执行。
- **
await
对执行流程的干预**
遇到
await
时:函数会暂停当前执行,将await
后的表达式(通常是Promise
)放入微任务队列,并交出主线程控制权。此时,外部同步代码会继续执行。例如:async function demo() { console.log("A"); await new Promise(resolve => setTimeout(resolve, 1000)); // 暂停 console.log("B"); // 异步执行(微任务) } demo(); console.log("C");
输出顺序为:
A → C → (1秒后) B
关键机制:
await
后的代码会被封装为微任务,等待当前同步代码执行完毕后才会继续执行。
async function heavyTask () {
console.log("开始耗时操作");
// 没有await
for (let i = 0; i < 1e9; i++);
console.log("耗时操作完成");
}
heavyTask();
console.log("外部代码");
async function heavyTask () {
console.log("开始耗时操作");
// 有await
await '123'
for (let i = 0; i < 1e9; i++);
console.log("耗时操作完成");
}
heavyTask();
console.log("外部代码");
可以看到,在await后的代码才会作为异步代码执行,否则async修饰的代码会像普通函数一样同步执行。
await
await关键字期待右侧是一个实现了Thenable接口的对象。
但如果不是,则await不会等待,而是将右侧视为一个已经fulfilled
的期约对象,直接返回。不会将值包装为Promise对象
const func = async () => {
console.log(await '123')
}
// 注意,123没有被包装为Promise对象
func() // '123'
如果await右侧是一个Promise对象,并且尚未settled,那么异步程序会在await处阻塞,停止运行直到右侧的Promise对象已经fulfilled
或者rejected
。
异步函数的特质不会扩展到嵌套函数,await只能在async标记的函数中使用
如果需要进行并行优化,不要每调用一次async函数就等待await返回值,而是一次性将async函数全部调用后,再按照需要的顺序等待await的返回值。
const asyncFunc1 = async () => { console.log(1); return 'a' }
const asyncFunc2 = async () => { console.log(2); return 'b' }
const asyncFunc3 = async () => { console.log(3); return 'c' }
const asyncFunc4 = async () => { console.log(4); return 'd' }
// 错误示范
const run1 = async () => {
console.log(await asyncFunc1())
console.log(await asyncFunc2())
console.log(await asyncFunc3())
console.log(await asyncFunc4())
}
// 正确示范
const run2 = async () => {
const res1 = asyncFunc1()
const res2 = asyncFunc2()
const res3 = asyncFunc3()
const res4 = asyncFunc4()
console.log(await res1)
console.log(await res2)
console.log(await res3)
console.log(await res4)
}
在错误示范中,每次调用async函数都等到async函数返回后才执行下一个async函数。
而正确示范中,将所有async函数全部执行后,再等待async函数的返回值。
正确示范也可以通过Promise.all()来实现。