【webpack】从一个简化后的webpac异步加载打包代码了解webpack异步加载原理

前言

本文分析webpack5的异步加载原理,代码是简化后的,原代码大概200行,简化后100行左右,但是功能依旧可以正常实现。

正文
首先贴出所有的代码,然后分析。
这是未打包的代码:
image.png

index.js代码,引入了test.js,但是是通过import异步引入。

// index.js
import("./test").then(val => {
    console.log(val)
})

test.js, 默认导出了一个字符串。

// test.js
export default "test代码"

这是打包后的代码。

// index.js打包代码

// 这个是存放所有的模块。
var webpackModules = {};
// 模块的缓存。
var webpackModuleCache = {};
// 模块引入
function webpackRequire(moduleId) {
    var cachedModule = webpackModuleCache[moduleId];
    if (cachedModule !== undefined) {
        return cachedModule.exports;
    }
    var module = webpackModuleCache[moduleId] = {
        exports: {}
    };
    webpackModules[moduleId](module, module.exports, webpackRequire);
    return module.exports;
}
webpackRequire.m = webpackModules;


(() => {
    webpackRequire.d = (exports, definition) => {
        for (var key in definition) {
            if (webpackRequire.o(definition, key) && !webpackRequire.o(exports, key)) {
                Object.defineProperty(exports, key, {
                    enumerable: true,
                    get: definition[key]
                });
            }
        }
    };
})();

(() => {
    webpackRequire.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
})();
(() => {
    webpackRequire.r = exports => {
        if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
            Object.defineProperty(exports, Symbol.toStringTag, {
                value: 'Module'
            });
        }
        Object.defineProperty(exports, 'esmodule', {
            value: true
        });
    };
})();



var webpackJsonpCallback = ([chunkIds, moreModules]) => {
    var resolves = []
    for (var i = 0; i < chunkIds.length; i++) {
        var resolve = installedChunks[chunkIds[i]][0]
        resolves.push(resolve)
        // 0表示加载完毕
        installedChunks[chunkIds[i]] = 0
    }
    for (var key in moreModules) {
        webpackRequire.m[key] = moreModules[key];
    }
    // 模块加载完毕,执行resolve。
    while (resolves.length > 0) {
        resolves.shift()()
    }
}

webpackRequire.f = {}

var installedChunks = {
    main: 0
}

webpackRequire.p = "";

webpackRequire.u = (chunkId) => chunkId + ".js";

// 3
webpackRequire.l = (url) => {
    var script = document.createElement("script");
    script.src = url;
    document.head.appendChild(script);
}

// 2
webpackRequire.f.j = (chunkId, promises) => {
    var installedChunkData = installedChunks[chunkId];
    // 说明这个chunk已经加载过了
    if (installedChunkData !== undefined) { return }
    var promise = new Promise((resolve, reject) => {
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
    })
    installedChunkData[2] = promise;
    promises.push(promise);
    var url = webpackRequire.p + webpackRequire.u(chunkId);
    webpackRequire.l(url)
}

// 1
webpackRequire.e = (chunkId) => {
    var promises = [];
    webpackRequire.f.j(chunkId, promises);
    return Promise.all(promises)
}


// 4
var wepackLoadingGlobal = window.webpackChunkwebpack2 = [];
// 重写push
wepackLoadingGlobal.push = webpackJsonpCallback


webpackRequire.e("src_test_js").then(webpackRequire.bind(webpackRequire, "./src/test.js")).then(val => {
    console.log(val);
});

这个是test.js打包后的代码

(self["webpackChunkwebpack2"] = self["webpackChunkwebpack2"] || []).push([["src_test_js"], {
  "./src/test.js": (unusedWebpackModule, webpackExports, webpackRequire) => {
    webpackRequire.r(webpackExports);
    webpackRequire.d(webpackExports, {
      "default": () => webpackDefaultExport
    });
    const webpackDefaultExport = "test代码";
  }
}]);

我们先来看这段代码比较重要的初始化。

// 这个是存放所有的模块。
var webpackModules = {};
// 模块的缓存。
var webpackModuleCache = {};
// 模块引入
function webpackRequire(moduleId) {
    var cachedModule = webpackModuleCache[moduleId];
    if (cachedModule !== undefined) {
        return cachedModule.exports;
    }
    var module = webpackModuleCache[moduleId] = {
        exports: {}
    };
    webpackModules[moduleId](module, module.exports, webpackRequire);
    return module.exports;
}
webpackRequire.m = webpackModules;

对象 webpackModules表示的是所有的模块的集合,无论我们是通过esModule还是commonjs导出的模块都在这里存放,这里之所以是一个空的对象是因为,我虽然导出了test模块,但是这个模块是被异步加载的,webpack会在后面执行import("./test.js")的时候把对应的模块放进 webpackModules,这个后面会说。
然后就是webpackModuleCache,这个对象是一个用于缓存的对象,当我们使用过某个模块后,这个模块就会被放进webpackModuleCache
函数webpackRequire是一个很重要的函数,这个函数的作用是用来获取webpackModules里面的模块的,要想知道webpackRequire是怎么获取模块的,我们就要先看懂导出后的模块打包好的代码是什么样子的。
来看上面的test.js打包后的代码:

(self["webpackChunkwebpack2"] = self["webpackChunkwebpack2"] || []).push([["src_test_js"], {
    // 暂时只看这个对象字段
  "./src/test.js": (unusedWebpackModule, webpackExports, webpackRequire) => {
    webpackRequire.r(webpackExports);
    webpackRequire.d(webpackExports, {
      "default": () => webpackDefaultExport
    });
    const webpackDefaultExport = "test代码";
  }
}]);

我们先不看上面的函数调用,就看 "./src/test.js"这个字段,这个字段就是test.js打包后的代码,webapck把它封装成为了一个函数,这样做的原因就是为了让外界能拿到这个模块导出的内容,这个函数接受了三个参数,分别是:unusedWebpackModulewebpackExportswebpackRequire,第一个参数就是commonjs的全局module对象,之所以翻译过来叫“没有使用的模块”,是因为在上面的webpackRequire函数里面检查了模块是否被缓存,有缓存就从缓存里面去取,就不会走到这个函数了

    // webpackRequire的过滤
if (cachedModule !== undefined) {
     return cachedModule.exports;
}

第二个参数webpackExports表示的是commonjs的module.exports,这个参数其实也是esModule导出变量的方法,但是commonjs和esModule有很大的不同,这个我下面说。
第三个参数是webpackRequire,这个参数的作用是提供一些工具方法。
函数分析:
webpackRequire.r(webpackExports);的作用是把当前模块标记为esmodule
简单看看webpackRequire.r的实现:

    webpackRequire.r = exports => {
      if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, {
          value: 'Module'
        });
      }
      Object.defineProperty(exports, 'esmodule', {
        value: true
      });
    };

代码很简单,就是添加了两个字段,标明是esModule。这个工具函数只有在模块是esModule的时候才会被使用。

webpackRequire.d(webpackExports, {
      "default": () => webpackDefaultExport
    });
    const webpackDefaultExport = "test代码";

这段代码是导出的核部分,这段代码通过 webpackRequire.d,把test.js导出的代码给添加到了 webpackExports上,函数的第二个参数包含了模块导出的代码。
来看 webpackRequire.d的实现:

    webpackRequire.d = (exports, definition) => {
        for (var key in definition) {
            if (webpackRequire.o(definition, key) && !webpackRequire.o(exports, key)) {
                Object.defineProperty(exports, key, {
                    enumerable: true,
                    get: definition[key]
                });
            }
        }
    };

这段代码的实现很简单,这里的webpackRequire.o的作用是用来判断参数1是否包含参数2。
webpackRequire.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
做完判断之后,函数对definition进行了遍历,并且把每个值都以 Object.defineProperty的方式定义到了exports上,这也就解释了刚刚为什么模块导出的参数是写成了一个函数,就是为了这里方便添加到get方法上。这同样解释了为什么commonjs导入的值可以修改,但是esModule导入的值不能修改,因为esModule导出的值根本没有set方法。webpack把值放在了exports上,最后webpackRequire返回了这个值,这样就完成了模块对值的导出。其他变量的作用我接下来会说。
接下来看index.js的import加载流程:
首先是入口

webpackRequire.e("src_test_js").then(webpackRequire.bind(webpackRequire, "./src/test.js")).then(val => {
    console.log(val);
});

这里调用了webpackRequire.e方法来加载对应的chunkId,webpackRequire.e返回一个Promise,在then拿到了模块里面的值,又返回了个Promise,最后拿到了导出的值。
那先来看webpackRequire.e

// 1
webpackRequire.e = (chunkId) => {
    var promises = [];
    webpackRequire.f.j(chunkId, promises);
    return Promise.all(promises)
}

webpackRequire.e的作用很简单,就是创建了个数组存放promise,然后就交给了webpackRequire.f.j去执行,这里的promises其实是用来存放每个模块的promise。
接着看webpackRequire.f.j

// 2
webpackRequire.f.j = (chunkId, promises) => {
    var installedChunkData = installedChunks[chunkId];
    // 说明这个chunk已经加载过了
    if (installedChunkData !== undefined) { return }
    var promise = new Promise((resolve, reject) => {
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
    })
    promises.push(promise);
    installedChunkData[2] = promise;
    var url = webpackRequire.p + webpackRequire.u(chunkId);
    webpackRequire.l(url)
}

这个函数的作用是为每个模块创建了一个Promise,并且给installedChunks添加了这个Promise的数组[resolve, reject],在installedChunks中,如果一个模块的值不是0,就代表着这个模块还没加载完毕。最后的webpackRequire.p 代表的是这个模块的地址,但是这里省略了,所以为空字符串,通过webpackRequire.p + webpackRequire.u(chunkId)创建了一个url地址,这个地址指向了模块的地址,然后通过 webpackRequire.l去加载这个模块,来看wenpack是怎么加载模块的:
webpackRequire.l的实现:

// 3
webpackRequire.l = (url) => {
    var script = document.createElement("script");
    script.src = url;
    document.head.appendChild(script);
}

可以看出,webpack其实是在通过Jsonp来加载模块,webapck动态创建了一个script脚本,并且把src指向了模块的地址,这样就完成了加载模块。
被加载的模块的代码是怎么执行的呢?

// test.js 打包后的代码
(self["webpackChunkwebpack2"] = self["webpackChunkwebpack2"] || []).push([["src_test_js"], {
  "./src/test.js": (unusedWebpackModule, webpackExports, webpackRequire) => {
    webpackRequire.r(webpackExports);
    webpackRequire.d(webpackExports, {
      "default": () => webpackDefaultExport
    });
    const webpackDefaultExport = "test代码";
  }
}]);

这里的self可以看做是window,因为这里没有用到Web-Worker,我们可以看到这里调用了全局对象的webpackChunkwebpack2.push方法,传入了模块的名称和模块本身,具体那么webpack是怎么处理这个模块的呢,来看webpackChunkwebpack2.push的实现

// 5
function webpackJsonpCallback([chunkIds, moreModules]) {
    var resolves = [];
    for (let i = 0; i < chunkIds.length; i++) {
        var chunkId = chunkIds[i];
        resolves.push(installedChunks[chunkId][0]);
        installedChunks[chunkId] = 0
    }
    for (var key in moreModules) {
        webpackModules[key] = moreModules[key];
    }
    resolves.forEach(r => r());
    resolves = []
}
...
var chunkLoadingGlobal = window.webpackChunkwebpack2 = []
chunkLoadingGlobal.push = webpackJsonpCallback

这里就是对加载模块的处理了,可以看到,push方法实际上就是这里webpackJsonpCallback,那么webpackJsonpCallback是啥呢,我在webpack的一个isuue里面找到了一个人的回复:

【webpack】从一个简化后的webpac异步加载打包代码了解webpack异步加载原理_第1张图片

翻译:

代码中的webpackJsonpCallback是什么意思?
webpackJsonpCallback是webpack在浏览器端异步加载模块的时候定义的全局函数。当webpack加载一个异步模块时,会生成一个script标签,指向异步模块的url,此时浏览器会异步加载这个js文件。在js文件中,会有一个webpackJsonpCallback函数被调用,它会把该异步模块的代码加入到当前网页的js代码中去。这就是webpack实现异步模块加载的方式之一。

正如这个人所说,webpackJsonpCallback做的就是把模块放到了webpackModules里面,方为了使webpackRequire.then的时候能取值,然后把installedChunks里面的resolve取出并执行,这样才触发了webpackRequire.e("src_test_js")返回的.then,完成了异步加载。

结语
第一次写这么多字的文章,文章比较粗糙,如有错误,希望可以指正,一起学习。

你可能感兴趣的:(webpack前端)