📕
余烬的小册
数据结构与算法GitHub
  • 总述
  • 经验记录
    • 经验总结
      • web component
      • 前端性能优化总结与分析
      • 我的长列表优化方案
      • 双向通讯解决方案
      • 🔧基于istanbul实现代码测试覆盖率工具
      • 表单系统(低代码表单)
      • 跨端小程序
      • 设计一个即时聊天功能
      • 跨页面通讯 3658699fe4cb4d0bbe22b0881390bacd
    • 踩坑记录
      • HTML踩坑记录
      • Flutter踩坑记录
      • CSS踩坑记录
  • 源码解析
    • Vue源码解析
      • Vue2源码解析系列-响应式原理
      • Vue2源码解析系列-模板编译
      • Vue2源码解析系列-渲染系统(待更新)
        • Patch
      • Vue2源码解析系列-调度系统(todo)
      • Vue2组件更新流程(todo)
      • 如何学习Vue源码
      • Vue3源码解析系列-响应系统
      • Vue3源码解析系列-渲染系统
      • Vue3源码解析系列-组件化和渲染优化(todo)
      • Vue router源码解析(todo)
    • React源码解析(todo)
    • 微前端
      • qiankun源码解析(todo)
    • Vite源码解析
      • Vite Client源码
      • Vite Server源码(todo)
  • 前端技术
    • javaScript
      • ES6
        • 变量声明
        • 模块化
        • 箭头函数
        • 你不知道的for...of
        • 新的数据结构Set和Map
        • JavaScript异步编程终极解决方案
        • ES6 Class 3a0c0a225a534984aabe9a943c5df975
      • JavaScript Error
      • JavaScript浅拷贝和深拷贝
      • JavaScript闭包
      • JavaScript最佳实践
      • JavaScript设计模式
      • async函数的polyfill
    • 深入理解JavaScript系列
      • JavaScript中的继承
      • JavaScript原始类型和引用类型
      • JavaScript浅拷贝和深拷贝
      • JavaScript手写系列
      • JavaScript之this
      • 词法环境和环境记录
      • JavaScript内存泄漏
      • 执行上下文
      • 从ECMAScript规范中学习this
    • TypeScript
      • TypeScript基础教程
      • Typescript高级操作
      • TypeScript工具类型
      • Typescript手写实现工具类型
      • Typescript总结(思维导图)
    • 浏览器原理
      • 页面渲染原理
      • 浏览器存储
      • JavaScript事件循环
      • 事件循环
      • 跨域
      • DOM事件流
      • 从输入url到页面渲染
      • 判断节点之间的关系及根据节点关系查找节点
      • history API
    • 跨端技术
      • Flutter
        • Flutter布局组件
    • 前端工程化
      • Babel插件开发指南
      • 循环依赖
      • pm2
    • React
      • React 状态管理
      • React组件通讯
      • Redux入门
      • Flux
      • React Hook(todo)
      • Effect
  • 服务器端
    • 计算机网络
      • 应用层
      • 运输层
      • 物理层
      • 数据链路层
      • HTTP缓存
      • HTTPS
      • 网络层
    • NodeJs
      • Node.js
      • nodejs最佳实践
      • 《深入浅出Nodejs》小结
      • mongoose填充(populate)
      • node事件循环
      • Node子进程
      • nestjs从零开始
      • nodejs流
      • Nodejs调试
      • Koa源码解析
    • 服务器
      • 操作系统
      • Linux
      • nginx常用指令
      • nginx常用配置
    • 数据库
      • Mysql常见语法
      • MongoDB Indexes索引
  • 前端安全与性能优化
    • 前端安全
      • 跨站脚本攻击(XSS)
      • 跨站点请求伪造(CSRF)
      • 点击劫持
      • 中间人攻击
      • 越权攻击与JWT
    • 前端性能优化
      • 前端监控系统
      • 前端性能优化总结与分析 7348bba0918645b1899006dc842a64c1
      • 衡量性能的核心指标 0dc15ef127cf4f4a9f1137c377420292
      • 图片懒加载
  • 杂项
    • 其他
      • Git
      • web component框架
      • 实现滚动框的懒加载
      • Stencil指南
    • CSS
      • 定位和层叠上下文
      • BFC
      • 盒模型
      • css选择器
      • css变量
由 GitBook 提供支持
在本页
  • 什么是循环依赖
  • CommonJs中的循环依赖
  • ES Module中的循环依赖
  • 解决循环依赖
  • 提前导出
  • 利用函数提升
  • 优化模块逻辑
在GitHub上编辑
  1. 前端技术
  2. 前端工程化

循环依赖

什么是循环依赖

循环依赖是模块化机制下的一种现象。循环依赖是指当模块a依赖于模块b,但是模块b又依赖于模块b的情况。ES Module和CommonJs两种模块化机制对循环依赖的处理是不相同。

可以用下面这个例子来理解循环依赖:

// a.js
const obj = require("./b.js");

exports.a = 1;

console.log(obj);

// b.js
const { a } = require("./a.js");

const obj = {
	a,
};

module.exports = obj 

在上面的例子中,模块a引用了模块b的obj对象,但是模块b的obj对象的a属性却又来源于模块a,这就导致了两个模块互相依赖,产生了循环依赖。

CommonJs中的循环依赖

上面这段代码在CommonJs模块化机制下的输出是undefined 。这是由于CommonJs的实现机制决定的。

对于上面的代码,我们想要的结果应当是obj = {a: 1},但是实际上结果确是 {a: undefined} 。并且nodejs还会提示你出现了循环依赖。

在介绍具体的原理前我们需要知道几个知识点:

  • CommonJs实现模块化的方式是利用对象的方式实现的,导入和导出的都是js对象

  • require函数导入的实际上是另一个模块的exports对象,exports默认是空对象,即{},并且exports和module.exports是同一个对象。

由此可以分析出,当模块a执行到require("./b.js") 语句时,此时模块a的exports对象是空的,即exports = {} ,这个时候js会执行模块b中的代码,当执行到require("./a.js") 时,由于模块a已经执行过了,所以不会再执行,而是直接返回模块a的exports对象,前面分析过模块a的exports对象是空对象,因此require("./a.js") 返回的也是这个空对象,因此a=undefined ,之后再将obj对象导出,模块a的require("./b.js") 就返回了{a: undefined} 。

你可以通过debugger的方式来分析此过程。

// a.js
debugger;
// 执行到require函数调用语句时会立即进入模块b中执行代码
// 注意这里require()不会马上返回结果,要等模块b代码都执行完毕后才会返回模块b的exports对象
const obj = require("./b.js");
// 导出变量a
exports.a = 1;

console.log(obj);
// 结果: {a: undefined}

// b.js
// require("./a.js")返回的是模块a的exports的对象,由于此时模块a还未导出,因此a是undefined。
const { a } = require("./a.js");

const obj = {
	a,
};
// 模块b导出对象obj
module.exports = obj 

ES Module中的循环依赖

同样是上面的例子,我们改写成ES Module(简称:ESM)的写法来看一下。

// a.mjs
import obj from "./b.mjs";

export let a = 1;

console.log(obj);

// b.mjs
import { a } from "./a.mjs";

export default {
	a,
};

// node a.mjs
// ReferenceError: Cannot access 'a' before initialization

这次的结果和CommonJs中的结果不同,会直接报错,报错信息提示不能在变量a初始化之前访问。

导致CommonJs和ES Module对循环依赖不同现象的原因是CommonJs是通过js对象来导入导出的,它的导入是在运行时确定的,require是一个函数,exports是一个对象;而ES Module(ESM)的导入导出(import、export)是一种JavaScript语法,ESM是静态的,它有一个静态编译阶段,它的导入导出是在编译时确定的。

首先js引擎执行到import obj from "./b.mjs"; 语句后会执行模块b的代码,在模块b中的又导入了模块a的变量a,此时js引擎会认为从模块a中导入的变量a已经定义好了,并不会去执行模块a的代码,而是继续执行模块b后面的代码,知道执行到export default {a}; 时去访问变量b,才发现并没有导入成功,因此报错。

解决循环依赖

提前导出

在CommonJs中,由于导出导入都是通过js对象实现的,因此我们可以通过提前导出来避免出现循环依赖的问题。

// a.js
// 导出语句先执行
exports.a = 1;

const obj = require("./b.js");

console.log(obj);

当然这里也有弊端,如果导出的变量取决于导入的值,那么就不能用这个方法。

// a.js
// 当导出的是明确的值时,可以提前导出
// exports.a = 1;
// 但是如果导出的值取决于导入的值时,那么就会出问题了
exports.b = obj.b;
const obj = require("./b.js");
console.log(obj);

// b.js
const { a } = require("./a.js");

const b = 1;

const obj = {
	a,
	b,
};

module.exports = obj;

// node a.js
// Uncaught ReferenceError ReferenceError: Cannot access 'obj' before initialization

另外在ESM中也无法起作用。

利用函数提升

在ESM即使你把export写在import语法的前面,js引擎也仍然会先执行import语句,解决的方法是利用函数提升来实现提前导出。

// a.mjs
import obj from "./b.mjs";

console.log(obj);

export function a() {
	return 1;
}

// b.mjs
import { a } from "./a.mjs";

export default {
	a: a(),
};

注意只有函数声明才会有函数提升,其他例如箭头函数、函数表达式等都没有函数提升现象,因此无法其作用。

这种方法的思路仍然是提前导出,只不过它利用了js函数提升的特性实现。而在CommonJs中由于导入导出都是通过对象完成的,因此可以直接吧导出语句写在导入语句的前面来实现。

优化模块逻辑

循环依赖很容易出现bug,在ESM下会报错提示,但是在CommonJs却不会有报错提示,只能靠开发者自己检测,这很容易出现意想不到的问题,因此最好的办法是优化模块逻辑来避免循环依赖的情况。

上一页Babel插件开发指南下一页pm2

最后更新于1年前