陳晨,微醫雲服務團隊前端工程師,一位「生命在於靜止」的程序員。javascript
JavaScript 是單線程語言,瀏覽器只分配了一個主線程執行任務,意味着若是有多個任務,則必須按照順序執行,前一個任務執行完成以後才能繼續下一個任務。html
這個模式比較清晰,可是當任務耗時較長的時候,好比網絡請求,定時器和事件監聽等,這個時候後續任務繼續等待,效率比較低。咱們常見的頁面無響應,有時候就是由於任務耗時長或者無限循環等形成的。那如今是怎麼解決這個問題呢。。。。前端
首先維護了一個「任務隊列」。JavaScript 雖然是單線程的,但運行的宿主環境(瀏覽器)是多線程的,瀏覽器爲這些耗時任務開闢了另外的線程,主要包括 http 請求線程,瀏覽器定時觸發器,瀏覽器事件觸發線程。這些線程主要把任務回調,放在任務隊列裏,等待主線程執行。
簡單介紹以下圖: java
這樣就實現了 JavaScript 的單線程異步,任務被分爲同步任務和異步任務兩種:
同步任務:排隊執行的任務,後一個任務等待前一個任務結束。
異步任務:放入任務隊列的任務,將來纔會觸發執行的事件。程序員
異步任務分爲宏任務和微任務。es6
宏任務,其實就是標準機制下的常規任務,即」任務隊列中「等待被主線程執行的事件,是由瀏覽器宿主發起的任務,例如:編程
宏任務會被放在宏任務隊列裏,先進先出的原則,兩個宏任務中間可能會被插入其餘系統任務,間隔時間不定,效率較低 。數組
因爲宏任務間隔不定,時間顆粒大,對於實時性要求比較高的場景就須要更精確地控制,須要把任務插入到當前宏任務執行,從而產生了微任務的概念。
微任務是 JavaScript 引擎發起的,是須要異步執行的函數。例如:promise
在執行 JavaScript 腳本,建立全局執行上下文的時候,JavaScript 引擎就會建立一個微任務隊列,在執行當前宏任務時,產生的微任務都會保存到微任務隊列裏。在宏任務主函數執行結束以後,宏任務結束以前,清空微任務隊列。
微任務和宏任務是綁定的,每一個宏任務都會建立本身的微任務: 瀏覽器
主線程運行 JavaScript 代碼時,會生成個執行棧(先進後出),管理主線程上函數調用關係的數據結構。
當執行棧中的全部同步任務執行完畢,系統就會不斷的從"任務隊列"中讀取事件,這個過程是循環不斷的,稱爲 Event Loop(事件循環)。
事件循環機制調度宏任務和微任務,機制以下:
由於微任務自身能夠入列更多的微任務,且事件循環會持續處理微任務直至隊列爲空,那麼就存在一種使得事件循環無盡處理微任務的真實風險。如何處理遞歸增長微任務是要謹慎而行的。
回調函數是一個函數被當作參數傳遞給另外一個函數,另外一個函數完成以後執行回調。好比 Ajax 請求、IO 操做、定時器的回調等。
下面是 setTimeout 例子:
console.log('setTimeout 調用以前')
setTimeout(() => {console.log('setTimeout 輸出')}, 0);
console.log('setTimeout 調用以後')
// 結果
setTimeout 調用以前
setTimeout 調用以後
setTimeout 輸出
複製代碼
setTimeout 回調放入任務隊列中,當主線程的同步代碼執行完以後,纔會執行任務隊列的回調,因此是如上的輸出結果。
優勢:回調函數相對比較簡單、容易理解。
缺點:不利於代碼的閱讀和維護,各個部分之間高度耦合,流程會很混亂,並且每一個任務只能指定一個回調函數,易造成回調函數地獄。以下:
setTimeout(function(){
let value1 = step1()
setTimeout(function(){
let value2 = step2(value1)
setTimeout(function(){
step3(value2)
},0);
},0);
},0);
複製代碼
Promise 是 ES6 新增的異步編程的方式,在必定程度上解決了回調地域的問題。簡單說就是一個容器,裏面保存着某個將來纔會結束的事件(一般是一個異步操做)的結果。從語法上說,Promise 是一個對象,從它能夠獲取異步操做的消息。
使用 Promise 首先要明白如下特色:
下面的例子就是常見的異步操做,主要是使用的 then 和 catch:
new Promise((resolve) => {
resolve(step1())
}).then(res => {
return step2(res)
}).catch(err => {
console.log(err)
})
複製代碼
step1 和 step2 是異步操做,step1 執行完以後的返回值會透傳給 then 回調,當作 step2 的入參,經過 then 一層層的代替回調地域。其中 then 的回調會加入微任務隊列。
當 Promise 入參是同步代碼時:
console.log('start')
new Promise((resolve) => {
console.log('開始 resolve')
resolve('resolve 返回值')
}).then(data => {
console.log(data)
})
console.log('end')
// 原生 promise 輸出結果
start
開始 resolve
end
resolve 返回值
複製代碼
首先看下 Promise 的極簡實現:
class Promise {
constructor (executor) {
// 回調值
this.value = ''
// 成功的回調
this.onResolvedCallbacks = []
executor(this.resolve.bind(this))
}
resolve (value) {
this.value = value
this.onResolvedCallbacks.forEach(callback => callback())
}
then (onResolved, onRejected) {
this.onResolvedCallbacks.push(() => {
onResolved(this.value)
})
}
}
// 此時上面例子執行結果以下
start
開始 resolve
end
複製代碼
因爲 Promise 是延遲綁定機制(回調在業務代碼的後面),executor 是同步代碼時,在執行到 resolve 的時候,尚未執行 then,因此 onResolvedCallbacks 是空數組。這個時候須要讓 resolve 延後執行,能夠先加一個定時器。以下:
resolve (value) {
setTimeout(() => {
this.value = value
this.onResolvedCallbacks.forEach(callback => callback())
})
}
複製代碼
輸出結果和預期是一致的,這裏使用 setTimeout 來延遲執行 resolve。可是 setTimeout 是宏任務,效率不高,這裏只是用 setTimeout 代替,在瀏覽器中,JavaScript 引擎會把 Promise 回調映射到微任務,既能夠延遲被調用,又提高了代碼的效率。
優勢:
缺點:
Generator 是 ES6 提供的異步解決方案,其最大的特色就是能夠控制函數的執行。整個 Generator 函數就是一個封裝的異步任務,或者說是異步任務的容器,異步操做須要暫停的地方,都用 yield 語句註明。
Generator 函數的特徵:
function* getData () {
let value1 = yield 111
let value2 = yield value1 + 111 // 這裏的 value1 就是下面傳入的 val1.value
return value2
}
let meth = getData()
let val1 = meth.next()
console.log(val1) // { value: 111, done: false }
let val2 = meth.next(val1.value)
console.log(val2) // { value: 222, done: false }
let val3 = meth.next(val2.value)
console.log(val3) // { value: 222, done: true }
複製代碼
{value: 111, done: false}
;{value: 222, done: false}
;{ value: 222, done: true }
。Generator 是協程的一種實現方式。
協程:協程是一種比線程更加輕量級的存在,協程處在線程的環境中,一個線程能夠存在多個協程,能夠將協程理解爲線程中的一個個任務。經過應用程序代碼進行控制。
上面的例子中協程具體流程以下:
meth 協程和父協程在主線程上交替執行,經過 next() 和 yield 進行控制,只有用戶態,切換效率高。
優勢:Generator 是以一種看似順序、同步的方式實現了異步控制流程,加強了代碼可讀性。
缺點:須要手動 next 執行下一步。
async/await 將 Generator 函數和自動執行器,封裝在一個函數中,是 Generator 的一種語法糖,簡化了外部執行器的代碼,同時利用 await 替代 yield,async 替代生成器的(*)號。
async 和 Generator 相比改進的地方:
下面來看個 sleep 的例子:
function sleep(time) {
return new Promise((resolve, reject) => {
time+=1000
setTimeout(() => {
resolve(time);
}, 1000);
});
}
async function test () {
let time = 0
for(let i = 0; i < 4; i++) {
time = await sleep(time);
console.log(time);
}
}
test()
// 輸出結果
1000
2000
3000
複製代碼
執行結果每隔一秒會輸出 time,await 是等待的意思,等待 sleep 執行完畢後經過 resolve 返回,纔會繼續執行,間隔至少一秒。
把 async/await 轉成 Generator 和 Promise 來實現。
function test () {
let time = 0
// stepGenerator 生成器
function* stepGenerator() {
for (let i = 0; i < 4; i++) {
let result = yield sleep(time);
console.log(result);
}
}
let step = stepGenerator()
let info
return new Promise((resolve) => {
// 自執行 next()
function stepNext () {
info = step.next(time)
// 執行結束則返回 value
if (info.done) {
resolve(info.value)
} else {
// 遍歷沒有結束 ,繼續執行
return Promise.resolve(info.value).then((res) => {
time = res
return stepNext()
})
}
}
stepNext()
})
}
test()
複製代碼
優勢:是 Generator 更簡化的方式,至關於自動執行 Generator,代碼更清晰,更簡單。
缺點:濫用 await 可能會致使性能問題,由於 await 會阻塞代碼,非依賴代碼失去併發性。
多個異步的執行順序問題是很考驗對異步的理解的。下面咱們把 setTimeout、Promise、async/await 放在一塊兒,看下返回結果和預想的是否一致:
console.log('start')
setTimeout(function() {
console.log('setTimeout')
}, 0);
async function test () {
let a = await 'await-result'
console.log(a)
}
test()
new Promise(function(resolve) {
console.log('promise-resolve')
resolve()
}).then(function() {
console.log('promise-then')
})
console.log('end')
//執行結果
start
promise-resolve
end
await-result
promise-then
setTimeout
複製代碼
上述例子中,外層主程序 和 setTimeout 都是宏任務,Promise 和 async/await 是微任務,因此整個流程以下:
前端程序員平常代碼常常會用到異步編程,瞭解異步運行的機制和順序有助於更流暢清晰的實現異步代碼,這裏主要分析了異步的由來和異步代碼實現,可結合不一樣的場景和要求進行選擇。