Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

模块化机制 #9

Open
UNDERCOVERj opened this issue May 11, 2018 · 0 comments
Open

模块化机制 #9

UNDERCOVERj opened this issue May 11, 2018 · 0 comments

Comments

@UNDERCOVERj
Copy link
Owner

UNDERCOVERj commented May 11, 2018

模块化机制

CommonJS规范

应用于服务器端,同步加载

nodejs有一个模块加载系统

每个文件被视为一个独立的模块

module.exports属性可以被赋予一个新的值(例如函数或对象)

module.filename === __filename

module === require.main

require.resolve返回路径

module.exports

module.exports 对象是由模块系统创建, 可以导出一个对象或者函数。

module.exports === exports // true

也就是说exportsmodule.exports是指向同一内存的。而模块导出的时候实际是导出module.exports的值

缓存:

模块在第一次加载后会被缓存,是基于其文件名来缓存的

核心模块:

nodejs/lib目录下,require(),会优先加载核心模块

循环:

当循环调用require()时,一个模块可能在为完成执行被返回

例子:

a.js

console.log('a 开始');
exports.done = false;
const b = require('./b.js');
console.log('在 a 中,b.done = %j', b.done);
exports.done = true;
console.log('a 结束');

b.js

console.log('b 开始');
exports.done = false;
const a = require('./a.js');
console.log('在 b 中,a.done = %j', a.done);
exports.done = true;
console.log('b 结束');

main.js

console.log('main 开始');
const a = require('./a.js');
const b = require('./b.js');
console.log('在 main 中,a.done=%j,b.done=%j', a.done, b.done);

结果:

main 开始
a 开始
b 开始
在 b 中,a.done = false
b 结束
在 a 中,b.done = true
a 结束
在 main 中,a.done=true,b.done=true

a.jsrequire b.js, 之后又在b.jsrequire a.js,此时的
const a = require('./a.js');a是在a.js中require(b.js)语句之前导出的对象

官方解释:

main.js 加载 a.js 时,a.js 又加载 b.js。 此时,b.js 会尝试去加载 a.js。 为了防止无限的循环,会返回一个 a.jsexports 对象的 未完成的副本 给 b.js 模块。 然后 b.js 完成加载,并将 exports 对象提供给 a.js 模块。

文件模块:

如果没有找到确切的模块,文件拓展名会尝试加上.js.json.node

从 node_modules 目录加载

require一个非核心模块,也没有'/''../''./' 开头。则会从当前模块的父目录开始,尝试从它的 /node_modules 目录里加载模块。没有找到就继续父目录

AMD/CMD规范

用于浏览器端,由于要保证效率,所以需要异步加载

AMD规范实现: RequireJS

AMD规范的实现

特点

  1. 预加载(加载某个模块前,其依赖模块需要先加载),在并行加载js文件同时,还会解析执行该模块

  2. 当在head中引用js时,是阻塞的,只也是为什么推荐放在body尾。但引用了RequireJS就能解决这个问题,引入RequireJS异步加载

判断是否支持AMD规范

typeof define === "function" && define.amd

起步:

  1. index.html中引入requireJS

<script data-main="./js/app.js" src="./js/require.js"></script>

data-main指向config文件

// ./js/app.js

require.config({
	baseUrl:'../',
    paths: {
        co: '../node_modules/co/index'
    }
});
require(['./js/a.js'], function (data) {
	console.log(data)
})

// a.js是定义的一个模块,用define定义

define(function () {
	var result = {
		a: 1, 
		b: 2
	}
	alert('haha')
	return result
})
  1. 在引用模块时,可以require(arr, callback),也可以用define(arr, callback),再次导出一个对象,供别的模块使用
  2. 对于第三方模块,需要用shim来加载非AMD模块
require.config({
    shim: {
        "underscore" : {
            exports : "_";
        }
    }
})

原理

RequireJS使用head.appendChild()将每一个依赖加载为一个script标签。

RequireJS等待所有的依赖加载完毕,计算出模块定义函数正确调用顺序,然后依次调用它们。

如: <script type="text/javascript" charset="utf-8" async="" data-requirecontext="_" data-requiremodule="co" src="./js/../node_modules/co/index.js"></script>

标注async属性的脚本是异步脚本,即异步下载脚本时,不会阻塞文档解析,但是一旦下载完成后,立即执行,阻塞文档解析, 不能保证执行顺序。

优先级高于defer

因此需要确认脚本间没有依赖关系

优点

加载快速,尤其遇到多个大文件,因为并行解析,所以同一时间可以解析多个文件。

缺点

并行加载,异步处理,处理顺序不一定,可能会造成一些困扰,甚至为程序埋下大坑。

CMD规范实现:SeaJS

判断:

typeof define === "function" && define.cmd

特点:

  1. 就近加载
  2. 模块导出

既可以用exports对象来导出,也可以用return导出

模块定义:

define(id?, deps?, function(require, exports, module) {

  // 模块代码

});

数组 deps 是模块依赖,带有iddepsdefine不属于CMD规范

异步加载

require.async

使用范例:

// index.html

<script src="./js/sea.js"></script>
<script>
	seajs.config({
		base: "./",
		alias: {
			"c": "js/c.js",
			"d": "js/d.js",
			"jquery": "node_modules/co/index.js"
		}
	});
	seajs.use('c');
</script>

// c.js

define(function (require, exports, module) {
	require.async('d', function (obj) {
		console.log(obj)
	})
	console.log('sucess')
	// success
	// {a:1,b:2}
})

// d.js

define((function (require, exports, module) {
	return {
		a:1,
		b:2
	}
}))

如何工作

SeaJS采用了和Node很相似的CMD规范

Seajs分为模块加载期和执行期,加载期需要将执行期所有用到的模块从服务端同步过来,在再执行期按照代码的逻辑顺序解析执行模块。

动态添加script标签, 在network中能看到js加载,但是Element中没有插入

优点

只有在使用的时候才会解析执行js文件,因此,每个JS文件的执行顺序在代码中是有体现的,是可控的

缺点

执行等待时间会叠加。因为每个文件执行时是同步执行(串行执行),因此时间是所有文件解析执行时间之和,尤其在文件较多较大时,这种缺点尤为明显。

ES6模块化

解决在node中import报错问题

npm install --save-dev babel-cli

npm babel-node your-script.js

或者

js文件命名为.mjs

node --experimental-modules your-script.mjs

模块加载

  1. 运行时加载

如CommonJS规范,在require的时候是全部加载的

  1. 编译时加载(静态加载)

如import,只加载导入的变量或方法,其它未导入的不加载

import

好处

  1. 不再需要UMD格式
  2. 不再需要对象最为命名空间,如Math

使用严格模式

use strict

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0
  • 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • eval和arguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.caller和fn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protected、static和interface)

export

export var a = 1;
export const b = function () {}

或者

var a = 1;
var b = 2;
var c = 3;

export { a, b ,c}

或者

function v1 () {}

export {
    v1 as a
}


错误应用及修正

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

export function f () {} // 正确

export default function() {} // 正确, 加载时整体加载

等同于

export {add as default};

注意

export 输出的是与值动态绑定的接口

// b.js

var a = 1

function add () {
	a++
}

export {a, add}

// a.js

import {
	add,
	a
} from './b.js'


console.log(a);
add();
console.log(a)

而CommonJS中输出的是值的缓存

// b.js

var a = 1

function add () {
	a++
}

module.exports = {a, add}

// a.js

var obj = require('./b.js')

console.log(obj.a); // 1
obj.add();
console.log(obj.a) // 1

export只能出现在模块顶层

import

加载的变量是const常量,是只读的。

import命令具有提升效果,意思加载的函数可以再之前执行

import是静态执行,不能使用表达式或者变量

import和export同时应用

export { foo, bar } from 'my_module';

foo, 和bar在当前模块不能用,相当于转发出去了

import和export命令只能在模块的顶层,不能在代码块之中

UMD中判断模块如何加载

  • AMD或者CMD

typeof define === "function"

  • CommonJS规范

typeof module === "object" && typeof module.exports === "object"

es6模块和CommonJS的区别

  1. ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块,这是两者的一个重大差异
  2. CommonJS是运行时加载,ES6模块是编译时加载
  3. ES6模块没有arguments,require,module,exports,__filename,__dirname
  4. 不允许import CommonJS模块,如import {readFile} from 'fs'
  5. 对于循环加载,CommonJS加载有缓存机制(输出值的拷贝),循环加载时是加载已经到处的变量,
// CommonJS

// a.js
exports.done = false;
console.log('begin'); // 在循环加载a.js的时候不再执行,只是取得已经导出的
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');

// b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');

ES6模块循环加载时,那些变量不会被缓存,而是指向被加载模块的引用


// a.mjs

let done = 1
console.log('haha')
let foo = function foo () {
	console.log('foo')
}
export {foo, done};
import './b.mjs'


// b.mjs

import {foo} from './a.mjs'
console.log('b====')
foo();

// b====
// ReferenceError: foo is not defined

解决:写成函数声明,函数具有提升作用

// a.js

let done = 1
console.log('haha')
function foo () {
	console.log('foo')
}
export {foo, done};
import './b.mjs'

AMD和CMD的区别

  1. AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块 ,CMD推崇就近依赖,只有在用到某个模块的时候再去require
  2. CMD加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的。AMD在加载模块完成后就会执行改模块,所有模块都加载执行完后会进入require的回调函数, 但是主逻辑一定在所有依赖加载完成后才执行。

也就是说,AMD先加载,先执行,CMD先加载,延迟执行

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant