• node.js开发指南读书笔记(1)


    3.1 开始使用Node.js编程

    3.1.1 Hello World

    将以下源代码保存到helloworld.js文件中

    console.log('Hello World!');
    console.log('%s:%d', 'hello', 25);

         找到文件位置,执行node helloworld.js。结果如下:

         

    3.1.2 Node.js命令行工具

      输入:node --help可以看到详细的帮助信息。

      

      除了直接运行脚本外,node --help显示的用法中说明了另一种输出hello world方式。

      

      使用node的REPL模式

      REPL(Read-eval-print loop),即输入-求值-输出循环。

      

      进入REPL模式后,会出现一个">"提示符提示你输入,输入后按回车,Node.js将会解析并执行命令。如果你执行了一个函数,那么REPL还会在下面显示这个函数的返回值,上面例子中的undefined就是console.log的返回值,如果你输入了一个错误指令,REPL会立即显示错误并输出调用栈。在任何时候连续按两次CTRL + C即可退出Node.js的REPL模式。

      node提出REPL在应用开发时会给人带来很大的便利,例如我们可以测试一个包能否正常使用,单独调用应用的某一个模块,执行简单的计算等。

    3.1.3 建立HTTP服务器

      node.js的服务器架构与PHP架构

      

      建立一个名为app.js的文件,代码如下:

    var http = require('http');
    
    http.createServer(function(req, res){
        res.writeHead(200, {'Content-type':'text/html'});
        res.write('<h1>Node.js</h1>');
        res.end('<p>Hello world</p>');
            }).listen(3000);
    console.log('HTTP server is listening at port 3000.')

      运行node app.js,打开浏览器http://127.0.0.1:3000,就可以看到图3-2所示的内容。

      

              图3-2 用Node.js实现的Http服务器

      用Node.js实现的最简单的HTTP服务器就这样诞生了。这个程序调用了Node.js提供的http模块,对所有HTTP请求答复同样的内容监听3000端口。在终端中运行这个脚本时,我们会发现它并不像Hello World一样结束后立即退出,而是一直等待,直到按下CTRL + C才会结束。这是因为listen函数中创建了事件监听器,使得Node.js进程不会退出事件循环。

      小技巧——使用supervisor。

      安装:$ npm install -g supervisor

      如果你使用的是linux或Mac,直接键入上面的命令很可能有权限错误。原因是npm需要把supervisor安装到系统目录,需要管理员权限,可以使用sudo npm install -g supervisor命令来安装。

      接下来使用supervisor命令启动app.js

      

    3.2 异步式I/O与事件式编程

      Node.js最大的特定就是异步式I/O(或者非阻塞I/O)与事件紧密结合的编程模式。这种模式与传统的同步式I/O线性的编程思想有很大的不同,因为控制流很大程度上要靠事件和回调函数来组织,一个逻辑要拆分为若干个单元。

    3.2.1 阻塞与线程

      什么是阻塞(block)呢?线程在执行中如果遇到磁盘读写或者网络通信(统称为I/O操作),通常需要耗费较长的时间,这时操作系统会剥夺这个线程的CPU控制权,使其暂停执行,同时将资源让给其他的工作线程,这个线程调度方式称为阻塞。当I/O操作完毕时,操作系统将这个线程的阻塞状态解除,恢复其对CPU的控制权,令其继续执行。这种I/O模式就是通常的同步式I/O(Synchronous I/O)或阻塞式(Blocking I/O)。

      相应地,异步式I/O(Asynchronous I/O)或非阻塞式I/O(Non-blocking I/O)则针对所有I/O操作采用不阻塞的策略。当线程遇到I/O操作时,不会以阻塞的方式等待I/O操作的完成或数据的返回,而只是将I/O请求发送给操作系统,继续执行下一条语句。当操作系统完成I/O操作时,以事件的形式通知执行I/O操作的线程,线程会在特定时候处理这个事件。为了处理异步I/O,线程必须有事件循环,不断地检查有没有未处理的事件,一次予以处理。

      阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须多线程。而非阻塞模式下,一个线程永远在执行计算操作,这个线程所使用的CPU核心利用率永远是100%,I/O以事件的方式通知。在阻塞模式下,多线程往往能提高系统吞吐量,因为一个线程阻塞时还有其他线程在工作,多线程可以让CPU资源不被阻塞中的线程浪费。而在非阻塞模式下,线程不会被I/O阻塞,永远在利用CPU。多线程带来的好处仅仅是在多核CPU的情况下利用更多的核,而Node.js的单线程也能带来同样的好处。这就是为什么Node.js使用了单线程、非阻塞的事件编程模式。

      图3-3和图3-4分别是多线程同步式与单线程异步式I/O的示例。假设我们有一项工作,可以分为两个计算部分和一个I/O部分,I/O部分占的时间比计算多得多(通常都是这样的)。如果我们使用阻塞I/O,那么要想获得高并发就必须开启多个线程。而使用异步式I/O时,单线程即可胜任。

        

      单线程事件驱动的异步式I/O比传统多线程阻塞式I/O究竟好在哪里呢?简而言之,异步式I/O就是少了多线程的开销。对操作系统来说,创建一个线程的代价是十分昂贵的,需要给它分配内存,列入调度,同时在线程键切换的时候,还要执行内存换页,CPU的缓存被清空,切换回来的时候,还要重新从内存中读取信息,破坏了数据的局部性。

      当然,异步式编程的缺点在于不符合人们一般的程序设计思维,容易让控制流变得晦涩难懂,给编码和调试都带来不小的困难。

      

    3.2.2 回调函数

      让我们看看在Node.js中如何使用异步的方式读取一个文件,下面是一个例子:

    // readfile.js
    var fs = require('fs');
    fs.readFile('file.txt', 'utf-8', function(err, data)
    {
        if (err)
        {
            console.error(err);
        } else
        {
            console.log(data);
        }
    });
    console.log('end')

      在新建一个文件file.txt,输入hello world!

      运行结果如下:

      D:01code011nodejs>node readfile.js
      end
      hello world!

      Node.js 也提供了同步读取文件的API:

    // readfilesync.js
    var fs = require('fs');
    var data = fs.readFileSync('file.txt', 'utf-8');
    console.log(data);
    console.log('end');

      运行结果如下:

      D:01code011nodejs>node readfilesync.js
      hello world!

      end

      比较运行结果:同步的先输出读取文件的内容,在输出end,而异步是先输出end,再输出文件的内容。fs.readFile调用时所做的工作只是将异步式I/O请求发送给了操作系统,然后立即返回并执行后面的语句,执行完以后进入事件循环监听事件。当fs收到I/O请求完成的事件时,事件循环会主动调用回调函数以完成后续工作。因此我们会先看到end,再看到file.txt文件的内容。

    3.2.3 事件

      Node.js所有的异步I/O操作在完成时都会发送一个事件到事件队列。在开发者看来,事件由EventEmitter对象提供。前面提到的fs.readFile和http.createServer的回调函数都是通过EventEmitter来实现的。下面我们用一个简单的例子说明EventEmitter的用法:

    // event.js
    var EventEmitter = require('events').EventEmitter;
    var event = new EventEmitter();
    
    event.on('some_event', function(){
        console.log('some_event occured.');        
    });
    
    setTimeout(function(){
            event.emit('some_event');
    }, 1000);

      运行这段代码, 一秒后控制台输出了some_event occured.其原理是event对象注册了事件some_event的一个监听器,然后我们通过setTimeout在1000毫秒以后向event对象发送事件some_event,此时会调用some_event监听器。

    Node.js的时间循环机制
      Node.js在什么时候会进入事件循环呢?答案是Node.js程序由事件循环开始,到事件循环结束,所有的逻辑都是事件的回调函数,所以Node.js始终在事件循环中,程序入口就是事件循环第一个事件的回调函数。事件的回调函数在执行的过程中,可能会发出I/O请求或直接发射(emit)事件,执行完毕后再事件循环,事件循环会检查事件队列中有没有未处理的事件,直到程序结束。

      与其他语言不同的是,Node.js没有显示的事件循环,类似Ruby的EventMachine::run()的函数在Node.js中是不存在的。Node.js的事件循环对开发者不可见,由libev实现。libev支持多种类型的事件,如ev_io, ev_timer, ev_signal,
    ev_idle等,在Node.js中均被EventEmitter封装。libev事件循环的每一次迭代,在Node.js中就是一次Tick,libev不断检查是否有活动的、可供检查的时间监听器,直到检测不到时才退出事件循环,进行结束。事件,执行完毕后再事件循环,事件循环会检查事件队列中有没有未处理的事件,直到程序结束。

      与其他语言不同的是,Node.js没有显示的事件循环,类似Ruby的EventMachine::run()的函数在Node.js中是不存在的。Node
    .js的事件循环对开发者不可见,由libev实现。libev支持多种类型的事件,如ev_io, ev_timer, ev_signal, ev_idle等,在Node.js中均被EventEmitter封装。libev事件循环的每一次迭代,在Node.js中就是一次Tick,libev不断检查是否有活动的、可供检查的时间监听器,直到检测不到时才退出事件循环,进行结束。

       

    3.3 模块和包

    3.3.1 什么是模块
      模块时Node.js应用程序的基本组成部分,文件和模块时一一对应的。换言之,一个Node.js文件就是一个模块,这个文件可以是javascript代码、JSON或者编译过的C/C++扩展。
    在前面的例子中,我们曾经用到var http = require('http'),其中http是Node.js的一个核心模块,其内部用C++实现的,外部用javascript封装,我们通过require函数获取了这个模块,然后才能使用其中的对象。

    3.3.2 创建及加载模块
    (1) 创建模块
      在Node.js中,创建一个模块非常简单,因为一个文件就是一个模块,我们要关注的问题仅仅在于如何在其他文件中获取这个模块。Node.js提供了exports和require两个对象,其中exports是模块公开的接口,require用于从外部获取一个模块的接口,即所获取模块的exports对象。
      让我们以一个例子来了解模块。创建一个module.js的文件,内容是:

    // module.js
    var name;
    
    exports.setName = function(thyName){
      name = thyName;
    };
    
    exports.sayHello = function(){
      console.log('Hello ' + name);
    }

      在同一目录下,创建getmodule.js,内容是:

    // getmodule.js
    
    var myModule = require('./module');
    myModule.setName('BYVoid');
    myModule.sayHello();

      运行 node getmodule.js,结果如下:

      hello BYVoid

     (2)单次加载

    /*===============================================
    # Last modified:2014-08-27 13:55
    # Filename:loadmodule.js
    # Description: 单次加载示例
    =================================================*/
    
    var hello1 = require('./module');
    hello1.setName('BYVoid1');
    
    var hello2 = require('./module');
    hello2.setName('BYVoide2');
    
    hello1.sayHello();

      运行 node loadmodule.js 结果如下:

      Hello BYVoide2

      这是因为变量hello1和hello2指向的是同一个实例,因此hello1.setName的结果被hello2.setName覆盖,最终结果是由后者决定。

      (3)覆盖exports

      a 第一种方法 exports.Hello

      模块

    /*===============================================
    # Author: RollerCoaster
    # Last modified:2014-08-27 14:02
    # Filename: singleobject.js
    # Description: 覆盖exports示例1
    =================================================*/
    function Hello(){
        var name;
        this.setName = function(thyName){
            name = thyName;
        };
    
        this.sayHello = function(){
        console.log('Hello ' + name + '!');    
        };
    };
    
    exports.Hello = Hello;

      调用代码:

    /*===============================================
    # Author: RollerCoaster 
    # Last modified:2014-08-27 14:05
    # Filename: runSingleObject.js
    # Description: 调用singleobject模块
    =================================================*/
    var Hello = require('./singleobject').Hello;
    hello = new Hello();
    hello.setName('roller coaster'); 
    hello.sayHello();

      运行结果:

      Hello roller coaster!

        b 第二种方法 module.exports = Hello;

      模块代码:

    /*===============================================
    # Author: RollerCoaster
    # Last modified:2014-08-27 14:17
    # Filename: hello.js
    # Description: hello 模块
    =================================================*/
    function Hello(){
        var name;
        this.setName = function(thyName){
        name=thyName;
        };    
    
        this.sayHello = function(){
        console.log('Hello ' + name + '!');
        }
    };
    
    module.exports = Hello;

      调用代码

    /*===============================================
    # Author: RollerCoaster
    # Last modified:2014-08-27 14:17
    # Filename: gethello.js
    # Description: 调用hello模块
    =================================================*/
    var Hello = require('./Hello');
    hello = new Hello();
    hello.setName('roller coaster');
    hello.sayHello();

      注意,模块接口的唯一变化是使用module.exports = Hello 代替了exports.Hello = Hello。在外部调用该模块时,其接口对象就是要输出Hello对象本身,而不是原先的exports。  

      事实上,exports本身仅仅是一个普通的空对象,即{},它专门用来声明接口,本质上是通过它为模块闭包内部建立了一个有限的访问接口。因为它没有任何特殊的地方。所以可以用其他东西来代替,譬如我们上面例子上的Hello对象。

      不可以通过对 exports直接复制代替对module.exports赋值。exports实际上只是一个和module.exports指向同一个对象的变量,它本身会在模块执行结束后释放,但module不会,因此只能通过指定module.exports来改变访问接口。

    作者:BestNow
    出处:http://www.cnblogs.com/BestNow/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    控制结构(Scala)
    《基于Spark的大数据访存行为跨层分析工具》学习笔记
    函数式对象(Scala)
    心脏病预测(SVM模型)
    类、对象、基础类型、操作(Scala)
    ElementUI对话框(dialog)提取为子组件
    ElementUI+命名视图实现复杂顶部和左侧导航栏
    ElementUI 复杂顶部和左侧导航栏实现
    Vue页面手动刷新,导航栏激活项还原到初始状态问题解决方案
    elementUI动态数据表格(带分页)
  • 原文地址:https://www.cnblogs.com/tianxue/p/3922459.html
Copyright © 2020-2023  润新知