提前了解一下 Node 的 API 文档,学习一下里面的方法是干什么用的,可以更好的理解书中举例的一些方法,以防看到某个案例方法懵逼呦。好的,我们继续。
异步I/O
现代的 Web 应用已经不再是单台服务器就能胜任了,在跨网络结构下,并发已经是现代编程的标配了,所以异步 I/O 在 Node 里非常重要。
Node 完成整个异步 I/O 环节包括:
- 事件循环
- 观察者
- 请求对象
事件循环
Node 的自身执行模型就是事件循环。
在进程启动时,Node 会创建一个类似 while(true)的循环,每执行一次循环循环体的过程我们称为 Tick。每个 Tick 的过程就是查看是否有事件待处理,如果有,就取出事件及相关的回调函数。如果存在关联的回调函数,就执行它们。然后进入下一个循环,如果不再有事件处理,就退出进程。
观察者
在每个 Tick 的过程中,判断是否有事件需要处理的角色就称为观察者。
书里举了一个很形象的例子:事件循环的过程就如同饭馆的厨房,厨房一轮一轮的制作菜肴,但是要具体制作哪些菜肴取决于收银台收到的客人的下单。厨房每做完一轮菜,就去吻收银台的小妹,接下来还有没有要做的菜,如果没有的话,就下班打烊了。
这个过程中,收银台的小妹就是观察者,他收到的客人点单就是关联的回调函数。当然,如果饭馆经营有方,它可能有多个收银员,就如同事件循环中有多个观察者一样。收到下单就是一个事件,一个观察者里可能有多个事件。
请求对象
这一节主要说的是从 JavaScript 代码到系统内核之间都发生了什么。
对于 Node 中的异步 I/O 调用而言,回调函数不由开发者调用。从 JavaScript 发起调用到内核执行完 I/O 操作的过渡过程中,存在一种中间产物,它就是请求对象。
以 fs.open()
方法作为例子,探索 Node 与底层之间是如何执行异步回调以及回调函数究竟如何被调用的:
fs.open = function(path,flags,mode,callback){
// ...
binding.open(pathModule._makeLong(path),stringToFlags(flags),mode,callback);
};
说实话,这里函数里面的代码并不是很明白,书中说是 JavaScript 层面的代码通过调用 C++ 核心模块进行下层操作。可能是里面的代码是内建模块编译出来的,js 调用核心模块。
JavaScript 调用 Node 的核心模块,核心模块调用 C++ 内建模块,内建模块进行系统调用,这是 Node 里的经典调用。
从上图可以看出fs.open()
方法,其实是调用底层的uv_fs_open()
方法,在调用这个方法的过程中,创建了一个请求对象,从 JavaScript 层面传入的参数和当前方法都被封装在这个请求对象中,对象包装完毕后,在 Windows 下,会将这个请求对象推入线程池(后边会有解释线程池)中等待执行。
将请求对象推入线程池后,由 JavaScript 层面发起的异步调用的第一阶段就结束了。JavaScript 线程就可以继续执行后边的 JavaScript 操作了。当前的 I/O 操作在线程池中等待执行,就此达到异步的目的。
执行回调
组装好请求对象,送入 I/O 线程池等待执行,实际上完成了异步 I/O 的第一部分,回调通知是第二部分。
线程池中的 I/O 操作调用完毕之后,会将结果存储到 result 属性上,然后告知当前对象操作已完成,并将线程归还线程池。
在这个过程中,其实还动用了事件循环的 I/O 观察者。在每次 Tick 的执行中,都会调用相关的方法检查线程池中是否还有执行完的的请求,有就将请求对象加入到 I/O 观察者的队列中,然后将其当做事件处理。
I/O 观察者回调函数的行为就是取出请求对象的 result 属性作为参数,取出里面的方法执行,以此达到调用 JavaScript 中传入的回调函数的目的。
从前面的异步 I/O 过程中,可以提取出异步 I/O 的几个关键词:单线程、事件循环、观察者、I/O 线程池。
注意!这里的单线程和 I/O 线程池似乎是冲突的。其实:在 Node 中,除了 JavaScript 是单线程外,Node 自身是多线程的,只是 I/O 线程使用 CPU 较少。
另一个需要重视的观点是:除了用户代码无法并行执行外,所有的 I/O (磁盘 I/O 和网络 I/O 等)则是可以并行起来的。
事件驱动与高性能服务器
其实如果看懂了异步的实现原理,事件驱动这个概念,也应该理解的差不多了,即通过主循环加事件触发的方式来运行程序。
上面是利用读取文件方法来解释异步 I/O,其实异步 I/O 不仅仅应用在文件操作中。在网络请求层(Node 接收到网络,作为服务器),侦听到的请求都会形成事件交给 I/O 观察者。事件循环会不停地处理这些网络 I/O 事件。如果 JavaScript 有传入回调函数,这些事件将会最终传递到业务逻辑层进行处理。利用 Node 构建 Web 服务器,正是在这样的一个基础上实现的。
几种经典的服务器模型,对比它们的优缺点:
- 同步式 (一次只能处理一个请求,其余请求处于等待状态)
- 每进程/每请求(为每个请求启动一个进程,这样可以处理多个请求,但不具备扩展性,因为系统资源有限)
- 每线程/每请求(为每个请求启动一个线程来处理。线程占内存,大并发时内存不足,服务器变缓慢)
Node 通过事件驱动方式处理请求,无需为每个请求创建额外线程,省掉创建和销毁线程的开销,同时系统调度任务时因为线程少,上下文切换代价也低。即使在大量并发时,也不受线程上下文切换开销的影响,这是 Node 高性能的一个原因。
总结
1、异步 I/O 的关键词:单线程、事件循环、观察者、I/O 线程池。
2、在 Node 中,除了 JavaScript 是单线程外,Node 自身是多线程的,只是 I/O 线程使用 CPU 较少。
3、事件循环是异步实现的核心。
异步编程
有异步 I/O ,必有异步编程。
这一章主要讲解的是高级函数的用法,异步编程的优势和难点,异步编程的解决方案和方案对应的原理,异步并发控制的解决方案及原理。我没有全部搞明白,只学习了一下常见的方法原理,精力有限。也可能是功力不够,研究不动了 [允悲] 。有能力的兄台可以自行查阅资料进行研究,也希望搞明白后可以指导指导。
函数式编程
熟悉 JavaScript 的前端开发者,肯定了解里面的高阶函数,说白了就是讲函数作为参数,或者返回值等操作。例如:
function fn(x){
return function(){
return x;
}
}
这种函数用法相信大部分前端工程师都有使用过的。
偏函数用法
偏函数用法是指:创建一个调用一个部分参数或变量已经预置好的函数的函数用法。
我听着也很拗口,意思就是:创建一个函数 A,这个函数 A 是用来调用另外一个函数 B 的,函数 B 的部分参数或变量是你定义好的,这种函数 A 就叫偏函数(希望你听懂了,哈哈)。看例子:
var toString = Object.prototype.toString;
var isString = function(obj){ // 判断对象是否为字符串
return toString.call(obj) == '[object String]';
};
var isFunction = function(obj){ // 判断对象是否为函数
return toString.call(obj) == '[object Function]';
};
但是这种函数有一个问题,你想判断几种对象,就要写几个判断的函数,为了解决这个问题:
var isType = function(type){
return function(obj){
return toString.call(obj) == '[object '+type+']';
};
};
这种写法就把你想判断的类型写活了。你想判断什么类型就传什么类型的 type ,这种形式就是偏函数。
异步编程的优势与难点
优势:
Node 带来的最大特性莫过于基于事件驱动的非阻塞 I/O 模型,这也是它的灵魂所在。带来的好处也是性能上的优势,让资源得到更好的利用。对于网络应用而言,也备受青睐。
可以看出两种模式在性能上的区别。
异步编程的难点主要有一下几点:
- 异常处理
- 函数嵌套过深
- 阻塞代码
- 多线程编程
- 异步转同步
异步编程难点解决方案
针对上面的几个难点,Node 也有专门的方案解决:
- 事件发布 / 订阅模式(注册 / 触发)
- Promise / Deferred 模式
- 流程控制库
事件发布 / 订阅模式:
这里讲解的是 Node 的 events 模块和一些相关的 API 方法的使用和原理,比如:addListener/on()
(注册方法),once()
(注册方法,只执行一次),removeListener()
(移除方法注册),removeAllListeners()
(移除所有注册方法),emit()
(触发方法)。例如:
var events = require('events');
var emitter = new events.EventEmitter(); // 初始化
// 订阅
emitter.on("event1",function(message){
console.log(message);
});
// 发布
emitter.emit("event1","This is message!");
Promise / Deferred 模式:
使用事件的方式时,执行流程需要被预先设定。即便是分支,也需要预先设定,这是由发布 / 订阅模式的运行机制所决定的。
这句话的意思是,你的异步函数里的选项必须齐全,不然就执行不了。例如:
$.get('/url',{
success: onSuccess,
error: onError,
complete: onComplete
});
// 这个异步ajax,你不写success项或error项就不行
Promise / Deferred 模式是一种先执行异步调用,延迟传递处理方式的模式。例如:
$.get('/url')
.success(onSuccess)
.error(onError)
.complete(onComplete)
// 这种方式即使不调用success()等方法,ajax也会执行。
流程控制库
这里没看太明白,记得后期补一补,只是知道各种类库各显神通。
事件发布 / 订阅模式相对算是一种较为原始的方式,Promise / Deferred 模式贡献了一个非常不错的异步任务模型的抽象。流程控制库方案与Promise / Deferred 模式不同,后者的重头在于封装异步的调用部分,前者将重点放在回调函数的注入上。
总结
异步编程是 Node 里比较难的一部分,就是在 JavaScript 中,高阶函数也是个难点。
其实是因为人的线性思维惯性,对异步编程这种思维方式不太习惯,所以比较难学,但是俗话说:世上无难事只怕有心人呐,相信经过大量练习和学习,这点是不难攻克的。
未完待续。。。。。。
文章只是本人学习 Node 过程中,按自己的理解总结的一些笔记,若有错误之处,欢迎各位及时指出,一起探讨更好的答案。
公众号:前端很忙
做一个喜欢分享的前端开发者!
获取更多干货分享,欢迎来搞!