原文地址
掘金
歡迎 GitHub star
大家好开缎,我是林一一棕叫,異步編程在 JS 中是無法避免的,也是面試必問的奕删。本文使用通俗易懂的語言解析異步編程中的原理俺泣,開始閱讀吧??
思維導(dǎo)圖
一、定時(shí)器
定時(shí)器:設(shè)定一個(gè)定時(shí)器完残,到了設(shè)定時(shí)間伏钠,瀏覽器會(huì)把對應(yīng)的方法執(zhí)行。每一個(gè)定時(shí)器執(zhí)行后都會(huì)有一個(gè)編號(hào)返回谨设,每個(gè)定時(shí)器編號(hào)不一樣熟掂。
1. 設(shè)定定時(shí)器
- setTimeout([function], [interval])
function 都是在到達(dá)設(shè)定時(shí)間后才執(zhí)行。且執(zhí)行一次
let count = 1
let timer = setTimeout(function(){
count++
console.log(count) // 2
}, 1000)
console.log(timer) // 1
- setInterval([function], [interval])
在設(shè)定時(shí)間內(nèi)執(zhí)行铝宵,不主動(dòng)停止的情況下一直執(zhí)行打掘。
let count = 1
let timer = setInterval(function(){
count++
console.log(count) // 2
}, 1000)
console.log(timer) // 1
/*
* 2
* 3
* 4
* ...
*/
2. 清除定時(shí)器
clearTimeout/clearInterval
兩者都可以清除上面的兩種定時(shí)器。
- 如何清除定時(shí)器鹏秋?
只需要定時(shí)器的返回值編號(hào)清除即可尊蚁。
let count = 1
let timer = setInterval(function(){
count++
console.log(count)
// count == 3 ? clearTimeout(timer) : null
count == 3 ? clearInterval(timer) : null
}, 1000)
二、異步編程的原理
先來看一個(gè)小例子
let a = 0
setTimeout(() =>{
console.log('a', ++a)
}, 0)
console.log(a)
/* 輸出
* 0
* 1
*/
上面的例子中侣夷,
setTimeout
是異步的横朋,瀏覽器會(huì)將異步的代碼加入到任務(wù)隊(duì)列中,等到同步的代碼執(zhí)行完成后才執(zhí)行異步的代碼
1. 同步
JS 是單線程的百拓,代碼至上而下執(zhí)行時(shí)遇到同步的代碼需要先執(zhí)行完才可以進(jìn)行下一步任務(wù)琴锭。比如循環(huán)等
2. 異步
所有需要等待的任務(wù)都是異步的晰甚。遇到異步代碼時(shí),不需要等待而是直接異步任務(wù)放入任務(wù)隊(duì)列决帖,等到后面的任務(wù)完成后厕九,才會(huì)返回來執(zhí)行沒有完成異步的代碼。比如事件綁定地回,所有定時(shí)器扁远,ajax 的異步處理,部分回調(diào)函數(shù)刻像,瀏覽器的渲染過程等等畅买。
let a = 0
setTimeout(() =>{
console.log('a', ++a)
}, 0)
console.log(a)
while(true){
}
上面的代碼死循環(huán)了,即使定時(shí)器的時(shí)間到了也不會(huì)執(zhí)行细睡。因?yàn)橥降拇a沒有執(zhí)行完一步就不會(huì)執(zhí)行谷羞。
三、promise
1.基本概念
Promise
只是一個(gè)管理異步編程的類溜徙,本身是同步湃缎。Promise
有三個(gè)狀態(tài)pending/fulfilled/rejected
,三個(gè)狀態(tài)只有兩個(gè)狀態(tài)出現(xiàn)要么成功要么失敗萌京。new Promise()
時(shí)必須要傳入回調(diào)函數(shù)executor
雁歌,否則報(bào)錯(cuò)宏浩。其中回調(diào)函數(shù)中有兩個(gè)參數(shù)resolve, reject
知残,這兩個(gè)參數(shù)可不寫。
-
pending
:初始化狀態(tài)比庄,開始執(zhí)行異步的任務(wù)求妹,只要執(zhí)行 new,new Promise(()=>{})
佳窑,promise 的狀態(tài)就會(huì)變成pending
-
fulfilled
:成功狀態(tài)制恍,執(zhí)行resolve()
。 -
rejected
:失敗狀態(tài), 執(zhí)行rejected()
神凑。
先看一個(gè)小栗子净神。
new Promise(()=> {
setTimeout(()=> {
console.log(1)
}, 0)
console.log(2)
}).then()
console.log(3)
/* 輸出
* 2
* 3
* 1
*/
創(chuàng)建一個(gè)新的
Promise
的實(shí)例也就是new
這個(gè)過程中會(huì)把Promise 中
的函數(shù)先執(zhí)行(不清楚new
創(chuàng)建實(shí)例的過程中發(fā)生了什么可以看看這篇 面試 | 你不得不懂得 JS 原型和原型鏈)。函數(shù)體內(nèi)有異步操作的仍會(huì)加入任務(wù)隊(duì)列溉委,等到同步執(zhí)行完成后才執(zhí)行異步任務(wù)鹃唯,比如函數(shù)體內(nèi)的setTimeout 函數(shù)
。所以輸出的結(jié)果就是2瓣喊,3坡慌,1
。
再來看一個(gè)小栗子
const promise = new Promise((resolve, reject) => {
resolve('success1')
reject('error')
resolve('success2')
})
promise.then((res) => {
console.log('then: ', res)
}).catch((err) => {
console.log('catch: ', err)
}
// then: success1
promise 的狀態(tài)只能改變一次藻三,最后的狀態(tài)不是
fulfilled
就是rejected
洪橘。也就是說同一個(gè)promise
中的resolve()/ reject()
只能執(zhí)行一個(gè)且一次跪者。
2. promise 是怎么管理異步的
promise
參數(shù)的回調(diào)函數(shù)體內(nèi)接收兩個(gè)參數(shù)resolve/ reject
,這兩個(gè)參數(shù)可以作為兩個(gè)回調(diào)函數(shù)熄求。
resolve()
:是異步操作執(zhí)行成功后執(zhí)行渣玲,promise 的狀態(tài)變成了fulfilled
,可以提供返回值弟晚,在.then()
中第一個(gè)參數(shù)接收reject()
:異步操作執(zhí)行失敗后執(zhí)行柜蜈,promise 的狀態(tài)變成了rejected
,可以提供返回值指巡,在.then()
中第二個(gè)參數(shù)接收
resolve()
和reject()
中只能傳遞一個(gè)參數(shù)淑履。promise 的狀態(tài)發(fā)生改變后不會(huì)再變化。
resolve() 和 reject() 是異步操作
藻雪,執(zhí)行這兩個(gè)方法時(shí)秘噪,會(huì)先執(zhí)行resolve/reject
下面的同步代碼,等到主任務(wù)為空時(shí)勉耀,再去調(diào)用resolve/reject
把存放的方法執(zhí)行指煎。
Promise
對象上有私有屬性Promise.resolve()/ Promise.reject()
等。
舉一個(gè)沒什么意義的小栗子
new Promise((resolve, reject)=> {
setTimeout(()=> {
console.log('林')
resolve('ok')
// reject('fail')
console.log('一一')
}, 0)
}).then( res => {
console.log('status:', res)
}, res => {
console.log('status:', res)
})
// 林
// 一一
// ok
上面的栗子直接看出
resolve/reject
是異步的便斥。
3. promise.then(onfulfilled, onrejected) / promise.catch()
promise.then(onfulfilled, onrejected)
promise.then()
方法中有兩個(gè)參數(shù)至壤,分別對應(yīng)著 promise 的兩種不同的狀態(tài),fulfilled, rejected
枢纠。對應(yīng)的狀態(tài)執(zhí)行對應(yīng)的方法像街。promise.then()
中的參數(shù)是函數(shù),如果傳遞的不是函數(shù)就會(huì)造成值穿透
晋渺,也就是resolve()/reject()
的返回值會(huì).then()
中接收镰绎。promise.then()
能夠鏈?zhǔn)降恼{(diào)用,能夠鏈?zhǔn)秸{(diào)用的原因不是.then()
方法中有return this
木西,而是每一個(gè).then()
方法中都會(huì)返回一個(gè)新的Promise
實(shí)例畴栖。
promise.catch()
promise.catch()
是promise.then()
第二個(gè)參數(shù)的簡便寫法,也就是用來捕獲reject()
執(zhí)行后的rejected
狀態(tài)八千。.catch()
也可以實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用吗讶,原因和.then()
方法一樣都是返回了一個(gè)新的promise
。
.then()/.catch()
中的返回值都不能是 promise
自己本身的實(shí)例恋捆,因?yàn)闀?huì)造成死循環(huán)
熱身題
1.舉一個(gè)小栗子
Promise.resolve(1)
.then((res) => {
console.log(res)
return 2
}).catch(err => {
console.log(err)
}).then( res => {
console.log(res)
})
// 1 2
最終輸出
1 2
,
2. 舉一個(gè)值穿透的小栗子
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
// 等價(jià)于
// Promise.resolve(1)
// .then(console.log)
3. then()/.catch() 內(nèi)不能返回自己本身的promise 實(shí)例照皆,舉一個(gè)栗子
let pro = Promise.resolve()
.then(() => {
console.log('promise', pro)
return pro
})
// Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
創(chuàng)建的
promise
實(shí)例是pro
,返回值就不能是pro
, 否則會(huì)造成死循環(huán)。
5. Promise.all([promise1, promise2...])
Promise.all()
中需要等待參數(shù)中所有promise
的狀態(tài)都成功才執(zhí)行回調(diào)的.then()
鸠信,如果有一個(gè)是失敗的那么就執(zhí)行.catch()
纵寝。接收的參數(shù)是一個(gè)包含promise
實(shí)例的數(shù)組,.all()
這個(gè)方法的返回值也是一個(gè)新的promise
實(shí)例。
- 全部執(zhí)行成功回調(diào)
.then()
接收到的就是一個(gè)數(shù)組 - 如果有執(zhí)行失敗的
promise
狀態(tài)爽茴,回調(diào).catch
中就會(huì)捕獲到執(zhí)行失敗的promise
葬凳,promise.all()
執(zhí)行結(jié)束。
var p1 = Promise.resolve(1)
var p2 = Promise.resolve(2)
var p3 = Promise.resolve(3)
let pro = Promise.all([p1, p2, p3])
.then(res => {
console.log(res) // [1, 2, 3]
})
.catch( err => {
console.log(err)
})
console.log(pro) // Promise {<pending>}
全部執(zhí)行成功室奏,那么
.then()
獲取到的值就是resolve()
的返回值數(shù)組火焰。
6. Promise.race()
.race()
的作用也是接收一組異步任務(wù),然后并行執(zhí)行異步任務(wù)胧沫,只保留取一個(gè)最快執(zhí)行完成的異步操作的結(jié)果昌简,其他的方法仍在執(zhí)行,不過執(zhí)行結(jié)果會(huì)被拋棄绒怨。
- 接收的是一組數(shù)組纯赎,只獲取一個(gè)最快執(zhí)行完成
resolve()/rejected()
的返回值,返回值不是一個(gè)數(shù)組
var p1 = new Promise(function(resolve, reject) {
setTimeout(reject, 500, "one");
});
var p2 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, "two");
});
Promise.race([p1, p2]).then(function(value) {
console.log(value); // "two"
});
兩個(gè)都完成南蹂,但 p2 更快
7. Promise.finally()
.finally
方法也是返回一個(gè)Promise
犬金,他在Promise
結(jié)束的時(shí)候,無論結(jié)果為resolved
還是rejected
六剥,都會(huì)執(zhí)行里面的回調(diào)函數(shù)晚顷。
let promise = new Promise((resolve, reject) => {
reject('error')
}).then( res => {
console.log('then', res)
return res
}).catch( err => {
console.log('catch', err) // `catch error`
return err
}).finally( () => {
console.log('finally') //'finally'
})
熱身題
熱身題1
const promise = new Promise((resolve, reject) => {
console.log(1)
resolve()
console.log(2)
})
promise.then(() => {
console.log(3)
})
console.log(4)
// 1, 2, 4, 3
因?yàn)?
resolve()
是異步的,promise.then
也是異步的疗疟,在沒有獲取到resolve()
的fulfilled
狀態(tài)時(shí).then()
不會(huì)執(zhí)行该默。
熱身題2
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('once')
resolve('success')
}, 1000)
})
const start = Date.now()
promise.then((res) => {
console.log(res, Date.now() - start)
})
promise.then((res) => {
console.log(res, Date.now() - start)
})
/* 輸出
* once
* success 1002
* success 1002
*/
promise.then()
即使有多個(gè),但是resolve()/reject()
后的調(diào)用時(shí)同時(shí)執(zhí)行的策彤。所以同時(shí)輸出了success 1002
三栓袖、async 和 await
先來看一個(gè)栗子
function fn(){
return new Promise((resolve, reject) => {
setTimeout( () => {
Math.random() < 0.5 ? resolve('resolve 001') : reject('reject 002')
}, 0)
})
}
async function get() {
let res = await fn()
console.log(res)
console.log(1212)
}
get()
1. async
async
和await
是 ES7 中增加來對promise
操作的方法,是 ES7 系列提供的語法糖锅锨,await
不能單獨(dú)使用一定要結(jié)合async
來使用叽赊。async
會(huì)返回一個(gè)promise
對象,async
函數(shù)調(diào)用不會(huì)造成代碼的阻塞
2. await
await
是用來等待獲取
一個(gè)promise
的resolve/reject
的執(zhí)行結(jié)果必搞,像上面的let res = await fn()
是先把fn()
執(zhí)行后,來獲取resolve/reject
返回的結(jié)果囊咏,不過await
后面也可以不跟著一個(gè)promise
恕洲,但是這樣寫就沒有意義了。await 或 await fn()
這個(gè)操作不是同步的梅割,而是異步的霜第。await
下面的代碼不會(huì)執(zhí)行,而是移入到任務(wù)隊(duì)列等待區(qū)户辞,等到主棧中的其他任務(wù)完成且fn()
中的promise
將結(jié)果返回泌类,await
下面的代碼才可以重新回到主棧中執(zhí)行。await
可以使promise
的操作更加像同步的代碼底燎。- 如果 await 等到的獲取結(jié)果是
reject()
返回的刃榨,那么await
下面的代碼弹砚,就不會(huì)再執(zhí)行,因?yàn)橐呀?jīng)報(bào)錯(cuò)了枢希。
舉一個(gè)小栗子,說明 async/await 是語法糖
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
// 上面的代碼等價(jià)于 ==>
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end')
})
}
思考
熱身1桌吃,await 是同步嗎?苞轿,求輸出的結(jié)果
console.log(1)
function fn(){
return new Promise((resolve, reject) => {
console.log(5)
resolve('resolve 001')
console.log(6)
})
}
async function get() {
console.log(2)
let res = await fn()
console.log(res)
console.log(3)
}
get()
console.log(4)
//1, 2, 5, 6, 4, resolve 001, 3
await
是異步的同時(shí)會(huì)讓出線程茅诱,fn()
執(zhí)行后線程開始讓出,那么await
的后面的代碼不會(huì)立即執(zhí)行會(huì)先到主棧的等待區(qū)搬卒,主棧中console.log(4)
執(zhí)行后瑟俭,再回來執(zhí)行await
處的代碼。
2. 熱身2契邀,求輸出結(jié)果
console.log(1)
async function get() {
console.log(2)
let res = await 200
console.log(res)
console.log(3)
}
get()
console.log(4)
// 1, 2, 4, 200, 3
根據(jù)上面一題
await
異步代碼的原因尔当,可以容易的分析出答案。同時(shí)說明await
后面也可以跟著一個(gè)非promise
的實(shí)例蹂安。
四椭迎、經(jīng)典面試題
1. promise 的優(yōu)缺點(diǎn)/為什么使用 promise?
- 優(yōu)點(diǎn):promise 可以解決回調(diào)地獄田盈,promise 大大增強(qiáng)了嵌套函數(shù)的可讀性和可維護(hù)性畜号,
- 缺點(diǎn):無法取消 Promise,錯(cuò)誤需要通過回調(diào)函數(shù)來捕獲允瞧;如果不設(shè)置回調(diào)函數(shù)简软,Promise 內(nèi)部拋出的錯(cuò)誤,不會(huì)反映到外部述暂;當(dāng)處于 pending(等待)狀態(tài)時(shí)痹升,無法得知目前進(jìn)展到哪一個(gè)階段,是剛剛開始還是即將完成
2. setTimeout畦韭、Promise疼蛾、Async/Await 的區(qū)別
- setTimeout: setTimeout 的回調(diào)函數(shù)放到宏任務(wù)隊(duì)列里,等到執(zhí)行棧清空以后執(zhí)行
- Promise: Promise 本身是同步的立即執(zhí)行函數(shù)艺配,當(dāng)在 executor 中執(zhí)行 resolve或者 reject 的時(shí)候察郁,此時(shí)是異步操作,會(huì)先執(zhí)行 then/catch 等转唉,當(dāng)主棧完成時(shí)皮钠,才會(huì)去調(diào)用 resolve/reject 方法中存放的方法。
- async: async 函數(shù)返回一個(gè) Promise 對象赠法,當(dāng)函數(shù)執(zhí)行的時(shí)候麦轰,一旦遇到 await 就會(huì)先返回,等到觸發(fā)的異步操作完成,再執(zhí)行函數(shù)體內(nèi)后面的語句款侵∧┘觯可以理解為,是讓出了線程喳坠,跳出了 async 函數(shù)體鞠评。
3. 實(shí)現(xiàn)一個(gè) sleep 函數(shù),比如 sleep(1000) 意味著等待 1000 毫秒壕鹉。
function sleep1(time) {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, time);
})
}
sleep1(1000).then(() => console.log("sleep1"));
4. Promise 構(gòu)造函數(shù)是同步執(zhí)行還是異步執(zhí)行剃幌,那么 then 方法呢
Promise
是同步的,執(zhí)行new promise(callback)
時(shí)回調(diào)函數(shù)callback
就會(huì)被立即執(zhí)行晾浴,then()
方法是異步的负乡。
const promise = new Promise((resolve, reject) => {
console.log(1)
resolve()
console.log(2)
})
promise.then(() => {
console.log(3)
})
console.log(4)
1243,promise 構(gòu)造函數(shù)是同步執(zhí)行的脊凰,then 方法是異步執(zhí)行的
5. 介紹下 Promise.all 使用抖棘、原理實(shí)現(xiàn)及錯(cuò)誤處理
const p = Promise.all([p1, p2, p3]);
Promise.all 方法接受一個(gè)數(shù)組作為參數(shù),p1狸涌、p2切省、p3 都是 Promise 實(shí)例,如果不是帕胆,就會(huì)先調(diào)用下面講到的 Promise resolve 方法务豺,將參數(shù)轉(zhuǎn)為 Promise 實(shí)例拨扶,再進(jìn)一步處理。(Promise.all 方法的參數(shù)可以不是數(shù)組住闯,但必須具有 Iterator 接口鲁驶,且返回的每個(gè)成員都是 Promise 實(shí)例没咙。)
5. 介紹下 Promise.all, Promise.race() 的區(qū)別
看上面的介紹
Promise 的各種 api 實(shí)現(xiàn)會(huì)在下一篇文章中實(shí)現(xiàn)秦驯。給個(gè)期待吧??
五掌动、參考
BAT前端經(jīng)典面試問題:史上最最最詳細(xì)的手寫Promise教程
六、結(jié)束
感謝閱讀到這里记餐,如果著篇文章能對你有一點(diǎn)啟發(fā)或幫助的話歡迎 github star, 我是林一一,下次見驮樊。