Promise,Generator(生成器),async(異步)函數


Promise

是什么

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

其實我們在jQuery的ajax中已經見識了部分Promise的實現,通過Promise,我們能夠將回調轉換為鏈式調用,也起到解耦的作用。

怎么用

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

三種狀態和兩種變化途徑

Promise對象只有三種狀態。

  • 異步操作“未完成”(pending)
  • 異步操作“已完成”(resolved,又稱fulfilled)
  • 異步操作“失敗”(rejected)

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

  • 異步操作從“未完成”到“已完成”
  • 異步操作從“未完成”到“失敗”。

這種變化只能發生一次,一旦當前狀態變為“已完成”或“失敗”,就意味着不會再有新的狀態變化了。因此,Promise對象的最終結果只有兩種。

異步操作成功,Promise對象傳回一個值,狀態變為resolved。

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

生成Promise對象

通過new Promise來生成Promise對象:

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方法接受一個數組作為參數,p1、p2、p3都是Promise對象的實例,如果不是,就會先調用下面講到的Promise.resolve方法,將參數轉為Promise實例,再進一步處理。(Promise.all方法的參數可以不是數組,但必須具有Iterator接口,且返回的每個成員都是Promise實例。)

p的狀態由p1、p2、p3決定,分成兩種情況。

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

(2)只要p1、p2、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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM