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

hubuke 9年前
   <p><img alt="如何优雅地写js异步代码(2)" src="https://simg.open-open.com/show/3e74552a76c94b85efe327087fabfd3e.jpg"></p>    <h2>Rock with async/await</h2>    <p>本篇文章是作为<a href="/misc/goto?guid=4959671366715340425">上一篇</a>的续集,考虑到第一篇的篇幅,还有更重要的一点就是上一篇讲的内容已经可以直接应用在最新版本的Node.js和一些高级浏览器(Chrome,FF)中,具体兼容性可参考:<a href="/misc/goto?guid=4958962495074776016">https://kangax.github.io/compat-table/es6/</a>。</p>    <p>而这一篇讲的内容,是ECMAScript 2016(ES7)的<a href="/misc/goto?guid=4958979698860247506"><code class="language-markup">async/await</code></a>特性,目前的兼容性可参考:<a href="/misc/goto?guid=4959671368204437693">http://kangax.github.io/compat-table/esnext/#test-async_functions</a>,虽然现在来看还不是非常乐观,但是我们可以通过第三方的代码转换工具(如<code class="language-markup">Traceur</code>和<code class="language-markup">Babel</code>),将这些新特性的代码转换为当前环境可运行的代码。</p>    <h3>一个简单的例子</h3>    <p>实现同步的sleep,同步的代码看起来应该是下面的样子:</p>    <pre>  <code class="language-javascript">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  </code></pre>    <p>但是在js中的执行结果却是<code class="language-markup">0ms</code>,这不是我们预期的呀。</p>    <p>改造</p>    <p>按照这种同步的代码流程,怎么样才能输出<code class="language-markup">3000ms</code>呢?看过<a href="/misc/goto?guid=4959671366715340425">上一篇</a>文章的童鞋应该很快就能想到使用<code class="language-markup">Generator</code>和<code class="language-markup">yield</code>,没看过的童鞋建议先看完<a href="/misc/goto?guid=4959671366715340425">上一篇</a>再回来。</p>    <p>sleep(5min)</p>    <p>好,我就当你们都回来了,接下来就说说如何使用<code class="language-markup">async/await</code>实现“同步”的sleep。</p>    <p><code class="language-markup">await</code>期望的值是一个<code class="language-markup">Promise</code>对象,改造<code class="language-markup">sleep</code>方法:</p>    <pre>  <code class="language-javascript">function sleep(timeout) {        return new Promise(function(resolve, reject) {          setTimeout(resolve, timeout);      });  }  </code></pre>    <p>除此之外,<code class="language-markup">main</code>方法还需要使用<code class="language-markup">async</code>显式声明成异步方法:</p>    <pre>  <code class="language-javascript">async function main() {        console.time('how long did I sleep');      await sleep(3000);      console.timeEnd('how long did I sleep');  }  </code></pre>    <p>将以上代码保存为<code class="language-markup">async-sleep.js</code>。前面也说了需要借助第三方代码转换工具,那我们就安装<code class="language-markup">Babel</code>:</p>    <pre>  <code class="language-javascript">> npm install --save-dev babel-cli  </code></pre>    <p>安装后执行:</p>    <pre>  <code class="language-javascript">> ./node_modules/babel-cli/bin/babel-node.js async-sleep.js  </code></pre>    <p>没出意外的话,我们应该看到...WTF,出错了</p>    <pre>  <code class="language-javascript">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');  </code></pre>    <p>这时需要安装一个<code class="language-markup">Babel</code>的插件用于转换<code class="language-markup">async</code>:</p>    <pre>  <code class="language-javascript">> npm install --save-dev babel-plugin-transform-async-to-generator  </code></pre>    <p>安装好后,需要在运行目录添加一个配置文件<code class="language-markup">.babelrc</code>:</p>    <pre>  <code class="language-javascript">> vi .babelrc  {      "plugins": ["transform-async-to-generator"]  }  </code></pre>    <p>或在命令中指定:</p>    <pre>  <code class="language-javascript">>  ./node_modules/babel-cli/bin/babel-node.js --plugins transform-async-to-generator async-sleep.js  </code></pre>    <p>没出意外的话,我们应该看到...WTF,又出错了</p>    <pre>  <code class="language-javascript">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    </code></pre>    <p>好吧,提示也很明显,我们使用<code class="language-markup">strict</code>模式,完整的代码如下:</p>    <pre>  <code class="language-javascript">'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  </code></pre>    <p>再执行,终于看到我们期望的<code class="language-markup">3003ms</code>。呃,怎么不是<code class="language-markup">3000ms</code>,不要在意这些细节。难道QQ空间曾经用这个实际延迟的误差来判断客户端CPU的繁忙程度也要告诉你。</p>    <p>看完这个例子,是不是发现<code class="language-markup">async</code>类似于<code class="language-markup">Generator</code>中的<code class="language-markup">*</code>,而<code class="language-markup">await</code>类似于<code class="language-markup">yield</code>,但是现在不需要再额外封装一个<code class="language-markup">run</code>方法了,这还是很方便的。</p>    <h3>重写回调地狱的例子</h3>    <p>继续重写那个回调地狱的例子:</p>    <ol>     <li>读取当前目录的package.json</li>     <li>检查backup目录是否存在,如果不存在就创建backup目录</li>     <li>将文件内容写到备份文件</li>    </ol>    <p><code class="language-markup">readPackageFile</code>、<code class="language-markup">checkBackupDir</code>和<code class="language-markup">backupPackageFile</code>直接使用上一篇中的定义:</p>    <pre>  <code class="language-javascript">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();          });      });  };  </code></pre>    <p>Let's Rock:</p>    <pre>  <code class="language-javascript">(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);      }  }());  </code></pre>    <h2>总结</h2>    <p>js正朝着越来越好的方向发展,不是吗?</p>    <h2>参考地址</h2>    <ul>     <li><a href="/misc/goto?guid=4959671368325644086">Async/Await: The Hero JavaScript Deserved</a></li>     <li><a href="/misc/goto?guid=4959671368428917006">babel 6 async / await: Unexpected token</a></li>     <li><a href="/misc/goto?guid=4959663418327117107">Babel cli usage</a></li>    </ul>    <p>题图引自:<a href="/misc/goto?guid=4959671366613648757">http://forwardjs.com/img/workshops/advancedjs-async.jpg</a></p>    <p>来自:<a href="/misc/goto?guid=4959671366215572600">http://iammapping.com/write-js-async-gracefully-2/</a></p>