ES6核心內容精講--快速實踐ES6(三)

Promise

是什麼

Promise是異步編程的一種解決方案。Promise對象表示了異步操做的最終狀態(完成或失敗)和返回的結果。html

其實咱們在jQuery的ajax中已經見識了部分Promise的實現,經過Promise,咱們可以將回調轉換爲鏈式調用,也起到解耦的做用。node

怎麼用

Promise接口的基本思想是讓異步操做返回一個Promise對象git

三種狀態和兩種變化途徑

Promise對象只有三種狀態。github

  • 異步操做「未完成」(pending)
  • 異步操做「已完成」(resolved,又稱fulfilled)
  • 異步操做「失敗」(rejected)

這三種的狀態的變化途徑只有兩種。ajax

  • 異步操做從「未完成」到「已完成」
  • 異步操做從「未完成」到「失敗」。

這種變化只能發生一次,一旦當前狀態變爲「已完成」或「失敗」,就意味着不會再有新的狀態變化了。所以,Promise對象的最終結果只有兩種。shell

異步操做成功,Promise對象傳回一個值,狀態變爲resolved。編程

異步操做失敗,Promise對象拋出一個錯誤,狀態變爲rejected。json

生成Promise對象

經過new Promise來生成Promise對象:api

var promise = new Promise(function(resolve, reject) {
  // 異步操做的代碼

  if (/* 異步操做成功 */){
    resolve(value)
  } else {
    reject(error)
  }
})

Promise構造函數接受一個函數做爲參數,該函數的兩個參數分別是resolve和reject。它們是兩個函數,由JavaScript引擎提供,不用本身部署。數組

resolve會將Promise對象的狀態從pending變爲resolved,reject則是將Promise對象的狀態從pending變爲rejected。

Promise構造函數接受一個函數後會當即執行這個函數

var promise = new Promise(function () {
    console.log('Hello World')
})
// Hello World

then和catch回調

Promise對象生成之後,能夠用then方法分別指定resolved狀態和rejected狀態的回調函數。then方法能夠接受兩個回調函數做爲參數。第一個回調函數是Promise對象的狀態變爲resolved時調用,第二個回調函數是Promise對象的狀態變爲rejected時調用。第二個函數是可選的。分別稱之爲成功回調和失敗回調。成功回調接收異步操做成功的結果爲參數,失敗回調接收異步操做失敗報出的錯誤做爲參數。

var promise = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve('成功')
    }, 3000)
})

promise.then(function (data){
    console.log(data)
})
// 3s後打印'成功'

catch方法是then(null, rejection)的別名,用於指定發生錯誤時的回調函數。

var promise = new Promise(function (resolve, reject) {
    setTimeout(function () {
        reject('失敗')
    }, 3000)
})

promise.catch(function (data){
    console.log(data)
})
// 3s後打印'失敗'

Promise.all()

Promise.all方法用於將多個Promise實例,包裝成一個新的Promise實例。

var p = Promise.all([p1, p2, p3])

上面代碼中,Promise.all方法接受一個數組做爲參數,p一、p二、p3都是Promise對象的實例,若是不是,就會先調用下面講到的Promise.resolve方法,將參數轉爲Promise實例,再進一步處理。(Promise.all方法的參數能夠不是數組,但必須具備Iterator接口,且返回的每一個成員都是Promise實例。)

p的狀態由p一、p二、p3決定,分紅兩種狀況。

(1)只有p一、p二、p3的狀態都變成resolved,p的狀態纔會變成resolved,此時p一、p二、p3的返回值組成一個數組,傳遞給p的回調函數。

(2)只要p一、p二、p3之中有一個被Rejected,p的狀態就變成Rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。

Promise.race()

與Promise.all()相似,不過是隻要有一個Promise實例先改變了狀態,p的狀態就是它的狀態,傳遞給回調函數的結果也是它的結果。因此很形象地叫作賽跑。

Promise.resolve()和Promise.reject()

有時須要將現有對象轉爲Promise對象,可使用這兩個方法。

Generator(生成器)

是什麼

生成器本質上是一種特殊的迭代器(參見本文章系列二之Iterator)。ES6裏的迭代器並非一種新的語法或者是新的內置對象(構造函數),而是一種協議 (protocol)。全部遵循了這個協議的對象均可以稱之爲迭代器對象。生成器對象由生成器函數返回而且遵照了迭代器協議。具體參見MDN。

怎麼用

執行過程

生成器函數的語法爲function*,在其函數體內部可使用yield和yield*關鍵字。

function* gen(x){
  console.log(1)
  var y = yield x + 2
  console.log(2)
  return y
}

var g = gen(1)

當咱們像上面那樣調用生成器函數時,會發現並無輸出。這就是生成器函數與普通函數的不一樣,它能夠交出函數的執行權(即暫停執行)。yield表達式就是暫停標誌。

以前提到了生成器對象遵循迭代器協議,因此其實能夠經過next方法執行。執行結果也是一個包含value和done屬性的對象。

遍歷器對象的next方法的運行邏輯以下。

(1)遇到yield表達式,就暫停執行後面的操做,並將緊跟在yield後面的那個表達式的值,做爲返回的對象的value屬性值。

(2)下一次調用next方法時,再繼續往下執行,直到遇到下一個yield表達式。

(3)若是沒有再遇到新的yield表達式,就一直運行到函數結束,直到return語句爲止,並將return語句後面的表達式的值,做爲返回的對象的value屬性值。

(4)若是該函數沒有return語句,則返回的對象的value屬性值爲undefined。

須要注意的是,yield表達式後面的表達式,只有當調用next方法、內部指針指向該語句時纔會執行。

g.next() 
// 1
// { value: 3, done: false }
g.next() 
// 2
// { value: undefined, done: true }

for...of遍歷

生成器部署了迭代器接口,所以能夠用for...of來遍歷,不用調用next方法

function *foo() {
  yield 1
  yield 2
  yield 3
  return 4
}

for (let v of foo()) {
  console.log(v)
}

// 1
// 2
// 3

yield*表達式

從語法角度看,若是yield表達式後面跟的是一個遍歷器對象,須要在yield表達式後面加上星號,代表它返回的是一個遍歷器對象。這被稱爲yield表達式。yield後面只能跟迭代器,yield*的功能是將迭代控制權交給後面的迭代器,達到遞歸迭代的目的

function* foo() {
  yield 'a'
  yield 'b'
}

function* bar() {
  yield 'x'
  yield* foo()
  yield 'y'
}

for (let v of bar()) {
  console.log(v)
}

// x
// a
// b
// y

自動執行

下面是使用Generator函數執行一個真實的異步任務的例子:

var fetch = require('node-fetch')

function* gen () {
  var url = 'https://api.github.com/users/github'
  var result = yield fetch(url)
  console.log(result.bio)
}

上面代碼中,Generator函數封裝了一個異步操做,該操做先讀取一個遠程接口,而後從JSON格式的數據解析信息。這段代碼很是像同步操做,除了加上了yield命令。

執行這段代碼的方法以下

var g = gen()
var result = g.next()

result
  .value
  .then(function (data) {
    return data.json()
  })
  .then(function (data) {
    g.next(data)
  })

上面代碼中,首先執行Generator函數,獲取遍歷器對象,而後使用next方法(第二行),執行異步任務的第一階段。因爲Fetch模塊返回的是一個Promise對象,所以要用then方法調用下一個next方法。

能夠看到,雖然Generator函數將異步操做表示得很簡潔,可是流程管理卻不方便(即什麼時候執行第一階段、什麼時候執行第二階段)。

那麼如何自動化異步任務的流程管理呢?

Generator函數就是一個異步操做的容器。它的自動執行須要一種機制,當異步操做有告終果,可以自動交回執行權。

兩種方法能夠作到這一點。

  1. 回調函數。將異步操做包裝成Thunk函數,在回調函數裏面交回執行權。

  2. Promise對象。將異步操做包裝成Promise對象,用then方法交回執行權。

Thunk函數

本節很簡略,可能會看不太明白,請參考Thunk 函數的含義和用法

Thunk函數的含義:編譯器的"傳名調用"實現,每每是將參數放到一個臨時函數之中,再將這個臨時函數傳入函數體。這個臨時函數就叫作Thunk函數。

JavaScript語言是傳值調用,它的Thunk函數含義有所不一樣。在JavaScript語言中,Thunk函數替換的不是表達式,而是多參數函數,將其替換成單參數的版本,且只接受回調函數做爲參數。

任何函數,只要參數有回調函數,就能寫成Thunk函數的形式,能夠經過一個Thunk函數轉換器來轉換。

Thunk函數真正的威力,在於能夠自動執行Generator函數。咱們能夠實現一個基於Thunk函數的Generator執行器,而後直接把Generator函數傳入這個執行器便可。

function run(fn) {
  var gen = fn()

  function next(err, data) {
    var result = gen.next(data)
    if (result.done) return
    result.value(next)
  }

  next()
}

function* g() {
  // ...
}

run(g)

Thunk函數並非Generator函數自動執行的惟一方案。由於自動執行的關鍵是,必須有一種機制,自動控制Generator函數的流程,接收和交還程序的執行權。回調函數能夠作到這一點,Promise對象也能夠作到這一點。

基於Promise對象的自動執行

首先,將方法包裝成一個Promise對象(fs是nodejs的一個內置模塊)。

var fs = require('fs')

var readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function (error, data) {
      if (error) reject(error)
      resolve(data)
    })
  })
}

var gen = function* () {
  var f1 = yield readFile('/etc/fstab')
  var f2 = yield readFile('/etc/shells')
  console.log(f1.toString())
  console.log(f2.toString())
}

而後,手動執行上面的Generator函數。

var g = gen()

g.next().value.then(function (data) {
  g.next(data).value.then(function (data) {
    g.next(data)
  })
})

觀察上面的執行過程,實際上是在遞歸調用,咱們能夠用一個函數來實現:

function run(gen){
  var g = gen()

  function next(data){
    var result = g.next(data)
    if (result.done) return result.value
    result.value.then(function(data){
      next(data)
    })
  }

  next()
}

run(gen)

上面代碼中,只要Generator函數還沒執行到最後一步,next函數就調用自身,以此實現自動執行。

co模塊

co模塊是nodejs社區著名的TJ大神寫的一個小工具,用於Generator函數的自動執行。

下面是一個Generator函數,用於依次讀取兩個文件

var gen = function* () {
  var f1 = yield readFile('/etc/fstab')
  var f2 = yield readFile('/etc/shells')
  console.log(f1.toString())
  console.log(f2.toString())
}

var co = require('co')
co(gen)

co模塊可讓你不用編寫Generator函數的執行器。Generator函數只要傳入co函數,就會自動執行。co函數返回一個Promise對象,所以能夠用then方法添加回調函數。

co(gen).then(function () {
  console.log('Generator 函數執行完成')
})

co模塊的原理:其實就是將兩種自動執行器(Thunk函數和Promise對象),包裝成一個模塊。使用co的前提條件是,Generator函數的yield命令後面,只能是Thunk函數或Promise對象。若是數組或對象的成員,所有都是Promise對象,也可使用co(co v4.0版之後,yield命令後面只能是Promise對象,再也不支持Thunk函數)。

async(異步)函數

是什麼

async函數屬於ES7。目前,它仍處於提案階段,可是轉碼器Babel和regenerator都已經支持。async函數能夠說是目前異步操做最好的解決方案,是對Generator函數的升級和改進。

怎麼用

1)語法

async函數聲明定義了異步函數,它會返回一個AsyncFunction對象。和普通函數同樣,你也能夠定義一個異步函數表達式。

調用異步函數時會返回一個promise對象。當這個異步函數成功返回一個值時,將會使用promise的resolve方法來處理這個返回值,當異步函數拋出的是異常或者非法值時,將會使用promise的reject方法來處理這個異常值。

異步函數可能會包括await表達式,這將會使異步函數暫停執行並等待promise解析傳值後,繼續執行異步函數並返回解析值。

注意:await只能用在async函數中。

前面依次讀取兩個文件的代碼寫成async函數以下:

var asyncReadFile = async function (){
  var f1 = await readFile('/etc/fstab')
  var f2 = await readFile('/etc/shells')
  console.log(f1.toString())
  console.log(f2.toString())
}

async函數將Generator函數的星號(*)替換成了async,將yield改成了await。

2)async函數的改進

async函數對Generator函數的改進,體如今如下三點。

(1)內置執行器。Generator函數的執行必須靠執行器,因此纔有了co函數庫,而async函數自帶執行器。也就是說,async函數的執行,與普通函數如出一轍,只要一行。

var result = asyncReadFile()

(2)更好的語義。async和await,比起星號和yield,語義更清楚了。async表示函數裏有異步操做,await表示緊跟在後面的表達式須要等待結果。

(3)更廣的適用性。co函數庫約定,yield命令後面只能是Thunk函數或Promise對象,而async函數的await命令後面,能夠跟Promise對象和原始類型的值(數值、字符串和布爾值,但這時等同於同步操做)。

3)基本用法

同Generator函數同樣,async函數返回一個Promise對象,可使用then方法添加回調函數。當函數執行的時候,一旦遇到await就會先返回,等到觸發的異步操做完成,再接着執行函數體內後面的語句。

function resolveAfter2Seconds (x) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x)
    }, 2000)
  })
}

async function add1 (x) {
  var a = resolveAfter2Seconds(20)
  var b = resolveAfter2Seconds(30)
  return x + await a + await b
}

add1(10).then(v => {
  console.log(v)  
})
// 2s後打印60

async function add2 (x) {
  var a = await resolveAfter2Seconds(20)
  var b = await resolveAfter2Seconds(30)
  return x + a + b
}

add2(10).then(v => {
  console.log(v)
})
// 4s後打印60

4)捕獲錯誤

可使用.catch回調捕獲錯誤,也可使用傳統的try...catch。

async function myFunction () {
  try {
    await somethingThatReturnsAPromise()
  } catch (err) {
    console.log(err)
  }
}

// 另外一種寫法
async function myFunction () {
  await somethingThatReturnsAPromise()
  .catch(function (err) {
    console.log(err)
  }
}

5)併發的異步操做

let foo = await getFoo()
let bar = await getBar()

多個await命令後面的異步操做會按順序完成。若是不存在繼發關係,最好讓它們同時觸發。上面的代碼只有getFoo完成,纔會去執行getBar,這樣會比較耗時。若是這兩個是獨立的異步操做,徹底可讓它們同時觸發。

// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()])

// 寫法二
let fooPromise = getFoo()
let barPromise = getBar()
let foo = await fooPromise
let bar = await barPromise
相關文章
相關標籤/搜索