03月20, 2018

node深入笔记(1)

之前有写过几篇node学习笔记,但写的比较浅,所以这次算是一个新的笔记开始。

node的更新迭代速度真的是不忍直视,怕是年底要突破10.x版本了。

alt

npm

概念我就不说了,说一下它的全局安装目录在哪里:

npm root -g

alt

在npm install模块的时候,先要确定当前文件夹下要有package.json(即先npm init -y或者不加y,进行项目初始化),不然可能模块会安装到父级或祖先级目录(也有可能会报错喔!)

我个人觉得判断一个是否了解npm,可以从几方面入手:

  • 是否了解package.json的常用key(如main,bin)
  • 是否有发布过第三方的包(这里会考查npm的发布相关命令,包括登录、授权等)
  • 就是一些常用的安装、卸载三方包,如何只安装dev包,如何只安装devDependencies的包
  • 三方包的查找依赖顺序是怎样的,比如说:require("webpack)"

其中第四个,我觉得与npm关系不是太大,但需要了解,这个是在module.paths里面就可以看到:

alt

REPL

这个是运行在命令行里面的,一般也不是太重要。我觉得需要知道两点即可:

  • 通过_可以得到上一次运行的结果

alt

  • 通过.help来得到所有的命令

alt

global

global是node中的全局对象。它里面包括了一些常用的:

  • console
  • process
  • Buffer
  • setImmediate
  • setTimeout
  • ...

process这个比较重要,它有一些属性和方法会被经常使用,如:

  • argv 执行时可能会传递参数 http-server --port 3000,通过它可以来获取--port3000
  • chdir 改变工作目录
  • cwd current working directory 当前工作目录
  • nextTick 微任务
  • stdout stderr stdin

cwd这个方法,类似__dirname(需要注意的是:这个不是global),但这两者是有区别的:

console.log(process.cwd());
process.chdir('..');
console.log(__dirname);
console.log(process.cwd());

所以在有些书籍上能看到,要深刻理解__dirnameprocess.cwd()的区别。

event loop

在node中,微任务大概有 then nextTick,宏任务有 setTimeout setInterval setImmediate,那么它们的先后顺序是怎样的?

比如下面的代码:

console.log(1);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('promise')
    });
})
setTimeout(function(){
    console.log('setTimeout2');
});

需要注意的是,上面的代码在浏览器端的运行结果和node端的运行结果是不一样的。浏览器端的处理方式是:先执行当前栈 执行完走微任务 走事件队列里的内容(拿出一个)放到栈里执行,再去执行微任务。而node端的处理方式,如下图所示:

alt

描述成文字就是先执行当前栈,执行完微任务,走timers,走完timer(指所有的timer)时这个阶段,再去看看有没有微任务,有就执行微任务,没有就走下一阶断(I/O callbacks处理网络、流的错误),执行完,再看是否有微任务,这样依次往下。

nextTick是优先于promise then执行的。

setTimeout和setImmediate的执行顺序是不确定的:

  • setImmediate 设计在poll阶段完成时执行,即check阶段;
  • setTimeout 设计在poll阶段为空闲时,且设定时间到达后执行;但其在timer阶段执行 其二者的调用顺序取决于当前event loop的上下文,如果他们在异步i/o callback之外调用(在i/o内调用因为下一阶段为check阶段),其执行先后顺序是不确定的,需要看loop的执行前的耗时情况。

alt

不过下面代码的顺序是可以确定的:

let fs = require('fs');
fs.readFile('./1.log',function(){
    console.log('fs');
    setTimeout(function(){
        console.log('timeout');
    })
    setImmediate(function(){
        console.log('setImmediate')
    })
});

结果是:fs setImmediate timeout,原因是:i/o操作阶段完成后 会走check阶段,所以setImmediate会优先于timeout。

所以看了上面这么多,问题来了,下面两段的代码执行结果是啥:

setImmediate(function(){
    console.log(1);
    process.nextTick(function(){
        console.log(4)
    }) 
})
process.nextTick(function(){
    console.log(2)
    setImmediate(function(){
        console.log(3);
    })
})
setTimeout(function(){
    console.log(1);
    process.nextTick(function(){
        console.log(4)
    }) 
})
process.nextTick(function(){
    console.log(2)
    setImmediate(function(){
        console.log(3);
    })
})

如果能回答出来,那么表示event loop,你已经彻底懂了。

node调试

这个在之前的文章中有发过浏览chrome的调试,所以我就简单记录一下在vs code中的配置:

配置debugger文件

alt

module模块

在node中,一个模块就是一个file,实现的思路就是通过闭包的方式,比如说,文件a的内容是:

module.exports = {
    a: 1
}

最终在require a文件的时候,node会将它里面的文本内容读出来,拼接成下面的闭包:

(function (exports, require, module, __filename, __dirname) {
    module.exports = {
         a: 1
    }
})

然后运行在一个沙箱中,传入对应的参数。

通过vs code的调试源码功能,我们可以进到require的方法里面,然后根据里面的实现思路,可以完成一个简单的require功能。

let path = require('path');
let fs = require('fs');
let vm = require('vm');

function req(filename){ // filename是文件名 文件名可能没有后缀
    // 我们需要弄出一个绝对路径来,缓存是根据绝对路径来的
    filename = Module._resolvePathname(filename); 
    //console.log(filename);
    // 先看这个路径在缓存中有没有,如果有直接返回
    let cacheModule = Module._cache[filename];
    if(cacheModule){ // 缓存里有 直接把缓存中的exports属性进行返回
        return cacheModule.exports
    }
    // 没缓存 加载模块
    let module = new Module(filename);  // 创建模块 {filename:'绝对路径',exports:{}}
    module.load(filename); // 加载这个模块     {filename:'xxx',exports = {a: 1}}
    Module._cache[filename] = module;
    module.loaded = true; // 表示当前模块是否加载完 
    return module.exports;
}

function Module(filename){ // 构造函数
    this.filename = filename;
    this.exports = {};
    this.loaded = false;
}
// 文件名后缀如果没有写,则依次进行添加查找
Module._extentions = ['.js','.json','.node']; 
// 用来存放文件缓存
Module._cache = {};
// 解析路径
Module._resolvePathname = function(filename){
    let p = path.resolve(__dirname,filename);
    if(!path.extname(p)){
        for(var i = 0;i<Module._extentions.length;i++){
            let newPath = p + Module._extentions[i];
            try{ // 如果访问的文件不存在 就会发生异常
                fs.accessSync(newPath);
                return newPath
            }catch(e){}
        }
    }
    return p; //解析出来的就是一个绝对路径
}
Module.wrapper = [
    "(function(exports,require,module,__dirname,__filename){",
    "\n})"
]
/*
    (function(exports,require,module,__dirname,__filename){
        this = module.exports;
        console.log('加载');
        module.exports = {a: 1}; 
    })

*/
Module.wrap = function(script){
    return Module.wrapper[0]+script+Module.wrapper[1];
}
Module._extentions["js"] = function(module){ // {filename,exports={}}
    let script = fs.readFileSync(module.filename);
    let fnStr = Module.wrap(script);
    vm.runInThisContext(fnStr).call(module.exports,module.exports,req,module)
}
Module._extentions["json"] = function(module){
    let script = fs.readFileSync(module.filename);
    // 如果是json直接拿到内容  json.parse即可
    module.exports = JSON.parse(script); 
}
Module.prototype.load = function(filename){ 
    // 模块可能是json 也有可能是js
    let ext = path.extname(filename).slice(1); // .js   .json
    Module._extentions[ext](this);
}

通过上面的代码,我们可以想一下,如果在一个文件中没有module.exports或者exports.xxx,而是这样写的:

this.a = "123";

那么在require这个文件的时候,也可以得到module.a的值为123,因为上面的this就是exports对象的指针。

同时通过上面简单版的源码,可以了解到,node读过一次文件之后,不会再次读取,而是会走缓存了。

当然上面的考虑场景只是简单的添加后缀,我们忽略了原生(核心)模块、第三方模块,还有一种文件夹的考虑。

文件夹的考虑是指:

let A = require("./floderA");

在找不到floderA.js、floderA.json、floderA.node之后,node会找floderA目录,找其下面的package.json(它下面有可能会有一个main入口,就找那个文件),如果没有package.json,会找floderA下面的index.js或者index.node,注意这时候不会找index.json了。

原生模块,用的比较多的有像:

  • vm (如runInThisContext)
  • path (如path.join,path.resolve,要清楚这两者的区别)
  • util (常用工具方法,如inherits、promisify等)
  • events (事件监听、发布订阅)
  • fs (文件处理模块)
  • http

event事件

个人觉得可以实现一个简单的EventEmitter,将文档中的这些方法都搞一遍

alt

下面放一个简单版的代码:

function EventEmitter() {
    /**
     * 放置一个私有属性,创建一个空对象
     * Object.create(null)跟{}的区别在于{}可以拿到__proto__,前者是undefined,相当于前者创建的对象比较干净
     */
    this._events = Object.create(null);
}
// 默认最大监听数为10个
EventEmitter.defaultMaxListeners = 10;
EventEmitter.prototype.addListener = EventEmitter.prototype.on;
EventEmitter.prototype.setMaxListeners = function(n){
    this._count = n;
}
EventEmitter.prototype.getMaxListeners = function(){
    return this._count ? this._count : EventEmitter.defaultMaxListeners
}
EventEmitter.prototype.eventNames = function(){
    return Object.keys(this._events)
}
/**
 * 
 * @param {*} type 事件类型
 * @param {*} callback  事件回调
 * @param {*} flag 用来标记是否插入到第一个
 */
EventEmitter.prototype.on = function(type,callback,flag){
    /**
     * 通过util.inherits方法并不能继承EventEmitter的私有属性,因此要判断一下_events是否是undefined
     */
    if(!this._events){
        this._events = Object.create(null);
    } 
    // 不是newListener 我就应该让newListener执行一下
    if(type !== 'newListener'){
        this._events['newListener']&& this._events['newListener'].forEach(listener=>{
            listener(type)
        })
    }
    if(this._events[type]){
        if(flag){
            this._events[type].unshift(callback);
        }else{
            this._events[type].push(callback);
        }
    }else{ 
        //内部没存放过
        this._events[type] = [callback]
    }
    if(this._events[type].length === this.getMaxListeners()){
        // 这里放警告
        console.warn('-------------------------------')
    }
}
EventEmitter.prototype.removeListener = function(type,callback){
    if(this._events[type]){
        // 不是removeListener 我就应该让removeListener执行一下
        if(type !== 'removeListener'){
            this._events['removeListener']&& this._events['removeListener'].forEach(listener=>{
                listener(type)
            })
        }
        this._events[type] = this._events[type].filter(function(listener){
                return callback != listener && listener.l !== callback;
        });
    }
}
EventEmitter.prototype.prependListener = function(type,callback){
    this.on(type,callback,true)
}
EventEmitter.prototype.prependOnceListener = function(type,callback){
    this.once(type,callback,true)
}
EventEmitter.prototype.listeners = function(type){
    return this._events[type];
}
EventEmitter.prototype.once = function(type,callback,flag){
    // 先绑定 调用后再删除
    function wrap (){
        callback(...arguments);
        this.removeListener(type,wrap);
    }
    // 自定义属性,用来放之前的回调函数,来保证下次removeListener的时候,可以删除,不然不是一个函数,无法删除
    wrap.l  = callback
    this.on(type,wrap,flag);
}
EventEmitter.prototype.removeAllListener = function(){
    this._events = Object.create(null);
}

EventEmitter.prototype.emit = function(type,...args){
    if(this._events[type]){
        this._events[type].forEach(listener => {
            listener.call(this,...args);
        });
    }
}
module.exports = EventEmitter ;

基本上我认为重要的都注释了。

本文链接:www.my-fe.pub/post/node-deep-note-1.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。