JavaScript的一个核心特性是单线程执行,这里的单线程是指代码的执行是单线程的(宿主环境的API可以多线程),其最初的目的是为了避免多线程的DOM操作可能带来的错乱。优点是更安全简单,缺点是当遇到一个耗时任务时,后面的任务会被阻塞,从而拖延整个程序的执行。
为了解决耗时任务阻塞线程的问题,JavaScript将任务的执行模式分为了同步模式和异步模式,运行环境也提供了以同步或者异步模式运行的API。
同步模式 : 任务排队执行,执行过程容易理解。
异步模式 : 对于耗时操作,开启过后就立即往后执行下一个任务,异步后续的逻辑一般通过回调函数的方式定义,在耗时任务执行过后就会自动执行传入的回调函数。
JavaScript引擎会先执行完调用栈中的任务,然后通过事件循环从“消息队列”中再取一个出来执行,在此过程中,可以随时再往消息队列中添加任务。也正是因为大量的异步模式的API导致代码一些复杂的异步逻辑不易阅读。
1. 回调函数
回调函数是所有异步编程方案的根基,回调函数由调用者定义,交给执行者执行,具体用法就是把函数作为参数传递罢了。
除了传递回调函数参数以外,还有几种常见的实现异步的方式如事件机制、发布订阅,可以理解为回调函数的变体。
当使用传统回调方式去完成复杂的异步流程时,会无法避免大量的回调函数嵌套,造成回调地狱问题
js setTimeout ( function () {
console . log ( ' 1.执行任务1 -> 三秒后执行任务2 ' )
setTimeout ( function () {
console . log ( ' 2.执行任务2 -> 2秒后执行任务1 ' )
setTimeout ( function () {
console . log ( ' 3.执行任务3 -> 1秒后执行完毕 ' )
// 回调地狱...
}, 1000 )
}, 2000 )
}, 3000 )
2. Promise
为了避免回调地狱问题 ,CommonJS社区提出了Promise规范,并在ES2015中被标准化。Promise实际上就是一个对象,用来表示一个异步任务执行过后究竟是成功还是失败,无论成功或失败只需要执行相应的回调函数即可。
2.1 Promise 的基本用法
构建Promise实例时传入一个函数,函数的参数为两个处理函数,在Promise里编写承诺的逻辑,即什么情况下成功,什么情况下失败,并在成功或失败时改变状态,执行提前注册的处理函数
使用then方法分别注册promise实例的onFulfilledon和onRejected回调函数
注意注册的函数不会立即执行,而是等待同步代码执行完才会执行(如下例)
js let a = 1 , b = 2
const promise = new Promise ( function ( res , rej ) {
if ( a > b )
res ( ' success ' ) // 成功时调用res函数
else
rej ( ' fail ' ) // 失败时调用rej函数
} )
promise . then ( function ( sucStr ) {
console . log ( sucStr )
}, function ( failStr ) {
console . log ( failStr )
} )
console . log ( ' 第一个同步任务执行才会执行提前注册的异步处理函数 ' )
2.2 Promise 封装一个异步任务函数
js // 使用promise封装一个ajax请求
function ajax ( url ) {
return new Promise ( function ( resolve , reject ) {
let xhr = new XMLHttpRequest ()
xhr . open ( ' GET ' , url ) // 指定请求方法和地址
xhr . responseType = ' json ' // 指定相应类型为json
xhr . onload = function () { // 请求完成后执行onload函数
if ( this. status === 200 ) {
resolve ( this. response )
}
else {
reject ( new Error ( this. status ))
}
xhr . send ()
}
} )
}
ajax ( ' /user.json ' ) . then ( function ( res ) {
console . log ( res )
}, function ( err ) {
console . log ( err )
} )
2.3 Promise 的链式调用
promise的本质也是使用回调函数,即通过then方法传递进去,而且promise将回调分成了两种即成功的回调和失败的回调。但是如果需要串联执行多个异步任务,还是会出现回调地狱的问题,这时候使用promise就没有任何意义了,还额外增加了复杂度。
所以嵌套使用promise是不可取的,正确的做法是使用promise方法链式调用的特点来尽量保证串联异步任务的 “扁平化”。
promise的then方法会返回一个全新的 promise对象,用于实现promise的链式调用
后面的then就是在为前面then返回的promise注册回调
前面then方法中调用函数的返回值会作为后面then方法回调的参数
若回调中返回的是一个promise对象,则后面then方法的回调会等待他的结束
js ajax ( ' api/url1 ' )
. then ( value => {
return ajax ( ' api/url2 ' )
} )
. then ( value => {
return ajax ( ' api/url3 ' )
} )
. then ( value => {
return ajax ( ' api/url4 ' )
} )
. catch ( error => {
console . log ( error )
} )
多次catch的链式调用(for test ):
js const p = Promise . reject ( new Error ( ' fail1 ' ))
p . catch ( err => console . log (err))
. then ( () => console . log ( ' ok1 ' ))
. then ( () => console . log ( ' ok2 ' ))
. then ( () => { throw new Error ( ' fail2 ' ) } )
. catch ( err => console . log (err))
. then ( () => console . log ( ' ok3 ' ))
. then ( () => console . log ( ' ok4 ' ))
2.4 Promise 的异常处理
除了promise的执行逻辑执行了reject函数,在promise在执行的过程中出现错误或者主动抛出异常也会执行reject函数,所以then方法中注册的onrejected函数就是为promise的异常做处理,即promise失败或者出现异常都会被执行。其实onrejected函数的注册还有一个更常见的用法,即使用promise实例的catch方法注册onrejected回调
js ajax ( ' /user.json ' )
. then ( function onFulfilled ( res ) {
console . log ( res )
} )
. catch ( function onRejcted ( error ) {
console . log ( error )
} )
// 等同于
ajax ( ' /user.json ' )
. then ( function onFulfilled ( res ) {
console . log ( res )
} )
. then ( undefined, function onRejcted ( error ) {
console . log ( error )
} )
用catch方法更常见,因为更适合链式调用。错误会随着promise链条传递,所以最后使用catch方法更像是给整个promise链条注册的失败回调
2.5 Promise 的静态方法
js Promise . resolve ( ' success ' ) // 直接返回一个成功的Promise对象
Promise . resolve (promise) // 如果接收到一个promise对象则原样返回
Promise . reject ( new Error ( ' rejected ' )) // 快速得到一个一定是失败的promise对象
. catch ( error => console . log (err))
2.6 Promise 并行执行
前面都是多个promise串联执行异步任务,而当多个任务彼此没有依赖时可以并行执行来提高执行速度,Promise.all方法可以将多个promise合并为一个promise统一管理。Promise.all()接收一个数组,数组中每个元素都是一个promise对象,并返回一个全新的promise对象,当其内部所有的promise都完成后,这个新的promise对象才会完成,且其拿到的结果也是一个数组,数组包含每个异步任务执行的结果。其中任何一个promise失败,这个新的promise也就失败了。
示例:先拿到要访问的url列表,再并行访问
js ajax ( ' /urls.json ' )
. then ( value => {
const urls = Object . values ( value )
const tasks = urls . map ( map => ajax ( url ))
return Promise . all ( tasks )
} )
. then ( values => {
console . log ( values )
} )
Promise.race()也可以将多个promise合并为一个promise对象,但Promise.all()等待所有任务结束才会结束,而Promise.race()只会等待第一个先结束的任务。
示例:一个请求promise+一个定时promise用于处理请求超时
js const request = ajax ( ' /urls.json ' )
const timeout = new Promise ( ( resolve , reject ) => {
setTimeout ( () => reject ( new Error ( ' timeout ' )) , 500 )
} )
Promise . race ([
request ,
timeout
])
. then ( value => { console . log ( value ) } )
. catch ( error => { console . log ( error ) } )
2.7 Promise 的执行时序(宏任务&微任务)
即使Promise没有任何的异步操作,它的回调函数仍然不会立即执行而是进入回调队列中排队,即等待所有同步代码执行完后才会执行。
但在回调队列中排队等待的任务并不是平等的。回调队列中的任务称为宏任务,宏任务的执行过程中可以临时加上一些额外需求,这些临时需求可以作为新的宏任务进到队列排队(如下setTimeout回调),也可以作为当前任务的微任务,直接在本轮调用的末尾立即执行(如下Promise回调),微任务的概念是后来才提到JS中的,目的是为了提高整体响应能力。目前绝大多数异步调用的API都是作为宏任务执行,而作为微任务的有Promise & MutationObserver & Node中的process.nextTick
js console . log ( ' start ' )
setTimeout ( () => console . log ( ' settimeout ' ) , 0 )
Promise . resolve ()
. then ( () => console . log ( ' promise1 ' ))
. then ( () => console . log ( ' promise2 ' ))
. then ( () => console . log ( ' promise3 ' ))
console . log ( ' end ' )
//