JS自己是一門單線程的語言,因此在執行一些須要等待的任務(eg.等待服務器響應,等待用戶輸入等)時就會阻塞其餘代碼。若是在瀏覽器中JS線程阻塞了,瀏覽器可能會失去響應,從而形成很差的用戶體驗。幸運的是JS語言自己和其運行的環境(瀏覽器,Node)都提供了一些解決方案讓JS能夠「異步」起來,在此梳理一下相關的知識點,若是你讀完以後有所收穫,那更是極好的。javascript
JS中每一個函數都伴有一個自身的做用域(execution context),這個做用域包含函數的一些信息(eg.參數,局部變量等),在函數被調用時,函數的做用域對象被推入執行棧(execution context stack),執行完畢後出棧。當執行一些異步任務時,JS僅調用相應的API並不去等待任務結果而是繼續執行後續代碼,這些異步任務被瀏覽器或者Node交由其餘線程執行(eg.定時器線程、http請求線程、DOM事件線程等),完成以後這些異步任務的回調函數會被推入相應的隊列中,直到執行棧爲空時,這些回調函數纔會被依次執行。html
舉個例子:前端
function main() { console.log('A) setTimeout(function display() { console.log('B') }, 0) console.log('C') } main()
以上代碼在Event Loop中的執行過程以下:java
相似於setTimeout這樣的任務還有:setInterval, setImmediate, 響應用戶操做的事件(eg. click, input等), 響應網絡請求(eg. ajax的onload,image的onload等),數據庫操做等等。這些操做有一個統一的名字:task,因此上圖中的message queue實際上是task queue,由於還存在一些像:Promise,process.nextTick, MutationObserver之類的任務,這些任務叫作microtask,__microtask會在代碼執行過程當中被推入microtask queue而不是task queue__,microtask queue中的任務一樣也須要等待執行棧爲空時依次執行。ajax
一個task中可能會產生microtask和新的task,其中產生的microtask會在本次task結束後,即執行棧爲空時執行,而新的task則會在render以後執行。microtask中也有可能會產生新的microtask,會進入microtask queue尾部,並在本次render前執行。數據庫
這樣的流程是有它存在緣由的,這裏僅僅談下我我的的理解,若有錯誤,還請指出:
瀏覽器中除了JS引擎線程,還存在GUI渲染線程,用以解析HTML, CSS, 構建DOM樹等工做,然而這兩個線程是互斥的,只有在JS引擎線程空閒時,GUI渲染線程纔有可能執行。在兩個task之間,JS引擎空閒,此時若是GUI渲染隊列不爲空,瀏覽器就會切換至GUI渲染線程進行render工做。而microtask會在render以前執行,旨在以相似同步的方式(儘量快地)執行異步任務,因此microtask執行時間過長就會阻塞頁面的渲染。promise
上文提到setTimeout,setInterval都屬於task,因此即使設置間隔爲0:瀏覽器
setTimeout(function display() { console.log('B') }, 0)
回調也會異步執行。服務器
setTimeout,setInterval常被用於編寫JS動畫,好比:網絡
// setInterval function draw() { // ...some draw code } var intervalTimer = setInterval(draw, 500) // setTimeout var timeoutTimer = null function move() { // ...some move code timeoutTimer = setTimeout(move, 500) } move()
這實際上是存在必定的問題的:
在這種背景下,Mozilla提出了requestAnimationFrame,後被Webkit優化並採用,requestAnimationFrame爲編寫JS動畫提供了原生API。
function draw() { // ...some draw code requestAnimationFrame(draw) } draw()
requestAnimationFrame爲JS動畫作了一些優化:
固然requestAnimationFrame存在必定的兼容性問題,具體可參考 can i use。
fs.readdir(source, function (err, files) { if (err) { console.log('Error finding files: ' + err) } else { files.forEach(function (filename, fileIndex) { console.log(filename) gm(source + filename).size(function (err, values) { if (err) { console.log('Error identifying file size: ' + err) } else { console.log(filename + ' : ' + values) aspect = (values.width / values.height) widths.forEach(function (width, widthIndex) { height = Math.round(width / aspect) console.log('resizing ' + filename + 'to ' + height + 'x' + height) this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) { if (err) console.log('Error writing file: ' + err) }) }.bind(this)) } }) }) } })
假設最初學JS時我看到的是上面的代碼,我必定不會想寫前端。這就是所謂的「callback hell」,而Promise把回調函數的嵌套邏輯替換成了符合正常人思惟習慣的線性邏輯。
function fetchSomething() { return new Promise(function(resolved) { if (success) { resolved(res); } }); } fetchSomething().then(function(res) { console.log(res); return fetchSomething(); }).then(function(res) { console.log('duplicate res'); return 'done'; }).then(function(tip) { console.log(tip); })
async await是ES2017引入的兩個關鍵字,旨在讓開發者更方便地編寫異步代碼,但是每每能看到相似這樣的代碼:
async function orderFood() { const pizzaData = await getPizzaData() // async call const drinkData = await getDrinkData() // async call const chosenPizza = choosePizza() // sync call const chosenDrink = chooseDrink() // sync call await addPizzaToCart(chosenPizza) // async call await addDrinkToCart(chosenDrink) // async call orderItems() // async call }
Promise的引入讓咱們脫離了「callback hell」,但是對async函數的錯誤用法又讓咱們陷入了「async hell」。
這裏其實getPizzaData和getDrinkData是沒有關聯的,而await關鍵字使得必須在getPizzaData resolve以後才能執行getDrinkData的動做,這顯然是冗餘的,包括addPizzaToCart和addDrinkToCart也是同樣,影響了系統的性能。因此在寫async函數時,應該清楚哪些代碼是相互依賴的,把這些代碼單獨抽成async函數,另外Promise在聲明時就已經執行,提早執行這些抽出來的async函數,再await其結果就能避免「async hell」,或者也能夠用Promise.all():
async function selectPizza() { const pizzaData = await getPizzaData() // async call const chosenPizza = choosePizza() // sync call await addPizzaToCart(chosenPizza) // async call } async function selectDrink() { const drinkData = await getDrinkData() // async call const chosenDrink = chooseDrink() // sync call await addDrinkToCart(chosenDrink) // async call } // return promise early async function orderFood() { const pizzaPromise = selectPizza() const drinkPromise = selectDrink() await pizzaPromise await drinkPromise orderItems() // async call } // or promise.all() Promise.all([selectPizza(), selectDrink()]).then(orderItems) // async call