提前了解一下 Node 的 API 文档, 学习一下里面的方法是干什么用的, 可以更好的理解书中举例的一些方法, 以防看到某个案例方法懵逼呦. 好的, 我们继续.
继续继续
异步 I/O
现代的 web 应用已经不再是单台服务器就能胜任了, 在跨网络结构下, 并发已经是现代编程的标配了, 所以异步 I/O 在 Node 里非常重要.
Node 完成整个异步 I/O 环节包括:
事件循环
观察者
请求对象
事件循环
Node 的自身执行模型就是事件循环.
在进程启动时, Node 会创建一个类似 while(true)的循环, 每执行一次循环循环体的过程我们称为 Tick. 每个 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 线程池.
注意! 这里的单线程和 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 构建 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 模型, 这也是它的灵魂所在. 带来的好处也是性能上的优势, 让资源得到更好的利用. 对于网络应用而言, 也备受青睐.
异步 I/O 调用示意图
传统同步 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 过程中, 按自己的理解总结的一些笔记, 若有错误之处, 欢迎各位及时指出, 一起探讨更好的答案.
https://github.com/zhangqian00/
这是我的 github 地址, 有一些我自己写的一些关于 require,angular,vue 等等的小项目, 最近在学习 Nodejs, 非常欢迎大牛们来指点, 交流, 分享.
来源: http://www.jianshu.com/p/0ab85abdbc6b