[JS高级]ESModule和commonJS的原理和执行过程

一、前言

文章最后有四个面试题,如果第三道和第四道能写出正确的答案,并且非常肯定,那么您对当前主流的模块化理解比较到位,本文章不能给与很多帮助。

二、模块化的演变和简单使用

模块化是将程序划分成一个个小的结构,每个结构中编写属于自己的逻辑代码,有自己的作用域,定义变量名词时不会影响到其他的结构,这个结构可以将自己希望暴露的变量、函数、对象等导出给其他结构使用,也可以通过某种方式,导入另外结构中的变量、函数、对象等

2.1 IIFE(立即执行函数)

早期没有模块化带来了很多的问题:比如命名冲突的问题,聪明的开发者开始利用 JavaScript 语言的函数作用域,使用闭包的特性来解决

	// index.html
	<script src="./mine.js"></script>
	<script src="./a.js"></script>
	<script src="./b.js"></script>
	 
	// mine.js
	app.mine = (function(){
	    var name = 'morrain'
	    var age = 18
	    return {
	        getName: function(){
	            return name
	        }
	    }
	})()
	 
	// a.js
	app.moduleA = (function(){
	    var name = 'lilei'
	    var age = 15
	    return {
	        getName: function(){
	            return name
	        }
	    }
	})()
	 
	// b.js
	app.moduleB = (function(){
	    var name = 'hanmeimei'
	    var age = 13
	    return {
	        getName: function(){
	            return name
	        }
	    }
	})()

这样的设计,已经有了模块化的影子,每个模块内部维护私有的东西,开放接口给其它模块使用,但依然不够优雅,不够完美。譬如上例中,模块B可以取到模块A的东西,但模块A却取不到模块B的,因为上面这三个模块加载有先后顺序,互相依赖。当一个前端应用业务规模足够大后,这种依赖关系又变得异常难以维护。

2.2 CommonJS

JavaScript社区为了解决上面的问题,涌现出一系列好用的规范,接受度最高的就是commonJs,
commonJs不是前端却革命了前端

// addA.js
var a = 1;
var addA = function(value) {
  return value + x;
}
module.exports.addA = addA;

CommonJs看起来是一个很不错的方案,拥有模块化所需要的严格的入口和出口,看起来一切都很美好,但它的一个特性却决定了它只能在服务器端大规模使用,而在浏览器端发挥不了太大的作用,那就是同步!这一特点决定了他只适合做服务器端的程序

2.3 ES module

JavaScript没有模块化一直是它的痛点,所以才会产生一些社区规范:CommonJS、AMD、CMD等,所以在ECMA在2015年推出自己的模块化系统时,大家也是异常兴奋。

// export关键字后直接跟变量声明
export const name = "lyj";
export const age = 18;
import { name, age } from "./foo.js";
console.log(name, age);

三、模块化的原理

3.1 commonJs的本质

整个commonJs的本质其实就是一个require函数
require 的源码在 Node 的 lib/module.js 文件。
require函数的伪代码如下:

const cache = {}//require函数外部维护了一个缓存容器
function require(modulePath){
	//1.根据传递的模块路径,得到模块完整的绝对路径
	var moduleId = getModuleId(modulePath)
	//2.判断缓存
	if(cache[moduleId]){
		return cache[moduleId].exports;
	}
	//3.真正运行模块代码的辅助函数
	function _require(exports,require,module,__filename,__dirname){
		//目标模块的代码在这里(直接把modulePath对应的代码复制到这里)
		//目标模块的代码在这里(直接把modulePath对应的代码复制到这里)
		//目标模块的代码在这里(直接把modulePath对应的代码复制到这里)
		//目标模块的代码在这里(直接把modulePath对应的代码复制到这里)
		//目标模块的代码在这里(直接把modulePath对应的代码复制到这里)
		//目标模块的代码在这里(直接把modulePath对应的代码复制到这里)
	}
	//4.准备并运行辅助函数
	var module = {
		exports:{},
	}
	var exports = module.exports;
	//得到模块文件的绝对路径
	var __filename = moduleId;
	//得到模块所在目录的绝对路径
	var __dirname  = getDirname(__finame);
	//5.缓存module.exports
	cache[moduleId] = module
	//6.执行
	_require.call(exports,exports,require,module,__filename,__dirname);
	//module.loaded = true,给module打个标记,这里就不在乎这些细节了
	//7.返回module.exports
	return module.exports;
}
function getModuleId(modulePath){
	//情况一:X是一个Node核心模块,比如path、http直接返回核心模块,并且停止查找
	//情况二:X是以 ./ 或 ../ 或 /(根目录)开头的
		//第一步:将X当做一个文件在对应的目录下查找;
			//1.如果有后缀名,按照后缀名的格式查找对应的文件
			//2.如果没有后缀名,会按照如下顺序:
				//1> 直接查找文件X
				//2> 查找X.js文件
				//3> 查找X.json文件
				//4> 查找X.node文件
		//第二步:没有找到对应的文件,将X作为一个目录,查找目录下面的index文件
				//1> 查找X/index.js文件
				//2> 查找X/index.json文件
				//3> 查找X/index.node文件
   		//这两步如果没有找到,那么报错:not found
   //情况三:直接是一个X(没有路径),并且X不是一个核心模块
   		//从本目录底下的node_modules下查找,如果找不到,那么依次找上一层的node_modules
}

如果有循环引入,形如:

在这里插入图片描述
由此可以发现,所有的模块加载,其实是一种数据结构:
并且在加载中使用的是图的深度优先搜索DFS:
main -> aaa -> ccc -> ddd -> eee ->bbb

因为require函数是同步的,所以服务器端是可以正常使用的,但是如果将它应用于浏览器环境,那么将会出现阻塞
在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD(本文不做介绍)
到现在,
一方面现代的浏览器已经支持ES Modules,
另一方面借助于webpack等工具可以实现对CommonJS或者ES Module代码的转换,转化成最原始的IIFE,这样保证所有浏览器都能正常运行

3.2 ESModule的本质及执行过程

ESModule是ESMA推出的新语法,源码是v8引擎的C++代码,不同于使用JS实现commonJS,所以源码就不去深究了
以这个场景为例子,介绍ESModule模块化的执行过程:
在这里插入图片描述
esmodule的执行分为两步执行:
第一步:模块解析

浏览器拿到html文件后,去下载main.js
拿到main.js后,解析内容里所有的静态导入语句
import foo from ‘./foo.js’;
import bar from ‘./bar.js’;
简单点说第一步就是递归查询代码里的import语句,然后对应文件下载下来
可能有人会问:这不是也是要下载文件吗,不是说esmodule是异步的吗?
下载文件是不可避免的,浏览器对有type="module"属性的script标签,都是异步加载,即等整个页面渲染完成,再执行模块脚本,等同于script标签的defer属性

第二步:模块的执行

执行的方式就是从main.js依次执行,此处参考这张图

在这里插入图片描述
1.解析所有的代码,找到import语句(不是import函数),将import的from后的文件下载下来
2.执行
main.js第一句:从foo.js读取foo,因为foo.js未执行过,所以执行foo.js
foo.js第一句:从bar.js读取bar,因为bar.js未执行过,所以执行bar.js
bar.js第一句:打印“bar”
bar.js第二句:导出“bar”,就是给bar.js模块的map添加default:“bar”,bar.js执行完成
foo.js第二句:打印“foobar”
foo.js第三句:导出“foo”,就是给foo.js模块的map添加default:“foo”,foo.js执行完成
main.js第二句:从bar.js读取bar,因为bar.js已经执行过,所以不再执行,而是直接读取,这里的bar变量与bar.js模块的map的default进行符号绑定,所以bar=“bar”
main.js第三句:动态导入语句,import方法暂时加入微任务队列,放到同步代码最后执行
main.js第六句:打印“mainfoobar”,此时所有同步代码执行完成,执行微任务队列中的代码
main.js第三句:执行动态import函数,动态import执行时,也是经过解析和执行两个过程,返回一个Promise,值是对应模块的这个map,对应这里就是下载dynamic.js及其引用的js,下载完成后,执行
dynamic第一句:从bar.js读取bar,因为bar.js已经执行过,所以不再执行,而是直接读取,这里的bar变量与bar.js模块的map的default进行符号绑定,所以bar=“bar”
dynamic第二句:打印‘dynamic bar’
dynamic第三句:导出“dynamic”,就是给dynamic.js模块的map添加default:“dynamic”,dynamic.js执行完成,下载和执行都完成,promise改为完成状态,then的回调开始执行
main.js第五句:打印main,m.default , m.default是“dynamic”,所以最后打印的是“main dynamic”

四、面试题

面试题一(commonJs):

请问:1.js中最后打印的是什么

//2.js
this.a = 1;
exports.b = 2;
exports = {
	c:3
}
module.exports = {
	d:4
}
exports.e = 5
this.f = 6
//1.js
const result = require('./2.js')
console.log(result)

答案:{d:4}
解析:

this.a = 1; 			//this,exports,module.exports=>{a:1}
exports.b = 2;  		//this,exports,module.exports=>{a:1,b:2}
exports = {c:3} 		//this,module.exports=>{a:1,b:2}  exports=>{c:3}
module.exports = {d:4}  //this=>{a:1,b:2}   			  exports=>{c:3}           module.exports=>{d:4}
exports.e = 5		    //this=>{a:1,b:2}            	  exports=>{c:3,e:5}       module.exports=>{d:4}
this.f = 6				//this=>{a:1,b:2,f:6}             exports=>{c:3,e:5}       module.exports=>{d:4}

而通过看require的伪代码,最终导出的是module.exports,所以是{d:4}

面试题二(ESModule)

当执行a.js时,控制台打印什么?

// a.js如下

import {bar} from './b.js';

console.log('a.js');

console.log(bar);

export let foo = 'foo';

// b.js

import {foo} from './a.js';

console.log('b.js');

console.log(foo);

export let bar = 'bar';

答案:

b.js
undefined
a.js
bar

解析:

上面的代码中,由于a.js的第一行是加载b.js,所以先执行的是b.js。而b.js的第一行又是加载a.js,这时由于a.js已经开始执行,所以不会重复执行,而是继续执行b.js,因此第一行输出的是"b.js"。

接着,b.js要打印变量foo,这时a.js还没有执行完,取不到foo的值,因此打印出来的是undefined。b.js执行完export后,这个模块中维护的map结构已经维护完成,之后会开始执行a.js,首先打印"a.js",然后打印bar变量,此时会进行符号绑定,把bar变量和b.js的map结构中的bar绑定,所以打印b.js的map中的bar的值,也就是“bar”

面试题三(ESModlue)

当执行main.js时,控制台打印什么?

//main.js
import {foo} from './a.js';
foo()
//a.js
import {bar} from './b.js';
export function foo() {
  console.log('foo');
  bar();
  console.log('执行完毕');
}
// b.js
import {foo} from './a.js';
export function bar() {
  console.log('bar');
  if (Math.random() > 0.5) {
    foo();
  }
}

答案:

foo
bar
执行完毕
//也有可能是
foo
bar
foo
bar
执行完毕
执行完毕
//也有可能是多次

解析:

首先是解析阶段,就是下载main.js , a.js和b.js 其次是执行阶段:
main.js第一句:是从a.js导入foo变量,因为a.js还未执行,所以开始执行a.js
a.js第一句:是从b.js导入bar变量,因为b.js还未执行,所以开始执行b.js
b.js第一句:是从a.js导入foo变量,因为a.js已经开始执行,所以从a模块的map结构中查找foo,没找到,所以foo目前是undefined
b.js第二句:b.js导出bar变量,就是填充b.js的map中加一个bar:此函数,b.js执行完成,继续执行a.js
a.js第二句:a.js导出foo变量,就是填充a.js的map中加一个foo:此函数,a.js执行完成,继续执行main.js
main.js第二句:执行foo:
a.js第三句:____打印“foo“
a.js第四句:____执行bar
b.js第三句:______打印”bar“
b.js第四句:______情况一、如果随机数大于0.5执行foo,(这里容易出错,foo是undefined呢,还是有值呢,答案是有值,原因就是因为符号绑定)
a.js第三句:________打印“foo”
a.js第四句:________执行bar
b.js第三句:__________打印“bar”
b.js第四句:__________如果随机数大于0.5执行foo…此处无穷不尽,省略
a.js第五句:________打印“执行完毕”
b.js第四句:______情况二、如果随机数小于于0.5,什么也不做
a.js第五句:______打印“执行完毕”

面试题四(commonJS)

同样的代码,将面试题三的esmodule换成commonjs
当执行main.js时,控制台打印什么?

//main.js
const {foo} = require('./a.js');
foo()
//a.js
const {bar}= require('./b.js');
module.exports.foo = function() {
  console.log('foo');
  bar();
  console.log('执行完毕');
}
// b.js
const {foo}=  require('./a.js');
module.exports.bar = function() {
  console.log('bar');
  if (Math.random() > 0.5) {
    foo();
  }
}

答案:

报错 foo is not a function
//或者
foo
bar
执行完毕

解析:

main.js第一句:执行require函数,首先查看有没有a.js对应模块的缓存,发现没有,那么执行a.js
a.js第一句:执行require函数,首先查看有没有b.js对应模块的缓存,发现没有,那么执行b.js
b.js第一句:执行require函数,首先查看有没有a.js对应模块的缓存,发现有,于是不执行a.js
直接读取cache[a.js]={},所以{}.foo是undefine,因为js是值绑定,所以foo除了重新赋值,否则值一直是undefined(这里区别于esmodule的符号绑定)
b.js第二句:将b模块的结果导出,此时a.js的第一句执行完成,bar赋值为一个函数
a.js第二句:将a模块的结果导出
mian.js第二句:执行foo
a.js第三句:打印“foo”
a.js第四句:执行bar
b.js第三句:打印“bar”
b.js第四句:情况1,随机数大于0.5 执行foo(报错)
b.js第四句:情况2,随机数小于0.5 不执行foo
a.js第五句:打印”执行完毕“

  • 15
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

刘乙江

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

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

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

打赏作者

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

抵扣说明:

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

余额充值