ES6異步方式全面解析

本文首發於 本人博客

衆所周知JS是單線程的,這種設計讓JS避免了多線程的各類問題,但同時也讓JS同一時刻只能執行一個任務,若這個任務執行時間很長的話(如死循環),會致使JS直接卡死,在瀏覽器中的表現就是頁面無響應,用戶體驗很是之差。前端

所以,在JS中有兩種任務執行模式:同步(Synchronous)和異步(Asynchronous)。相似函數調用、流程控制語句、表達式計算等就是以同步方式運行的,而異步主要由setTimeout/setInterval、事件實現。git

傳統的異步實現

做爲一個前端開發者,不管是瀏覽器端仍是Node,相信你們都使用過事件吧,經過事件確定就能想到回調函數,它就是實現異步最經常使用、最傳統的方式。es6

不過要注意,不要覺得回調函數就都是異步的,如ES5的數組方法Array.prototype.forEach((ele) => {})等等,它們也是同步執行的。回調函數只是一種處理異步的方式,屬於函數式編程中高階函數的一種,並不僅在處理異步問題中使用。github

舉個栗子🌰:ajax

// 最多見的ajax回調
this.ajax('/path/to/api', {
    params: params
}, (res) => {
    // do something...
})

你可能以爲這樣並無什麼不妥,可是如有多個ajax或者異步操做須要依次完成呢?編程

this.ajax('/path/to/api', {
    params: params
}, (res) => {
    // do something...
    this.ajax('/path/to/api', {
      params: params
    }, (res) => {
        // do something...
        this.ajax('/path/to/api', {
          params: params
        }, (res) => {
          // do something...
        })
        ...
    })
})

回調地獄就出現了。。。😢api

爲了解決這個問題,社區中提出了Promise方案,而且該方案在ES6中被標準化,現在已普遍使用。數組

Promise

使用Promise的好處就是讓開發者遠離了回調地獄的困擾,它具備以下特色:promise

  1. 對象的狀態不受外界影響:瀏覽器

    • Promise對象表明一個異步操做,有三種狀態:Pending(進行中)、Resolved(已完成,又稱 Fulfilled)和Rejected(已失敗)。
    • 只有異步操做的結果,能夠決定當前是哪種狀態,任何其餘操做都沒法改變這個狀態。
  2. 一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果。

    • Promise對象的狀態改變,只有兩種可能:從Pending變爲Resolved和從Pending變爲Rejected。
    • 只要這兩種狀況發生,狀態就凝固了,不會再變了,會一直保持這個結果。
    • 若是改變已經發生了,你再對Promise對象添加回調函數,也會當即獲得這個結果。
    • 這與事件(Event)徹底不一樣,事件的特色是,若是你錯過了它,再去監聽,是得不到結果的。
  3. 一旦聲明Promise對象(new Promise或Promise.resolve等),就會當即執行它的函數參數,若不是函數參數則不會執行

上面的代碼能夠改寫成以下:

this.ajax('/path/to/api', {
    params: params
}).then((res) => {
    // do something...
    return this.ajax('/path/to/api', {
        params: params
    })
}).then((res) => {
    // do something...
    return this.ajax('/path/to/api', {
        params: params
    })
})
...

看起來就直觀多了,就像一個鏈條同樣將多個操做依次串了起來,不再用擔憂回調了~😄

同時Promise還有許多其餘API,如Promise.allPromise.racePromise.resolve/reject等等(能夠參考阮老師的文章),在須要的時候配合使用都是極好的。

API無需多說,不過這裏我總結了一下本身以前使用Promise踩到的坑以及我對Promise理解不夠透徹的地方,但願也能幫助你們更好地使用Promise:

  1. then的返回結果:我以前天真的覺得then要想鏈式調用,必需要手動返回一個新的Promise才行

    Promise.resolve('first promise')
    .then((data) => {
        // return Promise.resolve('next promise')
        // 實際上兩種返回是同樣的
        return 'next promise'
    })
    .then((data) => {
        console.log(data)
    })

    總結以下:

    • 若是then方法中返回了一個值,那麼返回一個「新的」resolved的Promise,而且resolve回調函數的參數值是這個值
    • 若是then方法中拋出了一個異常,那麼返回一個「新的」rejected狀態的Promise
    • 若是then方法返回了一個未知狀態(pending)的Promise新實例,那麼返回的新Promise就是未知狀態
    • 若是then方法沒有返回值時,那麼會返回一個「新的」resolved的Promise,但resolve回調函數沒有參數
  2. 一個Promise可設置多個then回調,會按定義順序執行,以下

    const p = new Promise((res) => {
      res('hahaha')
    })
    p.then(console.log)
    p.then(console.warn)

    這種方式與鏈式調用不要搞混,鏈式調用其實是then方法返回了新的Promise,而不是原有的,能夠驗證一下:

    const p1 = Promise.resolve(123)
    const p2 = p1.then(() => {
        console.log(p1 === p2)
        // false
    })
  3. thencatch返回的值不能是當前promise自己,不然會形成死循環

    const promise = Promise.resolve()
    .then(() => {
        return promise
    })
  4. then或者catch的參數指望是函數,傳入非函數則會發生值穿透

    Promise.resolve(1)
      .then(2)
      .then(Promise.resolve(3))
      .then(console.log)
    // 1
  5. process.nextTickpromise.then都屬於microtask,而setImmediatesetTimeout屬於macrotask

    process.nextTick(() => {
      console.log('nextTick')
    })
    Promise.resolve()
      .then(() => {
        console.log('then')
      })
    setImmediate(() => {
      console.log('setImmediate')
    })
    console.log('end')
    // end nextTick then setImmediate

    有關microtaskmacrotask能夠看這篇文章,講得很細緻。

但Promise也存在弊端,那就是若步驟不少的話,須要寫一大串.then(),儘管步驟清晰,可是對於咱們這些追求極致優雅的前端開發者來講,代碼全都是Promise的API(thencatch),操做的語義太抽象,仍是讓人不夠滿意呀~

Generator

Generator是ES6規範中對協程的實現,但目前大多被用於異步模擬同步上了。

執行它會返回一個遍歷器對象,而每次調用next方法則將函數執行到下一個yield的位置,若沒有則執行到return或末尾。

依舊是再也不贅述API,對它還不瞭解的能夠查閱阮老師的文章

經過Generator實現異步:

function* main() {
   const res = yield getData()
   console.log(res)
}
// 異步方法
function getData() {
   setTimeout(() => {
       it.next({
           name: 'yuanye',
           age: 22
       })
   }, 2000)
}
const it = main()
it.next()

先無論下面的next方法,單看main方法中,getData模擬的異步操做已經看起來很像同步了。可是追求完美的咱們確定是沒法忍受每次還要手動調用next方法來繼續執行流程的,爲此TJ大神爲社區貢獻了co模塊來自動化執行Generator,它的實現原理很是巧妙,源碼只有短短的200多行,感興趣能夠去研究下。

const co = require('co')

co(function* () {
  const res1 = yield ['step-1']
  console.log(res1)
  // 若yield後面返回的是promise,則會等待它resolved後繼續執行以後的流程
  const res2 = yield new Promise((res) => {
    setTimeout(() => {
      res('step-2')
    }, 2500)
  })
  console.log(res2)
  return 'end'
}).then((data) => {
  console.log('end: ' + data)
})

這樣就讓異步的流程徹底以同步的方式展現出來啦😋~

Async/Await

ES7標準中引入的async函數,是對js異步解決方案的進一步完善,它有以下特色:

  1. 內置執行器:不用像generator那樣反覆調用next方法,或者使用co模塊,調用即會自動執行,並返回結果
  2. 返回Promise:generator返回的是iterator對象,所以還不能直接用then來指定回調
  3. await更友好:相比co模塊約定的generator的yield後面只能跟promise或thunk函數或者對象及數組,await後面既能夠是promise也能夠是任意類型的值(Object、Number、Array,甚至Error等等,不過此時等同於同步操做)

進一步說,async函數徹底能夠看做多個異步操做,包裝成的一個Promise對象,而await命令就是內部then命令的語法糖

改寫後代碼以下:

async function testAsync() {
  const res1 = await new Promise((res) => {
    setTimeout(() => {
      res('step-1')
    }, 2000)
  })
  console.log(res1)
  const res2 = await Promise.resolve('step-2')
  console.log(res2)
  const res3 = await new Promise((res) => {
    setTimeout(() => {
      res('step-3')
    }, 2000)
  })
  console.log(res3)
  return [res1, res2, res3, 'end']
}

testAsync().then((data) => {
  console.log(data)
})

這樣不只語義仍是流程都很是清晰,即使是不熟悉業務的開發者也能一眼看出哪裏是異步操做。

總結

本文彙總了當前主流的JS異步解決方案,其實沒有哪種方法最好或很差,都是在不一樣的場景下能發揮出不一樣的優點。並且目前都是Promise與其餘兩個方案配合使用的,因此不存在你只學會async/await或者generator就能夠玩轉異步。沒準之後又會出現一個新的方案,將已有的這幾種方案顛覆呢 ~

在這不斷變化、發展的時代,咱們前端要放開本身的眼界,擁抱變化,持續學習,才能成長,寫出優質的代碼😜~

相關文章
相關標籤/搜索