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

作者: 前端很忙 | 来源:发表于2018-05-02 16:39 被阅读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 也有专门的方案解决:

    • 事件发布 / 订阅模式(注册 / 触发)
    • 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,非常欢迎大牛们来指点,交流,分享。

    相关文章

      网友评论

      本文标题:前端修炼——Node.js(二)

      本文链接:https://www.haomeiwen.com/subject/sfjylftx.html