自學-ES6篇-非同步操作和Async函式
非同步程式設計對JavaScript語言太重要。Javascript語言的執行環境是“單執行緒”的,如果沒有非同步程式設計,根本沒法用,非卡死不可。
ES6誕生以前,非同步程式設計的方法,大概有下面四種。
- 回撥函式
- 事件監聽
- 釋出/訂閱
- Promise 物件
ES6將JavaScript非同步程式設計帶入了一個全新的階段,ES7的Async
函式更是提出了非同步程式設計的終極解決方案。
1、基本概念
非同步
所謂"非同步",簡單說就是一個任務分成兩段,先執行第一段,然後轉而執行其他任務,等做好了準備,再回過頭執行第二段。
比如,有一個任務是讀取檔案進行處理,任務的第一段是向作業系統發出請求,要求讀取檔案。然後,程式執行其他任務,等到作業系統返回檔案,再接著執行任務的第二段(處理檔案)。這種不連續的執行,就叫做非同步。
相應地,連續的執行就叫做同步。由於是連續執行,不能插入其他任務,所以作業系統從硬碟讀取檔案的這段時間,程式只能乾等著。
回撥函式
JavaScript語言對非同步程式設計的實現,就是回撥函式。所謂回撥函式,就是把任務的第二段單獨寫在一個函式裡面,等到重新執行這個任務的時候,就直接呼叫這個函式。它的英語名字callback,直譯過來就是"重新呼叫"。
讀取檔案進行處理,是這樣寫的。
fs.readFile('/etc/passwd', function (err, data) {
if (err) throw err;
console.log(data);
});
上面程式碼中,readFile函式的第二個引數,就是回撥函式,也就是任務的第二段。等到作業系統返回了/etc/passwd
一個有趣的問題是,為什麼Node.js約定,回撥函式的第一個引數,必須是錯誤物件err(如果沒有錯誤,該引數就是null)?原因是執行分成兩段,在這兩段之間丟擲的錯誤,程式無法捕捉,只能當作引數,傳入第二段。
Promise
回撥函式本身並沒有問題,它的問題出現在多個回撥函式巢狀。假定讀取A檔案之後,再讀取B檔案,程式碼如下。
fs.readFile(fileA, function (err, data) {
fs.readFile(fileB, function (err, data) {
// ...
});
});
不難想象,如果依次讀取多個檔案,就會出現多重巢狀。程式碼不是縱向發展,而是橫向發展,很快就會亂成一團,無法管理。這種情況就稱為"回撥函式噩夢"(callback hell)。
Promise就是為了解決這個問題而提出的。它不是新的語法功能,而是一種新的寫法,允許將回調函式的巢狀,改成鏈式呼叫。採用Promise,連續讀取多個檔案,寫法如下。
var readFile = require('fs-readfile-promise');
readFile(fileA)
.then(function(data){
console.log(data.toString());
})
.then(function(){
return readFile(fileB);
})
.then(function(data){
console.log(data.toString());
})
.catch(function(err) {
console.log(err);
});
上面程式碼中,我使用了fs-readfile-promise模組,它的作用就是返回一個Promise版本的readFile函式。Promise提供then方法載入回撥函式,catch方法捕捉執行過程中丟擲的錯誤。
可以看到,Promise 的寫法只是回撥函式的改進,使用then方法以後,非同步任務的兩段執行看得更清楚了,除此以外,並無新意。
Promise 的最大問題是程式碼冗餘,原來的任務被Promise 包裝了一下,不管什麼操作,一眼看去都是一堆 then,原來的語義變得很不清楚。
那麼,有沒有更好的寫法呢?
2、Generator函式
協程
- 第一步,協程A開始執行。
- 第二步,協程A執行到一半,進入暫停,執行權轉移到協程B。
- 第三步,(一段時間後)協程B交還執行權。
- 第四步,協程A恢復執行。
傳統的程式語言,早有非同步程式設計的解決方案(其實是多工的解決方案)。其中有一種叫做"協程"(coroutine),意思是多個執行緒互相協作,完成非同步任務。
協程有點像函式,又有點像執行緒。它的執行流程大致如下。
上面流程的協程A,就是非同步任務,因為它分成兩段(或多段)執行。
舉例來說,讀取檔案的協程寫法如下。
function *asyncJob() {
// ...其他程式碼
var f = yield readFile(fileA);
// ...其他程式碼
}
上面程式碼的函式asyncJob
是一個協程,它的奧妙就在其中的yield
命令。它表示執行到此處,執行權將交給其他協程。也就是說,yield
命令是非同步兩個階段的分界線。
協程遇到yield
命令就暫停,等到執行權返回,再從暫停的地方繼續往後執行。它的最大優點,就是程式碼的寫法非常像同步操作,如果去除yield命令,簡直一模一樣。
Generator函式的概念
Generator函式是協程在ES6的實現,最大特點就是可以交出函式的執行權(即暫停執行)。
整個Generator函式就是一個封裝的非同步任務,或者說是非同步任務的容器。非同步操作需要暫停的地方,都用yield
語句註明。Generator函式的執行方法如下。
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
上面程式碼中,呼叫Generator函式,會返回一個內部指標(即遍歷器)g 。這是Generator函式不同於普通函式的另一個地方,即執行它不會返回結果,返回的是指標物件。呼叫指標g的next方法,會移動內部指標(即執行非同步任務的第一段),指向第一個遇到的yield語句,上例是執行到x
+ 2
為止。
換言之,next方法的作用是分階段執行Generator函式。每次呼叫next方法,會返回一個物件,表示當前階段的資訊(value屬性和done屬性)。value屬性是yield語句後面表示式的值,表示當前階段的值;done屬性是一個布林值,表示Generator函式是否執行完畢,即是否還有下一個階段。
Generator函式的資料交換和錯誤處理
Generator函式可以暫停執行和恢復執行,這是它能封裝非同步任務的根本原因。除此之外,它還有兩個特性,使它可以作為非同步程式設計的完整解決方案:函式體內外的資料交換和錯誤處理機制。
next方法返回值的value屬性,是Generator函式向外輸出資料;next方法還可以接受引數,這是向Generator函式體內輸入資料。
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
上面程式碼中,第一個next方法的value屬性,返回表示式x
+ 2
的值(3)。第二個next方法帶有引數2,這個引數可以傳入 Generator 函式,作為上個階段非同步任務的返回結果,被函式體內的變數y接收。因此,這一步的 value 屬性,返回的就是2(變數y的值)。
Generator 函式內部還可以部署錯誤處理程式碼,捕獲函式體外丟擲的錯誤。
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出錯了');
// 出錯了
上面程式碼的最後一行,Generator函式體外,使用指標物件的throw方法丟擲的錯誤,可以被函式體內的try ...catch程式碼塊捕獲。這意味著,出錯的程式碼與處理錯誤的程式碼,實現了時間和空間上的分離,這對於非同步程式設計無疑是很重要的。非同步任務的封裝
下面看看如何使用 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 函式將非同步操作表示得很簡潔,但是流程管理卻不方便(即何時執行第一階段、何時執行第二階段)。
3、Thunk函式
引數的求值策略
Thunk函式早在上個世紀60年代就誕生了。
那時,程式語言剛剛起步,計算機學家還在研究,編譯器怎麼寫比較好。一個爭論的焦點是"求值策略",即函式的引數到底應該何時求值。
var x = 1;
function f(m){
return m * 2;
}
f(x + 5)
上面程式碼先定義函式f,然後向它傳入表示式x
+ 5
。請問,這個表示式應該何時求值?
一種意見是"傳值呼叫"(call by value),即在進入函式體之前,就計算x
+ 5
的值(等於6),再將這個值傳入函式f 。C語言就採用這種策略。
f(x + 5)
// 傳值呼叫時,等同於
f(6)
另一種意見是"傳名呼叫"(call by name),即直接將表示式x
+ 5
傳入函式體,只在用到它的時候求值。Haskell語言採用這種策略。f(x + 5)
// 傳名呼叫時,等同於
(x + 5) * 2
傳值呼叫和傳名呼叫,哪一種比較好?回答是各有利弊。傳值呼叫比較簡單,但是對引數求值的時候,實際上還沒用到這個引數,有可能造成效能損失。function f(a, b){
return b;
}
f(3 * x * x - 2 * x - 1, x);
上面程式碼中,函式f的第一個引數是一個複雜的表示式,但是函式體內根本沒用到。對這個引數求值,實際上是不必要的。因此,有一些計算機學家傾向於"傳名呼叫",即只在執行時求值。Thunk函式的含義
編譯器的"傳名呼叫"實現,往往是將引數放到一個臨時函式之中,再將這個臨時函式傳入函式體。這個臨時函式就叫做Thunk函式。
function f(m){
return m * 2;
}
f(x + 5);
// 等同於
var thunk = function () {
return x + 5;
};
function f(thunk){
return thunk() * 2;
}
上面程式碼中,函式f的引數x
+ 5
被一個函式替換了。凡是用到原引數的地方,對Thunk
函式求值即可。
這就是Thunk函式的定義,它是"傳名呼叫"的一種實現策略,用來替換某個表示式。
javascript語言Thunk函式
JavaScript語言是傳值呼叫,它的Thunk函式含義有所不同。在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);
};
};
上面程式碼中,fs模組的readFile方法是一個多引數函式,兩個引數分別為檔名和回撥函式。經過轉換器處理,它變成了一個單引數函式,只接受回撥函式作為引數。這個單引數版本,就叫做Thunk函式。
任何函式,只要引數有回撥函式,就能寫成Thunk函式的形式。下面是一個簡單的Thunk函式轉換器。
// ES5版本
var Thunk = function(fn){
return function (){
var args = Array.prototype.slice.call(arguments);
return function (callback){
args.push(callback);
return fn.apply(this, args);
}
};
};
// ES6版本
var Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};
使用上面的轉換器,生成fs.readFile
的Thunk函式。
var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);
下面是另一個完整的例子。
function f(a, cb) {
cb(a);
}
let ft = Thunk(f);
let log = console.log.bind(console);
ft(1)(log) // 1
Thunkify模組
生產環境的轉換器,建議使用Thunkify模組。
首先是安裝。
$ npm install thunkify
使用方式如下。
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
// ...
});
Thunkify的原始碼與上一節那個簡單的轉換器非常像。
function thunkify(fn){
return function(){
var args = new Array(arguments.length);
var ctx = this;
for(var i = 0; i < args.length; ++i) {
args[i] = arguments[i];
}
return function(done){
var called;
args.push(function(){
if (called) return;
called = true;
done.apply(null, arguments);
});
try {
fn.apply(ctx, args);
} catch (err) {
done(err);
}
}
}
};
它的原始碼主要多了一個檢查機制,變數called
確保回撥函式只執行一次。這樣的設計與下文的Generator函式相關。請看下面的例子。function f(a, b, callback){
var sum = a + b;
callback(sum);
callback(sum);
}
var ft = thunkify(f);
var print = console.log.bind(console);
ft(1, 2)(print);
// 3
上面程式碼中,由於thunkify
只允許回撥函式執行一次,所以只輸出一行結果。
你可能會問, Thunk函式有什麼用?回答是以前確實沒什麼用,但是ES6有了Generator函式,Thunk函式現在可以用於Generator函式的自動流程管理。
Generator函式可以自動執行。
function* gen() {
// ...
}
var g = gen();
var res = g.next();
while(!res.done){
console.log(res.value);
res = g.next();
}
上面程式碼中,Generator函式gen
會自動執行完所有步驟。
但是,這不適合非同步操作。如果必須保證前一步執行完,才能執行後一步,上面的自動執行就不可行。這時,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());
};
上面程式碼中,yield命令用於將程式的執行權移出Generator函式,那麼就需要一種方法,將執行權再交還給Generator函式。
這種方法就是Thunk函式,因為它可以在回撥函式裡,將執行權交還給Generator函式。為了便於理解,我們先看如何手動執行上面這個Generator函式。
var g = gen();
var r1 = g.next();
r1.value(function(err, data){
if (err) throw err;
var r2 = g.next(data);
r2.value(function(err, data){
if (err) throw err;
g.next(data);
});
});
上面程式碼中,變數g是Generator函式的內部指標,表示目前執行到哪一步。next方法負責將指標移動到下一步,並返回該步的資訊(value屬性和done屬性)。
仔細檢視上面的程式碼,可以發現Generator函式的執行過程,其實是將同一個回撥函式,反覆傳入next方法的value屬性。這使得我們可以用遞迴來自動完成這個過程。
Thunk函式的自動流程管理 Thunk函式真正的威力,在於可以自動執行Generator函式。下面就是一個基於Thunk函式的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);
上面程式碼的run
函式,就是一個Generator函式的自動執行器。內部的next
函式就是Thunk的回撥函式。next
函式先將指標移到Generator函式的下一步(gen.next
方法),然後判斷Generator函式是否結束(result.done
屬性),如果沒結束,就將next
函式再傳入Thunk函式(result.value
屬性),否則就直接退出。
有了這個執行器,執行Generator函式方便多了。不管內部有多少個非同步操作,直接把Generator函式傳入run
函式即可。當然,前提是每一個非同步操作,都要是Thunk函式,也就是說,跟在yield
命令後面的必須是Thunk函式。
var g = function* (){
var f1 = yield readFile('fileA');
var f2 = yield readFile('fileB');
// ...
var fn = yield readFile('fileN');
};
run(g);
上面程式碼中,函式g
封裝了n
個非同步的讀取檔案操作,只要執行run
函式,這些操作就會自動完成。這樣一來,非同步操作不僅可以寫得像同步操作,而且一行程式碼就可以執行。
Thunk函式並不是Generator函式自動執行的唯一方案。因為自動執行的關鍵是,必須有一種機制,自動控制Generator函式的流程,接收和交還程式的執行權。回撥函式可以做到這一點,Promise 物件也可以做到這一點。
4、co模組
基本用法
co模組是著名程式設計師TJ Holowaychuk於2013年6月釋出的一個小工具,用於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);
上面程式碼中,Generator函式只要傳入co函式,就會自動執行。
co函式返回一個Promise物件,因此可以用then方法添加回調函式。
co(gen).then(function (){
console.log('Generator 函式執行完成');
});
上面程式碼中,等到Generator函式執行結束,就會輸出一行提示。
co模組的原理為什麼co可以自動執行Generator函式?
前面說過,Generator就是一個非同步操作的容器。它的自動執行需要一種機制,當非同步操作有了結果,能夠自動交回執行權。
兩種方法可以做到這一點。
(1)回撥函式。將非同步操作包裝成Thunk函式,在回撥函式裡面交回執行權。
(2)Promise 物件。將非同步操作包裝成Promise物件,用then方法交回執行權。
co模組其實就是將兩種自動執行器(Thunk函式和Promise物件),包裝成一個模組。使用co的前提條件是,Generator函式的yield命令後面,只能是Thunk函式或Promise物件。
上一節已經介紹了基於Thunk函式的自動執行器。下面來看,基於Promise物件的自動執行器。這是理解co模組必須的。
基於Promise物件的自動執行
還是沿用上面的例子。首先,把fs模組的readFile方法包裝成一個Promise物件。
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) return 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);
上面程式碼中,只要Generator函式還沒執行到最後一步,next函式就呼叫自身,以此實現自動執行。co模組的原始碼
co就是上面那個自動執行器的擴充套件,它的原始碼只有幾十行,非常簡單。
首先,co函式接受Generator函式作為引數,返回一個 Promise 物件。
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
});
}
在返回的Promise物件裡面,co先檢查引數gen是否為Generator函式。如果是,就執行該函式,得到一個內部指標物件;如果不是就返回,並將Promise物件的狀態改為resolved。
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
});
}
接著,co將Generator函式的內部指標物件的next方法,包裝成onFulfilled函式。這主要是為了能夠捕捉丟擲的錯誤。
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
});
}
最後,就是關鍵的next函式,它會反覆呼叫自身。
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
上面程式碼中,next 函式的內部程式碼,一共只有四行命令。
第一行,檢查當前是否為 Generator 函式的最後一步,如果是就返回。
第二行,確保每一步的返回值,是 Promise 物件。
第三行,使用 then 方法,為返回值加上回調函式,然後通過 onFulfilled 函式再次呼叫 next 函式。
第四行,在引數不符合要求的情況下(引數非 Thunk 函式和 Promise 物件),將 Promise 物件的狀態改為 rejected,從而終止執行。
處理併發的非同步操作
co支援併發的非同步操作,即允許某些操作同時進行,等到它們全部完成,才進行下一步。
這時,要把併發的操作都放在陣列或物件裡面,跟在yield語句後面。
// 陣列的寫法
co(function* () {
var res = yield [
Promise.resolve(1),
Promise.resolve(2)
];
console.log(res);
}).catch(onerror);
// 物件的寫法
co(function* () {
var res = yield {
1: Promise.resolve(1),
2: Promise.resolve(2),
};
console.log(res);
}).catch(onerror);
下面是另一個例子。
co(function* () {
var values = [n1, n2, n3];
yield values.map(somethingAsync);
});
function* somethingAsync(x) {
// do something async
return y
}
上面的程式碼允許併發三個somethingAsync
非同步操作,等到它們全部完成,才會進行下一步。
5、async函式
含義
ES7提供了async
函式,使得非同步操作變得更加方便。async
函式是什麼?一句話,async
函式就是Generator函式的語法糖。
前文有一個Generator函式,依次讀取兩個檔案。
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());
};
寫成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
,僅此而已。
async
函式對
Generator 函式的改進,體現在以下四點。
(1)內建執行器。Generator函式的執行必須靠執行器,所以才有了co
模組,而async
函式自帶執行器。也就是說,async
函式的執行,與普通函式一模一樣,只要一行。
var result = asyncReadFile();
上面的程式碼呼叫了asyncReadFile
函式,然後它就會自動執行,輸出最後結果。這完全不像Generator函式,需要呼叫next
方法,或者用co
模組,才能得到真正執行,得到最後結果。
(2)更好的語義。async
和await
,比起星號和yield
,語義更清楚了。async
表示函式裡有非同步操作,await
表示緊跟在後面的表示式需要等待結果。
(3)更廣的適用性。 co
模組約定,yield
命令後面只能是Thunk函式或Promise物件,而async
函式的await
命令後面,可以是Promise物件和原始型別的值(數值、字串和布林值,但這時等同於同步操作)。
(4)返回值是Promise。async
函式的返回值是Promise物件,這比Generator函式的返回值是Iterator物件方便多了。你可以用then
方法指定下一步的操作。
進一步說,async
函式完全可以看作多個非同步操作,包裝成的一個Promise物件,而await
命令就是內部then
命令的語法糖。
語法
async
函式的語法規則總體上比較簡單,難點是錯誤處理機制。
(1)async
函式返回一個Promise物件。
async
函式內部return
語句返回的值,會成為then
方法回撥函式的引數。
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
上面程式碼中,函式f
內部return
命令返回的值,會被then
方法回撥函式接收到。
async
函式內部丟擲錯誤,會導致返回的Promise物件變為reject
狀態。丟擲的錯誤物件會被catch
方法回撥函式接收到。
async function f() {
throw new Error('出錯了');
}
f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出錯了
(2)async
函式返回的Promise物件,必須等到內部所有await
命令的Promise物件執行完,才會發生狀態改變。也就是說,只有async
函式內部的非同步操作執行完,才會執行then
方法指定的回撥函式。
下面是一個例子。
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
(3)正常情況下,await
命令後面是一個Promise物件。如果不是,會被轉成一個立即resolve
的Promise物件。async function f() {
return await 123;
}
f().then(v => console.log(v))
// 123
上面程式碼中,await
命令的引數是數值123
,它被轉成Promise物件,並立即resolve
。
await
命令後面的Promise物件如果變為reject
狀態,則reject
的引數會被catch
方法的回撥函式接收到。
async function f() {
await Promise.reject('出錯了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出錯了
注意,上面程式碼中,await
語句前面沒有return
,但是reject
方法的引數依然傳入了catch
方法的回撥函式。這裡如果在await
前面加上return
,效果是一樣的。
只要一個await
語句後面的Promise變為reject
,那麼整個async
函式都會中斷執行。
async function f() {
await Promise.reject('出錯了');
await Promise.resolve('hello world'); // 不會執行
}
上面程式碼中,第二個await
語句是不會執行的,因為第一個await
語句狀態變成了reject
。
為了避免這個問題,可以將第一個await
放在try...catch
結構裡面,這樣第二個await
就會執行。
async function f() {
try {
await Promise.reject('出錯了');
} catch(e) {
}
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// hello world
另一種方法是await
後面的Promise物件再跟一個catch
方面,處理前面可能出現的錯誤。
async function f() {
await Promise.reject('出錯了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// 出錯了
// hello world
如果有多個await
命令,可以統一放在try...catch
結構中。
async function main() {
try {
var val1 = await firstStep();
var val2 = await secondStep(val1);
var val3 = await thirdStep(val1, val2);
console.log('Final: ', val3);
}
catch (err) {
console.error(err);
}
}
(4)如果await
後面的非同步操作出錯,那麼等同於async
函式返回的Promise物件被reject
。async function f() {
await new Promise(function (resolve, reject) {
throw new Error('出錯了');
});
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出錯了
上面程式碼中,async
函式f
執行後,await
後面的Promise物件會丟擲一個錯誤物件,導致catch
方法的回撥函式被呼叫,它的引數就是丟擲的錯誤物件。具體的執行機制,可以參考後文的“async函式的實現”。
防止出錯的方法,也是將其放在try...catch
程式碼塊之中。
async function f() {
try {
await new Promise(function (resolve, reject) {
throw new Error('出錯了');
});
} catch(e) {
}
return await('hello world');
}
async函式的實現
async 函式的實現,就是將 Generator 函式和自動執行器,包裝在一個函式裡。
async function fn(args){
// ...
}
// 等同於
function fn(args){
return spawn(function*() {
// ...
});
}
所有的
非同步程式設計對JavaScript語言太重要。Javascript語言的執行環境是“單執行緒”的,如果沒有非同步程式設計,根本沒法用,非卡死不可。
ES6誕生以前,非同步程式設計的方法,大概有下面四種。
回撥函式事件監聽釋出/訂閱Promise 物件回撥函式
ES6
非同步程式設計對JavaScript語言太重要。Javascript語言的執行環境是“單執行緒”的,如果沒有非同步程式設計,根本沒法用,非卡死不可。
ES6誕生以前,非同步程式設計的方法,大概有下面四種。
回撥函式
事件監聽
釋出/訂閱
Promise 物 眾所周知的,Javascript是一種單執行緒的語言,所有的程式碼必須按照所謂的“自上而下”的順序來執行。本特性帶來的問題就是,一些將來的、未知的操作,必須非同步實現(關於非同步,我會在另一篇文章裡進行討論)。本文將討論一個比較常見的非同步解決方案——Promise,時至本文最後更新的日子,Promise的應
1.基本概念
Javascript語言的執行環境是“單執行緒”的,如果沒有非同步程式設計,根本沒法用,非卡死不可。
ES6誕生以前,非同步程式設計的方法,大概有下面四種。
回撥函式事件監聽釋出/訂閱Promise 物件
ES6將JavaScript非同步程式設計帶入了 // ES6嚴格模式
'use strict';
if (true) {
function f() {}
}
// 不報錯並且ES6規定,塊級作用域之中,函式宣告語句的行為類似於let,在塊級作用域之外不可引用。function f() { console.log('I am outside!'); }
const pipeline = (...funcs) =>
val => funcs.reduce((a, b) => b(a), val);
const plus1 = a => a + 1;
const mult2 = a => a * 2;
const addThe
ES6-非同步函式
Generator是一個狀態機,它封裝了多個內部狀態。執行它將生成一個遍歷器物件。
Generator有2個特徵:
宣告時使用function*
函式內部使用yield宣告內部狀態
呼叫函式後,函式並不執行,而且返回的也不是函式 springboot非同步操作可以使用@EnableAsync和@Async兩個註解,本質就是多執行緒和動態代理。
一、配置一個執行緒池
@Configuration
@EnableAsync//開啟非同步
public class ThreadPoolConfig {
@B
ES6為Array增加了find(),findIndex函式。
find()函式用來查詢目標元素,找到就返回該元素,找不到返回undefined。
findIndex()函式也是查詢目標元素,找到就返回元素的位置,找不到就返回-1。
他們的都是一個查找回調函式。
[1, 2, 3,
上一篇《多執行緒第一次親密接觸 CreateThread與_beginthreadex本質區別》中講到一個多執行緒報數功能。為了描述方便和程式碼簡潔起見,我們可以只輸出最後的報數結果來觀察程式是否執行出錯。這也非常類似於統計一個網站每天有多少使用者登入,每個使用者登入用一個執
ES6為Array增加了find(),findIndex函式。
find()函式用來查詢目標元素,找到就返回該元素,找不到返回undefined。
findIndex()函式也是查詢目標元素,找到就返回元素的位置,找不到就返回-1。
他們的都是一個查找回調函式。
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1這個方法的主要目的,是彌補陣列建構函式Array()的不足。因為引數個數的不同,會導致Array()的行為有差異。Array() // []
Array(3) //
非同步程式設計對 JavaScript 語言太重要。 Javascript 語言的執行環境是“ 單執行緒” 的, 如果沒有非同步程式設計, 根本沒法用, 非卡死不可。ES6 誕生以前, 非同步程式設計的方法, 大概有下面四種。回撥函式事件監聽釋出 / 訂閱Promise 物件
最近小編在學習號稱世界最牛框架–Laravel。其實學習框架也就是學習框架的思想! 我想在我的部落格中記錄我在laravel學習中的一些心得,歡迎大家關注我的其他Github部落格和簡書,互相交流!
版本:Laravel 5.2
資料庫:m
async函式例項:按順序完成非同步操作
實際開發中,經常遇到一組非同步操作,需要按照順序完成。比如,依次遠端讀取一組 URL,然後按照讀取的順序輸出結果。
ES6 Promise 的寫法如下。
function logInOrder(urls){// 遠端讀取所有U
手上的專案寫好的介面已經對接完了。最近閒下來了,上家公司的主管跟我提起了前端和node.js。node必定是以後前端的一項重要技術。這次不再看視訊從基礎開始了,直接看自己需要的東西。
說到非同步就會想到promise,在es6中的await和async也是非同步操作。
a
async函式與其他非同步處理方法的比較
我們通過一個例子,來看 async函式與 Promise、Generator函式的比較。
假定某個 DOM 元素上面,部署了一系列的動畫,前一個動畫結束,才能開始後一個。如果當中有一個動畫出錯,就不再往下執行,返回上一個成功執 最近在寫RN相關的東西,其中涉及到了redux-saga ,saga的實現原理就是ES6中的Generator函式,而Generator函式又和迭代器有著密不可分的關係。所以本篇部落格先學習總結了iterator相關的東西,然後又介紹了Generator相關的內容,最後介紹了使用Generator進行非同步程 my.cnf 默認 ctu 管理系 int 建立 系統 管理 種類型 一、知識儲備
數據庫服務器:一臺計算機(對內存要求比較高)
數據庫管理系統:如mysql,是一個軟件
數據庫:oldboy_stu,相當於文件夾
表:student,scholl,class_list,相 Python 文件操作和字符編碼 1、文件操作
1、文件操作流程:
打開文件,得到文件句柄並賦值給一個變量===> file = open("yesterday",encoding="utf-8")
通過句柄對文件進行操作
關閉文件 ==> file.close()
1.2、打開文件的
相關推薦
自學-ES6篇-非同步操作和Async函式
ES6學習15(非同步操作和Async函式)
ECMAScript 6 非同步操作和Async函式
ECMAScript 6 學習筆記----非同步操作和Async函式
自學-ES6篇-let和conts命令
自學-ES6篇-函式的擴充套件
ES6-Generator函式和async函式
springboot2.0 非同步操作,@Async失效,無法進入非同步
【Vue】ES6,Array.find()和findIndex()函式的用法
秒殺多執行緒第三篇 原子操作 Interlocked系列函式
ES6,Array.find()和findIndex()函式的用法
自學-ES6篇-陣列的擴充套件
es6 javascript 非同步操作
Laravel學習篇-資料庫操作和查詢構造器
es6 async函式例項:按順序完成非同步操作
es6非同步之await和async非同步
es6 async函式與其他非同步處理方法的比較
ES6中的迭代器、Generator函式以及Generator函式的非同步操作
MySQL數據庫學習【第二篇】基本操作和存儲引擎
Python入門篇(五)之文件操作和字符編碼