異步的JavaScript

JS自己是一門單線程的語言,因此在執行一些須要等待的任務(eg.等待服務器響應,等待用戶輸入等)時就會阻塞其餘代碼。若是在瀏覽器中JS線程阻塞了,瀏覽器可能會失去響應,從而形成很差的用戶體驗。幸運的是JS語言自己和其運行的環境(瀏覽器,Node)都提供了一些解決方案讓JS能夠「異步」起來,在此梳理一下相關的知識點,若是你讀完以後有所收穫,那更是極好的。javascript

Event Loop

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

Inner working during the execution by Anoop Raveendran

相似於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,其中產生的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、requestAnimationFrame

上文提到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()
複製代碼

這實際上是存在必定的問題的:

  • 從event loop的角度分析:setInterval的兩次回調之間的間隔是不肯定的,取決於回調中的代碼的執行時間;

  • 從性能的角度分析:不管是setInterval仍是setTimeout都「沒法感知瀏覽器當前的工做狀態」,好比當前頁面爲隱藏tab,或者設置動畫的元素不在當前viewport,setInterval & setTimeout仍會照常執行,實際是沒有必要的,雖然某些瀏覽器像Chrome會優化這種狀況,但不能保證全部的瀏覽器都會有優化措施。再好比多個元素同時執行不一樣的動畫,可能會形成沒必要要的重繪,其實頁面只須要重繪一次便可。

在這種背景下,Mozilla提出了requestAnimationFrame,後被Webkit優化並採用,requestAnimationFrame爲編寫JS動畫提供了原生API。

function draw() {
  // ...some draw code

  requestAnimationFrame(draw)
}

draw()
複製代碼

requestAnimationFrame爲JS動畫作了一些優化:

  • 大多數屏幕的最高幀率是60fps,requestAnimationFrame默認會盡量地達到這一幀率
  • 元素不在當前viewport時,requestAnimationFrame會極大地限制動畫的幀率以節約系統資源
  • 使用requestAnimationFrame定義多個同時段的動畫,頁面只會產生一次重繪。

固然requestAnimationFrame存在必定的兼容性問題,具體可參考 can i use。

Promise

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

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
複製代碼

參考文章 && 拓展閱讀

相關文章
相關標籤/搜索