Nodejs学习笔记

513 阅读7分钟

所以结论是Node是单进程、多线程的;JavaScript代码是单线程执行的

2、Nodejs如何处理循环依赖

什么叫做循环依赖?

比如A文件需要用到B文件的某些函数,而B文件又需要用到A文件的某些函数

贴上两段代码给大家看看:

A.js

let b = require('./B');

console.log('A: before logging b');
console.log(b);
console.log('A: after logging b');

module.exports = {
    A: 'this is a Object'
};

B.js

let a = require('./A');

console.log('B: before logging a');
console.log(a);
console.log('B: after logging a');

module.exports = {
    B: 'this is b Object'
};

那么运行A.js结果会是怎么样的呢?

官网上写着为了防止模块载入的死循环,Nodejs在模块第一次载入后会把它的结果进行缓存,下一次再对它进行载入的时候会直接从缓存中取出结果。所以在这种依赖循环的时候,是不会出现死循环的,但是会因为缓存造成模块没有按照我们预想的那样被导出

OK,话不多说,我们来运行一下A.js来验证我们的猜想,

*

解释一下,在这个例子中,A.js先require了B.js,程序进入B.js,在B.js中第一行又require了A.js

所以Nodejs运行A.js后,它就被缓存了,此时缓存的仅仅是一个未完工的A.js,所以在B.js中输出的a仅仅是一个空对象。之后B.js顺利执行完,回到A.js的require语句之后,继续执行完成

那么如何解决循环依赖呢?

那就是在循环依赖的每个模块中先导出自身,再导入其他模块。

A.js
module.exports = {
    A: 'this is a Object'
};

let b = require('./B');

console.log('A: before log b');
console.log(b);
console.log('A: after log b');


B.js
module.exports = {
    B: 'this is b Object'
};

let a = require('./A');

console.log('B: before log a');
console.log(a);
console.log('B: after log a');

那么执行A.js的结果就是

*

3、JS前端四大模块化

CommonJS规范

服务端实现:nodejs、浏览器端实现:Browserify(也称为CommonJS的浏览器打包工具)

通过require方法同步加载所依赖的模块,通过exportsmodule.exports导出需要暴露的数据。**一个文件就是一个模块**

加载模块:使用require函数加载

1、按路径进行加载模块

2、通过查找node_modules目录加载模块

3、核心模块拥有最高的加载优先级,如果出现模块命名冲突,那么优先加载核心模块

导出模块:exports.属性 = 值;exports.方法 = 函数
  • Node.js 为每个模块提供一个 exports 变量,指向 module.exports。相对于在每个模块头部,有一行这样的命令:var exports = module.exports;

  • exports对象 和 module.exports对象,指同一个内存空间, module.exports对象才是真正的暴露对象

  • exports对象 是 module.exports对象的引用,不能改变指向,只能添加属性和方法,若直接改变exports 的指向,等于切断了 exports 与 module.exports 的联系,返回空对象

  • console.log(module.exports === exports); // true

用法:

*

此时获取Hello函数需要var Hello = require('./singleObject').Hello,略显冗余

*

此时获取可以这样,var Hello = require('./hello');

特点:
  1. 同步加载方式,适用于服务端,因为模块都放在服务器端,对于服务端来说模块加载较快,不适合在浏览器环境中使用,因为同步意味着阻塞加载。

  2. 所有代码都运行在模块作用域,不会污染全局作用域。

  3. 模块可以多次加载,但只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。

  4. 模块加载的顺序,按照其在代码中出现的顺序

AMD规范(Async Module Definition)

采用**异步方式**加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。**推崇依赖前置**

require.js 是目前 AMD 规范最热门的一个实现

AMD 也采用 require语句加载模块,但是不同于 CommonJS,它要求两个参数:require([module], callback)

  • [module]:是一个数组,成员就是要加载的模块

  • callback:加载成功之后的回调函数;

require (['math'], function (math) {

math.add(2, 3);

});

创建模块

模块必须采用 **define()** 函数来定义。

特点
  1. AMD允许输出的模块兼容CommonJS

  2. 异步并行加载,不阻塞 DOM 渲染。

  3. **推崇依赖前置**,也就是提前执行(预执行),在模块使用之前就已经执行完毕。

CMD规范
  • CMD 是通用模块加载,要解决的问题与 AMD 一样,只不过是对依赖模块的执行时机不同 ,**推崇就近依赖**

  • sea.js 是 CMD 规范的一个实现代表库

  • 定义模块使用全局函数define,接收一个 factory 参数,可以是一个函数,也可以是一个对象或字符串;

  1. factory 是函数时有三个参数,function(require, exports, module):
  • require:函数用来获取其他模块提供的接口require(模块标识ID)

  • exports: 对象用来向外提供模块接口

  • module :对象,存储了与当前模块相关联的属性和方法

AMD 与 CMD 的区别
  1. AMD 是提前执行,CMD 是延迟执行

  2. AMD 是依赖前置,CMD 是依赖就近

    // AMD

    define(['./a', './b'], function(a, b) { // 在定义模块时 就要声明其依赖的模块 a.doSomething()
    // .... b.doSomething()
    // ....

    })

    // CMD

    define(function(require, exports, module) { var a = require('./a') a.doSomething()

    // ... var b = require('./b') // 可以在用到某个模块时 再去require b.doSomething()
    // ... })

ES6规范

ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。

ES6 中,import引用模块,使用export导出模块。默认情况下,Node.js默认是不支持import语法的,通过babel项目将 ES6 模块 编译为 ES5 的 CommonJS。因此Babel实际上是将import/export翻译成Node.js支持的require/exports

*

刚刚讲到使用babelimport编译为nodejs支持的require,即可使用node命令执行,而浏览器默认是不支持import和require的,此时还需要借助另一个工具,即上文中,在讲述CommonJs时,提到的browserify

CommonJS

AMD

CMD

ES6

引用模块

require

require

require

import

暴露接口

module.exports || exports

define函数返回值 return

exports

export

加载方式

运行时加载,同步加载

并行加载,提前执行,异步加载

并行加载,按需执行,异步加载

编译时加载,异步加载

实现模块规范

NodeJS

RequireJS

SeaJS

原生JS

适用

服务器

浏览器

浏览器

服务器/浏览器

4、ES6模块的循环加载

ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值。

ES6模块是动态引用,不存在缓存值的问题,而且模块里面的变量,绑定其所在的模块。

// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);

上面代码中,m1.js的变量foo,在刚加载时等于bar,过了500毫秒,又变为等于baz

我们运行m2.js看到

$ babel-node m2.js

bar
baz

这导致ES6处理"循环加载"与CommonJS有本质的不同。ES6根本不会关心是否发生了"循环加载",只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

5、require与import的区别

1、写法区别

对于require/exports的写法

*

对于import/exports的写法

import fs from'fs'//常用
import {default as fs} from 'fs'
import * as fs from 'fs'
import {readFile} from 'fs'//常用
import {readFile as read} from 'fs'//常用

export default fs
export function readFile
export {readFile,read}//常用
export * from 'fs'

2、输入值的区别

require输入的变量,基本数据类型是赋值,引用类型是浅拷贝,可修改

import输入的变量都是只读的,如果输入a是一个对象,允许改写对象属性

*

3、执行顺序

**require**:不具有提升效果,到底加载哪一个模块,只有运行时才知道。

**import**:具有提升效果,会提升到整个模块的头部,首先执行。import的执行早于foo

的调用。本质就是import命令是编译阶段执行的,在代码运行之前。

*

总结

a、CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用

b、CommonJS模块是运行时加载,ES6模块是编译时输出接口

c、CommonJS模块的require()是同步的,ES6模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。

体会