webpack4之《模块运行机制原理》

webpack4之《模块运行机制原理》

在webpack的开发环境中,大多数人都可以自豪的说是采用的模块化开发,一问原理,也能谈到一些es6 module、babel和CommonJS的概念等等。然而,当打包后的静态资源运行在浏览器中,又是如何实现模块化的呢?

接下来,我们就通过分析打包后的代码,结合加载流程,来揭开浏览器模块化机制的神秘面纱~~

一.先来看项目结构和效果

有三个JS文件:

  1. first.js
  2. second.js
  3. third.js

first和second都引入了third,并且调用了third。

//first.js
import Third from './third'
export default class First {
    say() {
        Third('First')
    }
}
const f = new First();
f.say();

//////////////////////////////////////////////////

//second.js
import Third from './third'
export default class Second {
    say() {
        Third('Second')
    }
}
const s = new Second();
s.say();

//////////////////////////////////////////////////

//third.js
export default function Third(moduleName) {
    console.log(moduleName);
}

在webpack.config.js中,我们设置了entry为first和second,并且将runtime代码提取了进来:

entry: {
    first: './src/first.js',
    second: './src/second.js',
},
mode: 'production',
output: {
    filename: '[id].[contenthash].js',
    chunkFilename: '[id].[contenthash].js',
    path: path.resolve(__dirname, 'dist')
},
optimization:{
    minimize: false,// 关闭js文件压缩,方便我们进行分析
    runtimeChunk: true // 提取runtime代码
}

所以打包出来后,运行index.html,就会输出 'First' 和 'Second'。

控制台中打印了两个字符串

二.分析浏览器运行流程

可以看到,js依次运行了上面四个js文件,我们来看第一个runtime~first:

runtime~first.js 函数折叠后

我先把函数折叠后,可以看到,整个文件是个立即执行函数,接受一个空数组modules作为入参,接着,我们来看这个函数是如何执行的:

函数真正开始运行的部分

上面的代码是整个函数真正运行的地方,总结来说做了以下几件事情:

  1. 获取全局数组变量 webpackJsonp ,如果不存在,初始化为空数组。
  2. 将 webpackJsonp数组 的push方法改为 webpackJsonpCallback。
  3. 遍历当前的 webpackJsonp数组,并且调用 webpackJsonpCallback。
  4. 调用 checkDeferredModules。

由于当前 webpackJsonp 数组长度是0,所以只执行了checkDeferredModules,继续来分析(webpackJsonpCallback方法后续会用到,再来解释):

checkDeferredModules方法实现

在 checkDeferredModules 中,遍历数组 deferredModules,由于deferredModules默认是空数组,所以这里也就先跳过。

所以,第一个runtime~first文件其实就只是定义运行的环境:

  1. 全局数组变量 webpackJsonp
  2. webpackJsonpCallback方法。
  3. checkDeferredModules方法。
  4. __webpack_require__方法。
  5. deferredModules 数组、installedChunks对象。
  6. 其他的暂时用不到的。

到这里,只需要大家对这些对象和方法有印象即可,当我们开始分享first.js后,就能豁然开朗~

接下来,浏览器开始运行first.js:

first.js的内容

可以看到,first.js直接调用了runtime中声明的webpackJsonp.push方法,也就是webpackJsonpCallback方法,传入了一个二维数组,分别是:

  1. ['first']
  2. [func1, func2]
  3. [1, 'runtime~first']

['first'] 是 当前chunk的chunkID,[func1,func2]是当次chunk中的模块,[1, 'runtime~first']是该chunk的入口模块和运行时依赖的其他chunk。

结合之前的代码功能来看,由于first.js被设置成了entry,所以被打包成了单独的chunk,然后first.js内部引入了 third.js,所以third.js成为了first这个chunk中的一个模块,并且first.js自身的代码也成为了一个模块。由上图看出,first.js的模块是[func1, func2]中的func2,下标就是1,由于first.js的代码最先运行,所以入口模块设置为1。

到这一步后,只是把first.js和third.js的内容注册到了模块系统中,但具体怎么注册和怎么运行的,目前还看不出来。

接着,我们来分析webpackJsonpCallback方法:

webpackJsonpCallback方法的具体实现
  1. chunkIds = ['first']
  2. moreModules = [func1, func2]
  3. executeModules = [1, 'runtime~first']

在这个方法中,先是对chunkId进行操作:

  1. 判断installedChunks对象上chunkId对应的值不为空也不等于0,如果为true,则存入到resolves数组中,后续依次执行。
  2. 将installedChunks[chunkId] 设置为0,表示当前这个chunk已经加载过了。
ChunkA异步加载ChunkB的时候,会将 installedChunks[ChunkB]的值设置为promise,值是[resolve, reject];当ChunkB加载完成后会执行webpackJsonpCallback,此时installedChunks[ChunkB]的值还是[resolve, reject],调用resolve方法后,ChunkA通过ChunkB的then方法就可以继续执行后续的代码了。并且把 installedChunks[ChunkB]的值设置为0,表示加载完成,下一次再加载ChunkB,直接跳过加载过程。

接着,对该chunk中的module进行注册:

  1. 遍历 [func1, func2],通过下标将模块注册到全局的modules中。
加载chunk的目的,其实就是把chunk本身的module注册到全局modules中,方便其他chunk下通过moduleID来调用。

最后,对 [1, 'runtime~first'] 进行操作:

将 [1, 'runtime~first'] 放入到deferredModules中,执行checkDeferredModules方法,从下标1开始遍历deferredModules,因为下标0是当前chunk的入口moduleID,后面的才是该chunk依赖的chunkID。

checkDeferredModules方法

从上图可以看到,如果该chunk依赖的所有chunk都加载完成,也就是所有的installedChunks[依赖chunkId] 都等于0,就通过__webpack_require__方法执行当前chunk的入口moduleID。

__webpack_require__方法

从上图可以看出,如果当前moduleID在installModules上有值了,直接去取当前函数,如果不存在,就立马执行该module,并且存到installModules上。

modules是注册module,所有执行或者未执行的module,只有chunks加载,都会注册上去,但是只有执行完成后,才会注册到installModules上去。

总结

大概的流程来说,其实就是:

  1. 先把模块系统依赖的运行环境(方法、全局变量等)建立好。
  2. 加载chunk文件。
  3. chunk文件加载完成后,会触发webpackJsonpCallback,传入chunkID、包含的modules、chunk的入口moduleID和依赖的其他chunkID。
  4. 将chunkID存入到installedChunks对象中,设置为0,缓存起来,下次加载相同的chunkID,直接跳过。
  5. 将包含的modules通过moduleID注册到全局的modules中。
  6. 等module注册完成后,判断依赖的其他chunk是否加载完成,如果加载完成就执行当前chunk的入口moduleID,程序就运行起来了~~
发布于 2019-09-03 10:54