如何優雅地寫js異步代碼(2)
Rock with async/await
本篇文章是作為上一篇的續集,考慮到第一篇的篇幅,還有更重要的一點就是上一篇講的內容已經可以直接應用在最新版本的Node.js和一些高級瀏覽器(Chrome,FF)中,具體兼容性可參考:https://kangax.github.io/compat-table/es6/。
而這一篇講的內容,是ECMAScript 2016(ES7)的async/await
特性,目前的兼容性可參考:http://kangax.github.io/compat-table/esnext/#test-async_functions,雖然現在來看還不是非常樂觀,但是我們可以通過第三方的代碼轉換工具(如Traceur
和Babel
),將這些新特性的代碼轉換為當前環境可運行的代碼。
一個簡單的例子
實現同步的sleep,同步的代碼看起來應該是下面的樣子:
function sleep(timeout) {
setTimeout(function() {}, timeout);
}
function main() {
console.time('how long did I sleep');
sleep(3000);
console.timeEnd('how long did I sleep');
}
main();
// how long did I sleep: 0ms
但是在js中的執行結果卻是0ms
,這不是我們預期的呀。
改造
按照這種同步的代碼流程,怎么樣才能輸出3000ms
呢?看過上一篇文章的童鞋應該很快就能想到使用Generator
和yield
,沒看過的童鞋建議先看完上一篇再回來。
sleep(5min)
好,我就當你們都回來了,接下來就說說如何使用async/await
實現“同步”的sleep。
await
期望的值是一個Promise
對象,改造sleep
方法:
function sleep(timeout) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, timeout);
});
}
除此之外,main
方法還需要使用async
顯式聲明成異步方法:
async function main() {
console.time('how long did I sleep');
await sleep(3000);
console.timeEnd('how long did I sleep');
}
將以上代碼保存為async-sleep.js
。前面也說了需要借助第三方代碼轉換工具,那我們就安裝Babel
:
> npm install --save-dev babel-cli
安裝后執行:
> ./node_modules/babel-cli/bin/babel-node.js async-sleep.js
沒出意外的話,我們應該看到...WTF,出錯了
SyntaxError: async-sleep.js: Unexpected token (7:6)
5 | }
6 |
> 7 | async function main() {
| ^
8 | console.time('how long did I sleep');
9 | await sleep(3000);
10 | console.timeEnd('how long did I sleep');
這時需要安裝一個Babel
的插件用于轉換async
:
> npm install --save-dev babel-plugin-transform-async-to-generator
安裝好后,需要在運行目錄添加一個配置文件.babelrc
:
> vi .babelrc
{
"plugins": ["transform-async-to-generator"]
}
或在命令中指定:
> ./node_modules/babel-cli/bin/babel-node.js --plugins transform-async-to-generator async-sleep.js
沒出意外的話,我們應該看到...WTF,又出錯了
async-sleep.js:1
(function (exports, require, module, __filename, __dirname) { let main = (() => {
^^^
SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode
好吧,提示也很明顯,我們使用strict
模式,完整的代碼如下:
'use strict';
function sleep(timeout) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, timeout);
});
}
async function main() {
console.time('how long did I sleep');
await sleep(3000);
console.timeEnd('how long did I sleep');
}
main();
// how long did I sleep: 3003ms
再執行,終于看到我們期望的3003ms
。呃,怎么不是3000ms
,不要在意這些細節。難道QQ空間曾經用這個實際延遲的誤差來判斷客戶端CPU的繁忙程度也要告訴你。
看完這個例子,是不是發現async
類似于Generator
中的*
,而await
類似于yield
,但是現在不需要再額外封裝一個run
方法了,這還是很方便的。
重寫回調地獄的例子
繼續重寫那個回調地獄的例子:
- 讀取當前目錄的package.json
- 檢查backup目錄是否存在,如果不存在就創建backup目錄
- 將文件內容寫到備份文件
readPackageFile
、checkBackupDir
和backupPackageFile
直接使用上一篇中的定義:
var readPackageFile = new Promise(function(resolve, reject) {
fs.readFile('./package.json', function(err, data) {
if (err) {
reject(err);
}
resolve(data);
});
});
var checkBackupDir = new Promise(function(resolve, reject) {
fs.exists('./backup', function(exists) {
if (!exists) {
resolve(mkBackupDir);
} else {
resolve();
}
});
});
var mkBackupDir = new Promise(function(resolve, reject) {
// throw new Error('unexpected error');
fs.mkdir('./backup', function(err) {
if (err) {
return reject(err);
}
resolve();
});
});
function backupPackageFile(data) {
return new Promise(function(resolve, reject) {
fs.writeFile('./backup/package.json', data, function(err) {
if (err) {
return reject(err);
}
resolve();
});
});
};
Let's Rock:
(async function() {
try {
// 1. 讀取當前目錄的package.json
var data = await readPackageFile;
// 2. 檢查backup目錄是否存在,如果不存在就創建backup目錄
await checkBackupDir;
// 3. 將文件內容寫到備份文件
await backupPackageFile(data);
console.log('backup successed');
} catch (err) {
console.error(err);
}
}());
總結
js正朝著越來越好的方向發展,不是嗎?
參考地址
題圖引自:http://forwardjs.com/img/workshops/advancedjs-async.jpg