CommonJS规范
模块引用
1 | var math = require('math') |
模块定义
上下文提供了exports对象用于导出当前模块的方法或者变量,并且是唯一的出口。在模块中,还存在一个module对象,它代表模块自身,而exports是module的属性。将方法挂载在exports对象上作为属性即可定义导出的方式。
1 | exports.add = function () {} |
模块标识
即传递给require()
的参数
Node的模块实现
引入模块需要经历如下3个步骤
- 路径分析
- 文件定位
- 编译执行
模块分为两类:
- Node提供的模块,称为核心模块:不需要文件定位和编译执行,在路径分析中优先判断,加载速度最快
- 用户编写的模块,称为文件模块
接下来,详细展开模块加载过程
优先从缓存加载
Node对引入过的模块都会进行缓存,缓存的是编译和执行之后的对象。
路径分析和文件定位
模块标识符分析
主要分为以下几类(按加载速度排序):
- 核心模块,如http、fs、path等
- .或..开始的相对路径文件模块
- 以/开始的绝对路径文件模块
- 非路径形式的文件模块,如node_modules中的模块,例如
require('express')
会依次在当前路径、上一级路径、上上级路径…的node_modules
下去找,所以当js文件的路径越深时,该加载速度就越慢
文件定位
- 文件拓展名分析:
.js>.json>.node
,为了优化如果不是.js
的文件,最好加上拓展名 - 目录分析和包:如果查找得到的是一个目录的话,会按照如下顺序:package.json(main属性指定的文件)>index.js>index.json>index.node
模块编译
node中,每个文件模块都是一个对象,定义如下:
1 | function Module(id, parent) { |
不同的文件拓展名,载入方式不同:
- .js文件。通过fs模块同步读取后编译执行
- .node文件。这是用C/C++编写的拓展文件,通过
dlopen()
加载编译 - .json。fs模块同步读取,
JSON.parse()
解析返回结果 - 其他。当做.js文件
js模块的编译
一个正常的js文件会被包装成如下样子:
1 | (function (exports, require, module, __filename, __dirname) { |
执行之后,模块的exports
属性被返回给了调用方,其上的任何方法和属性都可以被外部调用到。
前后端共用模块
AMD & CMD
AMD
1 | require(['jquery','创建了全局变量的module'],function($,b){ |
实际做的事情是:
- require函数检查依赖的模块,根据配置文件,获取js文件的实际路径
- 根据js文件实际路径,在dom中插入script节点,并绑定onload事件来获取该模块加载完成的通知。
- 依赖script全部加载完成后,调用回调函数
1 | define(function(require,exports,modules){ |
真相是:
- 通过回调函数的Function.toString函数,使用正则表达式来捕捉内部的require字段,找到require(‘jquery’)内部依赖的模块jquery
- 根据配置文件,找到jquery的js文件的实际路径
- 在dom中插入script标签,载入模块指定的js,绑定加载完成的事件,使得加载完成后将js文件绑定到require模块指定的id(这里就是jquery这个字符串)上
- 回调函数内部依赖的js全部加载(暂不调用)完后,调用回调函数
- 当回调函数调用require(‘jquery’),即执行绑定在’jquery’这个id上的js文件,即刻执行,并返回
都会并行加载依赖,但是AMD会解析(执行)完所有的依赖后,再执行回调函数;CMD则是在执行require
时才会去解析(执行),即“懒加载”。