我认为大多数开发者都在以一种错误的方式学习node。大多数关于node的教程都在关注Node生态,而不是Node运行本身,它们教学的终点在于使用各种各样的包(例如Express和Socket.IO)能做到什么,而不是node本身能够做到什么。
这有很多优势。Node是原生和灵活的,它并不提供一个完整的解决方案,而只是提供了一个丰富的运行环境让你完成你自己的解决方案。像Express.js和Socket.io这样的库更多的是一种完整的解决方案,因此教这些库显得更有意义,因为你可以让学习者使用这些完整的解决方案。
一般人会认为只有写像Express.js和Socket.io这样的库的程序员才需要了解关于node环境的一切,但是我不这样认为。一个扎实的node本体环境的理解是你在使用完整解决方案之前的最佳选择,你至少应该有足够的知识和信心去通过它的`源码`对一个包进行评估,这样你才能够对它的使用做出足够明智的选择。
我先给你一些在看完这本书之后能够答上来的问题,以此检验一下你的Node知识水平,如果你能够回答出大多数的问题,那么这本书可能并不适合你。
-
Node和V8引擎的关系是什么?Node不借助V8能否运行?
-
为什么在任何Node文件中声明全局变量时,它并不是所有模块的全局变量?
-
当你导出一个Node模块的api时,为什么有时候用`exports`而有时候用的是`module.exports`?
-
什么是调用栈?这是V8引擎的一部分吗?
-
什么是事件循环?这是V8引擎的一部分吗?
-
`seetImmediate`和`process.nextTick`的区别是什么?
-
spawn
、`exec`和`fork`的主要区别是什么? -
集群(cluster)模块是如何工作的?他和负载均衡器的区别是什么?
-
当调用栈和事件轮询都为空的时候会发生什么?
-
V8中的对象和函数模板是什么?
-
libuv是什么?node是如何使用它的?
-
在一个node进程退出时怎么样进行最后一个操作?这个操作可以异步完成吗?
-
除了V8和libiv,node还有没有其他的外部依赖?
-
`uncaughtExecption`事件抛出说明进程出现了什么问题?它和`exit`事件有什么不同?
-
require函数的五个主要步骤是什么?
-
如何检测本地模块是否存在?
-
什么是Node模块的循环依赖,如何避免它?
-
require函数会自动尝试的三个文件扩展是什么?
-
在创建一个http服务器并为请求编写响应的时候,为什么需要end()函数?
-
使用文件系统的`*Sync`方法在什么时候可用?
-
如何只输出深度嵌套的对象的第一层?
-
为什么最上层的变量不是全局的?
-
对象的`exports`、`require`和`module`在每一个模块中都是全局可用的,但是为何在每一个模块中的表现都不同?
-
如果你用node运行只有一行`console.log(argument)`代码的JavaScript文件的,node会输出什么?
-
一个模块如何被其他模块和node命令同时获取到?
-
node的内置流中有没有同时可读与可写的?
-
当`cluster.fork()`语句在node脚本中被调用时会发生什么?
-
使用事件发射器和使用简单的回调函数来允许异步执行代码的区别是什么?
-
可读流的*Paused*和*Flowing*模式有什么区别?
-
如何通过已连接的套接字读取数据?
-
`require`函数总是缓存它获取的模块,如果你需要调用引用的模块中的代码多次的话要做什么?
-
当使用流工作的时候,什么时候用pipe函数,而什么时候使用事件?这两个方法可以结合使用吗?
我会将其中的一些问题按原理分类。首先回答下列问题:
调用栈毫无疑问是V8的一部分,它是V8引擎用来跟踪函数调用的数据结构。每当我们调用一次函数,V8便会在调用栈中放置一个该函数的引用,对于每一个嵌套调用的其它函数也进行同样的操作,也包括递归调用自身的函数。
当函数的嵌套调用结束,V8就会*弹出*一个函数,然后使用该函数的返回值代替原来的位置
*为什么这对理解Node很重要? * 因为一个Node进程中只会有一个调用栈,如果你的调用栈很繁忙,那么整个Node进程也会变得繁忙。一定要牢记这一点。
事件轮询是由*libuv*库提供的,它并不是V8引擎的一部分。
事件轮询是一个处理外部事件并把它们转换为回调调用的实体。它是一个从事件队列中捕获事件、并把它们的回调函数放入调用栈的一个循环,它也是一个多阶段循环。
事件轮询是你需要理解的更大图景的一部分,为了理解事件轮询,你需要理解V8引擎发挥的作用,Node Api,并且理解代码是如何通过V8引擎排序好被调用的。
Node Api提供像`setTimeout`和`fs.readFile`之类的函数,他们并不是JavaScript本身的一部分,他们只是Node提供的函数。
事件循环处在V8引擎的调用栈和不同的阶段和回调队列中间,它表现得像一个组织者,当调用栈为空的时候,事件循环便可以决定接下来运行什么。
进程会直接退出。
当你运行一个Node程序,Node会自动开始事件循环;当事件循环空闲并未没有其他的任务可做,进程便会退出。
为了使一个Node进程保持运行,你需要在事件队列中放置一些东西。例如,当你启动一个定时器或者一个HTTP服务器的时候,便是在告诉事件循环保持运行并关注发生的事件。
以下都是一个Node进程可能用到的独立库:
-
http-parser
-
c-ares
-
OpenSSL
-
zlib
他们都属于node外部的库,都有自己的源码,它们也有自己的协议,Node只是在使用它们而已。
你可能想要记住这些库因为你想知道你的项目运行在哪里。如果你在进行数据压缩,你可能会在zlib库的栈中遇到问题,那你便是在解决zlib的bug。不要把所有的问题都推给node。
如果你有一个定义了顶层变量`g`的模块`module1`。
var g = 42;
然后你又定义了引入`module1`的模块`module2`,尝试使用变量`g`,会发现报错说`g`未定义。
*为什么??*如果你在浏览器环境下进行同样的操作,你是可以在定义后在所包括的脚本中使用所有的顶层变量。
每一个Node文件都有它自己的*IIFE*(立即调用的函数表达式),在一个Node文件中定义的所有变量的作用域都是它对应的IIFE。
学习Node环境很有挑战性,以下是一些指导,希望可以帮到你。
node是一组能在VM引擎上编译JavaScript的库的集合,所以自然熟练掌握JavaScript是熟练掌握Node的前提,你应该先学习JavaScript。
你是否理解函数、作用域、绑定、`this`关键字、`new`关键字、闭包、类、模块模式、原型、回调和期约?你知道可以用在数值类型、字符串、数组、集合、对象和Map上的各种方法吗?熟悉上述的这些名词会让你学习Node api的时候更加轻松。举个例子,在你很好的理解回调函数之前就学习`fs`模块中的方法会让你感到莫名的困惑。
回调函数和期约(还有生成器和异步模式)对于Node来说尤为重要。你需要理解为什么异步操作在Node中是*一等公民*。
你可以把Node程序中代码非阻塞的特性比作你在星巴克中点咖啡(在店里而不是汽车餐厅)
-
下单 | 给Node一些需要执行的指令(例如一个函数)
-
编辑你的订单,比如不要生奶油 | 给函数一些参数:
({whippedCream: false})
-
把你的订单和名字告诉星巴克员工 | 在你函数回调告诉Node:
({whippedCream: false}, callback)
-
在旁边等,然后员工会去取排在你后面的人发的订单。| Node会运行你给的函数之后的代码。
-
当你的点单完成时,星巴克员工会叫你的名字然后把咖啡给你。 | 当你的函数运行完毕,Node会把结果给你,带着结果调用你给的回调函数`callback(result)`。
这个简化的图像中有一个调用栈和一些事件队列,而事件循环在中间组织二者的交流。Node的异步api把回调函数放在事件队列中,事件循环会把它们从队列中取出并放入调用栈中。
一个Node进程可以处于空闲状态,但其永远不回属于休眠状态。它会跟踪所有正在处理的回调函数,当所有的回调全部处理完毕,它会直接退出。想要让一个Node进程一直运行,你不妨试试使用`setInterval`函数,因为这会在事件循环中创建一个永久处于pending状态的回调。
他们都在全局变量中被声明(通常在浏览器环境中被称作window变量)。在一个node的交互式解释器(REPL)中,输入`global.(别忘了带点)然后敲击两次Tab键,就可以查看所有可用的全局变量了(当然,直接在空行中敲两下Tab键也可以)。其中的一些是JavaScript结构体(例如`Array`和`Object
);一些是Node库中的函数(比如`setTimeout`,或者控制台输出到`stdout`或者`stderr`);还有一些是你可以用在某些特定任务中的Node全局对象(例如,`process.env`可以也来读取主机环境的变量)
你需要理解你在列表中看到的大部分变量。
它们中的一些看起来很熟悉,例如*Timers*,因为它们在浏览器环境中也存在,而Node是对浏览器环境的模拟。然而还有更多的需要去学习,例如`fs`、path
、readline
、http
、net
、`stream`和`cluster`等。上面的那个自动完成清单包括了所有的这类内置库。
举例来说,你可以使用`fs`来读写文件;使用`http`来运行一个拥有流的网络服务器;也可以使用`net` 来运行一个有套接字的TCP服务器。现在的Node比之前更加强大,通过社区维护代码而变得更完美。在你寻找一个包完成任务时,首先确保你用Node的内置包无法完成任务。
`event`模块尤其重要,因为大多数的Node架构都是事件驱动的。
你可以构建一些简单的单进程构建块(节点),这些构建块可以用良好的网络协议进行组织,使它们相互通信并扩展以构建大型分布式程序。扩展Node应用程序并不是事后才想到的,它是直接内置在名称中的。
选择一个框架,例如Express,并且理解它的部分源码,对一些你不理解的地方进行提问。
最后,不用任何框架写一个基于Node的Web应用。试着处理尽可能多的情况:带有一个HTML文件的响应、解析查询字符串、通过表单输入、或者创建一个发送JSON响应的终止点。
也可以试试写一个聊天服务器、上传一个npm包、或者为一个开源的Node项目贡献代码。
这本书并不适合初学者。笔者假定读者熟悉JavaScript,并且具备基本的Node知识。确切地说,如果你不知道如何运行一个Node脚本、引入一个npm包、或者使用Node运行一个简单的web服务器,你可能不太适合这本书。我在Pluralsight上创建了一套课程,其中有一些是很好的Node新手教程。通过链接访问该网站的Node课程 pluralsight.com/paths/node-js.
笔者在本书中的所有样例都是在linux下运行的,对于Windows平台,你需要把笔者使用的命令行换成对应的Windows命令。
出于简洁,本书中的所有Node.js全都简称为Node。它的官方名称为Node.js,但是其也被经常称为Node。