「读书笔记」第四版JavaScript高级程序设计(第二十六章)

205 阅读7分钟

前言

读书笔记以及额外的补充

模块模式

逻辑分块,各自封装,相互独立,每个块自行决定对外暴露什么,同时自行决定引入执行哪些外部代码。

模块系统的本质是键值对的实体,每一个模块都有可用于引用它的标示符。这个标示符,可能是字符串或者实际的路径。将模块标示符,转化为真正的模块, 不同的模块系统有不同的实现。

模块加载

加载模块其中的代码,必须在所有依赖加载完成后。如果浏览器没有收到依赖模块的代码,则必须发送请求并等待网络返回。收到模块代码之 19 后,浏览器必须确定刚收到的模块是否也有依赖。然后递归地评估并加载所有依赖,直到所有依赖模块 都加载完成。只有整个依赖图都加载完成,才可以执行入口模块。

入口

入口模块的加载,必须等待依赖加载完成后,才能执行。

异步依赖

模块在必要时加载,并提供回调,

动态依赖

动态依赖可以支持更复杂的依赖关系,但代价是增加了对模块进行静态分析的难度。

if (loadCondition) {
    require('./moduleA');
}

静态分析

对静态分析友好的模块系统可以让模块打包系统更容易将代码处理 为较少的文件。对于动态依赖,静态分析将会变得更加困难。

循环依赖

CommonJS,AMD,ES6模块都支持循环依赖,加载器会执行深度优先的依赖加载。

凑合的模块系统

在ES6模块之前,使用IIFE(立即调用函数表达式)将模块封装在匿名闭包之中。

// FOO模块
const Foo = (function() {
  return {
    bar: 'baz',
    baz: function() {
        return this.bar
    };
})();

Foo.bar
Foo.baz()

// 泄漏模块模式
const Foo = (function() {
    const bar = 'baz';
    const baz = function() {
        console.log(bar);
    };
    return {
        bar: bar,
        baz: baz
    };
})();
Foo.bar
Foo.baz()

IIFE模块也可以接受外部传参

const params = 100;
const Foo = (function(params) {
    const bar = params;
    const baz = function() {
        console.log(bar);
    };
    return {
        bar: bar,
        baz: baz
    };
})(params);

CommonJS模块

Nodejs采用的是经过修改的CommonJS规范

CommonJS模块,使用require指定依赖,使用exports对象定义模块的API。模块赋值给变量是常见的形式。但不是必须的。在一个模块中,引入另外一个模块多次,都只会加载一次,模块是单例模式。模块在加载过一次后,会被缓存(如果是第一次加载,会加载模块,如果不是第一次加载改模块,会直接使用模块的引用)。

const moduleB = require('./moduleB');

module.exports = {
    stuff: moduleB.doStuff();
};

在CommonJS中模块加载是同步的操作,CommonJS支持动态依赖

if (loadCondition) {
    require('./moduleA');
}

CommonJS模块如果想在浏览器中使用,需要使用到打包工具。打包工具通常会将模块的代码,打包到函数闭包中(还是依赖IFEE实现的😂)。

AMD模块

AMD模块使用函数包装模块定义,模块代码放在函数里,避免了全局污染,可以防止全局变量的出现,方便加载器库控制何时加载模块,包装模块的函数是define,define函数是由AMD自己实现的,AMD会在依赖模块异步加载完成后,立即调用模块的工厂函数

// 模块名为moduleA,依赖了moduleB,导出tuff
// moduleB是异步加载的
define('moduleA', ['moduleB'], function(moduleB) {
    return {
        tuff: moduleB.doStuff();
    };
});

AMD模块内部同样支持require,exports关键字, 实现CommonJS风格的模块。

define('moduleA', ['require', 'exports'], function(require, exports) {
    var moduleB = require('moduleB');
    exports.stuff = moduleB.doStuff();
});

define('moduleA', ['require'], function(require) {
    if (condition) {
        var moduleB = require('moduleB');
    }
});

UMD模块

UMD模块是一种通用的模式,用于兼容AMD和CommonJS的规范。UMD规范同时兼容amd和commonjs,并支持传统的全局变量的模式。

// UMD示例,不过这些样板代码,应该由构建工具自动生成
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(['jquery'], factory);
  } else if (typeof exports === 'object') {
    // CommonJS
    module.exports = factory(require('jquery'));
  } else {
    // 全局变量模式
    // root是window对象
    root.returnExports = factory(root.jQuery);
  }
}(this, function ($) {
  // factory 函数,用于消费模块
}));

ES6模块

带有type="module"的script标签,会告诉浏览器应作为模块执行。当浏览器解析到

<!-- 第二个执行 -->
<script type="module"></script>
<!-- 第三个执行 -->
<script type="module"></script>
<!-- 第一个执行 -->
<script></script>

模块加载

完全支持 ECMAScript 6 模块的浏览器可以从顶级模块加载整个依赖图,且是异步完成的。浏览器 会解析入口模块,确定依赖,并发送对依赖模块的请求。这些文件通过网络返回后,浏览器就会解析它 们的内容,确定它们的依赖,如果这些二级依赖还没有加载,则会发送更多请求。这个异步递归加载过 程会持续到整个应用程序的依赖图都解析完成。解析完依赖图,应用程序就可以正式加载模块了。

模块行为

  1. 模块代码只在加载后执行。
  2. 模块只能加载一次。
  3. 模块是单例。
  4. 模块可以定义公共接口,其他模块可以基于这个公共接口观察和交互。
  5. 模块可以请求加载其他模块。
  6. 支持循环依赖。
  7. ES6模块默认在严格模式下执行。
  8. ES6模块不共享全局命名空间。
  9. 模块顶级 this 的值是 undefined(常规脚本中是 window)。  模块中的 var 声明不会添加到 window 对象。
  10. ES6模块是异步加载和执行的。

模块导出

导出分为默认导出和命名导出,export只允许在顶级块中使用,export的位置对模块没有影响,export可以出现在导出值的之前。

// 命名导出
// 行内命名导出
export const foo = 'foo';
// 命名导出
const foo = 'foo';
export { foo };
// 命名导出的别名
const foo = 'foo';
export { foo as myFoo };
// 一个模块可以有多个命名导出
const foo = 'foo';
const bar = 'bar';
const baz = 'baz';
export { foo, bar as myBar, baz };
// 或者这样做
export const foo = 'foo';
export const bar = 'bar';
export const baz = 'baz';


// 默认导出
// 默认导出使用default关键字
// 一个模块内,可以同时包含命名导出,和默认导出
const foo = 'foo';
export default foo;

// 命名导出如果设置别名为default等同于默认导出
// 下面的两者等同
export { foo as default };
export default foo;
// 既实现了默认导出也实现命名导出
export { foo as default, foo };

模块导入

import只能出现在模块的顶级,import会被自动提升到模块的顶级,所以import在模块中的位置并不重要。导入的内容对于模块是只读的。

默认导出与命名导出的区别主要体现在导入上的不同

// 对于默认导出
import { default as foo } from './foo.js';
import foo from './foo.js';

// 对于命名导出
import { foo, bar, baz as myBaz } from './foo.js';
import * as Foo from './foo.js';

模块转移导出

这快阮老师的书讲解的更细致

export * from './foo.js';

// 等同于
import { a, b } from './foo.js';
export a;
export b;

// 或者直接确认要导出的值
export { foo, bar as myBar } from './foo.js';

对于默认导出,可以这样做

// 对于默认模块
export { default } from './foo.js';

// 等同于
import Foo from './foo.js';
export default Foo;

也可以将命名导出,修改为默认导出

export { foo as default } from './foo.js';

或者将默认导出修改为命名导出

export { default as foo } from './foo.js'

工作者模块

Worker可以接受模块文件,但是需要在第二个参数中说明


const scriptWorker = new Worker('scriptWorker.js');
const moduleWorker = new Worker('moduleWorker.js', { type: 'module' });

向后兼容

推荐看一下这两篇文章,

philipwalton.com/articles/de…

philipwalton.com/articles/us…

这是向后兼容的示例,经过我的测试,包的大小缩小了一半。

github.com/peoplesing1…

参考