Handling of circular dependency in JavaScript

Posted by aesthetics1 on Fri, 26 Nov 2021 15:44:46 +0100

What is circular dependency

Circular dependency generally appears with modularization, that is, it depends on module b in module a, and module b depends on module a.

This situation has been encountered in previous development. During the initialization of the store, the methods in the utils module are used, and the data in the store is used in utils

There is no problem during development, but an error is reported when running after packaging, which is actually a problem of circular dependency.

For circular dependency, the default CommonJS module of Node, the module of ES6 and the processing of Webpack are different

Handling of circular dependency in CommonJS

You can see the official pair of nodes Introduction to circular dependency:

a.js, through require, references the b module:

console.log('a start');
exports.done = false;

const b = require('./b.js');
console.log('in a, b.done = %j', b.done);

exports.done = true;
console.log('a done');

In b.js, module a is referenced through require:

console.log('b start');
exports.done = false;

const a = require('./a.js');
console.log('in b, a.done = %j', a.done);

exports.done = true;
console.log('b done');

In main.js, reference modules a and b successively:

console.log('main start');

const a = require('./a.js');
const b = require('./b.js');

console.log('in main, a.done = %j, b.done = %j', a.done, b.done);

Then execute node main.js. What will be the output result

main starting

a starting

b starting

in b, a.done = false

b done

in a, b.done = true

a done

in main, a.done = true, b.done = true

When running module a, if you encounter require('b '), run b.js. If you encounter require('a'), in order to avoid circular reference, an unfinished copy of exports of a.js will be returned to B as the result of require('a ')

Therefore, at this time, a.done in B is false. After executing B, continue to execute module a, and b.done of module a becomes true

As can be seen from the above example, the CommonJS module handles circular dependency well, which mainly depends on its two characteristics:

  1. Modules are loaded at runtime
  2. Loaded (including incomplete) modules are cached

ESM processing

In a.mjs, obtain the exported bar in b.mjs through import, and b.mjs obtains the foo in a.mjs through import

. mjs is used to identify the use of ES6 module

a. In MJS:

import {bar} from './b.mjs';

console.log('a.mjs');
console.log(bar);

export const foo = 'foo';

b. In MJS:

import {foo} from './a.mjs';

console.log('b.mjs');
console.log(foo);

export const bar = 'bar';

Then execute in the Node environment:

node --experimental-modules a.mjs

Execution results:

b.mjs
console.log(foo);
            ^
ReferenceError: Cannot access 'foo' before initialization

According to Mr. Ruan Yifeng's explanation, after executing a.mjs, the engine finds that b.mjs is loaded, and then b.mjs is executed first.

When b.mjs is executed, it is found that foo is imported from A. at this time, a.mjs will not be executed. It will be considered that foo already exists and continue to complete the execution. It is not found that foo is not defined at all until it runs to console.log(foo), so an error is reported

If the declaration of the last variable in a.mjs is changed from const to var, because foo has variable promotion, the output result will change and no error will be reported:

b.mjs
undefined
a.mjs
bar

The above results also conform to the characteristics of ESM:

  1. The ESM module outputs a reference to a value
  2. Output interface dynamic execution
  3. Static interface

Webpack's handling of circular dependency

First, Webpack and Webpack cli are installed:

npm install webpack webpack-cli -D

Then, a new webpack.config.js configuration file is created in the project:

const path = require('path');

module.exports = {
  entry: path.resolve(__dirname, 'demo12/commonjs/index.js'),
  output: {
    path: path.resolve(__dirname, 'demo12/dist'),
    filename: 'my-first-webpack.bundle.js'
  },
};

After configuring the simplest packaging configuration and running the webpack command configured in package.json:

> webpack

asset my-first-webpack.bundle.js 533 bytes [compared for emit] [minimized] (name: main)
./demo12/commonjs/index.js 193 bytes [built] [code generated]
./demo12/commonjs/a.js 203 bytes [built] [code generated]
./demo12/commonjs/b.js 203 bytes [built] [code generated]

Webpack does not detect circular dependency, and there are no errors in the packaging process. The results after packaging in the browser are exactly the same as those in CommonJS

If you need Webpack to detect circular dependency, you need to use the plug-in circular dependency plugin:

npm i circular-dependency-plugin -D

Then add the following configuration in webpack.config.js:

const path = require('path');
const CircularDependencyPlugin = require('circular-dependency-plugin');

module.exports = {
  entry: path.resolve(__dirname, 'demo12/commonjs/index.js'),
  output: {
    path: path.resolve(__dirname, 'demo12/dist'),
    filename: 'my-first-webpack.bundle.js'
  },
  plugins: [
    new CircularDependencyPlugin({
      exclude: /node_modules/,
      include: /demo12/,
      failOnError: true,
      allowAsyncCycles: false,
      cwd: process.cwd()
    })
  ]
};

After packaging, the plug-in detects the circular dependency and fails the packaging according to our configuration:

reference resources

Topics: Javascript Front-end