本文通过一个简单的需求:读取文件并备份到指定目录(详见第一段代码的注释),以不同的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
错误从哪个方法抛出来的?
神马?你觉的这个代码写得很好,优雅得无可挑剔?那么你现在可以忽略下文直接去最后的评论写:楼主敏感词
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'); } }
我们现在可以快速定位抛出异常的方法
借助第三方库,优化异步代码
Async
ECMAScript 6
在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); });
使用方法参考: https://github.com/caolan/async
优点:
缺点:
ES6的目标,是使得JavaScript语言可以用来编写大型的复杂的应用程序,成为企业级开发语言。
接下来介绍ES6的新特性:Promise对象和Generator函数,是如何让代码看起来更优雅。
更多ES6的特性参考: ECMAScript 6 入门
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环境下可以使用的库:
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
合体前的准备工作,参考 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 。不管使用那种方式,都是为了增强代码的可读性和可维护性;如果是在已有的项目中修改,还要考虑对现有代码的侵略性。
续集: 如何优雅地写js异步代码(2)
题图引自: http://forwardjs.com/img/workshops/advancedjs-async.jpg