Node Hero: 3. 理解异步编程

ChristaRzj 8年前
   <p>本章我将指导你学习异步编程的原理,并向你展示如何在 JavaScript 和 Node.js 中实现异步编程。</p>    <h2><strong>异步编程</strong></h2>    <p>在传统编程实践中,大多数 I/O 操作都是同步发生的。如果想想 Java,想想如何用 Java 读取一个文件,你会得到下面这样的代码:</p>    <pre>  <code class="language-javascript">try(FileInputStream inputStream = new FileInputStream("foo.txt")) {        Session IOUtils;      String fileContent = IOUtils.toString(inputStream);  }</code></pre>    <p>这背后发生了什么?主线程会被阻塞,直到文件读完,这意味着读文件的同时其它什么事情都做不了。要解决此问题,更好利用 CPU,就不得不手动管理线程。</p>    <p>如果有更多阻塞操作,那么事件队列就变得更糟糕:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/84a27b829378fe2e979be65574020392.png"></p>    <p>(红色块表示在进程等待外部资源的响应而被阻塞时,黑色块表示在代码运行时,绿色块表示应用的其余部分)</p>    <p>为解决这个问题,Node.js 引入了一种异步编程模型。</p>    <h2><strong>Node.js 中的异步编程</strong></h2>    <p>异步 I/O 是一种输入/输出处理的形式,它允许在传输完成之前,其它处理能继续进行。</p>    <p>在如下的示例中,我将展示 Node.js 中一个简单的文件读写过程 - 同时采用同步和异步的方式,目的是向你展示通过避免阻塞应用程序,能实现什么。</p>    <p>下面我们先从一个简单的示例开始 - 以同步的方式用 Node.js 读一个文件:</p>    <pre>  <code class="language-javascript">const fs = require('fs')    let content    try {      content = fs.readFileSync('file.md', 'utf-8')  } catch (ex) {    console.log(ex)  }  console.log(content)</code></pre>    <p>这里刚刚发生了什么?我们试图用 fs 模块的同步接口读一个文件。它按预期方式工作 - content 变量会包含 file.md 的内容。这种方式的问题是,Node.js 会被阻塞,直到操作完成 - 也就是说在文件正在被读取时,它什么事都做不了。</p>    <p>下面我们看看如何修复!</p>    <p>我们直到,在 JavaScript 中,异步编程只能用函数这个该语言的一等公民来实现:函数可以像所有其它变量一样传给其它函数。 <strong> 将其它函数作为参数的函数被称为 高阶函数 。 </strong></p>    <p>如下是一个高阶函数的最简单示例:</p>    <pre>  <code class="language-javascript">const numbers = [2,4,1,5,4]    function isBiggerThanTwo (num) {      return num > 2  }    numbers.filter(isBiggerThanTwo)</code></pre>    <p>在上例中,我们将一个函数传递给 filter 函数。通过这种方式我们可以定义过滤的逻辑。</p>    <p>这就是回调诞生的方式:如果你把一个函数传递给另一个函数作为参数,那么就可以在另一个函数完成任务时,在该函数内调用传进来的函数。不需要返回值,只用值调用另一个函数。</p>    <p>这些所谓错误优先(error-first)的回调是 Node.js 本身的核心 - 核心模块用了它,大多数 <a href="/misc/goto?guid=4959725959861255701" rel="nofollow,noindex">NPM</a> 中的模块也是。</p>    <pre>  <code class="language-javascript">const fs = require('fs')    fs.readFile('file.md', 'utf-8', function (err, content) {      if (err) {      return console.log(err)    }      console.log(content)  })</code></pre>    <p>这里要注意:</p>    <ul>     <li><strong>错误处理</strong> : 必须在回调中检测错误,而不是用 try-catch 块。</li>     <li><strong>没有返回值</strong> : 异步函数不返回值,但是值将被传递给回调。</li>    </ul>    <p>下面我们对这个文件做点修改,看看它实际上是如何工作的:</p>    <pre>  <code class="language-javascript">const fs = require('fs')    console.log('start reading a file...')    fs.readFile('file.md', 'utf-8', function (err, content) {      if (err) {      console.log('error happened during reading the file')      return console.log(err)    }      console.log(content)  })    console.log('end of the file')</code></pre>    <p>这段脚本的输出将是:</p>    <pre>  <code class="language-javascript">start reading a file...    end of the file    error happened during reading the file</code></pre>    <p>正如你所见,一旦我们开始读文件,执行继续,应用程序打印出 end of the file 。一旦文件读取完成,我们的回调就只被调用一次。这怎么可能呢? <strong>迎接事件循环。</strong></p>    <h2><strong>事件循环</strong></h2>    <p>事件循环是 Node.js / JavaScript 的核心 - 它负责安排异步操作。</p>    <p>在深入了解之前,要确保理解什么是事件驱动的编程。</p>    <p>事件驱动的编程是一种编程范式,在这种范式中程序流程是由事件决定的,比如用户行为(鼠标点击、按键)、传感器输出或者其它程序/线程的消息。</p>    <p>实际上,它意味着应用程序按照事件行事。</p>    <p>并且,我们在第一章已经学过,从开发者的观点看,Node.js 是单线程的。这意味着不必处理线程和线程同步,Node.js 远离了这种复杂性。除了你的代码,所有东西都是并行执行的。</p>    <h2><strong>异步控制流</strong></h2>    <p>至此你已经对 JavaScript 中的异步编程工作机制有了一个基本认识,下面我们来看看几个如何组织代码的示例。</p>    <h3><strong>Async.js</strong></h3>    <p>为避免所谓 回调地狱 ,可以做的一件事情是开始使用 async.js 。</p>    <p>Async.js 帮助组织应用程序结构,让流程控制更容易。</p>    <p>下面我们看一个使用 Async.js 的简短示例,然后用 Promises 重写。</p>    <p>如下的代码片段映射三个文件上的状态:</p>    <pre>  <code class="language-javascript">async.parallel(['file1', 'file2', 'file3'], fs.stat, function (err, results) {        // 现在结果是是一个每个文件的状态数组  })</code></pre>    <h3>Promises</h3>    <p>Promise 对象用于延迟及异步计算。一个 Promise 代表还没有完成,但是未来会执行的操作。</p>    <p>在实践中,前面的示例可以重写为如下:</p>    <pre>  <code class="language-javascript">function stats (file) {      return new Promise((resolve, reject) => {      fs.stat(file, (err, data) => {        if (err) {          return reject (err)        }        resolve(data)      })    })  }    Promise.all([      stats('file1'),    stats('file2'),    stats('file3')  ])  .then((data) => console.log(data))  .catch((err) => console.log(err))</code></pre>    <p>当然,如果使用一个有 Promise 接口的方法,那么 Promise 示例的行数也会少很多。</p>    <p> </p>    <p> </p>    <p>来自:http://www.zcfy.cc/article/node-hero-understanding-async-programming-in-node-js-1759.html</p>    <p> </p>