Skip to content

Latest commit

 

History

History
581 lines (404 loc) · 29.3 KB

File metadata and controls

581 lines (404 loc) · 29.3 KB

四、事件和计时器

前一章介绍了 Node 的事件驱动编程模型。本章对事件和事件处理进行了更深入的研究。对事件处理的深刻理解将允许您创建复杂的、事件驱动的应用,例如 web 服务器。本章介绍事件发射器,即用于创建新事件的对象。在学习了如何创建事件之后,本章将转向事件处理。最后,本章讨论了 Node 中的定时器和功能调度。

事件发射器

在 Node 中,生成事件的对象称为事件发射器。创建一个事件发射器就像导入events核心模块并实例化一个EventEmitter对象一样简单。然后,EventEmitter实例可以使用它的emit()方法创建新事件。清单 4-1 中显示了一个创建事件发射器的例子。在这个例子中,事件发射器创建了一个foo事件。

清单 4-1 。一个简单事件发射器的例子

var events = require("events");
var emitter = new events.EventEmitter();

emitter.emit("foo");

事件名称可以是任何有效的字符串,但是按照惯例使用 camelCase 命名。例如,创建一个事件来表明一个新用户被添加到系统中,这个事件可能被命名为userAdded或类似的名称。

通常,事件需要提供事件名称之外的附加信息。例如,当按下一个键时,该事件还指定键入哪个键。为了支持这一功能,emit()方法可以在事件名称后接受任意数量的可选参数。回到创建新用户的例子,清单 4-2 展示了额外的参数是如何传递给emit()的。这个例子假设执行了一些 I/O(可能是一个数据库事务)操作,这会创建一个新用户。一旦 I/O 操作完成,事件发射器emitter创建一个新的userAdded事件,并传入用户的用户名和密码。

清单 4-2 。向发出的事件传递参数的示例

var events = require("events");
var emitter = new events.EventEmitter();var username = "colin";var password = "password";

// add the user
// then emit an event
emitter.emit("userAdded", username, password);

监听事件

清单 4-2 的例子中,一个事件发射器被用来创建一个事件。不幸的是,如果没有人在听,一个事件是毫无意义的。在 Node 中,事件监听器使用on()addListener()方法连接到事件发射器。这两种方法可以互换使用。这两种方法都将事件名称和处理函数作为参数。当发出指定类型的事件时,会调用相应的处理函数。例如,在清单 4-3 中,使用on()方法将一个userAdded事件处理程序附加到emitter上。接下来,emitter发出一个userAdded事件,导致处理程序被调用。这个例子的输出如清单 4-4 所示。

清单 4-3 。使用on()设置事件监听器

var events = require("events");
var emitter = new events.EventEmitter();

var username = "colin";
var password = "password";

// an event listener
emitter.on("userAdded", function(username, password) {
  console.log("Added user " + username);
});

// add the user
// then emit an event
emitter.emit("userAdded", username, password);

image 注意事件监听器只能检测那些在监听器被连接后发生的事件。也就是说,收听者不能检测过去的事件。因此,如清单 4-3 所示,确保在发出事件之前附加一个监听器。

清单 4-4 。运行清单 4-3 中代码的输出

$ node user-event-emitter.js
Added user colin

一次性事件侦听器

有时你可能只对事件第一次发生时的反应感兴趣。在这些情况下,您可以使用once()方法。once()的用法与on()addListener().完全一样,但是,使用 once()附加的监听器最多执行一次,然后被删除。清单 4-5 显示了一个once()方法的使用示例。在本例中,once()用于监听foo事件。然后使用emit()方法创建两个foo事件。但是,因为事件监听器是使用once()注册的,所以只处理第一个foo事件。如果事件监听器是使用on()addListener()注册的,那么两个 foo 事件都会得到处理。运行该示例的输出如清单 4-6 所示。

清单 4-5 。使用once()的一次性事件监听器的示例

var events = require("events");
var emitter = new events.EventEmitter();

emitter.once("foo", function() {
  console.log("In foo handler");
});

emitter.emit("foo");
emitter.emit("foo");

清单 4-6 。运行清单 4-5 中代码的输出

$ node once-test.js
In foo handler

检查事件侦听器

在事件发射器的生命周期中,它可以有零个或多个侦听器。每种事件类型的侦听器可以通过几种方式进行检查。如果您只对确定附加侦听器的数量感兴趣,那么只需看看EventEmitter.listenerCount()方法就可以了。该方法将一个EventEmitter实例和一个事件名作为参数,并返回附加侦听器的数量。例如,在清单 4-7 中,创建了一个事件发射器,并附加了两个无趣的foo事件处理程序。该示例的最后一行显示了通过调用EventEmitter.listenerCount()连接到emitterfoo处理程序的数量。在这种情况下,该示例输出数字 2。请注意,listenerCount()调用被附加到了EventEmitter类,而不是特定的实例。许多语言称之为静态方法。然而,Node 文档将listenerCount()标识为一个类方法,因此本书也是如此。

清单 4-7 。使用EventEmitter.listenerCount()确定听众人数

var events = require("events");
var EventEmitter = events.EventEmitter; // get the EventEmitter constructor from the events module
var emitter = new EventEmitter();

emitter.on("foo", function() {});
emitter.on("foo", function() {});
console.log(EventEmitter.listenerCount(emitter, "foo"));

如果获取附加到事件发射器的处理程序的数量还不够,那么可以使用listeners()方法来获取事件处理程序函数的数组。该数组通过length属性提供处理程序的数量,以及事件发生时调用的实际函数。也就是说,修改由listeners()返回的数组不会影响由事件发射器对象维护的处理程序。

清单 4-8 提供了一个使用listeners()方法的例子。在这个例子中,一个foo事件处理程序被添加到一个事件发射器中。然后使用listeners()来检索事件处理程序的数组。然后使用数组forEach()方法遍历事件处理程序,一路调用每个事件处理程序。因为本例中的事件处理程序不接受任何参数,也不改变程序状态,所以对forEach()的调用实质上复制了emitter.emit("foo")的功能。

清单 4-8 。一个通过listeners()方法迭代事件处理程序的例子

var events = require("events");
var EventEmitter = events.EventEmitter;
var emitter = new EventEmitter();

emitter.on("foo", function() { console.log("In foo handler"); });
emitter.listeners("foo").forEach(function(handler) {
  handler();
});

newListener事件

每次注册新的事件处理程序时,事件发射器都会发出一个newListener事件。此事件用于检测新的事件处理程序。当您需要为每个新的事件处理程序分配资源或执行某些操作时,通常会使用newListener。一个newListener事件的处理方式和其他事件一样。处理程序需要两个参数:字符串形式的事件名称和处理程序函数。例如,在清单 4-9 中,一个foo事件处理程序被附加到一个事件发射器上。在幕后,发射器发出一个newListener事件,导致newListener事件处理程序被调用。

清单 4-9 。添加一个newListener事件处理器

var events = require("events");
var emitter = new events.EventEmitter();

emitter.on("newListener", function(eventName, listener) {
  console.log("Added listener for " + eventName + " events");
});

emitter.on("foo", function() {});

重要的是要记住newListener事件是在创建自己的事件时存在的。清单 4-10 显示了如果你忘记了会发生什么。在这个例子中,开发人员创建了一个定制的newListener事件处理程序,该程序期望被传递一个Date对象。当发出一个newListener事件时,一切都按预期工作。然而,当创建一个看似不相关的foo事件处理程序时,会抛出一个异常,因为内置的newListener事件是以字符串foo作为第一个参数发出的。因为Date对象有一个getTime()方法,而字符串没有,所以抛出一个TypeError

清单 4-10newListener事件的无效处理程序

var events = require("events");
var emitter = new events.EventEmitter();

emitter.on("newListener", function(date) {
  console.log(date.getTime());
});

emitter.emit("newListener", new Date());
emitter.on("foo", function() {});

删除事件侦听器

事件侦听器可以在附加到事件发射器后被删除。例如,要将事件发射器重置到某个没有监听器的初始状态,最简单的方法是使用removeAllListeners()方法。可以不带任何参数调用此方法,在这种情况下,所有事件侦听器都会被移除。或者,传入事件名称会导致命名事件的处理程序被移除。removeAllListeners()的语法如清单 4-11 所示。

清单 4-11removeAllListeners()方法的语法

emitter.removeAllListeners([eventName])

如果removeAllListeners()对于您的需求来说过于粗糙,那么就求助于removeListener()方法。此方法用于移除单个事件侦听器,并接受两个参数—要移除的事件的名称和处理函数。清单 4-12 展示了一个removeListener()的使用示例。在这种情况下,一个foo事件监听器被添加到一个事件发射器,然后立即被移除。发出事件时,不会发生任何事情,因为没有附加的侦听器。注意,removeListener()的用法与on()addListener()方法的用法相同,尽管它们执行相反的操作。

清单 4-12 。使用removeListener()删除事件处理程序

var events = require("events");
var emitter = new events.EventEmitter();

function handler() {
  console.log("In foo handler");
}

emitter.on("foo", handler);
emitter.removeListener("foo", handler);
emitter.emit("foo");

如果你打算使用removeListener() ,避免匿名处理函数。就其本质而言,匿名函数不会绑定到命名引用。如果创建了匿名事件处理程序,第二个相同的匿名函数将无法成功移除该处理程序。这是因为两个不同的Function对象不被认为是等价的,除非它们指向内存中的同一个位置。因此,清单 4-13 中的例子而不是删除一个事件监听器。

清单 4-13 。匿名函数对removeListener()的不正确使用

var events = require("events");
var emitter = new events.EventEmitter();

emitter.on("foo", function() {
  console.log("foo handler");
});
emitter.removeListener("foo", function() {
  console.log("foo handler");
});
emitter.emit("foo");

检测潜在的内存泄漏

通常,单个事件发射器只需要少量的事件侦听器。因此,如果应用以编程方式向事件发射器添加事件侦听器,而该发射器突然拥有了几百个事件侦听器,这可能表明出现了某种类型的逻辑错误,从而导致内存泄漏。这方面的一个例子是添加事件侦听器的循环。如果循环包含逻辑错误,可能会创建大量事件处理程序,消耗不必要的内存。默认情况下,如果为任何单个事件添加了十个以上的侦听器,Node 会打印一条警告消息。该阈值可以使用setMaxListeners()方法进行控制。这个方法将一个整数作为它唯一的参数。通过将该值设置为0,事件发射器将接受无限制的侦听器,而不会输出警告消息。请注意,程序语义不受setMaxListeners()的影响(它只会打印一条警告消息)。相反,它只是提供了一个有用的调试机制。setMaxListeners()的用法如清单 4-14 所示。

清单 4-14setMaxListeners()方法的语法

emitter.setMaxListeners(n)

从事件发射器继承

到目前为止,所有的例子都明确涉及到了对EventEmitter实例的管理。或者,您可以创建从EventEmitter继承的定制对象,并包含额外的特定于应用的逻辑。清单 4-15 显示了这是如何完成的。第一行导入熟悉的EventEmitter构造函数。第二行导入util核心模块。顾名思义,util提供了许多有用的实用函数。本例中特别有趣的inherits()方法有两个参数,都是构造函数。使第一个构造函数继承第二个构造函数的原型方法。在这个例子中,自定义的User构造函数继承自EventEmitter。在User构造函数内部,调用了EventEmitter构造函数。此外,定义了一个方法addUser(),它发出userAdded事件。

清单 4-15 。创建一个扩展EventEmitter的对象

var EventEmitter = require("events").EventEmitter;
var util = require("util");

function UserEventEmitter() {
  EventEmitter.call(this);

  this.addUser = function(username, password) {
    // add the user
    // then emit an event
    this.emit("userAdded", username, password);
  };
};

util.inherits(UserEventEmitter, EventEmitter);

image 注意 JavaScript 采用了一种被称为原型继承的继承类型,它不同于传统继承 Java 等语言中使用的那种继承。在原型继承中,没有类。相反,对象充当其他对象的原型。

清单 4-16 展示了如何使用定制的User事件发射器。出于这个例子的目的,假设在同一个文件中定义了User构造函数——尽管理论上它可以在其他地方定义并使用require()函数导入。在这个例子中,一个新的User被实例化。接下来,添加一个userAdded事件监听器。然后调用addUser()方法来模拟新用户的创建。由于addUser()发出一个userAdded事件,事件处理程序被调用。另外,请注意示例最后一行的 print 语句。该语句检查user变量是否是EventEmitter的实例。由于UserEventEmitter继承而来,这将计算为true

清单 4-16 。使用自定义事件发射器

var user = new UserEventEmitter();
var username = "colin";
var password = "password";

user.on("userAdded", function(username, password) {
  console.log("Added user " + username);
});

user.addUser(username, password)
console.log(user instanceof EventEmitter);

使用事件来避免回调地狱

第 3 章探讨了许多避免回调地狱的方法,其中之一就是使用async模块。事件发射器提供了另一种避免死亡金字塔的好方法。举个例子,让我们用清单 4-17 来重温一下清单 3-5 中的文件阅读器应用。

清单 4-17 。一个带有回调地狱的文件阅读器程序开始悄悄进入

var fs = require("fs");
var fileName = "foo.txt";

fs.exists(fileName, function(exists) {
 if (exists) {
   fs.stat(fileName, function(error, stats) {
     if (error) {
       throw error;
     }

     if (stats.isFile()) {
       fs.readFile(fileName, "utf8", function(error, data) {
         if (error) {
           throw error;
         }

         console.log(data);
       });
     }
   });
 }
});

清单 4-18 展示了如何使用事件发射器重写文件阅读器应用。在本例中,创建了一个封装了所有文件读取功能的FileReader对象。需要EventEmitter构造函数和util模块来设置事件发射器继承。此外,需要使用fs模块来访问文件系统。

FileReader构造函数中,你会注意到的第一件事是this是私有_self变量的别名。这样做是为了在异步文件系统回调函数中维护对FileReader对象的引用。在这些回调函数中,this变量并不指向FileReader。这意味着在这些回调中不能通过关键字this访问emit()方法。

除了_self变量,代码相当简单。exists()方法用于检查文件是否存在。如果是,就会发出一个stats事件。然后触发stats监听器,调用stat()方法。如果该文件是一个正常文件,并且没有错误发生,则发出一个read事件。read事件触发了read监听器,它试图读取并打印文件的内容。

清单 4-18 。使用事件发射器重构文件阅读器应用

var EventEmitter = require("events").EventEmitter;
var util = require("util");
var fs = require("fs");

function FileReader(fileName) {
  var _self = this;

  EventEmitter.call(_self);

  _self.on("stats", function() {
    fs.stat(fileName, function(error, stats) {
      if (!error && stats.isFile()) {
        _self.emit("read");
      }
    });
  });

  _self.on("read", function() {
    fs.readFile(fileName, "utf8", function(error, data) {
      if (!error && data) {
        console.log(data);
      }
    });
  });

  fs.exists(fileName, function(exists) {
    if (exists) {
      _self.emit("stats");
    }
  });
};

util.inherits(FileReader, EventEmitter);

var reader = new FileReader("foo.txt");

定时器和时间安排

由于所有熟悉的用于处理定时器和时间间隔的 JavaScript 函数都可以在 Node 中作为全局变量使用,所以您不需要使用require()来导入它们。setTimeout()函数用于调度一个一次性回调函数在未来某个时间执行。setTimeout()的参数是要执行的回调函数、执行前等待的时间(以毫秒为单位),以及传递给回调函数的零个或多个参数。清单 4-19 显示了如何使用setTimeout()来安排一个回调函数在一秒钟的延迟后执行。在这个例子中,回调函数接受两个参数,foobar,它们由setTimeout()的最后两个参数填充。

image 注意记住 JavaScript 时间(实际上是一般的计算机时间)并不是 100%准确的,所以回调函数很可能不在指定的时间执行。因为 JavaScript 是单线程的,所以长时间运行的任务会完全影响时间。

清单 4-19 。创建一个延迟一秒后执行的计时器

setTimeout(function(foo, bar) {
  console.log(foo + " " + bar);
}, 1000, "foo", "bar");

The setTimeout()函数还返回一个超时标识符,可以用来在回调函数执行前取消定时器。通过将超时标识符传递给clearTimeout()函数来取消定时器。清单 4-20 显示了一个定时器在执行前被取消。在本例中,定时器在创建后立即被取消。然而,在实际应用中,定时器通常基于一些事件的发生而被取消。

清单 4-20 。使用clearTimeout()功能取消定时器

var timeoutId = setTimeout(function() {
  console.log("In timeout function");
}, 1000);

clearTimeout(timeoutId);

间隔时间

本质上,时间间隔是一个周期性重复的计时器。创建和取消间隔的功能分别是setInterval()clearInterval()。像setTimeout()一样,setInterval()接受一个回调函数、delay和可选的回调参数。它还返回一个可以传递给clearInterval()的间隔标识符,以便取消间隔。清单 4-21 展示了如何使用setInterval()clearInterval()创建和取消间隔。

清单 4-21 。创建和取消间隔的示例

var intervalId = setInterval(function() {
  console.log("In interval function");
}, 1000);

clearInterval(intervalId);

ref()unref()方法

事件循环中剩下的唯一一项计时器或时间间隔将阻止程序终止。然而,这种行为可以通过使用定时器或间隔标识符的ref()unref()方法以编程方式改变。调用unref()方法允许程序在定时器/间隔是事件循环中唯一剩下的项目时退出。例如,在清单 4-22 的中,interval 是在调用setInterval()之后的事件循环中唯一安排的项目。然而,因为unref()在这个区间被调用,程序终止了。

清单 4-22 。不使程序保持活动状态的时间间隔示例

var intervalId = setInterval(function() {
  console.log("In interval function");
}, 1000);

intervalId.unref();

如果已经在计时器或时间间隔上调用了unref(),但是您希望恢复到默认行为,那么可以调用ref()方法。ref()的用法如清单 4-23 所示。

清单 4-23ref()方法的使用

timer.ref()

即时

Immediates 用于安排回调函数立即执行。这允许在当前执行的函数之后调度函数。使用setImmediate()函数创建即时消息,该函数将回调和可选的回调参数作为其参数。与setTimeout()setInterval()不同,setImmediate()不接受delay参数,因为延迟被假定为零。也可以使用clearImmediate()功能取消即时消息。清单 4-24 显示了一个创建和取消即时消息的例子。

清单 4-24 。创建和取消即时消息的示例

var immediateId = setImmediate(function() {
  console.log("In immediate function");
});

clearImmediate(immediateId);

拆分长时间运行的任务

任何熟悉浏览器中 JavaScript 开发的人无疑都遇到过这样的情况:一段长时间运行的代码使用户界面没有响应。这种行为是 JavaScript 单线程特性的产物。例如,清单 4-25 中的函数包含一个长时间运行的循环,模拟计算密集型代码,即使循环体为空,也会导致应用的响应时间明显滞后。

清单 4-25 。合成计算密集型函数

function compute() {
 for (var i = 0; i < 1000000000; i++) {
   // perform some computation
 }
}

compute();
console.log("Finished compute()");

在浏览器世界中,这个问题的一个常见解决方案是使用setTimeout().将计算量大的代码分割成更小的块。同样的技术也适用于 Node,但是,首选的解决方案是setImmediate()。清单 4-26 展示了如何使用setImmediate()将计算密集型代码分解成更小的部分。在本例中,每次调用compute()时都会处理一次迭代。这个过程允许其他代码运行,同时仍然向事件循环添加compute()的迭代。但是,请注意,执行速度将明显慢于原始代码,因为每个函数调用只处理一次循环迭代。通过对每个函数调用执行更多的工作,可以更好地平衡性能和响应。例如,setImmediate()可以在每 10,000 次迭代后被调用。最佳方法将取决于您的应用的需求。

清单 4-26 。使用setImmediate()分解计算密集型代码

var i = 0;

function compute() {
 if (i < 1000000000) {
   // perform some computation
   i++;
   setImmediate(compute);
 }
}

compute();
console.log("compute() still working…");

使用process.nextTick()进行调度

Node 的process对象包含一个名为nextTick()的方法,该方法提供了一种类似于 immediate 的高效调度机制。nextTick() 将回调函数作为其唯一的参数,并在事件循环的下一次迭代中调用回调函数,称为 tick 。因为回调函数被安排在下一个时钟周期,nextTick()不需要delay参数。根据官方的 Node 文档,nextTick()也比类似的调用setTimeout(fn, 0)更有效,因此更受青睐。清单 4-27 显示了一个使用nextTick()的函数调度的例子。

清单 4-27 。使用process.nextTick()安排功能

process.nextTick(function() {
  console.log("Executing tick n+1");
});

console.log("Executing nth tick");

image 注意在 Node 的旧版本中,process.nextTick()是分解计算密集型代码的首选工具。然而,现在不鼓励递归调用nextTick();应当用setImmediate()来代替。

不幸的是,没有办法将参数传递给回调函数。幸运的是,这个限制可以通过创建一个绑定任何所需参数的函数来轻松克服。例如,清单 4-28 中的代码不会像预期的那样工作,因为没有办法将参数传递给回调函数。然而,清单 4-29 中的代码将会工作,因为函数的参数在传递给nextTick()之前是绑定的。

清单 4-28 。向process.nextTick()传递参数的错误尝试

process.nextTick(function(f, b) {
  console.log(f + " " + b);
});
// prints "undefined undefined"

清单 4-29 。将带有绑定参数的函数传递给process.nextTick()

function getFunction(f, b) {
  return function myNextTick() {
    console.log(f + " " + b);
  };
}

process.nextTick(getFunction("foo", "bar"));
// prints "foo bar"

实现异步回调函数

process.nextTick()通常用于创建接受异步回调函数作为最终参数的函数。如果不使用nextTick(),回调函数就不是真正的异步,它的行为就像普通(同步)函数调用一样。同步回调函数会阻止事件循环中的其他任务执行,从而导致资源匮乏。如果使用您的代码的人期望异步行为,那么它们也会给他们带来困惑。

清单 4-30 展示了一个简单的函数,将两个数相加,然后将它们的和传递给一个回调函数。Node 的调用约定规定回调函数应该异步执行。因此,人们会期望代码打印出The sum is:,后跟实际的总和 5。但是,回调函数不是使用nextTick()异步调用的。因此,总和实际上是先打印后打印,如清单 4-31 所示。为了避免混淆,这个函数命名为addSync()可能更合适。

清单 4-30 。一个同步回调函数的例子

function add(x, y, cb) {
  cb(x + y);
}

add(2, 3, console.log);
console.log("The sum is:");

清单 4-31 。运行清单 4-30 中代码的输出

$ node sync-callback.js
5
The sum is:

幸运的是,将同步回调函数转换成异步回调函数相当简单,如清单 4-32 所示。在这个例子中,回调函数被传递给nextTick()。另外,请注意,将回调函数包装在匿名函数中允许xy的值通过nextTick()传递。这些简单的更改会导致程序按照最初的预期运行。清单 4-33 显示了正确的输出结果。

清单 4-32 。使用process.nextTick()的适当异步回调函数

function add(x, y, cb) {
  process.nextTick(function() {
    cb(x + y);
  });
}

add(2, 3, console.log);
console.log("The sum is:");

清单 4-33 。运行清单 4-32 中的异步代码的输出

$ node async-callback.js
The sum is:
5

保持一致的行为

任何非平凡函数都可能有多个控制流路径。重要的是,所有这些路径都是一致异步或一致同步的。换句话说,函数不应该对一组输入异步运行,而应该对另一组输入同步运行。此外,您必须确保回调函数只被调用一次。这是一个常见的问题来源,因为许多开发人员认为调用回调函数会导致当前函数返回。实际上,一旦回调函数返回,函数就会继续执行。解决这个问题的一个非常简单的方法是每次调用nextTick()时返回。

考虑清单 4-34 中的函数,它决定一个数是否为负。如果n参数小于 0,则将true传递给回调函数。否则,false就通过了。不幸的是,这个例子有两个主要问题。第一个是true回调是异步的,而false回调是同步的。第二种是当n为负时,回调函数执行两次,一次在isNegative()结束时,第二次在执行nextTick()回调时。

清单 4-34 。回调函数的不一致实现

function isNegative(n, cb) {
  if (n < 0) {
    process.nextTick(function() {
      cb(true);
    });
  }

  cb(false);
}

清单 4-35 显示了同一个函数的正确实现(注意回调函数的两次调用现在是异步的)。此外,对nextTick()的两次调用都会导致isNegative()返回,确保回调函数只能被调用一次。

清单 4-35 。清单 4-34 中回调函数的一致实现

function isNegative(n, cb) {
  if (n < 0) {
    return process.nextTick(function() {
      cb(true);
    });
  }

  return process.nextTick(function() {
    cb(false);
  });
}

当然,这是一个人为的例子。代码可以大大简化,如清单 4-36 所示。

清单 4-36 。清单 4-35 中代码的简化版本

function isNegative(n, cb) {
  process.nextTick(function() {
    cb(n < 0);
  });
}

摘要

本章探讨了 Node.js 世界中的事件、计时器和调度控制。这一章和前一章一起,应该给你一个坚实的 Node 基础的掌握。以这种理解为基础,本书的其余部分将重点探讨各种 Node API,并使用它们创建令人兴奋的应用。下一章将向您展示如何创建命令行界面——这是构建真实 Node 应用的第一步。