文献 www.manongjc.com/detail/51-l…
es6.ruanyifeng.com/#docs/modul…
js加载机制(假设总是本地脚本index.js下载更快)
- index.js
try {
console.log(_.VERSION);
} catch (error) {
console.log('Lodash Not Available');
}
console.log(document.body ? 'YES' : 'NO');
示例1
- index.html
<head>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.10/lodash.min.js"></script>
<script src="./index.js"></script>
</head>
<body>
</body>
- 控台结果
4.17.10
NO
- 结果说明:浏览器加载脚本是采用同步模型的,都会阻塞浏览器的解析器,位于该 script 标签以上的 DOM 元素是可用的,位于其以下的 DOM 元素不可用
示例2 script上加上async属性
- index.html
<head>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.10/lodash.min.js" async></script>
<script src="./index.js" async></script>
</head>
<body>
</body>
- 控台结果
Lodash Not Available
YES
- 结果说明:
async不会阻塞 HTML 解析器, async 脚本每个都会在下载完成后立即执行,无关 script 标签出现的顺序
示例3 script上加上defer属性
- index.html
<head>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.10/lodash.min.js" defer></script>
<script src="./index.js defer></script>
</head>
<body>
</body>
- 控台结果
4.17.10
YES
-
结果说明:
defer不会阻塞 HTML 解析器, - defer 脚本会根据 script 标签顺序先后执行
defer与async的区别是:defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer是“渲染完再执行”,async是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。
模块化之前 变量污染,请求多个script脚本性能浪费,依赖模糊
// 全局定义变量 容易变量污染
function foo(){
}
function bar(){
}
// Namespace模式: 减少全局变量数量,但还是存在污染问题,还是可以通过对象改写数据,不安全
var myUtils = {
foo:function(){},
bar:function(){}
}
// 匿名闭包(IIFE模式):
var module = (function () {
var age = 9;
var foo = function () {
console.log('age', age);
}
return {
foo,
age
}
})()
module.age++
console.log('module.age', module.age);// 10
module.foo() // 9
// IIFE模式,传入依赖
var module = (function ($, window) {
var age = 9;
var foo = function () {
console.log('age', age);
}
return {
foo,
age
}
})(jQuery, window)
模块化的诞生
目的:为避免全局污染,命名冲突,按需加载,提高复用和可维护性
- 分类:commonjs amd es6
commonjs
- CommonJS期初主要用于Node.js环境,采用同步加载模式,如果想运行于浏览器端,需要运行其编译后的产物
- commonjs表现形式:对外暴露
exports / module.exports,引入则用var module = require('xxx.js')
// module1.js 1个文件可以有多个exports
exports.foo = function(){
console.log('module1---foo')
}
exports.bar = function(){
console.log('module1---bar')
}
// module2.js 1个文件只有1个module.exports
module.exports = {
foo(){
console.log('module2---foo')
}
}
// 入口文件 main.js
var module1 = require('./module1.js')
var module2 = require('./module2.js')
module1.foo()
module2.foo()
//index.html需要运行main.js编译后的产物才不会报错
- CommonJS模块缓存
// lib.js
console.log('run lib.js')
module.exports = {
num: 1
}
//main.js
let number1 = require("./lib");
let number2 = require("./lib");
number2.num = 2
let number3 = require("./lib");
console.log(number3)
// run lib.js
// { num: 2 }
// 多次require加载number模块,但是内部只有一次打印输出;
// 第二次加载时还改变了内部变量的值,第三次加载时内部变量的值还是上一次的赋值,这就证明了后面的`require`读取的是缓存。
- CommonJS对于基本类型的输出是深拷贝(一旦模块输出了值,模块
内部的变化就影响不到这个值),对应引用类型是浅拷贝(对于复杂数据类型,由于CommonJS进行了浅拷贝,因此如果两个脚本同时引用了同一个模块,对该模块的修改会影响另一个模块)
// CommonJS 一旦输出一个值,模块内部的变化就影响不到这个值
// lib.js
var counter = 3;
var user = {
name: "bwf",
list: ["11", "22"],
};
function incCounter() {
counter++;
}
function updateUser() {
user.name = "xxxx";
}
module.exports = {
counter: counter,
incCounter: incCounter,
user: user,
updateUser: updateUser,
};
//main.js
var mod = require("./lib");
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
console.log("mod.user", mod.user); // { name: 'bwf', list: [ '11', '22' ] }
mod.updateUser();
console.log("update---mod.user", mod.user); // { name: 'xxxx', list: [ '11', '22' ] }
- CommonJS 运行时加载(
但也正是由于这种动态加载,导致没有办法在编译时做静态优化。)
let num = 10;
if (num > 2) {
var a = require("./a");
} else {
var b = require("./b");
}
var moduleName = 'number.js'
var number = require(`./${moduleName}`)
amd(Requirejs)
- amd 运行于浏览器端,通过异步加载模块,主要解决全局命名空间污染,相互依赖脚本加载顺序问题
- amd表现形式:定义模块define([‘依赖项’],function(依赖对象){}),引用则通过requirejs
// module1.js (没有依赖的模块)
define(function(){
var age = 9;
function getAge(){
return age;
}
// 暴露
return {
getAge
}
})
// module2.js(有依赖的模块)
define(['module1'], function(module1){
function showAge(){
console.log('module2---showAge', module1.getAge)
}
return {
showAge
}
})
// main.js
requirejs.config({
// baseUrl:'', // 基础路径
paths:{
module1: './module1', // key:define第一个参数的模块命名, 第二个参数是该模块对应的文件路径
module2: './module2'
}
})
requirejs(['module2'], function(module2){
module2.showAge()
})
// index.html require.js是提前准备好的库
<script data-main="./main.js" src="./lib/require.js">
es6模块
- es6模块 模块的导入是静态的,并且模块只有在被引用时才会执行,支持 Tree Shaking, 需要经过编译后运行于浏览器端
- es6表现形式:
export / export default,引入import {命名一致} from '' / import utils from ''
// 导出变量、函数和类
export const variable = 42;
export function add(a, b) { return a + b; }
export class Person { /* ... */ }
// 默认导出
export default function() {
console.log('This is the default export.');
}
// 命名导入
import { variable, add, Person } from './module.js';
// 默认导入
import myFunction from './module.js';
es6模块的延伸(vite开发环境运行快也是基于现代浏览器对ESM的支持)
- 本身es6模块需要经过编译,浏览器运行编译后的产物才不会报import等语法错误,但是基于现代浏览器对##
<script type=module>的支持,使得浏览器以 ES Module 的方式加载脚本 - 默认情况下 ES 脚本是 defer 的,无论内联还是外联,- 给 script 标签显式指定
async属性,可以覆盖默认的 defer 行为 - 安全策略更严格,非同域脚本的加载受 CORS 策略限制
ES6 模块与 CommonJS 模块的差异
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。(`CommonJS会缓存结果,ES6不会)
// lib.mjs
export let counter = 3;
export function incCounter() {
counter++;
}
// main.mjs
import { counter, incCounter } from './lib.mjs';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
// node main.mjs
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJS 模块的
require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。