面试题答案
一键面试循环依赖产生的场景
在模块化开发中,当模块A依赖模块B,同时模块B又依赖模块A时,就会产生循环依赖。例如:
- 模块A:
// moduleA.js
import { bFunction } from './moduleB.js';
console.log('moduleA starting');
function aFunction() {
console.log('aFunction');
bFunction();
}
export { aFunction };
console.log('moduleA ending');
- 模块B:
// moduleB.js
import { aFunction } from './moduleA.js';
console.log('moduleB starting');
function bFunction() {
console.log('bFunction');
aFunction();
}
export { bFunction };
console.log('moduleB ending');
如果在一个脚本中导入moduleA
,就会触发循环依赖。
ES6模块处理循环依赖
ES6模块在遇到循环依赖时,会以“冻结”的状态处理。当一个模块被导入时,它会先创建该模块的实例,即使模块尚未完全执行完毕。这个实例的属性值在导入时都是undefined
,直到模块执行完毕才会填充真实的值。
例如上面的例子,当执行导入moduleA
时:
- 开始导入
moduleA
,创建moduleA
的实例。 - 在
moduleA
中导入moduleB
,创建moduleB
的实例。 - 在
moduleB
中导入moduleA
,由于moduleA
的实例已存在,所以直接使用,此时moduleA
实例的属性aFunction
为undefined
。 moduleB
继续执行,定义bFunction
,但由于aFunction
为undefined
,如果bFunction
中调用aFunction
会报错(在实际执行到bFunction
调用aFunction
时)。moduleB
执行完毕,moduleA
继续执行,定义aFunction
,moduleA
执行完毕。
CommonJS模块处理循环依赖
CommonJS模块在遇到循环依赖时,会返回已经执行的部分。当一个模块被第一次加载时,它会开始执行,在执行过程中如果遇到对另一个模块的require,会暂停当前模块的执行,去加载并执行被依赖的模块。如果出现循环依赖,被依赖的模块会返回它已经执行的部分。 例如:
- 模块A:
// moduleA.js
const moduleB = require('./moduleB.js');
console.log('moduleA starting');
function aFunction() {
console.log('aFunction');
moduleB.bFunction();
}
exports.aFunction = aFunction;
console.log('moduleA ending');
- 模块B:
// moduleB.js
const moduleA = require('./moduleA.js');
console.log('moduleB starting');
function bFunction() {
console.log('bFunction');
// 如果在这里调用moduleA.aFunction会报错,因为此时aFunction还未定义
}
exports.bFunction = bFunction;
console.log('moduleB ending');
当执行require('moduleA')
时:
- 开始加载
moduleA
,执行到require('./moduleB.js')
。 - 开始加载
moduleB
,执行到require('./moduleA.js')
,由于moduleA
正在加载中,所以返回moduleA
已经执行的部分(此时aFunction
还未定义)。 moduleB
继续执行,定义bFunction
。moduleB
执行完毕。moduleA
继续执行,定义aFunction
,moduleA
执行完毕。