所以结论是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
方法同步加载所依赖的模块,通过exports
或module.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');
特点:
-
同步加载方式,适用于服务端,因为模块都放在服务器端,对于服务端来说模块加载较快,不适合在浏览器环境中使用,因为同步意味着阻塞加载。
-
所有代码都运行在模块作用域,不会污染全局作用域。
-
模块可以多次加载,但只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。
-
模块加载的顺序,按照其在代码中出现的顺序
AMD规范(Async Module Definition)
采用**异步方式**
加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。**推崇依赖前置**
require.js 是目前 AMD 规范最热门的一个实现
AMD 也采用 require
语句加载模块,但是不同于 CommonJS,它要求两个参数:require([module], callback)
-
[module]:是一个数组,成员就是要加载的模块
-
callback:加载成功之后的回调函数;
require (['math'], function (math) {
math.add(2, 3);
});
创建模块
模块必须采用 **define()**
函数来定义。
特点
-
AMD允许输出的模块兼容CommonJS
-
异步并行加载,不阻塞 DOM 渲染。
-
**推崇依赖前置**
,也就是提前执行(预执行),在模块使用之前就已经执行完毕。
CMD规范
-
CMD 是通用模块加载,要解决的问题与 AMD 一样,只不过是对依赖模块的执行时机不同 ,
**推崇就近依赖**
。 -
sea.js 是 CMD 规范的一个实现代表库
-
定义模块使用全局函数define,接收一个 factory 参数,可以是一个函数,也可以是一个对象或字符串;
- factory 是函数时有三个参数,function(require, exports, module):
-
require
:函数用来获取其他模块提供的接口require(模块标识ID) -
exports
: 对象用来向外提供模块接口 -
module
:对象,存储了与当前模块相关联的属性和方法
AMD 与 CMD 的区别
-
AMD 是提前执行,CMD 是延迟执行。
-
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
*
刚刚讲到使用babel将import编译为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命令是异步加载,有一个独立的模块依赖的解析阶段。