[JS] ECMAScript 6 - Async : compare with c#

一段引言:javascript

Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。html

它由社區最先提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象。java

 

 

 

開胃菜 - 四種方法


 

Ref: Javascript異步編程的4種方法node

Javascript語言將任務的執行模式分紅兩種:同步(Synchronous)和異步(Asynchronous)。git

*** "同步模式"就是上一段的模式,後一個任務等待前一個任務結束,而後再執行,程序的執行順序與任務的排列順序是一致的、同步的;es6

*** "異步模式"則徹底不一樣,每個任務有一個或多個回調函數(callback),前一個任務結束後,不是執行後一個任務,而是執行回調函數,後一個任務則是不等前一個任務結束就執行,因此程序的執行順序與任務的排列順序是不一致的、異步的。github

  

1、回調函數

function f1(callback){

  setTimeout(function () {  // ----> 將耗時的操做推遲執行,什麼垃圾的初級思想,固然不可行
    // f1的任務代碼
    callback();
  }, 1000);

}

  

2、事件監聽

另外一種思路是採用事件驅動模式。任務的執行不取決於代碼的順序,而取決於某個事件是否發生shell

// 當f1發生done事件,就執行f2。
f1.on('done', f2);
---------------------------------------------------------------------
function f1(){   setTimeout(function () {     // f1的任務代碼
   // 執行完成後,當即觸發done事件,從而開始執行f2
    f1.trigger('done');  
  }, 1000);
}

 

3、發佈/訂閱

這就叫作"發佈/訂閱模式"(publish-subscribe pattern),又稱"觀察者模式"(observer pattern)。編程

就是多出了一個「訂閱中心」統一管理信號。json

// f2向"信號中心"jQuery訂閱"done"信號。
jQuery.subscribe("done", f2);
-----------------------------------------------------------------------
function f1(){   setTimeout(function () {     // f1的任務代碼
    // f1執行完成後,向"信號中心"jQuery發佈"done"信號,從而引起f2的執行
    jQuery.publish("done");     }, 1000); }

 

4、Promises對象     (重點,接下來的內容)

f1的回調函數f2,回調函數變成了鏈式寫法

 f1().then(f2);

f1要進行以下改寫:

function f1(){
  var dfd = $.Deferred();
  setTimeout(function () {
    // f1的任務代碼
    dfd.resolve();
  }, 500);

  return dfd.promise;
}

 

 

 

 

Promise 對象 - 最後也是最流行的一個


Ref: ECMAScript 6 入門 - Promise 對象

 

1、Promise 的含義

所謂Promise,簡單說就是一個容器,裏面保存着某個將來纔會結束的事件(一般是一個異步操做)的結果。

從語法上說,Promise 是一個對象,從它能夠獲取異步操做的消息。

Promise 提供統一的 API,各類異步操做均可以用一樣的方法進行處理。

 

  • Promise對象有如下兩個特色:

(1)對象的狀態不受外界影響。

Promise對象表明一個異步操做,有三種狀態:

 - pending  (進行中)

 - fulfilled(已成功)

 - rejected (已失敗)

只有異步操做的結果,能夠決定當前是哪種狀態,任何其餘操做都沒法改變這個狀態。

這也是Promise這個名字的由來,它的英語意思就是「承諾」,表示其餘手段沒法改變。

 

(2)一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果。

Promise對象的狀態改變,只有兩種可能:

 -  pending ----> fulfilled

 -  pending ----> rejected

只要這兩種狀況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱爲 resolved(已定型)。

若是改變已經發生了,你再對Promise對象添加回調函數,也會當即獲得這個結果。

【晚一步也能獲得信息:這與事件(Event)徹底不一樣,事件的特色是,若是你錯過了它,再去監聽,是得不到結果的】

 

  • 帶來的好處

有了Promise對象,就能夠將異步操做以同步操做的流程表達出來避免了層層嵌套的回調函數【這是相對於事件監聽而言】

此外,Promise對象提供統一的接口,使得控制異步操做更加容易。

 

  • 不免的缺點

Promise也有一些缺點。

 -  首先,沒法取消Promise,一旦新建它就會當即執行,沒法中途取消。

 -  其次,若是不設置回調函數,Promise內部拋出的錯誤,不會反應到外部。

 -  第三,當處於pending狀態時,沒法得知目前進展到哪個階段(剛剛開始仍是即將完成)。

 若是某些事件不斷地反覆發生,通常來講,使用 Stream 模式是比部署Promise更好的選擇。

 

 

2、基本用法

  • 基本介紹

創造了一個Promise實例。

/**
* 函數做爲參數
*/
const promise = new Promise( function(resolve, reject) { // ... some code if (/* 異步操做成功 */){ resolve(value);      // pending ----> resolved,在異步操做成功時調用,並將異步操做的結果,做爲參數傳遞出去; } else { reject(error);       // pending ----> rejected,在異步操做失敗時調用,並將異步操做報出的錯誤,做爲參數傳遞出去。 } });

Promise實例生成之後,能夠用then方法分別指定resolved狀態rejected狀態的回調函數,這裏是兩個參數哦,第二個函數是可選的,不必定要提供。

 

 

三個例子,用來講明:promise分裝一個須要「異步」的流程。

 

Ref: 淺談ES6的Promise對象

Jeff: 在某種狀況下執行預先設定好的方法,可是使用它卻可以讓代碼變得更簡潔清晰

使用已經較爲成熟的有大量小夥伴使用的第三方Promise庫,下面就爲小夥伴推薦一個—— Bluebird

 

  • 來個例子

Promise內部的setTimeout這樣的函數,

  執行成功:走then這個策略;

  執行失敗:應該走error的策略。

/* 延遲執行 */
// 1.返回一個實例,一段時間之後纔會發生的結果(算是一種承諾)

function
timeout(ms) { return new Promise( (resolve, reject) => { setTimeout(resolve, ms, 'done');   // [開始異步流程] } ); } timeout(100).then( (value) => {  // 2.過了指定的時間(參數)之後,實例的狀態變爲,就會觸發方法綁定的回調函數 console.log(value); } );PromisemsPromiseresolvedthen

 

  • 再來一個

then的方法執行優先級略低。

/* 當即執行 */
// 1.Promise 新建後就會當即執行

let
promise = new Promise(function(resolve, reject) { console.log('Promise'); // [開始異步流程] resolve(); }); promise.then(function() { console.log('resolved.');  // 2. 方法指定的回調函數,將在當前腳本全部同步任務執行完纔會執行 }); console.log('Hi!'); // Promise // Hi! // resolvedthen

 

  • 加載圖片
function loadImageAsync(url) {
// 使用包裝了一個圖片加載的異步操做
return new Promise( function(resolve, reject){ const image = new Image(); // [開始異步流程] image.onload = function() {  // --> 加載成功,就執行這個 resolve(image); }; image.onerror = function() { // --> 加載失敗,就執行這個 reject(new Error('Could not load image at ' + url)); }; image.src = url; }); }Promise

  

參數promise

若是調用resolve函數和reject函數時帶有參數,那麼它們的參數會被傳遞給回調函數。

 - reject函數的參數一般是Error對象的實例,表示拋出的錯誤;

 - resolve函數的參數除了正常的值之外,還多是另外一個 Promise 實例,好比像下面這樣。

 

"p2resolve方法將p1做爲參數,即一個異步操做的結果是返回另外一個異步操做"

注意,這時p1的狀態就會傳遞給p2,也就是說,p1的狀態決定了p2的狀態。

若是p1的狀態是pending,那麼p2的回調函數就會等待p1的狀態改變;

若是p1的狀態已是resolved或者rejected,那麼p2的回調函數將會馬上執行。

Detail:

const p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000)  // 1. 是一個 Promise,3 秒以後變爲
})

const p2 = new Promise(function (resolve, reject) {  // 2. 的狀態在 1 秒以後改變,方法返回的是
  setTimeout(() => resolve(p1), 1000)           // 3. 返回的是另外一個 Promise,致使本身的狀態無效了,被p1決定
})

p2.then(result => console.log(result))          // 4. 語句都針對的是後者()
  .catch(error => console.log(error))           // 5. 又過了 2 秒,變爲,致使觸發方法指定的回調函數。
// Error: failp1rejectedp2resolvep1p2p2thenp1p1rejectedcatch

注意:調用resolvereject並不會終結 Promise 的參數函數的執行。

new Promise((resolve, reject) => {
  resolve(1);       // 當即 resolved 的 Promise 是在本輪事件循環的末尾執行,老是晚於本輪循環的同步任務【變爲:return resolve(1)就不會執行後面的了】
  console.log(2);  // 仍然會執行,而且仍是首先打印出來【後繼操做應該放到方法裏面,而不該該直接寫在或的後面】
}).then(r => {
  console.log(r);
});
// 2
// 1thenresolvereject

 

/* implement */

 

Ref:nodejs與Promise的思想碰撞【有必要一讀】

 

 

 

 

RUAN

以上只是基礎概念,要進入實踐體系,須要研讀下面四篇文章。

 

《深刻掌握 ECMAScript 6 異步編程》系列文章

 

 

Generator 函數


 

ES2017 標準引入了 async 函數,使得異步操做變得更加方便,它就是 Generator 函數的語法糖。

那麼,Generator函數是什麼?是協程在 ES6 的實現。

Ref: Generator 函數的語法

Ref: Generator 函數的異步應用

 

1、基本概念

Generator 函數是 ES6 提供的一種異步編程解決方案,語法行爲與傳統函數徹底不一樣

Generator 函數有多種理解角度。

 

- 狀態機

語法上,首先能夠把它理解成,Generator 函數是一個狀態機,封裝了多個內部狀態

 

- 遍歷器對象生成函數

執行 Generator 函數會返回一個遍歷器對象,也就是說,Generator 函數除了狀態機,仍是一個遍歷器對象生成函數。

返回的遍歷器對象,能夠依次遍歷 Generator 函數內部的每個狀態。

 

- 兩大特色

形式上,Generator 函數是一個普通函數,可是有兩個特徵

 (1) function關鍵字與函數名之間有一個星號;

 (2) 函數體內部使用yield表達式,定義不一樣的內部狀態。

function* helloWorldGenerator() { yield 'hello'; // 狀態一 yield 'world';     // 狀態二 return 'ending';   // 狀態三 } var hw = helloWorldGenerator();  // 不會當即執行,返回的是:一個指向內部狀態的指針對象

 

下一步,必須調用遍歷器對象的next方法,使得指針移向下一個狀態。

調用 Generator 函數,返回一個遍歷器對象,表明 Generator 函數的內部指針。

之後,每次調用遍歷器對象的next方法,就會返回一個有着value和done兩個屬性的對象。

value屬性表示當前的內部狀態的值,是yield表達式後面那個表達式的值;

done屬性是一個布爾值,表示是否遍歷結束。
總結一下

 

 

2、yield表達式 與 next()

  • 基本示範

提供了一種能夠暫停執行的函數。yield表達式就是暫停標誌

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

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

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

(4)若是該函數沒有return語句,則返回的對象的value屬性值爲undefined。
遍歷器對象的next方法的運行邏輯以下

爲 JavaScript 提供了手動的「惰性求值」(Lazy Evaluation)的語法功能。

 

Generator 函數能夠不用yield表達式,這時就變成了一個單純的暫緩執行函數

由於:函數f若是是普通函數,在爲變量generator賦值時就會執行。

注意:yield表達式只能用在 Generator 函數裏面,用在其餘地方都會報錯。 

如若否則:瞧這個反例子

var arr = [1, [[2, 3], 4], [5, 6]];

var flat = function* (a) {
  a.forEach(function (item) {
    if (typeof item !== 'number') {
      yield* flat(item);
    } else {
      yield item;
    }
  });
};

for (var f of flat(arr)){
  console.log(f);
}

 

function* outer() {
  yield 'open'
  yield inner()  // --> (A)
  yield 'close'
}

function* inner() {
  yield 'hello!'
}

(A) 加了星號,意思爲:看這個表達式的本質,而非表象。

== yield inner() ==
var gen = outer()
gen.next() // -> 'open'
gen.next() // -> a generator,這是表象
gen.next() // -> 'close'


== yield* inner() ==
var gen = outer()
gen.next() // -> 'open'
gen.next() // -> 'hello!',這是表象背後的本質
gen.next() // -> 'close'

 

 

 

  

Thunk 函數


1、基本概念

"傳值調用"(call by value)

"傳名調用"(call by name) 

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

function f(m){
  return m * 2;     
}

f(x + 5);

// 等同於

var thunk = function () {
  return x + 5;
};

function f(thunk){
  return thunk() * 2;
}

 

2、兩大特色

在 JavaScript 語言中,Thunk 函數替換的不是表達式,而是多參數函數,將其替換成單參數的版本,且只接受回調函數做爲參數

// 正常版本的readFile(多參數版本)
fs.readFile(fileName, callback);

--------------------------------------------
// Thunk版本的readFile(單參數版本) var readFileThunk = Thunk(fileName); readFileThunk(callback); var Thunk = function (fileName){ return function (callback){ return fs.readFile(fileName, callback); }; };

任何函數,只要參數有回調函數,就能寫成 Thunk 函數的形式。

 

3、Thunkify 模塊

生產環境的轉換器,建議使用 Thunkify 模塊

 

4、Thunk的用法

做爲Generator 函數的流程管理而使用。

var fs       = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);

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

 

 

 

 

co 函數庫


1、基本概念

目的:co 能夠自動執行 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()); };

co 函數庫可讓你不用編寫 Generator 函數的執行器。

var co = require('co'); co(gen);  // ----> 使gen自動執行

上面代碼中,Generator 函數只要傳入 co 函數,就會自動執行。

 

  • 返回 Promise

co 函數返回一個 Promise 對象,所以能夠用 then 方法添加回調函數。

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

上面代碼中,等到 Generator 函數執行結束,就會輸出一行提示。

 

  • 實現原理

當異步操做有告終果,可以自動交回執行權

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

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

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

co 函數庫其實就是將兩種自動執行器(Thunk 函數和 Promise 對象),包裝成一個庫。

使用 co 的前提條件是,Generator 函數的 yield 命令後面,只能是 Thunk 函數或 Promise 對象

 

(1) 基於 Thunk 函數的自動執行器。【上一部分】

(2) 基於 Promise 對象的自動執行器。【以下】

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

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); }); }) 

手動執行其實就是用 then 方法,層層添加回調函數。理解了這一點,就能夠寫出一個自動執行器。

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);

 

 

 

 

async 函數


 

異步I/O不就是讀取一個文件嗎,幹嗎要搞得這麼複雜?異步編程的最高境界,就是根本不用關心它是否是異步。

async 函數就是隧道盡頭的亮光,不少人認爲它是異步操做的終極解決方案。

一句話,async 函數就是 Generator 函數的語法糖。

 

1、基本認知

  • 「寫法」上的對比
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);
    });
  });
};


# 一個 Generator 函數,依次讀取兩個文件 var gen = function* (){
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};


# 寫成 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());
};

 

 

  • 支持 try...catch 方法
var sleep = function (time) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            // 模擬出錯了,返回 ‘error’
            reject('error');
        }, time);
    })
};

var start = async function () {
    try {
        console.log('start');
        await sleep(3000); // 這裏獲得了一個返回錯誤
        
        // 因此如下代碼不會被執行了
        console.log('end');
    } catch (err) {
        console.log(err); // 這裏捕捉到錯誤 `error`
    }  
};

 

  • await看起來就像是同步代碼

故,能夠理所固然的寫在for循環裏,沒必要擔憂以往須要閉包才能解決的問題。

..省略以上代碼

var start = async function () {
    for (var i = 1; i <= 10; i++) {
        console.log(`當前是第${i}次等待..`);
        await sleep(1000);
    }
};

 

  • await必須在async函數的上下文中
..省略以上代碼

let 一到十 = [1,2,3,4,5,6,7,8,9,10];

// 錯誤示範
一到十.forEach(function (v) {
    console.log(`當前是第${v}次等待..`);
    await sleep(1000); // 錯誤!! await只能在async函數中運行
});

// 正確示範
for(var v of 一到十) {
    console.log(`當前是第${v}次等待..`);
    await sleep(1000); // 正確, for循環的上下文還在async函數中
}

 

  • 根據電影文件名,自動下載對應的海報
import fs      from 'fs';
import path    from 'path';
import request from 'request';

var movieDir = __dirname + '/movies',
    exts     = ['.mkv', '.avi', '.mp4', '.rm', '.rmvb', '.wmv'];


/////////////////
// 讀取文件列表
/////////////////
var readFiles = function () { return new Promise(function (resolve, reject) { fs.readdir(movieDir, function (err, files) { resolve(files.filter((v) => exts.includes(path.parse(v).ext))); }); }); };
/////////////////
// 獲取海報
/////////////////
var getPoster = function (movieName) { let url = `https://api.douban.com/v2/movie/search?q=${encodeURI(movieName)}`; return new Promise(function (resolve, reject) { request({url: url, json: true}, function (error, response, body) { if (error) return reject(error); resolve(body.subjects[0].images.large); }) }); };
///////////////
// 保存海報
///////////////
var savePoster = function (movieName, url) { request.get(url).pipe(fs.createWriteStream(path.join(movieDir, movieName + '.jpg'))); }; ////////////////////////////////////////////////////////////////////////////////////////
(async ()
=> { let files = await readFiles(); // await只能使用在原生語法 for (var file of files) { let name = path.parse(file).name; console.log(`正在獲取${name}的海報`); savePoster(name, await getPoster(name)); } console.log('=== 獲取海報完成 ==='); })();
相關文章
相關標籤/搜索