Skip to content

Latest commit

 

History

History
114 lines (70 loc) · 9.15 KB

52.精读《图解 ES 模块》.md

File metadata and controls

114 lines (70 loc) · 9.15 KB

52.精读《图解 ES 模块》.md

ES Module 为 js 开发者带来官方和标准化的模块系统,等待十年的标准,所有主流浏览器都支持 ES 模块,Node 模块工作组也正尝试将 ES 模块支持到 Node 环境,本文可以了解一下其解决的问题和其他模块系统之间的区别

问题点 && 解答

  1. 模块旨在解决什么问题

    • js 提供一种方式(函数作用域),一个函数内只需要考虑这个函数变量的问题,不需要考虑其他函数影响本函数作用域中的变量,但是带来的问题是变量无法共享,在不同函数作用域下无法相互共享变量,作用域外共享变量只能穿透外层作用域或者全局作用域,全局作用域下的变量比较容易被修改。
  2. 模块为开发者带来什么

    • 模块提供了更好的方式组织变量和函数,将函数和变量放到一个作用域内,在模块间共享变量
    • 与函数作用域不同,模块内部的变量实现了在其他模块内共享,可以指定某些变量、类或者函数可以被共享
    • export 的方式进行导出,模块之间的依赖是一种明确的关系,被移除也会有相关的出错提示
    • 有了模块间导出和引用的能力,代码就被赋予可以打包的能力,可以想乐高一样进行组合
    • 目前主流模块系统为 CJS ESM, CJS 是 node 遗留下来的产物,ESM 是 ES6+ 的新规范
  3. ES 模块化的工作机制

    • 模块化开发(以 node_modules/为例) 会将依赖构建为树形结构,通过 import 语句通知浏览器或者 Node 加载相关模块,依赖树以根节点作为入口文件

    • 浏览器环境下这些文件会转化为一种叫做"模块记录"的数据结构,模块记录需要被转化为模块实例, 每个实例包含了两个东西:代码和状态

    • 代码就是指令集,状态提供了原始材料,为这些变量的值,这些变量仅仅是内存中储存值得别名。模块将代码和状态结合到一起

      Module Instance = code + state

    • 入口文件到完整模块树形实例经过三个步骤:

      • 构建:查找,下载,将所有文件转化为模块记录

        • 查找文件依赖树需要通过 import 语句去通知加载器去哪里查找到其他模块,模块规范会区分浏览器和 Node 两个不同的环境,每个宿主环境处理模块标识符的方式不同,会使用到一个模块识别算法区分不同的平台,目前还有 Node 模块规范无法在浏览器端工作

        • 修复前做的兼容是,浏览器仅仅接受 URL 模块标识符,通过 URL 加载模块文件,但是转化前是不知道模块有哪些依赖项,加载文件前是无法转化文件的,所以必须一层一层遍历文件树,转化文件并找出依赖,最后查找并加载这些依赖,如果主线程等待下载这些文件,会有很多任务堆积在队列中,浏览器环境需要网络进程去下载这些文件(HTML, html parser 解析到有 css link 和 js script 会开启预解析进程进行网络请求下载这些 css、js 文件),阻塞主线程导致应用所需模块变慢,将构建过程分片进行实现了在全部下载前进行获取和构建,这种查分构建的方式是 ESM 和 CJS 最本质的不同:

          构建流程

          ​ CJS 主要是由于相对通过网络请求从文件系统加载文件耗时更少,意味着 Node 可以在加载文件的时候可以阻塞主进程,文件加载完后进行实例化和计算,也就意味着在返回模块实例前完成遍历整个树,加载,实例化并计算依赖。

          ​ Node 环境下,模块内部可以声明变量,在查找下一个模块前,在执行本模块的代码,在执行模块前,变量会有一个值,但是在 ESM 中,需要构建整个模块树。

           // main.js
          let path = "module-" + lang;
          let formatter = require(path);
          
          formatter.format(content) 
          /*
          Node evaluates main.js up to require(), and then switches over to synchronously loading and evaluating module-en.js and any of its dependencies
          */
          
          // transform module
          // module-en.js
          export.format = function (content) {
          // Do Something language specific  
          }
          /*
          ...before returning to evaluation of main.js
          */
        • 文件转化为一个模块记录(Temp)

          • 加载文件后,会转化为模块记录,让浏览器理解模块的不同部分,模块记录经过创建后会放在模块映射中,模块记录被请求时加载器可以从模块映射中拉出来

          模块映射

          • 浏览器上只要在 script 上添加 type="module" ,这回通知浏览器文件应该被转换成一个模块,同样只有模块才能被导入,浏览器知道模块有哪些引用导出, 但是在 Node 中没有 HTML 这种 tag 标记,也没有 type 声明,所以社区使用的是.mjs拓展告诉 Node 此文件是一个模块,所有的文件都由加载器决定文件是否转化为一个模块
      • 安装:将所有导出的变量放到内存中去,变量还没被赋初始值,然后将导出和导入变量全部放到内存中,称之为链接.

        • 安装这一步基本关于的是如何写入内存,JS 引擎会创建一个模块环境记录,为模块记录维护变量,在内存中开辟空间让这些变量可导出,模块环境记录会追踪内存中的值导出的每个变量,内存空间不会获取到变量的值,而是计算后拿到对应值

        • 为了实例化模块树,引擎会完成一个叫深度优先的后序遍历,从树的底部开始,底部依赖不会再依赖其他东西,并且创建他们的导出

        • 引擎绘制出一个模块下的所有导出,然后绘制这个模块的所有导入,注意:导出和导入在内存中指向同一个地址,这里和 CJS 有区别,CJS 中所有导出对象的值都是一个拷贝,相反的是 ESM 使用了类似绑定的东西,模块会指向内存中的同一个地址,意味着当导出模块修改了一个值,这个修改不会在导入模块中表现出来。

        • 能动态绑定的原因就是可以在不执行代码的情况下连接所有的模块,最后会将实例和内存地址连接起来

          模块安装

      • 赋值:执行代码,将变量值添加到代码中

        • 最后一步就是做填充内存空间,JS 引擎通过执行顶层代码完成,也就是函数外的代码,遇到异步调用的情况还会出现一些负面的影响:

          // top level code 顶层提升
          let count = 5;
          updateCountOnServer();
          export {count, updateCount};
          
          function updateCount() {...}
          function updateCountOnServer() {...}
        • 因为存在这种负面影响,所以赋值得到的结果可能是不同的,这就是模块映射机制出现的一个原因,模块映射会通过 URL 来缓存模块,每个模块仅有一个模块记录,这样确保模块只执行一次,跟初始化一样,这也是一个深度优先的后序遍历。

        • 循环依赖的情况则需要遍历树

          • CJS 的工作流程:首先模块会执行 require 语句,然后加载模块,模块会接着访问导出对象的信息,但是由于还没有在模块中进行计算,会返回 undefined,也就是说 js 在为本地变量分配内存空间后,由于提升初始化的原因会赋值为 undefined,然后向下计算执行到模块的顶部代码,可以设置一个延时器看是否能正确获取到模块属性的值,如果导出时用了动态绑定处理的,模块会拿到最终准确的值,执行延时后也会拿到最终的值。
    • 之所以说 ESM 是异步的,是因为 ESM 将这三个步骤划分开,实际上 CJS 中模块和相关的依赖都是一次加载、安装、赋值的,ESM 需要借助模块加载器来实现这三步,加载器在不同平台下有不同的规范,浏览器端就是 HTML 规范

  4. ES 模块化的现状

summary

  • 模块化提供了更好的方式组织变量和函数,将相关变量或者函数组织到一块,具体就是放到一个模块作用域内,实现模块间的共享变量,与函数作用域不同的是,函数内部的变量实现了其他模块内共享,可以指定导出对象、值、函数共享。
  • 由于 Nodejs 的缘故,CJS 模块系统使用量更大,目前的 CJS 还无法兼容 ESM,但是工作组在这方面尝试,两个模块系统最大的区别是运行时,CJS 是一个动态的模块系统,而 ESM 只是静态模块系统,动态模块导出只有在执行后才能得到,静态模块导入和导出时不可变化的。
  • 目前主流方式是使用 Webpack 这种构建工具使用 ESM,一定程度上模拟环境,期待 Node 工作组早日实现对 ESM 的支持。