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

你真的了解esModule吗 #6

Open
maicFir opened this issue Aug 11, 2022 · 0 comments
Open

你真的了解esModule吗 #6

maicFir opened this issue Aug 11, 2022 · 0 comments

Comments

@maicFir
Copy link
Owner

maicFir commented Aug 11, 2022

项目中我们常常会接触到模块,最为典型代表的是esModulecommonjs,在es6之前还有AMD代表的seajs,requirejs,在项目模块加载的文件之间,我们如何选择,比如常常因为某个变量,我们需要动态加载某个文件,因此你想到了require('xxx'),我们也常常会用import方式导入路由组件或者文件,等等。因此我们有必要真正明白如何使用好它,并正确的用好它们。

以下是笔者对于模块理解,希望在实际项目中能给你带来一点思考和帮助。

正文开始...

关于script加载的那几个标识,deferasyncmodule

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>module</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="./js/2.js" defer></script>
    <script src="./js/1.js" async></script>
    <script src="./js/3.js">
      console.log('同步加载', 3)
    </script>
  </body>
</html>
// js/2.js
console.log('defer加载', 2);
// js/1.js
console.log('async异步加载不保证顺序', 1);

// js/3.js
console.log('同步加载', 3);

我们会发现执行顺序是3,1,2

deferasync是异步的,而同步加载的 3,在页面中优先执行了。在执行顺序中,我们可以知道标识的defer是等同步的3async的1执行后才最后执行的。

为了证明这点,我们在1.js中加入一段代码

// 1.js
console.log('没有定时器的async', 1);
setTimeout(() => {
  console.log('有定时器的async,异步加载不保证顺序', 1);
}, 1000);

最后我们发现打印的顺序,同步加载3(没有定时器的async)1defer加载2有定时器的async,异步加载不保证顺序1

因为1.js加入了一段定时器,在事件循环中,它是一段宏任务,我们知道在浏览器处理事件循环中,同步任务>异步任务[微任务 promise>宏任务 setTimeout,事件等],在2.js中用defer标识了自己是异步,但是1.js中有定时器,2.js实际上是等了1.js执行完了,再执行的。

如果我在2.js中也加入定时器呢

console.log('没有定时器的defer加载', 2);
setTimeout(() => {
  console.log('有定时器的defer加载', 2);
}, 1000);

我们会发现结果依然是如此

3.js 同步加载 3
1.js 没有定时器的async 1
2.js 没有定时器的defer加载 2
1.js 有定时器的async,异步加载不保证顺序 1
2.js 有定时器的defer加载 2

不难发现 defer中的定时器脚本虽然在async标识的脚本前面,但是,注意两个定时器实际上是会有前后顺序的,跟脚本的顺序没有关系

两个任务都是定时器,都是宏任务,在脚本的执行顺序中第一个定时器会先放到队列任务中,第二个定时器也会放到队列中,遵循先进先出,第一个宏任务(1.js 有定时器)先进队列,然后2.js定时器再进入队列,后面再执行。

但是注意,定时器时间短的优先进入队列。

好了,搞明白deferasync的区别了,总结一句,defer会等其他脚本加载完了再执行,而async是异步的,并不一定是在前面的就先执行。

module

接下来我们来看看module

module是浏览器直接加载es6,我们注意到加载module中有哪些特性?

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>module</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="./js/2.js" defer></script>
    <script src="./js/1.js" async></script>
    <script src="./js/3.js"></script>
    <script type="module">
      import userInfo, { cityList } from './js/4.js';
      console.log(userInfo);
      // { name: 'Maic', age: 18}
      console.log(cityList);
      console.log(this); // undefined
      /* 
        [ {
         value: '北京',
         code: 1
        },
        {
          value: '上海',
          code: 0
        }
      ] 
     */
    </script>
  </body>
</html>

js/4.js中,我们可以看到可以用esModule的方式输出

export default {
  name: 'Maic',
  age: 18
};
const cityList = [
  {
    value: '北京',
    code: 1
  },
  {
    value: '上海',
    code: 0
  }
];
export { cityList };

scripttype="module"后,内部顶层this就不再是window对象了,并且引入的外部路径不能省略后缀,且脚本自动采用严格模式。

es6 的模块与 commonJS 的区别

通常我们在项目中都是es6模块,在nodejs中大量模块代码都是采用commonjs的方式,既然项目里都有用到,那么我们再次回顾下他们有什么区别

参考module 加载实现中写道

1、commonjs输出的是一个值的拷贝,而es6模块输出的是一个只读值的引用

2、commonjs是在运行时加载,而es6模块是在编译时输出接口

3、commonjsrequire()是同步加载,而es6import xx from xxx是异步加载,有一个独立的模块解析阶段

另外我们还要知道commonjsrequire引入的是module.exports出来的对象或者属性。而es6模块不是对象,它对外暴露的接口是一种静态定义,在代码解析阶段就已经完成。

举个例子,commonjs

// 5.js
const userInfo = {
  name: 'Maic',
  age: 18
};
let count = 1;
const countAge = () => {
  userInfo.age += 1;
  count++;
  console.log(`count:${count}`);
};
module.exports = {
  userInfo,
  countAge,
  count
};
// 6.js
const { userInfo, countAge, count } = require('./5.js');
console.log(userInfo); // {name: 'Maic', age: 18}
countAge(); // count:2
console.log(userInfo); // {name: 'Maic', age: 19}
console.log(count); // 1

node 6.js
从打印里可以看出,一个原始的输出count,外部调用countAge并不会影响count输出的值,但是在内部countAge打印的仍是当前++后的值。

如果是es6模块,我们可以发现

const userInfo = {
  name: 'Maic',
  age: 18
};
let count = 1;
const countAge = () => {
  userInfo.age += 1;
  count++;
  console.log('count', count);
};
export { userInfo, countAge, count };

在页面中引入,我们可以发现

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>module</title>
  </head>
  <body>
    <div id="app"></div>
    ...
    <script type="module">
      import userInfo, { cityList } from './js/4.js';
      import { userInfo as nuseInfo, count, countAge } from './js/7.js';
      console.log(userInfo, cityList);
      console.log(this);
      // { name: 'Maic', age: 18}
      countAge();
      console.log(nuseInfo, count);
      // {name: 'Maic', age: 19} 2
    </script>
  </body>
</html>

我们发现count导出后的值是实时的改变了。因为它是一个值的引用。

接下来有疑问,比如我有一个工具函数

function Utils() {
  this.sum = 0;
  this.add = function () {
    this.sum += 1;
  };
  this.sub = function () {
    this.sum-=1;
  }
  this.show = function () {
    console.log(this.sum);
  };
}

export new Utils;

这工具函数,在很多地方会有引用,比如A,B,C...等页面都会引入它,那么它会每次都会实例化Utils

接下来我们实验下

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>module</title>
  </head>
  <body>
    <div id="app"></div>
    ...
    <script type="module">
      // A
      import { utils } from './js/7.js';
      utils.add();
      console.log(utils);
    </script>
    <script type="module">
      // B
      import { utils } from './js/7.js';
      console.log('sum=', utils.sum);
      console.log(utils);
    </script>
  </body>
</html>
// 7.js
const userInfo = {
  name: 'Maic',
  age: 18
};
let count = 1;
const countAge = () => {
  userInfo.age += 1;
  count++;
  console.log('count', count);
};
function Utils() {
  this.sum = 0;
  this.add = function () {
    this.sum += 1;
  };
  this.sub = function () {
    this.sum -= 1;
  };
  this.show = function () {
    console.log(this.sum);
  };
}
const utils = new Utils();
export { userInfo, countAge, count, utils };

我们会发现在A模块里调用utils.add()后,在B中打印utils.sum1,那么证明B引入的utilsA是一样的。

如果我输出的仅仅是一个构造函数呢?看下面

// 7.js
...
function Utils() {
  this.sum = 0;
  this.add = function () {
    this.sum += 1;
  };
  this.sub = function () {
    this.sum-=1;
  }
  this.show = function () {
    console.log(this.sum);
  };
}
const utils = new Utils;
const cutils = Utils;
export {
  userInfo,
  countAge,
  count,
  utils,
  cutils
};

页面同样引入

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>module</title>
  </head>
  <body>
    <div id="app"></div>
    ...
    <script type="module">
      // A
      import { utils, cutils } from './js/7.js';
      countAge();
      console.log(nuseInfo, count);
      utils.add();
      new cutils().add();
      console.log(utils);
    </script>
    <script type="module">
      // B
      import { utils, cutils } from './js/7.js';
      console.log('sum=', utils.sum);
      console.log(utils);
      console.log('sum2=', new cutils().sum); // 0
    </script>
  </body>
</html>

我们会发现Anew cutils().add()Bnew cutils().sum)访问,依然是0,所以当模块中导出的是一个构造函数时,每一个模块对应new 导出的构造函数都是重新开辟了一个新的内存空间。

因此可以得出结论,在es6模块中直接导出实例化对象的性能开销比直接导出构造函数更小些。

CommonJS 模块的加载原理

我们初步了解下CommonJS的加载

// A.js
module.exports = {
  a: 1
};
// B.js
const { a } = require('./A.js');
console.log(a); // 1

在执行require时,实际上内部会在内存中生成一个对象,require是一个nodejs环境提供的一个全局函数。

{
  id: '...',
  exports: { ... },
  loaded: true,
  ...
}

优先会从缓存中取值,缓存中没有就直接从exports中取值。具体更多可以参考这篇文章require 源码解读

另外,我们通常项目里可能会见到下面的代码

// A
exports.a = 1;
exports.b = 2;
// B
const a = require('./A.js');
console.log(a); // {a:1, b:2}

以上与下面等价

// A.js
module.exports = {
  a: 1,
  b: 2
};
// B.js
const a = require('./A.js');
console.log(a); // {a:1,b:2}

所以我们可以看出require实际上获取就是module.exports输出{}的一个值的拷贝。

exports.xxx时,实际上require获取的值结果依旧是module.exports值的拷贝。也就是说,在运行时,当使用exports.xx时实际上会中间悄悄转换成module.exports了。

总结

1、比较script``type中引入的三种模式deferasyncmodule的不同。

2、在module下,浏览器支持es模块,import方式加载模块

3、commonjs是在运行时同步加载的,并且导出的值是值拷贝,无法做到像esMoule一样做静态分析,而且esModule导出是值是值引用。

4、esModule导出的对象,多个文件引用不会重复实例化,多个文件引入的对象是同一份对象。

5、commonjs加载原理,优先会从缓存中获取,然后再从loader加载模块

6、本文示例code example

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