(译) CommonJS Modules 规范

规范原文:CommonJS Modules 1.0

此文:按照 个人理解 翻译了一下 commonjs 规范

导语

此规范解决“如何编写能够在交互式模块化系统中使用的模块”。这个模块化系统可以是客户端系统或服务端系统、安全的系统或不安全系统、现在已有的系统或通过语法扩展而被支持的未来系统。这些模块不仅在各自的顶级作用域内拥有一些私有内容,而且能够从其他模块中导入单例对象,并能够导出自己内部的 API 给其他模块使用。换一种方式来说,此规范定义了一个模块化系统为了支持交互式模块所需要的最基本特性。

协议

模块上下文(Module Context)

  1. 在一个模块中,有一个变量 require, 值指向了一个函数。
    1. 这个 require 函数接收一个参数:模块的标识符
    2. require 函数返回外部模块(即 1 中 模块的标识符 对应的模块)导出的 API。
    3. 依赖循环/闭环(dependency cycle) 的定义:一个模块 B 在完成它的初始化[2] 之前,require(依赖) 了一个此次依赖链中较为靠前的模块 A,此时发生的就叫做依赖循环。如果发生了依赖循环,那么模块 A 必须在模块 B 初始化之前,导出了模块 B 初始化所需要的内容。(有点绕:可以看下 CommonJS in NodeJS#循环依赖 中的我的个人理解)
  2. 在一个模块中,有一个变量 exports,值指向了一个对象,当模块在初始化的时候,可以向此模块中添加 API。
  3. 模块必须使用 2 中的 exports 对象作为唯一的导出。

模块标识符(Module Identifiers)

  1. 模块标识符是由 正斜杠/ 分隔的多个 术语term 组成的字符串。
  2. 一个 术语term 必须是:驼峰式的标识符...
  3. 模块标识符可以不使用文件后缀名,如 .js
  4. 模块标识符可以是 相对的顶级的。一个标识符如果以 ... 开头,那么它就是 相对的
  5. 顶级标识符应当从模块的根命名空间开始解析。
  6. 相对标识符应当从这个模块被 require 的模块位置开始解析。

[1] interoperable modules: 可交互式的模块。即:可以互相交换信息的模块。
[2] 初始化:即执行该模块。模块仅会在第一次被 require 的时候执行,以获得所导出的 API。之后再次被 require 的时候,会直接从内存中读取该内容。

未定义

此规范并未对以下交互式的内容进行定义:

  1. 模块的存储方式:可以是数据库、文件件系统或工厂函数。但模块也可以通过链接库进行交互。
  2. 模块是否支持通过 PATH 变量来对模块标识符进行解析。

单元测试

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// math.js
exports.add = function() {
var sum = 0,
i = 0,
args = arguments,
l = args.length;
while (i < l) {
sum += args[i++];
}
return sum;
};

// increment.js
var add = require("math").add;
exports.increment = function(val) {
return add(val, 1);
};

// program.js;
var inc = require("increment").increment;
var a = 1;
inc(a); // 2

CommonJS in NodeJS

前端模块化开发中少不了用到 module.exports require 这一对兄弟。有时候看别人代码还会看到 exports。现在来深入研究下这几个小东西。

来源

commonjs 只是一个规范,而 node 采用了 commonjs 规范来实现自己的模块化系统 (原因:服务器端的代码都在服务器的磁盘上,读取速度非常快,而 commonjs 的规则即是同步加载)。


一个简单的例子

为了方便,我们从一个简单的计算器工具开始:

1
2
3
// calc.js

module.exports.square = num => num * num;

使用:

1
2
3
4
// use-calc.js

const calc = require("./calc.js");
console.log(calc.square(2)); // 4

module 对象

为了明白 module 这个变量是什么,我们对 calc.js 进行简单的修改:

1
2
3
4
// calc.js

module.exports.square = num => num * num;
+ console.log(module);

当执行 require("./calc.js") 的时候,会输出类似下面的东西:

1
2
3
4
5
6
7
8
9
10
11
Module {
id: '/path-to/calc.js',
exports: { square: [Function] },
parent:[
Module { ... }
]
filename: '/path-to/calc.js',
loaded: false,
children: [],
paths: [ ... ]
}

module.exports

从上面的输出很容易能看出:

  • module.exports 只是 Module 对象的一个属性。
  • 这个属性由 nodejs 定义。
  • 这个属性的值完全由我们自己定义。
  • 默认值是空对象。

因此我们有两种使用方式:

  1. 直接在这个属性的默认值(空对象)上添加属性: 就像上面的例子 module.exports.square = [Fucntion]
  2. 用我们自定义的其他变量替换: module.exports = OtherVariable

替换的话,就可以替换为任意类型的变量,如:Number、String、Class 等。

别名:exports

  • exports 是一个变量,但它只是 module.exports 的一个别名,只是为了让我们在代码里少写几个字母。
  • exports 的有效导出只有这一种用法: exports.xxx = [Something]

下面是正常可用的导出方式:

1
2
3
4
5
// calc.js

- module.exports.square = num => num * num;
- console.log(module);
+ exports.square = num => num * num;

但是如果改为下面的写法,那么 use-calc.js 中只能得到一个空对象:

1
2
3
4
5
6
7
// calc.js

- module.exports.square = num => num * num;
- console.log(module);
+ exports = {
+ square: num => num * num
+ }

如果有点疑惑,只要明白这个就好了:

  • require() 导出时候,是从 module 对象中查找导出内容的。
  • exports 只是 nodejs 声明的一个模块级别的变量。
  • exports 的初始值只是指向了 module.exports, 它可以被任意赋值。但被赋值的同时,它也不再指向 module.exports 了。
  • exports 的初始化发生在模块执行前。

可以理解为模块文件的顶部有这么一句代码:exports = module.exports;

1
2
3
exports = module.exports = { name: "Jack" };
exports.gender = "male"; // 此时修改的是 module.exports 指向的对象。
exports = { name: "John" }; // 此时直接将 exports 指向了其他对象,并未对原module.exports产生任何影响。

如果你能明白下面这段代码能输出什么,你基本就明白了 exports 和 module.exports 的规则了:

1
2
3
// run.js
const M = require("./c-module.js");
console.log(M);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 情况1-1的c-module.js:
module.exports = { count: 1 };
module.exports.name = 1;
// 情况1-2的c-module.js:
module.exports.name = 1;
module.exports = { count: 1 };

// 情况2的c-module.js:
module.exports.count = 1;
exports.count = 2;

// 情况3的c-module.js:
exports.count = 1;
module.exports.count = 2;

// 情况4的c-module.js:
module.exports = { count: 1 };
exports.count = 2;
exports = { count: 3 };

结论

  • module.exports 只是模块作用域变量 module 的一个属性,默认值是一个空对象,但可以被任意赋值修改。
  • 模块作用域变量 exports 只是一个初始值被指向了 module.exports 的变量。

同时 nodejs 的文档中也写明了:

As a guideline, if the relationship between exports and module.exports seems like magic to you, ignore exports and only use module.exports.
如果你不清楚 exports 和 module.exports 之间的关系,那就不要用 exports 了,只管用 module.exports 就行了。

循环依赖

commonjs 中对模块的循环引用是有说明的:

If there is a dependency cycle, the foreign module may not have finished executing at the time it is required by one of its transitive dependencies; in this case, the object returned by “require” must contain at least the exports that the foreign module has prepared before the call to require that led to the current module’s execution.
如果出现依赖闭环(dependency cycle),那么外部模块在被它的传递依赖(transitive dependencies)所 require 的时候可能并没有执行完成;在这种情况下,”require”返回的对象必须至少包含此外部模块在调用 require 函数(会进入当前模块执行环境)之前就已经准备完毕的输出。

可能看起来有点绕。看个下面的例子,基本就能清楚了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// a.js
console.log("a start");
module.exports.name = "a";
let b = require("./b");
console.log("a required b is:", b);
module.exports.b_required = true;
console.log("a end");

// b.js
console.log("b start");
module.exports.name = "b";
let a = require("./a");
console.log("b required a is:", a);
module.exports.a_required = true;
console.log("b end");

// main.js
console.log("main start");
let a = require("./a");
console.log("main required a is:", a);
let b = require("./b");
console.log("main required b is:", b);

// 执行 node main.js

执行:node main.js 可先按照自己的理解写一下打印顺序。
下面是输出结果:

1
2
3
4
5
6
7
8
9
main start
a start
b start
b required a is: { name: 'a' }
b end
a required b is: { name: 'b', a_required: true }
a end
main required a is: { name: 'a', b_required: true }
main required b is: { name: 'b', a_required: true }

如果你的答案和上面一样,那恭喜你了。如果不太一样,可以看下我的理解:

  • 依赖闭环仅可能发生在 依赖/模块 的执行过程中(即第一次引用 依赖/模块 的时候)。
  • 这个例子中的依赖链条是这样的:
    • main->a->b->a(产生了闭环,因为 a 和 b 都是第一次引用)
    • main->b(b 在第一次已经执行过了,此次并没有发生执行,所以不会产生闭环)
  1. 在 a->b->a 执行过程中,a 执行到 require(‘./b’) 的时候,会去执行 b 以期获得 b。
  2. b 执行到一半的时候,引用了 a。因为 b 此次依赖执行的祖先模块中有 a(意思就是 a 还没有执行完),于是发现了依赖闭环。
  3. 于是,b 中对 a 的引用,便只返回 a 中已执行的部分(即 require(‘./b’) 之前的内容)。

Event Loop

MDN 中的解释很清晰。这里解释几个概念

JS 单线程

JS 单线程指的是,JS 的执行运行时,有且仅会有一个线程执行(你在想什么? webworker 当然也是一个独立的 JS 单线程了!)。那么,一个线程是如何实现异步模型呢?

异步模型 - 浏览器端

先来张图。
图 1

执行完毕

这里必须要讲清楚这个概念。那么当单线程遇到了 setTimeout(cb, 3000) 的时候,JS 单线程要怎么处理呢?JS 单线程是不会像人一样:噢,我过 3 秒之后再来执行这个 cb 函数。因此当 JS 线程遇到它时,直接把它扔给浏览器,并标记为执行完毕,然后把它从执行栈里推出。浏览器自有对应的 WebAPI 去实现这个 setTimeout(cb, interval)

  1. 浏览器注册一个定时器,并将回调函数 cb 注册到 EventTable 中。
  2. 当定时器倒计时完毕,通知 EventTable,将 cb 扔进事件队列中。

恰当的时机、相应的队列

在上面的例子中,很容易就知道 3000ms 并不是 cb 执行的间隔,而是浏览器定时器倒计时(即 cb 加入事件队列)的时间。

事件队列中分为宏任务(Task)和微任务队列(JobsQueue)。他们的执行顺序可以用以下代码说明:

1
2
3
4
5
6
while (There_is_sth_in_TaskQueue_or_JobsQueue) {
while (There_is_sth_in_JobsQueue) {
processNextJob();
}
processNextTask();
}

并发模型

首先,如果有需要,可以自行搜索下 并发(Concurrency) 的概念。
然后,在图 1 中,能够很明显的看到,由于这个 EventLoop 循环的存在,得以实现了诸多的异步操作 : 譬如 setTimeoutdocument的点击事件 等。也正是由于浏览器实现的这些 webApi,得以让这些 异步任务 能够在恰当的时机进入的事件队列中。继而实现了 JS 独有的并发模型。

prototype 和 __proto__

prototype 只是原型的英文单词。而在实际的浏览器中,使用 __proto__ 来指向一个对象的原型。

继承(extends)

受中文的影响,在我最早接触继承时候,以为是类似法律中那种继承关系:

继承是法律基本名词,继承法即关于自然人死后由其继承人对其财产权利和义务予以承受的法律规范的总称。

让我最开始认为:子类 extends 父类之后,子类就拥有了父类中的属性 or 方法。然而实际上是错的:不管是 java 或者 js,编译后的代码中,子类中是并没有存在一份和父类一模一样的代码。

js 中的继承不是传统意义上经典继承模型(如 java/c/c++)。而是一种基于原型(prototype)的继承模型。

原型(prototype)

在一些文章中,画原型链的图时,经常用到 protoytpe 这个单词。但是同时由于 js 中的构造函数(Function/Object/Arrary 属于原生的构造函数)也拥有 prototype 这个属性,常常会给初学者造成一些困扰。现在把这些区分开画一个图,应该会比较清晰吧。

属性__proto__ 和 属性prototype

这里讲的是作为对象 属性__proto__prototype。必须声明一点:js 中仅有 function(函数) 原生 拥有 prototype 这个属性。

__proto__

普通的对象是没有 prototype 这个属性的(除非你自己定义了 a.prototype = 123)。但所有的 普通对象都有 __proto__这个私有属性:
MDN 中有这样一段

This is equivalent to the JavaScript property __proto__ which is non-standard but de-facto implemented by many browsers.
这个等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__

(就是说,__proto__是浏览器厂商们自己加的,用来实现标准中所说的 [[Prototype]])
所以,就可以确定了。一个object的原型就是 object.__proto__

prototype

由于 js 中并没有经典的 class 类模型,然后我们则使用 function 这个关键字作为 构造类的方式。

1
2
3
4
var A = function B() {
this.name = "a";
};
var a = new A();

上述例子中, new A() 会返回一个基于 A 的对象 a。而 a 的原型则指向了 A.prototype。
按照 MDN 中的解释, 这句 var a = new A(); 则相当于以下写法:

1
2
3
var a = new Object(); // 使用 Object 的构造函数构造一个对象 a
a.__proto__ = A.prototype; // 将 a 的原型指向 A 的 prototype 属性
A.call(a); // 然后在 a 上调用 A(),完成对 a 的初始化

基于原型的继承

let 和 var

我认为 js 中的变量是有生命周期的: 声明->初始化->赋值->销毁。当然,赋值阶段是可以不断重新赋值的。

var 的变量提升

1
2
3
4
5
6
7
8
9
console.log(a); // undefined
var a = 1;
console.log(a); // 1

// 由于var存在变量提升,相当于以下写法
(declare a) && (a = undefined); // 声明变量 a 并初始化为 undefined
console.log(a); // undefinedg
a = 1; // 赋值 a 为 1
console.log(a); // 1

let 的块级作用域

1
2
3
4
5
console.log(a); // Uncaught ReferenceError: a is not defined
let a;
console.log(a);
a = 1;
console.log(a);

上面这段代码是没办法执行的,因为第一行已经报错了。如果我们将它 catch 住,继续执行后面的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
try {
console.log(a);
} catch (e) {
console.log(e);
}
let a;
console.log(a);
a = 1;
console.log(a);

// 由于let的块级作用域问题,相当于以下写法
declare a; // 仅仅是声明,并未给a初始化
try {
console.log(a); // Throw an Error
} catch (e) {
console.log(e); // Uncaught ReferenceError: a is not defined
}
a = undefined; // 初始化 a 为undefined
console.log(a); // undefined
a = 1; // 赋值 a 为1
console.log(a); // 1

结论

通过以上两个对比,能看出来不管是 var or let,都存在变量提升。区别是:

  • var 会将变量的声明和初始化一并提升至作用域头部。
  • let 仅将变量的声明提升至作用域头部。

然后再看看下面这两个例子,应该差不多就懂了:

1
2
3
4
5
6
var name = "Alice";
function sayName() {
console.log(name);
let name = "Bob";
}
sayName();
1
2
3
4
5
6
let name = "Alice";
function sayName() {
console.log(name);
var name = "Bob";
}
sayName();

闭包 closure

闭包在网上有各种各样的解释,就像是哈姆雷特一样。

个人偏向 MDN 的解释

A closure is the combination of a function and the lexical environment within which that function was declared.
闭包是函数和声明该函数的词法环境的组合。

我理解的是,闭包描述的是一种现象 phenomenon 。由于 js 存在作用域链的机制,导致一些函数在声明时使用了(n 级)祖先作用域的变量。我强调了声明,是因为看到了 MDN 解释中的这一句

Running this code has exactly the same effect as the previous example of the init() function above; what’s different — and interesting — is that the displayName() inner function is returned from the outer function before being executed.

重点在于:函数在执行前被返回。
下面看一段函数:

1
2
3
4
5
6
7
function sayHello(name) {
return function() {
console.log("hello" + name);
};
}
let greet = sayHello("John");
greet();

这是经典的闭包例子。上面代码中在执行前被返回匿名函数 function anonymous(){console.log("hello" + name)} 与其所在的词法环境(fucntion sayHello)组合成为一个闭包。

同时,也经常看到一个没有利用闭包造成的反例:

1
2
3
4
5
6
7
// 预期结果是1秒后输出 0,1,2,3...9
for (var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// 然而,实际是最后输出了 10 个 10

分析时需要知道的是,for + var 并没有自己的作用域 scope,上面的 for 循环相当于以下写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 循环开始前准备(注意,var是没有块级作用域的)
var i = 0;

// 循环体
if(i < 10){
setTimeout(/*function-console*/, 1000)
}
// 循环体的后操作
i++;

// 下一个循环体及后操作
if(i < 10){
setTimeout(/*function-console*/, 1000)
}
i++;

//再重复几次循环体及后操作

而每次 setTimeout 的时候,第一个入参为一个函数,由于 if 内没有自己的 scope,此函数使用了全局变量 i。声明的时候,function-console 并没有和它所在的环境形成闭包。同时,由于 JS 单线程的机制,function-console 是在 1000ms 后才被推入事件队列中,而此时,全局变量的 i 早已经变为了 10。

如果改为一下使用 let 的写法:

1
2
3
4
5
6
for (let i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// 就能得到预期的输出

这是因为 let 在 for 中形成了一个块级作用域,相当于以下写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
// 循环开始前准备
let i = 0;

// 循环体
if(i < 10){
let temp_i = i;
setTimeout(/*function-console, use temp_i*/, 1000)
}
// 循环体的后操作
i++;

// 下一个循环体及后操作
if(i < 10){
let temp_i = i;
setTimeout(/*function-console, use temp_i*/, 1000)
}
i++;

//再重复几次循环体及后操作
}

IOS下-webkit-overflow-scrolling:touch滚动引发页面空白

ios中,我们会在css中使用 -webkit-overflow-scrolling:touch 来使元素的滚动更加顺滑。但是偶尔也会造成滚动时候,部分元素“未渲染”,呈现出空白。

为了解决这个问题,可以尝试:

(A元素:添加了-webkit-overflow-scrolling:touch)

  1. 重置A元素所有子元素的 -webkit-overflow-scrolling 属性为 unset
  2. 扩大A元素子元素的高度。(可以通过 将多个子元素放在一个div中 的方式扩大)

webpack dll-plugin 的使用方法

dll需要一份单独的webpack打包配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module.exports = {
entry: {
dlls: ['vue', '其他第三方包']
},
output: {
path: path.resolve(__dirname, './dist/dlls/'),
filename: '[name].js',
library: '[name]_[hash:6]'
},
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new webpack.DllPlugin({
path: path.join(__dirname, "dist/dlls/", "[name].manifest.json"),
name: "[name]",
context: __dirname
})
]
}

在html中添加其他script前添加

1
<script type="text/javascript" src="dlls.js的路径"></script>