深入javascript计划八:深入浅出模块化

传统加载

HTML 网页中,浏览器通过<script>标签加载 JavaScript 脚本。

页面内嵌脚本:

<script type="application/javascript">
  // module code
</script>

由于浏览器脚本的默认语言是 JavaScript,因此type="application/javascript"可以省略。

外部脚本:

<script type="application/javascript" src="path/to/myModule.js"></script>

 异步脚本:

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

defer与async有什么区别?

defer:要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行(通俗来讲就是:(顺序加载,渲染完再执行)。

多个defer:会按照它们在页面出现的顺序加载。

async:一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染(通俗来讲就是:乱序加载,下载完就执行)。

多个async:脚本是不能保证加载顺序的。

浏览器是如何加载js脚本的?

默认情况下:浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染(如果是外部脚本,还必须加入脚本下载的时间)。

所以我们经常把<script>标签放在后面。

如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。

这显然是很不好的体验,所以浏览器允许脚本异步加载,下面就是两种异步加载的语法:

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

上面代码中,<script>标签打开defer或async属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。

模块化加载

为什么要使用模块化?

  • 解决命名冲突
  • 提供复用性
  • 提高代码可维护性

外部脚本:

type属性设为module,所以浏览器知道这是一个 ES6 模块。

<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>

如果网页有多个<script type="module">,它们会按照在页面出现的顺序依次执行。 

也可以设置成async,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。

<script type="module" src="./foo.js" async></script>

页面内嵌脚本:

<script type="module">
  import utils from "./utils.js";

  // other code
</script>

注意点:

  1. 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
  2. 模块脚本自动采用严格模式,不管有没有声明use strict。
  3. 模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。
  4. 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。
  5. 同一个模块如果加载多次,将只执行一次。

AMD 和 CMD

鉴于目前这两种实现方式已经很少见到,所以不再对具体特性细聊,只需要了解这两者是如何使用的。

AMD:

define(['./a', './b'], function(a, b) {
  // 加载模块完毕可以使用
  a.do()
  b.do()
})

CMD:

define(function(require, exports, module) {
  // 加载模块
  // 可以把 require 写在函数体的任意地方实现延迟加载
  var a = require('./a')
  a.doSomething()
})

CommonJS

CommonJS 最早是 Node 在使用,目前也仍然广泛使用,比如在 Webpack 中你就能见到它,当然目前在 Node 中的模块管理已经和 CommonJS 有一些区别了。

模块导出:

关键字:module.exports、exports

// foo.js

// 一个一个 导出
module.exports.age = 1
module.exports.foo = function() {}
exports.a = 'hello'

// 整体导出
module.exports = { 
    age: 1, 
    a: 'hello', 
    foo:function(){} 
}

// 整体导出不能用`exports` 用exports不能在导入的时候使用
exports = { 
    age: 1, 
    a: 'hello', 
    foo:function() {} 
}
// 这里需要注意 exports 不能被赋值,可以理解为在模块开始前exports = module.exports, 因为赋值之后exports失去了 对module.exports的引用,成为了一个模块内的局部变量。

模块导入:

关键字:require

const foo = require('./foo.js')
console.log(foo.age) //1

ES6 Module

由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。

模块导出:

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。

关键字:export

// a.js

// 声明命名导出
export var name= 'an';
export var year = 1998;

// 函数导出
export function multiply(x, y) {
  return x * y;
};

// 命名导出
var name= 'an';
var year = 1958;
export { name, year };

// 重命名导出
export { name as names, year as years };

需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

// 报错
export 1;

// 报错
var m = 1;
export m;

// 分割线

// 写法一
export var m = 1;

// 写法二
var m = 1;
export {m};

// 写法三
var n = 1;
export {n as m};

// 分割线

// 报错
function f() {}
export f;

// 正确
export function f() {};

// 正确
function f() {}
export {f};

另外,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新。

注意点:

export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,import命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。

模块导入:

关键字:import

// main.js
import { name, year } from './a.js';

// 重命名
import { name as n, year as y } from './a.js';

import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。

import {a} from './xxx.js'

a = {}; // Syntax Error : 'a' is read-only;

上面代码中,脚本加载了变量a,对其重新赋值就会报错,因为a是一个只读的接口。但是,如果a是一个对象,改写a的属性是允许的。

import {a} from './xxx.js'

a.foo = 'hello'; // 合法操作

import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js后缀可以省略。

注意,import命令具有提升效果,会提升到整个模块的头部,首先执行。

foo();

import { foo } from 'my_module';

上面的代码不会报错,因为import的执行早于foo的调用。这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。

由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

// 报错
import { 'f' + 'oo' } from 'my_module';

// 报错
let module = 'my_module';
import { foo } from module;

// 报错
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}

上面三种写法都会报错,因为它们用到了表达式、变量和if结构。在静态分析阶段,这些语法都是没法得到值的。

最后,import语句会执行所加载的模块,因此可以有下面的写法。

import 'xxx.js';

如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。

import 'xxx.js';
import 'xxx.js';

整体加载

导出:

// a.js
export function area(radius) {
  return Math.PI * radius * radius;
}
export function circumference(radius) {
  return 2 * Math.PI * radius;
}

导入:

import * as circle from './a.js';

console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

ES Module与CommonJS区别

  1. CommonJS 支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
  2. CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  3. CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  4. ES Module 会编译成 require/exports 来执行的

参考:

https://es6.ruanyifeng.com/#docs/module

https://segmentfault.com/a/1190000017878394?utm_source=tag-newest

https://juejin.im/book/5bdc715fe51d454e755f75ef/section/5bdd0d83f265da615f76ba57#heading-6

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

An_s

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值