源码调试
- 打包后的文件就是一个函数自调用,当前函数调用时传入一个对象
- 这个对象我们为了方便将之称为模块定义,它就是一个键值对
- 这个键名就是当前被加载模块的文件名与某个目录的拼接
- 这个键值就是一个函数,和 node.js 里的模块加载有一些类似,会将被加载模块中的内容包裹于一个函数中
- 这个函数在将来某个时间点上会被调用,同时会接收到一定的参数,利用这些参数就可以实现模块的加载操作
- 针对于上述的代码就相当于是将
{}
(模块定义)传递给了modules
打开调试工具,创建 launch.json
- 导出的内容放到
module.exports
中
功能函数
(function (modules) {
// 定义对象用于缓存已加载过的模块
var installedModules = {}
// webpack 自定义的一个加载方法,核心功能就是返回被加载模块中导出的内容
function __webpack_require__(moduleId) {}
// 将模块定义保存一份,通过 m 属性挂载到自定义的方法身上
__webpack_require__.m = modules
// 导出加载过的模块
__webpack_require__.c = installedModules
// 判断被传入的对象 obj 身上是否具有指定的属性 **** ,如果有则返回 true
__webpack_require__.o = function (object, property) {
return Object.prototype.hasOwnProperty.call(object, property)
}
// 可以给对象身上添加属性 name,并添加访问器
__webpack_require__.d = function (exports, name, getter) {
// 如果当前 exports 身上不具备 name 属性,则条件成立
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter })
}
}
// 给对象身上加一个标记,通过这个标记就可以知道它是 esModule 还是非 esModule
__webpack_require__.r = function (exports) {
// 下面的条件如果成立就说明是一个 esModule
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
}
// 如果条件不成立,我们也直接在 exports 对象的身上添加一个 __esModule 属性,它的值就是true
Object.defineProperty(exports, '__esModule', { value: true })
}
// 1 调用 t 方法之后,我们会拿到被加载模块中的内容 value
// 2 对于 value 来说我们可能会直接返回,也可能会处理之后再返回
__webpack_require__.t = function (value, mode) {}
// 如果 module 是 ES Modules 模块,返回 default
__webpack_require__.n = function (module) {}
// webpack 配置里面的 public
__webpack_require__.p = ''
// __webpack_require__.s 存储模块 id 值
return __webpack_require__((__webpack_require__.s = './src/index.js'))
})
CommonJS 模块打包
webpack 默认使用 CommonJS 规范处理打包结果
- 如果模块时使用 CommonJS 方式导入,webpack 不需要额外处理
- 如果模块时使用 ES Modules 方式导入,webpack 会进行处理
__webpack_require__.r
方法给 exports
添加标记
Symbol.toStringTag
:Object.prototype.toString()
方法会读取这个标签并作为返回值
__webpack_require__.d
给属性 age
添加 getter
方法
/* index.js */
let obj = require('./login.js')
console.log('index.js内容执行了')
console.log(obj.default, '---->', obj.age)
/* login.js */
// 01 采用 cms 导出模块内容
// module.exports = 'zcegg'
// 02 采用 esModule 导出模块内容
export default 'zcegg'
export const age = 18
/* 打包后的文件 */
{
'./src/index.js': function (module, exports, __webpack_require__) {
let obj = __webpack_require__(/*! ./login.js */ './src/login.js')
console.log('index.js内容执行了')
console.log(obj.default, '---->', obj.age)
},
'./src/login.js': function (module, __webpack_exports__, __webpack_require__) {
'use strict'
// 01 采用 cms 导出模块内容
// module.exports = 'zcegg'
// 02 采用 esModule 导出模块内容
__webpack_require__.r(__webpack_exports__)
__webpack_require__.d(__webpack_exports__, 'age', function () {
return age
})
__webpack_exports__['default'] = 'zcegg'
const age = 18
}
}
ES Modules 模块打包
/* index.js */
import name, { age } from './login.js'
console.log('index.js内容加载了')
console.log(name, '---->', age)
/* login.js */
// 01 采用 cms 导出模块内容
// module.exports = 'zce'
// 02 采用 esModule 导出模块内容
export default 'zce'
export const age = 100
/* 打包后的文件 */
{
'./src/index.js': function (module, __webpack_exports__, __webpack_require__) {
'use strict'
__webpack_require__.r(__webpack_exports__)
var _login_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
/*! ./login.js */ './src/login.js'
)
console.log('index.js内容加载了')
console.log(
_login_js__WEBPACK_IMPORTED_MODULE_0__['default'],
'---->',
_login_js__WEBPACK_IMPORTED_MODULE_0__['age']
)
},
'./src/login.js': function (module, __webpack_exports__, __webpack_require__) {
'use strict'
// 01 采用 cms 导出模块内容
// module.exports = 'zce'
// 02 采用 esModule 导出模块内容
__webpack_require__.r(__webpack_exports__)
__webpack_require__.d(__webpack_exports__, 'age', function () {
return age
})
__webpack_exports__['default'] = 'zce'
const age = 100
}
}
手写功能函数
当我们使用 webpack 打包时,不论前面经历了什么,最终都会产出一个或多个目标 js 文件,这里主要就是生成一个自调用函数,它会接收一个对象作为参数(模块定义),用它的键作为查询模块 ID,用它的值作为要执行的函数,执行函数的过程中就完成了当前模块 ID 对应的加载,并针对不同类型使用不同工具方法
;(function (modules) {
// 01 定义对象用于将来缓存被加载过的模块
let installedModules = {}
// 02 定义一个 __webpack_require__ 方法来替换 import require 加载操作
function __webpack_require__(moduleId) {
// 2-1 判断当前缓存中是否存在要被加载的模块内容,如果存在则直接返回
if (installedModules[moduleId]) {
return installedModules[moduleId].exports
}
// 2-2 如果当前缓存不存在则需要我们自己定义 {} 执行被导入的模内容加载
let module = (installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
})
// 2-3 调用当前 moduleId 对应的函数,然后完成内容的加载
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
// 2-4 当上述的方法调用完成之后,我们就可以修改 l 的值用于表示当前模块内容已经加载完成了
module.l = true
// 2-5 加载工作完成之后,要将拿回来的内容返回至调用的位置
return module.exports
}
// 03 定义 m 属性用于保存 modules
__webpack_require__.m = modules
// 04 定义 c 属性用于保存 cache
__webpack_require__.c = installedModules
// 05 定义 o 方法用于判断对象的身上是否存在指定的属性
__webpack_require__.o = function (object, property) {
return Object.prototype.hasOwnProperty(object, property)
}
// 06 定义 d 方法用于在对象身上添加指定的属性,同时给该属性提供一个 getter
__webpack_require__.d = function (exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter })
}
}
// 07 定义 r 方法用于标识当前模块时 es6 类型
__webpack_require__.r = function (exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
}
Object.defineProperty(exports, '__esModule', { value: true })
}
// 08 定义 n 方法,用于设置具体的 getter
__webpack_require__.n = function (module) {
let getter =
module && module.__esModule
? function getDefault() {
return module['default']
}
: function getModuleExports() {
return module
}
__webpack_require__.d(getter, 'a', getter)
return getter
}
// 09 定义 p 属性,用于保存资源访问路径
__webpack_require__.p = ''
// 10 调用 __webpack_require__ 方法执行模块导入与加载操作
return __webpack_require__((__webpack_require__.s = './src/index.js'))
})({
'./src/index.js': function (module, __webpack_exports__, __webpack_require__) {
'use strict'
__webpack_require__.r(__webpack_exports__)
var _login_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
/*! ./login.js */ './src/login.js'
)
console.log('index.js 执行了')
console.log(_login_js__WEBPACK_IMPORTED_MODULE_0__['default'], '<------')
console.log(_login_js__WEBPACK_IMPORTED_MODULE_0__['age'], '<------')
},
'./src/login.js': function (module, __webpack_exports__, __webpack_require__) {
'use strict'
__webpack_require__.r(__webpack_exports__)
__webpack_require__.d(__webpack_exports__, 'age', function () {
return age
})
__webpack_exports__['default'] = 'zce'
const age = 40
}
})
懒加载流程
懒加载流程
import()
可以实现指定模块的懒加载操作- 当前懒加载的核心原理就是 jsonp
- t 方法可以针对内部进行不同的处理(处理方式取决于传入的数值:8、7、6、3、2、1)
&
运算符(位与)用于对于两个二进制操作数逐位进行比较
- 位运算中,数值 1 表示 true,0 表示 false
let mode = 0b0001
if (mode & 1) {
console.log('第四位上是1')
}
if (mode & 8) {
console.log('第一位上是1')
}
// 第四位上是1
t 方法的作用:
- 接收两个参数,一个是 value 一般用于表示被加载的模块 id,第二个值 mode 是一个二进制的数值
- t 方法内部做的第一件事情就是调用自定义的
require
方法加载 value 对应的模块导出,重新赋值给 value - 当获取到了这个 value 值之后余下的 8、4、ns、2 都是对当前的内容进行加工处理,然后返回使用
- 当
mode & 8
成立时直接将 value 返回(CommonJS) - 当
mode & 4
成立时直接将 value 返回(esModule) - 如果上述条件都不成立,还是要继续处理 value,定义一个 ns
{}
- 如果拿到的 value 是一个可以直接使用的内容,例如是一个字符串,将它挂载到 ns 的
default
属性上 - 如果返回的是对象,则需要遍历
- 如果拿到的 value 是一个可以直接使用的内容,例如是一个字符串,将它挂载到 ns 的
// 11 定义 t 方法,用于加载指定 value 的模块内容,之后对内容进行处理再返回
__webpack_require__.t = function (value, mode) {
// 加载 value 对应的模块内容(value 一般就是模块 id)
// 加载之后的内容又重新赋值给 value 变量
if (mode & 1) {
value = __webpack_require__(value)
}
if (mode & 8) {
// 加载了可以直接返回使用的内容
return value
}
if (mode & 4 && typeof value === 'object' && value && value.__esModule) {
return value
}
// 如果 8 和 4 都没有成立则需要自定义 ns 来通过 default 属性返回内容
let ns = Object.create(null)
__webpack_require__.r(ns)
Object.defineProperty(ns, 'default', { enumerable: true, value: value })
if (mode & 2 && typeof value !== 'string') {
for (var key in value) {
__webpack_require__.d(
ns,
key,
function (key) {
return value[key]
}.bind(null, key)
)
}
}
return ns
}
增加测试案例
{
'./src/index.js': function (module, exports, __webpack_require__) {
let name = __webpack_require__.t(/*! ./login.js */ './src/login.js', 0b0111)
console.log(name)
},
'./src/login.js': function (module, exports) {
module.exports = 'zce'
}
}
懒加载源码分析
installedChunks
- 如果是
0
代表以及加载过 - 如果是
promise
代表当前 chunk 正在加载 - 如果是
undefined
代表当前 chunk 没有被加载 - 如果是
null
代表当前 chunk 预加载(preloaded/prefetched)
第一次进入,installedChunks
的值为 undefined
会进入判断,之后判断是否存在,因为有些时候可能是一个 promise
,需要对其进行保存,如果不进行保存,Promise.all([])
会直接被调用,结果显然不合理
经过一系列操作把创建的 script
标签放到 head 里,最终执行 Promise.all(promises)
之后会执行 window["webpackJsonp"]
,传入的值是一个二维数组
- 第一个值是一个数组(需要懒加载的 ids)
- 第二个值是一个对象(模块定义)
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["login"], {
"./src/login.js":
(function (module, exports) {
module.exports = "懒加载导出内容"
})
}]);
加载完成之后会把 installedChunks
改为 0
,最终执行 resolve
自此方法 e
就走完了,接下来会走方法 t
,传入 './src/login.js', 7
7 & 1
为 true,会把当前模块进行加载并把值赋值给 value 上7 & 4
为 true,但是这里 value 为string
类型,会创建一个空对象,并把 value 挂载上去,并返回ns
最终即可拿到懒加载模块的内容
手写单文件懒加载
(function (modules) {
// 15 定义 webpackJsonpCallback 实现:合并模块定义,改变 promise 状态执行后续行为
function webpackJsonpCallback(data) {
// 01 获取需要被动态加载的模块 id
let chunkIds = data[0]
// 02 获取需要被动态加载的模块依赖关系对象
let moreModules = data[1]
let chunkId,
resolves = []
// 03 循环判断 chunkIds 里对应的模块内容是否已经完成了加载
for (let i = 0; i < chunkIds.length; i++) {
chunkIds = chunkIds[i]
if (
Object.prototype.hasOwnProperty.call(installedChunks, chunkId) &&
installedChunks[chunkId]
) {
// 把 resolve 放进去
resolves.push(installedChunks[chunkId][0])
}
// 更新当前的 chunk 状态
installedChunks[chunkId] = 0
}
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId]
}
}
while (resolves.length) {
resolves.shift()()
}
}
// 16 定义 installedChunks 用于标识某个 chunkId 对应的 chunk 是否完成加载
let installedChunks = {
main: 0
}
// 18 定义 jsonpScriptSrc 实现 src 处理
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + '' + chunkId + '.built.js'
}
// 17 定义 e 方法用于实现 jsonp 加载内容,利用 promise 实现异步加载
__webpack_require__.e = function (chunkId) {
// 01 定义一个数组用于存放 promise
let promises = []
// 02 获取 chunkId 对应的 chunk 是否已经完成了加载
let installedChunkData = installedChunks[chunkId]
// 03 依据当前是否已完成加载状态来执行后续逻辑
if (installedChunkData !== 0) {
if (installedChunkData) {
promises.push(installedChunkData[2])
} else {
let promise = new Promise((resolve, reject) => {
installedChunkData = installedChunks[chunkId] = [resolve, reject]
})
promises.push((installedChunkData[2] = promise))
// 创建标签
let script = document.createElement('script')
// 设置 src
script.src = jsonpScriptSrc(chunkId)
// 写入 script 标签
document.head.appendChild(script)
}
}
// 04 执行 promise
return Promise.all(promises)
}
// 12 定义变量存放数组
let jsonpArray = (window['webpackJsonp'] = window['webpackJsonp'] || [])
// 13 保存原生的 push 方法
let oldJsonpFunction = jsonpArray.push.bind(jsonpArray)
// 14 重写原生的 push 方法
jsonpArray.push = webpackJsonpCallback
}