原理

JavaScript最初被设计为一种简单的脚本语言,仅仅用于增强网页的交互性,所以采用单线程模型,避免复杂的线程同步、互斥锁等问题。直到如今在Web编程中被广泛使用,JS仍然只有一个主线程

但众所周知,网络请求耗时较长,阻塞线程会造成极差的用户体验

JS采用异步编程来解决这个问题,基本原理是将耗时长的操作延后执行。例如网路请求,真正耗时的是等待远程服务器的响应,因此JS主线程在发出请求后不再等待,继续执行其他操作,比如执行按钮点击事件绑定的函数。直到服务器的响应传回,JS主线程才着手处理响应结果

你可能会疑惑,这岂不是得有另一个JS线程监控远程服务的响应,并通知JS主线程?实际上,这个工作是浏览器完成的

JS代码经过JS解释器(例如V8引擎),在主线程发出一个网络请求,并将这个请求交给浏览器处理。浏览器等待响应,收到后将响应放到JS主线程事件队列的末尾。不要忘了,浏览器本身是多线程或多进程的应用

Nodejs环境下,完成异步操作的由浏览器变为 libuvlibuv也是个多线程应用

从JS代码执行的视角,当遇到异步函数时,会将异步函数推入事件队列,直接执行下方的同步代码,最后访问事件队列

最经典的异步函数是 setTimeout,它在一定时间后执行回调函数。这段代码中,打印不是500ms后触发,而是1500ms后

let start = new Date
setTimeout(function() {
    let end = new Date;
    console.log(`${end - start}ms`)
}, 500)
while (new Date - start < 1500) {}

Promise

Promise是个同步对象,也是异步执行模式的媒介,有三种状态:pending(待定),resolved/fulfilled(解决),rejected(拒绝),后两者常被合称为settled(落定)

构造Promise时,内部执行 resolve()reject()后,Promise的状态才会落定

// 等价的两种写法
let p1 = new Promise((resolve, reject) => resolve(3))
let p2 = Promise.resolve(3)

以网络请求举例,等待响应时Promise的状态是pending,收到响应后,无论有没有出错,Promise的状态都落定了

为了避免用户使用同步代码处理Promise对象,Promise的状态是私有的,无法通过js代码检测到

对Promise调用 .then(foo)/catch/finally方法后,会立刻注册处理函数foo,等Promise状态落定后,将foo推入事件队列

let p = Promise.resolve('promise p')

p.then((res) => console.log(res) )
console.log('run this first')
console.log(`p is an ${typeof(p)}`)

// run this first
// p is an object
// promise p

在创建 Promise对象的那一刻,Promise内部引用的变量值被暂存起来,不会被后续的修改影响。此处使用 number原始类型做示例,对于 Number等引用类型也同样适用

let x = 6

let p = new Promise((resolve, reject) => {
	(x > 3) ? resolve(`x=${x} is larger than 3`) : reject(`x=${x} is smaller than 3`)
})

x = 4

p.then( res => console.log(`resolve handler: ${res}`))
 .catch( res => console.log(`reject handler: ${res}`))
 .finally( () => console.log('finally handler'))

x = 2
console.log(`run this first, but now x=${x}`)

// run this first, but now x=2
// resolve handler: x=6 is larger than 3
// finally handler

.then(foo)/catch/finally会返回一个新的 Promise对象,允许以上实例中的链式调用

需要注意的是,调用 .catch()的并不是 p这个最初的 Promise,而是 p.then()后新生成的 Promise。如果 p落定后状态为 reject,新 Promise的状态也会设为 reject,并继承 p的错误信息

多个 Promise可以组合为一个新的 Promise

// p1, p2都落定后,p才落定。p1, p2任一拒绝,会导致p拒绝
let p = Promise.all([p1, p2])
// p1, p2任一落定,p就如此落定
let p = Promise.race([p1, p2])

由于 Promise对象的优良特性,很多时候我们把稍显过时的 setTimeout封装起来,返回 Promise

function sleep(sec){
	return new Promise(resolve=>{  
		setTimeout(resolve, sec*1000)
	})
}

async

async是基于 Promise的语法糖。它声明一个异步函数或异步方法

async function foo() {}
const foo = async function() {}
const foo = async () => {}

class Base {
	async foo() {}
}

声明本身并不会将函数推入事件队列延迟执行,所以执行仍然是同步的,只是将函数的返回值包装为 Promise

const foo = async () => { console.log(1); return 'this is promise 1' }
p = foo()
p.then(res => console.log(res))
console.log(p)
console.log(2)

// 1
// Promise {<fulfilled>: 'this is promise 1'}
// 2
// this is promise 1

以上代码将首行替换如下,结果不变

const foo = () => { console.log(1); return Promise.resolve('this is promise 1') }

await

用在 Promise前,等待 Promise的解决,让异步代码的执行顺序看起来像同步代码

const foo = async () => { console.log(1); return 'this is resolved promise 1' }
p = await foo()
console.log(p)
console.log(2)

// 1
// this is resolved promise 1
// 2

调用 await后,事件队列中排在前的其他 Promise,也会一并落定

let p1 = Promise.resolve('promise 1')
let p2 = Promise.resolve('promise 2')

情况一,都使用 .then()方法,则先执行同步代码,再依次落定 p1,p2

p1.then(res => console.log(res))
p2.then(res => console.log(res))
console.log('first')

// first
// promise 1
// promise 2

情况二,await p1,会先落定 p1,再执行同步代码,最后落定 p2

console.log(await p1)
p2.then(res => console.log(res))
console.log('first')

// promise 1
// first
// promise 2

情况三,await p2,会沿着事件队列(注意p1推入事件队列是在 .then()处,而不是定义处)依次落定 p1,p2,最后执行同步代码

p1.then(res => console.log(res))
console.log(await p2)
console.log('first')

// promise 1
// promise 2
// first

如果用在异步函数内部,执行到 await时会跳过函数剩余部分,执行函数外的同步代码,最后回到 await处,无论之后是不是 Promise对象

这也就意味着,如果 await放在顶层上下文的同步代码前,不会影响执行顺序;放在函数体内的同步代码前,就可能影响执行顺序

async function foo() {
	console.log(2)
	// console.log(4)
	// console.log(await 4)
	console.log(await Promise.resolve(4))
	console.log(await Promise.resolve(5))
	console.log(6)
}

console.log(1)
foo()
console.log(3)

// 1
// 2
// 3
// 4
// 5
// 6

await必须在异步函数或顶级上下文中使用,否则会报错

// 顶级上下文,正常
let p = Promise.resolve('promise 1')
console.log(await p)

// 异步函数内部,正常
async function func() {
	let p = Promise.resolve('promise 1')
	console.log(await p)
}

// 同步函数内部,报错
function func() {
	let p = Promise.resolve('promise 1')
	console.log(await p)
}

其他

异步也会暴露一些全局变量的问题。如下例,在循环时,var 声明的变量具有函数作用域或全局作用域,而不是块作用域。也就是说,整个循环中的 i 实际上是同一个变量。当 setTimeout 回调函数执行时,循环已经完成,i 的值已经被增加到4。用 let声明临时变量则没有这个问题

// 4,4,4
for (var i=1; i<=3; i++) {
	setTimeout( () => console.log(i), 0)
}

// 1,2,3
for (let i=1; i<=3; i++) {
	setTimeout( () => console.log(i), 0)
}

// 使用Promise也仍然是 4,4,4
function sleep(sec){
	return new Promise(resolve=>{  
		setTimeout(resolve, sec*1000)
	})
}
for (var i=1; i<=3; i++) {
	sleep(0).then( () => console.log(i) )
}

// 使用Array遍历,由于最后不会+1,是3,3,3
for (var i of [1,2,3]) {
	setTimeout( () => console.log(i), 0)
}