写在前面
这几天太懒了, 过年前还有点时间, 就想自己学点东西. 写这个纯粹是为了督促自己.
大学生活就整下这最后半年了, 现在想想, 当时觉得学了很多东西, 但是现在觉得也没学到什么东西. 不得不承认: 一些难的东西, 数据结构啊, 编译原理啊, 机组啊, 操作系统啊, 并不常用, 忘得太快了. 而语言方面用的又太少了, 剩下的只有大体内容, 各种边边角角的, 也说不定什么时候忘记了. 始终觉得自己动手能力不够. 最后这半年, 就尽力做点东西吧.
首先总结下自己. 语言上, 软件工程学的那些都会点, 说出来会的语言挺多, 但是觉得都是会用而已的程度. 看过一些设计模式, 但是仅仅能理解... 也许能拿出来说的, 只有喜欢写点东西而已了吧.
Node.js
忘记在什么时候接触了, 应该挺早的, 因为当时还没有中文资料, 好像半年多之后才出了第一本中文Node教程, 好像是<Node.js开发指南>. 当时就觉得Bootstrap + Node.js + MongoDB一套很好, 因为一种语言打通前后端. 但是可恨自己当时没耐下心, 出去打工补贴生活费了. 后来提出了"全栈", MEAN什么的, 觉得当时如果没有中断学习, 大概现在也能自己做出来点什么了.
虽然说, 有视频教程, 也有中文的书籍了, 但是我还是觉得, 看官方文档也许能更快地有个大致地理解. 如果向深入一点, 看看相关的"动物书", 一般是去Salttiger, 或者Bookdl上找. 穷学生, 当然是能省则省了.
这里不得不吐槽一下, 终于有一个稳定版本了!
重新思考了一下, 觉得对于Node.js来说, 有几个比较难理解的概念, 然后熟悉下标准库, 就可以去npm找框架去开发了.
异步
这个其实主要体现在IO上, 例如读取文件, 查看其中有多少the
单词. 一般情况下是你的程序先打开文件, 在这里, 操作系统会对文件系统进行操作, 文件会被部分缓冲到内存中, 创建文件描述符等等, 等一切准备妥当之后, 再交给用户进行操作, 操作完了以后, 你的程序关闭文件, 这个时候, 操作系统会对资源进行释放等等操作后, 你的程序再推出. 那么问题就是你的程序始终是在等操作系统完成后再进行自己的任务.
你的程序 |-打开文件-| |数the的个数, 关闭文件|
v ^ v
系统 |-打开文件-| |关闭文件|
于是有人想, 我可不可以利用程序等待系统完成操作的这部分时间做点别的呢? 于是流程变成了你的程序告诉系统, 我要读取文件, 读取完毕了就数里面有多少个the
, 我先干点别的. 当系统读取完文件之后就会查看你告诉我做什么, 直接去做. 在数the
的个数之后, 再告诉系统, 关闭文件, 这个时候你又可以干点别的了
你的程序 |-打开文件, 巴拉巴拉-||数the的个数, 关闭文件, 巴拉巴拉|
v v
系统 |-打开文件-| |关闭文件|
处理明显利用了CPU以外, IO操作被击中起来, 也利于系统对IO请求的优化.
回调
回调就像菜谱一样, 厨艺丰富的人把如何做菜都告诉你了, 你只要按照上面做就差不多了, 先放油, 油温起来了就放入打好的鸡蛋, 等鸡蛋饼熟了, 两面略有硬度就可以盛盘了(忽略个人厨艺影响).
事件 | 该干什么 |
---|---|
油温升起来了 | 倒入打好的鸡蛋 |
鸡蛋饼熟了, 两面略有硬度 | 盛盘 |
Node.js主要是依赖V8和libuv. 由于libuv是异步事件驱动的, 所以Node很自然, 也是异步事件驱动的. 这个感觉和Win32的回调函数一样, 传入函数指针, 这样, 你的代码在一些情况下就会被调用, 不必担心里面到底是什么情况. V8更不用说了, 有JIT, 性能很是厉害.
之所以是回调, 我猜是因为普遍情况下, 都是外层程序调用内层程序提供的API接口, 外部程序是调用者, 内部程序是被调用者, 而这些回调函数的调用, 正好反过来了.
EventEmitter
大部分Node对象都是EventEmitter
, EventEmitter
就是可以触发事件的对象. 你可以在EventEmitter
上设置监听函数, 当某个事件触发时, 你的监听函数就会被调用. 用代码解释的话就像下面这样.
'use strict'
class Emitter{
constructor(){
this._handlers = {};
}
on(event, callback){
this._handlers = this._handlers || {};
this._handlers[event] = this._handlers[event] || [];
if(this._handlers[event].indexOf(callback) < 0){
this._handlers[event].push(callback);
}
}
off(event, callback){
if(this._handlers && this._handlers[event] instanceof Array){
if(event && callback){
this._handlers[event].splice(this._handlers[event].indexOf(callback), 1);
}else if(event){
this._handlers[event] = [];
}else{
console.log('invalid argument');
}
}
}
emit(event){
if(this._handlers[event] instanceof Array){
this._handlers[event].forEach(function(handler){
handler();
});
}else{
console.log('unhandled event', event);
}
}
}
var person = new Emitter();
function hungry(){
console.log('I\'m hungry.');
}
function needFood(){
console.log('I need food.');
}
person.on('hungry', needFood);
person.on('hungry', hungry);
person.emit('hungry');
当然, 功能不止这些, 可以看EventEmitter 手册. 很多Node对象都是EventEmitter
, 例如process
, http.Server
等等. 每个类都有自己的事件, 接受的回调函数也会有不同的参数, 这个就得查手册了. 比如http.Server
就会有这么多事件:
- Event: 'checkContinue'
- Event: 'clientError'
- Event: 'close'
- Event: 'connect'
- Event: 'connection'
- Event: 'request'
- Event: 'upgrade'
Event Loop
Node.js是单线程的, 也是异步事件驱动的, 那么他的执行就是不断的循环. 每次循环都进行检查, 如果有需要处理的事件就处理, 没有就休眠, 腾出CPU, 如果所有的事件都处理完了, 那么就自动退出. 事件有很多, 比如IO操作, 定时器等等.
首先触发一个事件, 就是执行你的程序. 然后Node就会运行你的代码, 非阻塞的操作会作为请求, 发送出去, 然后不管请求的结果怎么样, 操作会怎么样, 直接执行你后面的代码. 执行完所有的阻塞的代码后, 结束当前的事件循环
在下一个事件循环中再次看是否有命令需要执行, 没有就退出. 因为你之前发出了操作的请求, 所以会有事件需要处理. 可是此时没有事件发生, 那么就让出CPU, 只到被唤醒.
当某个, 或者某些操作结束了, 就会触发事件, Node.js知道事件发生了, 就会利用非阻塞请求时提供的回调函数去处理. 所有的事件都会排队, 等待Node一一处理. Node.js在每次事件循环中只会处理一个事件, 因为Node.js是单线程的.
同样, 问题容易出现在单线程这里. 假设当前有很多事件需要处理, 那么如果你在当前事件循环中做了很耗时的操作, 那么下个事件就不能被及时处理.
process.nextTick(function(){
console.log('next event loop')
});
for(var i = 0; i < 1999999999; i++){
i++;i--;
}
console.log('current event loop');
process.nextTick
是用来将一个函数放到下一个事件循环中执行的. 这里会首先将一个函数放到下一个事件循环中执行, 然后进行一个耗时操作, 再输出一行字. 执行结果就是当前事件循环输出current event loop
后, 进入下一个事件循环周期, 输出next event loop
. 如果没有耗时操作, 两条输出信息会立刻出现. 但是这里有一个耗时操作, 使Node在第一个事件循环中停留很长事件, 才进入第二个事件循环, 没有完成当前事件循环的node是没法进入下一个事件循环的, 导致第二个事件循环中的输出语句没有立即显示出来.
这个和多线程不一样, 因为两个线程执行两个操作, 在一个线程上阻塞了, 另一个线程不受影响.
因为模型的优势, 在占用更少资源的情况下, Node.js能处理同样多的网络请求数量, 甚至更多. 至于单线程无法利用多核CPU的问题, 可以启用多个Node实例来达成多线程, 而每个Node.js实例是独立的, 其中一个崩溃不会影响其他的Node.js, 比多线程更加稳定. 这个也和Chrome浏览器的多进程设计不谋而合. Node.js本身的优劣势已经很清晰了, 性能好, 但是不怎么适合写复杂逻辑, JavaScript语法有点坑, 但是适合前端的人来写逻辑等等.
官方例子
const http = require('http');
const hostname = '127.0.0.1';
const port = 1337;
http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World\n');
}).listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
这个是官方说明里的代码, 用来阐述Node.js写Web的样子. 先不管里面的ES6. 总体的意思是加载http
模块[1], 然后利用http
模块创建一个server
对象并监听在hostname:ip
上[2]. 每次接到HTTP请求的时候, 就会自动调用createServer
接受的函数.
在看Node.js的时候, 总感觉和写C语言的程序一样, 设置一个个函数指针作为回调函数. 这个不免有几个问题, 比如层层回调, 套嵌的代码越来越多[3]; 改变了上下文, 让this指向不明确[4]; ES5里, 循环体内引用循环变量的问题[5]. 但是解决方案也有很多, 所以需要看的东西就更多了, 这就导致, 学习曲线不是很陡峭, 但是需要学的东西非常多.
-
const http = require('http');
↩ -
http.createServer
会创建http.Server
对象, 然后直接调用该对象的listen
方法 ↩ -
回调金字塔, 参考SegmentFault ↩
-
在回调函数中, 函数运行的上下文在全局上, 而不在某个对象或者new的上下文中, 参考SegmentFault ↩
-
由于异步, 循环会先结束, 此时循环体内引用的循环变量是循环结束的值. ↩