我是程序员;您好程先生;叫我序员就好了天干物燥,前端别闹前端开发,日常踩坑

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

2018-05-02  本文已影响53人  前端很忙

提前了解一下 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 也有专门的方案解决:

事件发布 / 订阅模式:
这里讲解的是 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,非常欢迎大牛们来指点,交流,分享。

上一篇下一篇

猜你喜欢

热点阅读