Webpack系列-Scope Hoisting使用

1,123 阅读3分钟

webpack 在打包时把各个模块打包为一个闭包,会有很多多余的代码,这些打包函数使你的 JavaScript 在浏览器中处理的更慢。Scope Hoisting 又译作 "作用域提升",它可以让 Webpack 打包出来的代码文件更小、运行的更快。

认识 Scope Hoisting

让我们先来看看在没有 Scope Hoisting 之前 Webpack 的打包方式。

假如现在有两个文件分别是 util.js:

export default 'Hello,Webpack';

和入口文件 main.js:

import str from './util.js';
console.log(str);

以上源码用 Webpack 打包后输出中的部分代码如下:

[
    (function (module, __webpack_exports__, __webpack_require__) {
        var __WEBPACK_IMPORTED_MODULE_0__util_js__ = __webpack_require__(1);
        console.log(__WEBPACK_IMPORTED_MODULE_0__util_js__["a"]);
    }),
    (function (module, __webpack_exports__, __webpack_require__) {
        __webpack_exports__["a"] = ('Hello,Webpack');
    })
]

在开启 Scope Hoisting 后,同样的源码输出的部分代码如下:

[
    (function (module, __webpack_exports__, __webpack_require__) {
        var util = ('Hello,Webpack');
        console.log(util);
    })
]

从中可以看出开启 Scope Hoisting 后,函数申明由两个变成了一个,util.js 中定义的内容被直接注入到了 main.js 对应的模块中。 这样做的好处是:

  • 代码体积更小,因为函数申明语句会产生大量代码;
  • 代码在运行时因为创建的函数作用域更少了,内存开销也随之变小。

如何开启Scope Hoisting

在 webpack 中,设置 mode: 'production' 之后,就默认添加了插件webpack.optimize.ModuleConcatenationPlugin(),开启了 Scope Hoisting。
如果想要关闭Scope Hoisting 可以通过下面的配置完成。

optimization: {
    concatenateModules: false   // 关闭Scope Hoisting
}

原理

Scope Hoisting 的实现原理其实很简单:分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。 因此只有那些被引用了一次的模块才能被合并。
由于 Scope Hoisting 需要分析出模块之间的依赖关系,因此源码必须采用 ES6 模块化语句,不然它将无法生效。

实例

入口JS

// index.js
import foo from './foo'
import bar from './bar'

console.log('run => index.js')
console.log(`log => foo.name: ${foo.name}`)
console.log(`log => bar.name: ${bar.name}`)

export default {
    name: 'index'
}
// foo.js
import bar from './bar'

console.log('run => foo.js')
console.log(`log => bar.name: ${bar.name}`)

export default {
    name: 'foo'
}
// bar.js
console.log('run => bar.js')

export default {
    name: 'bar'
}

打包后的代码

关注立即执行函数的入参

(function (modules) {
    // webpackBootstrap
    // The module cache
    var installedModules = {}
    // The require function
    function __webpack_require__(moduleId) {
        // Check if module is in cache
        if (installedModules[moduleId]) {
            return installedModules[moduleId].exports
        }
        // Create a new module (and put it into the cache)
        var module = (installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {},
        })
        // Execute the module function
        modules[moduleId].call(
            module.exports,
            module,
            module.exports,
            __webpack_require__
        )
        // Flag the module as loaded
        module.l = true
        // Return the exports of the module
        return module.exports
    }
    // expose the modules object (__webpack_modules__)
    __webpack_require__.m = modules
    // expose the module cache
    __webpack_require__.c = installedModules
    // define getter function for harmony exports
    __webpack_require__.d = function (exports, name, getter) {
        if (!__webpack_require__.o(exports, name)) {
            Object.defineProperty(exports, name, {
                enumerable: true,
                get: getter,
            })
        }
    }
    // define __esModule on exports
    __webpack_require__.r = function (exports) {
        if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
            Object.defineProperty(exports, Symbol.toStringTag, {
                value: 'Module',
            })
        }
        Object.defineProperty(exports, '__esModule', { value: true })
    }
    // create a fake namespace object
    // mode & 1: value is a module id, require it
    // mode & 2: merge all properties of value into the ns
    // mode & 4: return value when already ns object
    // mode & 8|1: behave like require
    __webpack_require__.t = function (value, mode) {
        if (mode & 1) value = __webpack_require__(value)
        if (mode & 8) return value
        if (mode & 4 && typeof value === 'object' && value && value.__esModule)
            return value
        var 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
    }
    // getDefaultExport function for compatibility with non-harmony modules
    __webpack_require__.n = function (module) {
        var getter =
            module && module.__esModule
                ? function getDefault() {
                    return module['default']
                }
                : function getModuleExports() {
                    return module
                }
        __webpack_require__.d(getter, 'a', getter)
        return getter
    }
    // Object.prototype.hasOwnProperty.call
    __webpack_require__.o = function (object, property) {
        return Object.prototype.hasOwnProperty.call(object, property)
    }
    // __webpack_public_path__
    __webpack_require__.p = '../'
    // Load entry module and return exports
    return __webpack_require__((__webpack_require__.s = 0))
})(
    /************************************************************************/
    [
        /* 0 */
        /***/ function (module, __webpack_exports__, __webpack_require__) {
            'use strict'
            __webpack_require__.r(__webpack_exports__)

            // CONCATENATED MODULE: ./src/yh-webpack/bar.js
            console.log('run => bar.js')
            /* harmony default export */ var bar = {
                name: 'bar',
            }
            // CONCATENATED MODULE: ./src/yh-webpack/foo.js

            console.log('run => foo.js')
            console.log('log => bar.name: '.concat(bar.name))
            /* harmony default export */ var foo = {
                name: 'foo',
            }
            // CONCATENATED MODULE: ./src/yh-webpack/index.js

            console.log('run => index.js')
            console.log('log => foo.name: '.concat(foo.name))
            console.log('log => bar.name: '.concat(bar.name))
            /* harmony default export */ var yh_webpack = (__webpack_exports__[
                'default'
            ] = {
                name: 'index',
            })

            /***/
        },
    ]
)