在Typescript編程當中,我們如何優雅的實現異步編程呢?
利用Await/Async寫上層邏輯,利用Deferred/Promise封裝回調函數。
我們先來看一下實際工作環境中的一段代碼:
這段代碼實現的功能是將本地的文件或文件夾上傳至hdfs上。
async upload(localPath: string, hdfsPath: string, recursive = false): Promise<void> {
if (recursive === true) { // 如果上傳的是一個目錄的話,recursive: true
const fileDescriptors = await io.readdir(localPath); // 讀取文件夾名字
const promises = fileDescriptors.map((file: string) => this.fnUpload(file, localPath, hdfsPath)); // 得到一個元素爲promise的數組
await Promise.all(promises);等promises這個數組內所有的promise執行完後再繼續向後執行
} else { // 如果上傳的是一個文件
await this.pipe(io.createReadStream(localPath), this._hdfsClient.createWriteStream(hdfsPath.substr(6))); // 建立一個管道把數據發送上去
}
}
async fnUpload(file: string, localPath: string, hdfsPath: string): Promise<void> {
const stats = await io.stat(path.join(localPath, file)); // 查看當前路徑的文件狀態
if (stats.isDirectory()) { // 如果是一個目錄
await this.upload(path.join(localPath, file), hdfsPath.concat(file), true);
} else { // 如果是一個文件
await this.upload(path.join(localPath, file), hdfsPath.concat(file));
}
}
我們主要利用async和await這兩個語法糖來用同步的思維方式實現異步,在上面的代碼中,我們的想法很自然,如果這個過程希望是同步的,那麼我們就在這個方法前加一個await,那麼這個await爲我們做了什麼東西呢?
首先,我們介紹一下promise,promise有三種狀態,pending(進行中), fulfilled(已成功), rejected(已失敗),當一個函數返回的是一個promise對象的時候,他會立即返回一個pending狀態的promise,這樣就不會阻塞你其它代碼的執行,而這個函數也在異步執行。
function test () {
return new Promise(resolve, reject) {
console.info('my code'); // 你的代碼
if (你的代碼執行成功了) {
resolve(result); // 將結果result傳給resolve()
} else {
reject(); // 如果你的代碼執行沒有成功或者出現了異常
}
}
}
test();
console.info('hello, guys');
上面這段代碼會輸出hello, guys,然後再輸出my code.
await 後面執行的就是一個promise(如果不是promise會把它轉換成promise.resolve(code)),而await要做的事情就是等promise狀態變爲fullfilled後繼續往後執行,這樣就實現了暫停等待的效果,而後面的promise的內部可以有更多的異步,只是在上層邏輯需要等待的地方做了等待,很自然。
然而await只能在async聲明過的方法內使用,爲什麼呢?我們來介紹一下async做了什麼,
當你在函數錢聲明關鍵字async的時候,編譯器認爲你這是一個異步函數,他會幫你聲明一個promise,將你函數內所有的代碼都放在一個promise裏面,類似這樣:
function test () {
console.info('in');
};
function test () {
return new Promise() {
console.info('in');
}
}
這樣的好處是我保證了所有await的環境都是異步的,是不會阻塞上層代碼的運行的。
那麼如果我希望在一個過程中多個部分異步,但是總的這個過程同步呢?
就是我們最初的代碼中使用到的Promise.all(array[promise]),參數是一個元素爲promise的數組,會返回一個promise,這樣你await Promise.all()就可以等這個數組中所有的promise都成功後再繼續執行。
通過這種語法糖,我們很自然的把所有的異步和同步邏輯都寫完了。代碼的可讀性也非常高,但這個時候有一個問題,如果我當前的函數是一個異步函數,但是返回的不是promise怎麼辦呢?
比如我們許多常見的庫函數都是通過回調函數來實現在一段代碼執行完後再執行另一端代碼,
function add (num1, num2, callback) {
var sum = num1 + num2;
callback(sum);
}
add(1, 2, function (sum) {
console.log(sum); // 3
});
但我們希望看到的是這樣的:
await add(1, 2);
console.log(sum); // 3
怎麼實現呢?
答案就是利用 Deferred/Promise把回調函數封裝成一個返回promise的函數,
function add (num1, num2, callback) {
var sum = num1 + num2;
callback(err, sum); // 假裝這兒可能會有異常傳出來
}
function add_promise (num1, num2) {
const deferred = new Deferred<number>();
add (num1, num2, (err, sum) => {
if (err) {
deferred.reject();
} else {
deferred.resolve(sum);
}
});
return deferred.promise;
}
const sum = await add_promise(1, 2);
console.log(sum); // 3
對於上層的方法調用者來說,是不是代碼的可讀性強多了?也跳出了回調地獄,通過Async/Await, Deferred/Promise的配合,異步編程這塊兒,我們就徹底解決了。