前端修炼——Node.js(二)

提前了解一下 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 过程中,按自己的理解总结的一些笔记,若有错误之处,欢迎各位及时指出,一起探讨更好的答案。

公众号:前端很忙

做一个喜欢分享的前端开发者!

获取更多干货分享,欢迎来搞!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容