入门JavaScript(三):异步编程
原理
JavaScript最初被设计为一种简单的脚本语言,仅仅用于增强网页的交互性,所以采用单线程模型,避免复杂的线程同步、互斥锁等问题。直到如今在Web编程中被广泛使用,JS仍然只有一个主线程
但众所周知,网络请求耗时较长,阻塞线程会造成极差的用户体验
JS采用异步编程来解决这个问题,基本原理是将耗时长的操作延后执行。例如网路请求,真正耗时的是等待远程服务器的响应,因此JS主线程在发出请求后不再等待,继续执行其他操作,比如执行按钮点击事件绑定的函数。直到服务器的响应传回,JS主线程才着手处理响应结果
你可能会疑惑,这岂不是得有另一个JS线程监控远程服务的响应,并通知JS主线程?实际上,这个工作是浏览器完成的
JS代码经过JS解释器(例如V8引擎),在主线程发出一个网络请求,并将这个请求交给浏览器处理。浏览器等待响应,收到后将响应放到JS主线程事件队列的末尾。不要忘了,浏览器本身是多线程或多进程的应用
Nodejs环境下,完成异步操作的由浏览器变为 libuv
。libuv
也是个多线程应用
从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)
}