浅谈Webpack 的循环依赖,揭露 "莫名其妙" 的 undfined

1,372 阅读3分钟

循环依赖相信大家都不陌生了,没了解过的可以看看# JavaScript 模块的循环加载 做一下了解,这里不再赘述 。

CommonJS 和 ES6 的模块运行机制不一样, 处理方式分别为

  • CommonJS的做法是,一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
  • ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值。 ES6根本不会关心是否发生了"循环加载",只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

webpack 自己实现了一套模块加载机制,可能很多人都遇到因为循环依赖导致的 undefined 问题,那么我们来看看它又是如何处理循环依赖的。

我们准备了 3 个文件,代码如下

// index.js
import './a';

// a.js
import { bar } from './b';
console.log('a run ')
const a = 'aaaa'
export function foo() {
  console.log('执行完毕', a);
  bar();
}

export const b = a;
foo();


// b.js
import { foo } from './a';

console.log('b run');
export const bar = () => {
  console.log('bar...')
}
foo();


a 和 b 互相依赖

image.png

编辑后执行会报®

image.png

我们先来简单回顾一下 webpack 的 module 实现

        /**
         * __webpack_module_cache__: 缓存了所有 module 的执行结果
         * __webpack_modules__: 保存了所有 module
         */
 	var __webpack_modules__ = ({
          "./src/a.js": (module, module.export, __webpack_require__) => void;
          "./src/b.js": (module, module.export, __webpack_require__) => void;
           "./src/index.js": (module, module.export, __webpack_require__) => void;
        })
 	// The module cache
 	var __webpack_module_cache__ = {};
 	
 	// The require function
 	function __webpack_require__(moduleId) {
 		// Check if module is in cache
 		var cachedModule = __webpack_module_cache__[moduleId];
 		if (cachedModule !== undefined) {
 			return cachedModule.exports;
 		}
 		// Create a new module (and put it into the cache)
 		var module = __webpack_module_cache__[moduleId] = {
 			// no module.id needed
 			// no module.loaded needed
 			exports: {}
 		};
 	
 		// Execute the module function
 		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
 	
 		// Return the exports of the module
 		return module.exports;
 	}
        
         __webpack_require__("./src/index.js");

看看编译后的代码,就知道之前执行的时候为什么会报错了

有 2 个关键点需要注意

1,exports 会被提到最前面,通过 Object.defineProperty 将模块需要导出的变量添加到 module.exports,通过 get 方法保存了引用,在使用时才会去读取

2,js 变量提升

之前为什么执行会报错呢?

我们看到 const a = 'aaaa' 是在 var bModule = __webpack_require__("./src/b.js"); 之后定义的。

const 并不存在变提升

而 a.js 的 foo 方法作为一个函数函数声明和初始化都会被提升

这就解释了在 b.js 中调用 aModule.foo(); 时,为什么会出现 ReferenceError: Cannot access 'a' before initialization 的错误了。

你可以试试将 const a 改成 var a 看看有什么效果

  "./src/a.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
        /**
         * __webpack_require__.d 的作用:
         * 调用 Object.defineProperty 将 模块需要导出的变量添加到 module.exports,通过 get 方法保存了引用,在使用时才会去读取
         * 
         */
        __webpack_require__.d(__webpack_exports__, {
          "b": () => (b),
          "foo": () => (foo)
        });
        var bModule = __webpack_require__("./src/b.js");
        console.log('a run ')
        const a = 'aaaa'
        function foo() {
          console.log('执行完毕', a);
          bModule.bar();
        }
        const b = a;
        foo();

   }),

   "./src/b.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

      __webpack_require__.d(__webpack_exports__, {
        "bar": () => (bar)
      });
      var aModule = __webpack_require__("./src/a.js");
      console.log('b run');
      const bar = () => {
        console.log('bar...')
      }
      aModule.foo();

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

前面的代码可能有点复杂,看起来不方便,我写个段简化版本,帮助理解

const exportTest = {
  getLibrary: (key) => {
    return getLibrary(key);
  }
}
// 模拟别的模块调用本模块的 getLibrary 方法
console.log(exportTest.getLibrary("React"));

var library = {
  React: 'React',
  Vue: 'Vue'
}
function getLibrary (key) {
  return library[key];
}

个人感觉虽然 webpack 编译后的代码看着像 CommonJS,但是处理循环依赖的方式跟 ES6 比较类似(不一定对,没去细查)

在平时开发中,应尽量避免循环依赖,如果出现了,多关注下JS 变量提升 很多时候都是它的锅

另外也可以使用 dependency-cruiser 来检测出项目中的循环依赖