[译]你不知道的NodeJS

你不知道的NodeJS

更新:这篇文章如今是个人书《Node.js进阶》的一部分。 在jscomplete.com/node-beyond…中阅读此内容的更新版本以及有关Node.js的更多信息。node

在今年的Forward.js会议(关于JavaScript的会议)上,我分享了题为“你不知道的NodeJS”的演讲。 在那次演讲中,我向观众提出了一系列有关Nodejs运行时的问题,大多数有技术背景的观众没法回答其中大多数问题。git

我没有真正去统计这个数据但确实能在会议室里感受到。演讲后一些有勇气的人走近我而且认可了这个事实。github

这就是让我发表演讲的缘由。 我认为咱们没有以正确的方式教授Node.js! 关于Node.js的大多数学习内容都聚焦于Node包上,而不是它的运行时上。 大多数Node包将模块封装在自身的Node运行时中(例如http或stream)。 当你遇到问题时,这些问题多是在自身运行时发生,而且若是你不了解Node运行时,就会遇到麻烦。浏览器

关于Node.js的大多数学习内容都聚焦于Node包上,而不是它的运行时上。

我为这篇文章精选一些问题和回答。列以下的标题中,能够尝试先闹中回答它们。(若是你在这里发现了错误或者有歧义的回答,请让我知道)bash

问题 #1: 什么是调用堆栈?它是V8的一部分吗?

调用确定是V8的一部分。它是V8用于保存函数调用轨迹的一种数据结构。每次咱们运行一个函数,V8都会将该函数的引用放入调用堆栈,并对该函数中嵌套的其余函数进行相同的操做。这也包括递归调用的函数。服务器

当嵌套的函数运行结束,V8将一次弹出一个函数并用它的返回值替换它的位置。

为何这对于Node很重要? 由于每一个Node进程仅得到一个调用堆栈。 若是使该调用堆栈处于繁忙状态,则整个Node进程都处于繁忙状态。记住这一点。数据结构

问题 #2: 什么是事件轮询? 它是V8的一部分吗?

你认为下图中的事件轮询在哪里?架构

事件轮询由 libuv模块提供,它不是V8的一部分。

事件轮询是处理外部事件并将它们转换回调函数运行的一种机制。这种轮询会循环的从事件队列中选择事件执行,并将它们的回调函数推入调用堆栈中。异步

若是这是你第一次听到事件循环,则这些定义不会有太大帮助。 事件循环只是更大架构下中的一部分:函数

你须要理解事件轮询背后更大的架构、V8所扮演的角色、 Node.js的APIs以及知道这些事情是如何推入队列并被V8执行的。

Node.jsAPIs是像setTimeoutfs.readFile这样的函数。这些并非JavaScript的一部分,而由Node.js提供的函数。

事件循环位于这张照片的中间(其实是它的一个更复杂的版本),而且像一个组织者。 当V8调用堆栈为空时,事件循环能够决定下一步执行什么。

问题 #3: 当调用堆栈和事件轮询队列所有为空时,Node.js会作什么?

它简单的退出.

当你启动一个Node.js进程时,Node将自动启动事件轮询。当事件轮询处于空闲状态而且无其余事件去处理时,程序将退出。

To keep a Node process running, you need to place something somewhere in event queues. For example, when you start a timer or an HTTP server you are basically telling the event loop to keep running and checking on these events. 为了保持Node进程运行,你须要向事件队列中放入一些内容。好比,当你能够启动一个定时器或者一个 HTTP服务时,你就至关于告诉事件轮询保持运行,同时去监听一些事件。

问题 #4: 除了V8和Libuv,Node还具备其余哪些外部依赖项?

如下是一个Node进程全部可使用的独立库:

  • http-parser
  • c-ares
  • OpenSSL
  • zlib 它们全部都独立于Node,它们都有拥有本身独立的源码以及证书。Node仅仅是使用它们。因此你须要记住这些,若是你想知道你的程序运行在什么地方。若是你正在处理数据压缩相关的事情,你可能会在zlib库底层堆栈遇到遇到麻烦,那么你将面对一个zilb相关的错误,而不是归责于Node。

问题 #5: Node可否不依赖于V8运行?

这多是一个棘手的问题。 你确实须要一个VM来运行Node进程,可是V8并非惟一可使用的VM。 您可使用Chakra

问题 #6: module.exportsexports有什么不一样 ?

你能够一直使用module.exports去导出模块的API。除一种状况外,你也可使用export:

module.exports.g = ...  // Ok

exports.g = ...         // Ok

module.exports = ...    // Ok

exports = ...           // Not Ok
复制代码

为何?

export仅仅是module.export的一个别名或引用。 更改导出时,你将更改该引用,而再也不更改官方API(即module.exports)。 你只须要在模块做用域中获取局部变量便可。

问题 #7: 为何顶级变量不是全局变量?

若是你在模块module1中定义了一个顶级变量g:

// module1.js

var g = 42;
复制代码

同时你有一个模块module2引用了模块module1,而且尝试访问变量g,你将获得g is not defined.的错误。

为何?

若是你在浏览器端作相同的事情,你能够在该定义该顶级变量脚本以后的全部脚本里访问该顶级变量。 每一个Node文件在后台都有其本身的IIFE(函数调用表达式)。 在Node文件中声明的全部变量都做用于该IIFE。

相关问题: 下面这个仅仅含有一行代码的Node文件将会输出什么?

// script.js

console.log(arguments);
复制代码

你将会看一些参数!

为何?

由于Node执行的是一个函数。Node将你的代码包装到一个函数中。该函数中明肯定义了上图中你所见的5个参数。

问题 #8: 这些对象:exportrequiremodule都是全局可用的,然而在它们在每一个文件又有所不一样,为何?

当你须要使用require对象,你就是像一个全局变量那样直接使用它。然而,若是你在两个不一样的文件中检查require,你将看到两个不一样的对象。为何? 由于 因为具备相同的IIFE魔法:

如你所见, “这IIFE魔法”向你的代码中传递如下五个参数: exports, require, module, __filename, 和 __dirname。当你在Node中使用这5个参数时,它们看起来像全局变量,但实际上它们仅仅是函数参数。

问题 #9: 什么是Node中的循环依赖?

若是你定义一个模块module1引用了模块module2,同时模块module2内部又引用了模块module1。将发生什么?报错?

// module1
require('./module2');

// module2
require('./module1');
复制代码

你不会获得报错。由于Node容许那种状况。 因此模块module1引用模块module2,可是由于模块module2依赖模块module1且模块module1没有加载完成,模块module1将仅仅获取到模块module2的一个部分版本。程序将提示警告。

问题 #10: 何时适合使用文件系统的同步方法(如 readFileSync)?

Node模块fs中的每个方法都有一个同步版本。为何你会使用一个同步方法代替一个异步方法?

有时候,使用同步方法会更好。好比服务器仍在加载时,能够在任何初始化步骤中使用它。 一般状况下,初始化步骤以后执行的全部操做都取决于在那里获取的数据。 只要你使用同步方法是一次性的,就可使用同步方法来避免引入回调狱。

可是,若是您在处理程序(例如HTTP服务器请求回调)中使用同步方法,那简直就是100%错误。 不要那样作。

我但愿你可以回答以上部分或者所有的问题。

感谢阅读。