如何优雅地写js异步代码
本文通过一个简单的需求:读取文件并备份到指定目录(详见第一段代码的注释),以不同的js代码实现,来演示代码是如何变优雅的。对比才能分清好坏,想知道什么是优雅的代码,先看看糟糕的代码。
不优雅的代码是什么样的?
1、 回调地狱
/**
* 读取当前目录的package.json,并将其备份到backup目录
*
* 1. 读取当前目录的package.json
* 2. 检查backup目录是否存在,如果不存在就创建backup目录
* 3. 将文件内容写到备份文件
*/
fs.readFile('./package.json', function(err, data) {
if (err) {
console.error(err);
} else {
fs.exists('./backup', function(exists) {
if (!exists) {
fs.mkdir('./backup', function(err) {
if (err) {
console.error(err);
} else {
// throw new Error('unexpected');
fs.writeFile('./backup/package.json', data, function(err) {
if (err) {
console.error(err);
} else {
console.log('backup successed');
}
});
}
});
} else {
fs.writeFile('./backup/package.json', data, function(err) {
if (err) {
console.error(err);
} else {
console.log('backup successed');
}
});
}
});
}
});
2、 匿名调试
取消上面代码中抛出异常的注释再执行
wtf,这个unexpected
错误从哪个方法抛出来的?
神马?你觉的这个代码写得很好,优雅得无可挑剔?那么你现在可以忽略下文直接去最后的评论写:楼主敏感词
怎样写才能让js回调看上去优雅?
1、 消除回调嵌套
2、 命名方法
fs.readFile('./package.json', function(err, data) {
if (err) {
console.error(err);
} else {
writeFileContentToBackup(data);
}
});
function writeFileContentToBackup(fileContent) {
checkBackupDir(function(err) {
if (err) {
console.error(err);
} else {
backup(fileContent, log);
}
});
}
function checkBackupDir(cb) {
fs.exists('./backup', function(exists) {
if (!exists) {
mkBackupDir(cb);
} else {
cb(null);
}
});
}
function mkBackupDir(cb) {
// throw new Error('unexpected');
fs.mkdir('./backup', cb);
}
function backup(data, cb) {
fs.writeFile('./backup/package.json', data, cb);
}
function log(err) {
if (err) {
console.error(err);
} else {
console.log('backup successed');
}
}
我们现在可以快速定位抛出异常的方法
他山之石 可以攻玉
借助第三方库,优化异步代码
browser js
- jQuery Deferred
- ajax
- animate
NodeJs
jQuery Deferred
在jQuery-1.5中引进,被应用在ajax、animate等异步方法上
一个简单的例子:
function sleep(timeout) {
var dtd = $.Deferred();
setTimeout(dtd.resolve, timeout);
return dtd;
}
// 等同于上面的写法
function sleep(timeout) {
return $.Deferred(function(dtd) {
setTimeout(dtd.resolve, timeout);
});
}
console.time('sleep');
sleep(2000).done(function() {
console.timeEnd('sleep');
});
一个复杂的例子:
function loadImg(src) {
var dtd = $.Deferred(),
img = new Image;
img.onload = function() {
dtd.resolve(img);
}
img.onerror = function(e) {
dtd.reject(e);
}
img.src = src;
return dtd;
}
loadImg('http://www.baidu.com/favicon.ico').then(
function(img) {
$('body').prepend(img);
}, function() {
alert('load error');
}
)
那么问题来了,我想要过5s后把百度Logo显示出来?
普通写法:
sleep(5000).done(function() {
loadImg('http://www.baidu.com/favicon.ico').done(function(img) {
$('body').prepend(img);
});
});
二逼写法:
setTimeout(function() {
loadImg('http://www.baidu.com/favicon.ico').done(function(img) {
$('body').prepend(img);
});
}, 5000);
文艺写法(睡5s和加载图片同步执行):
$.when(sleep(5000), loadImg('http://www.baidu.com/favicon.ico')).done(function(ignore, img) {
$('body').prepend(img);
});
Async
使用方法参考:https://github.com/caolan/async
优点:
- 简单、易于理解
- 函数丰富,几乎可以满足任何回调需求
- 流行
缺点:
- 额外引入第三方库
- 虽然简单,但还是难以掌握所有api
ECMAScript 6
ES6的目标,是使得JavaScript语言可以用来编写大型的复杂的应用程序,成为企业级开发语言。
接下来介绍ES6的新特性:Promise对象和Generator函数,是如何让代码看起来更优雅。
更多ES6的特性参考:ECMAScript 6 入门
Promise
Promise对象的初始化以及使用:
var promise = new Promise(function(resolve, reject) {
setTimeout(function() {
if (true) {
resolve('ok');
} else {
reject(new Error('unexpected error'));
}
}, 2000);
});
promise.then(function(msg) {
// throw new Error('unexpected resolve error');
console.log(msg);
}).catch(function(err) {
console.error(err);
});
JavaScript Promise 的 API 会把任何包含有 then 方法的对象当作“类 Promise”(或者用术语来说就是 thenable)
与上面介绍的jQuery Deferred对象类似,但api方法和错误捕捉等不完全一样。
可以使用以下方法转换:
var promise = Promise.resolve($.Deferred());
那怎么使用Promise改写回调地狱那个例子?
// 1. 读取当前目录的package.json
readPackageFile.then(function(data) {
// 2. 检查backup目录是否存在,如果不存在就创建backup目录
return checkBackupDir.then(function() {
// 3. 将文件内容写到备份文件
return backupPackageFile(data);
});
}).then(function() {
console.log('backup successed');
}).catch(function(err) {
console.error(err);
});
这么简单?
看看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();
});
});
};
是不是感觉到满满的欺骗,说好的简单呢,先别打,至少调用起来还是很简单的XD。个人觉得使用Promise最大的好处就是让调用方爽。
流程优化,使用js的无阻塞特性,我们发现第一步和第二步可以同步执行:
Promise.all([readPackageFile, checkBackupDir]).then(function(res) {
return backupPackageFile(res[0]);
}).then(function() {
console.log('backup successed');
}).catch(function(err) {
console.error(err);
});
在ES5环境下可以使用的库:
Generator
NodeJs默认不支持Generator的写法,但在v0.12后可以添加--harmony
参数使其支持:
> node --harmony generator.js
允许函数在特定地方像
return
一样退出,但是稍后又能恢复到这个位置和状态上继续执行
function * foo(input) {
console.log('这里会在第一次调用next方法时执行');
yield input;
console.log('这里不会被执行,除非再调一次next方法');
}
var g = foo(10);
console.log(Object.prototype.toString.call(g)); // [object Generator]
console.log(g.next()); // { value: 10, done: false }
console.log(g.next()); // { value: undefined, done: true }
如果觉得比较难理解,就把yield
看成return
语句,把整个函数拆分成许多小块,每次调用generator
的next
方法就按顺序执行一小块,执行到yield
就退出。
告诉你一个惊人的秘密,我们现在可以“同步”写js的sleep
了:
var sleepGenerator;
function sleep(time) {
setTimeout(function() {
sleepGenerator.next(); // step 5
}, time);
}
var sleepGenerator = (function * () {
console.log('wait...'); // step 2
console.time('how long did I sleep'); // step 3
yield sleep(2000); // step 4
console.log('weakup'); // step 6
console.timeEnd('how long did I sleep'); // step 7
}());
sleepGenerator.next(); // step 1
合体,使用Promise和Generator重写回调地狱的例子
合体前的准备工作,参考Q.async:
function run(makeGenerator) {
function continuer(verb, arg) {
var result;
try {
result = generator[verb](arg);
} catch (err) {
return Promise.reject(err);
}
if (result.done) {
return result.value;
} else {
return Promise.resolve(result.value).then(callback, errback);
}
}
var generator = makeGenerator.apply(this, arguments);
var callback = continuer.bind(continuer, "next");
var errback = continuer.bind(continuer, "throw");
return callback();
}
readPackageFile
、checkBackupDir
和backupPackageFile
直接使用上面Promise中的定义,是不是很爽。
合体后的执行:
run(function *() {
try {
// 1. 读取当前目录的package.json
var data = yield readPackageFile;
// 2. 检查backup目录是否存在,如果不存在就创建backup目录
yield checkBackupDir;
// 3. 将文件内容写到备份文件
yield backupPackageFile(data);
console.log('backup successed');
} catch (err) {
console.error(err);
}
});
是不是感觉跟写同步代码一样了。
总结
看完本文,如果你感慨:“靠,js还能这样写”,那么我的目的就达到了。本文的写作初衷不是介绍Async
、Deferred
、Promise
、Generator
的用法,如果对于这几个概念不是很熟悉的话,建议查阅其他资料学习。写js就像说英语,不是write in js,而是think in js。不管使用那种方式,都是为了增强代码的可读性和可维护性;如果是在已有的项目中修改,还要考虑对现有代码的侵略性。
参考地址
- 回调地狱
- JavaScript Promise启示录
- Promises/A+
- ECMAScript 6入门
- JavaScript Promises
- 使用 (Generator) 生成器解决 JavaScript 回调嵌套问题
- 拥抱Generator,告别回调
题图引自:http://forwardjs.com/img/workshops/advancedjs-async.jpg