转载

如何优雅地写 js 异步代码

如何优雅地写 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、 匿名调试

取消上面代码中抛出异常的注释再执行 如何优雅地写 js 异步代码

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');     } } 

我们现在可以快速定位抛出异常的方法

如何优雅地写 js 异步代码

他山之石 可以攻玉

借助第三方库,优化异步代码

browser js

  • jQuery Deferred
    • ajax
    • animate

NodeJs

  • Async

    • async.each
    • async.map
    • async.waterfall
  • ECMAScript 6

    • Promise
    • Generator

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

优点:

  1. 简单、易于理解
  2. 函数丰富,几乎可以满足任何回调需求
  3. 流行

缺点:

  1. 额外引入第三方库
  2. 虽然简单,但还是难以掌握所有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); }); 

这么简单?

看看 readPackageFilecheckBackupDirbackupPackageFile 的定义:

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环境下可以使用的库:

  • bluebird
  • Q
  • when
  • WinJS
  • RSVP.js

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 语句,把整个函数拆分成许多小块,每次调用 generatornext 方法就按顺序执行一小块,执行到 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(); } 

readPackageFilecheckBackupDirbackupPackageFile 直接使用上面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还能这样写”,那么我的目的就达到了。本文的写作初衷不是介绍 AsyncDeferredPromiseGenerator 的用法,如果对于这几个概念不是很熟悉的话,建议查阅其他资料学习。写js就像说英语,不是 write in js ,而是 think in js 。不管使用那种方式,都是为了增强代码的可读性和可维护性;如果是在已有的项目中修改,还要考虑对现有代码的侵略性。

续集: 如何优雅地写js异步代码(2)

参考地址

  • 回调地狱
  • JavaScript Promise启示录
  • Promises/A+
  • ECMAScript 6入门
  • JavaScript Promises
  • 使用 (Generator) 生成器解决 JavaScript 回调嵌套问题
  • 拥抱Generator,告别回调

题图引自: http://forwardjs.com/img/workshops/advancedjs-async.jpg

原文  http://iammapping.com/write-js-async-gracefully/
正文到此结束
Loading...