记一次依赖循环引用

发现报错

在项目升级了依赖包后,出现了 eslint 报如下错误:
屏幕快照 2019-04-17 下午12.28.05.png

奇怪啊,之前也有 eslint, 完全没有这个报错啊?怀疑是之前 eslint 版本太低的原因,导致还没进行依赖循环引用的 lint. 在 eslint no-circle 规则的文档里有循环引用的示例。

1
2
3
4
5
6
7
// dep-b.js
import './dep-a.js'

export function b() { /* ... */ }

// dep-a.js
import { b } from './dep-b.js' // reported: Dependency cycle detected.

两个文件相互引用就会报 // reported: Dependency cycle detected.

根据报错寻找出错点

根据 eslint 的报错,发现大多数报错集中在 index.js 文件。项目中的 index.js 文件大部分作用是将代码集中导出,方便引入。
继续跟进,根据 index.js 报错的行数,进入具体文件查看, 终于发现了问题。
我们的组件都放到了 components 目录下,然后 components 下的 index.js 里将所有组件引入并导出,同时 在 webpack 配置文件里对 components 目录起了别名,以后其他文件用到某几个组件的话,可以类似下面这样引入:

1
import { Button, Table, Form } from 'components';

简化了写路径的麻烦。
但是,在 components 下的组件里,我发现也有直接从 components 根目录 index.js 文件里引其他组件的,
比如 Table 组件

1
import { Toolbar } from '../../';

这样就造成了循环引用,Table 引 index.js, index.js 引入Table。
找到原因,于是进行一通大改,把类似的改成如下格式:

1
import { Toolbar } from './Toolbar';

引入时直接引入所需组件目录,这样避免了相互引用。

问题解决

改完后,发现报错消失了。虽然项目相互引用没有导致页面崩溃,但是应该避免出现,防止出现意外的 bug。
相互引用类似递归,如果无限循环的话会导致页面崩溃,内存溢出。

相互引用小栗子

在网上看到一个很好的例子,并进行了改造。

event.js

1
2
3
4
5
6
7
8
9
import { odd } from './odd'

export const obj = {
counter: 0,
};
export function even(n) {
obj.counter++;
n !== 0 && odd(n - 1);
}

odd.js

1
2
3
4
5
6
import { even, obj } from './even';

export function odd(n) {
obj.counter++;
n != 0 && even(n - 1);
}

test.js

1
2
3
4
import { even, obj } from './even.js';

even(10);
console.log('counter result:', obj.counter);

这里,even 文件引用了 odd 文件, odd 文件同时引用了 even 文件,存在着循环引用的问题。
在 test.js 这个测试文件中, 我们引入 even.js 进行测试,向 even 方法传入参数 10,当执行 even 时,计数器 counter 自加,若 n 大于零会 执行 odd 方法并传入参数 n-1, 同理, 当执行 odd 时, 计数器 counter 自加, 传入的参数如果大于 0,会调用 even 方法并传入参数 n - 1。这样循环递归调用,直到 n 变为 0 截止。
那么我们可能会有疑问,这样循环引用,js 能顺利执行吗? 假设能顺利执行的话,odd.js 中能改变 even.js 文件中的计数器吗? 最后输出的 counter, 是只在 even 文件中自加的结果还是两个文件共同自加的结果?
接下来, 我们看下测试结果:

image.png

很明显,程序顺利执行,并且 odd 确实改变了 even 文件内的计数器参数。

image.png

那么, 我们同时把 even 和 odd 方法的 n == 0 这个判断条件去掉呢?
可以想象,两个方法会无限循环的相互调用下去。

image.png

正如我们所预料的那样。

从中我们可以总结出至少两个结论:

  • ES6 模块的循环加载,如果存在着相互调用,且存在截止条件,并不会是程序崩溃。但是,如果造成了无限循环调用,会使得程序崩溃,内存溢出。
  • ES6 模块,使用 import 引入时,其实是建立了与模块之间的引用,当用到引入的模块中的变量时,再去模块里取值。