Today, while developing a React Native requirement, I wrote some code and found it didn’t work. I reported an error accessing an unregistered page.

Went to the console to check the information and found an error

I guess it is because of this error that JS is not executed, and then our page is not registered. But why is this error reported? I examined the code and found no logical errors.

Continue to look at the console, find a lot of warning information, you can see some module loop dependency warning, and the risk of the value of undefined

What are circular dependencies?

For example, module A references module B, which in turn references module A

For example, module A references module B, which references module C, which in turn references module A, thus forming a reference cycle

Let’s start with a quick look at two of the most popular modular specifications on the front end: CommonJS and ES Module

CommonJS

The CommonJS specification for node.js implementation is analyzed here. In Node.js, each script file is a module, and the require command executes the entire script when it is first loaded, and then generates a description object for the module in memory:

{
  this.id = id;
  this.exports = {};
  this.loaded = false; . }Copy the code

No matter how many times a module is loaded, it is only run once on the first load and then reloaded to the exports property of the object.

Module. Exports, exports, __dirname, etc.

const path = require('path')

console.log(__dirname);
console.log(__filename);

module.exports = {
  name: 'rn'};Copy the code

The following simple simulation of node.js require, the specific implementation of the source code can be seen here

const path = require('path');
const fs = require('fs');
const vm = require('vm');

function Module(id) {
  this.id = id;
  this.exports = {};
}

Module._extenstions = {
  '.js'(module) {
    const script = fs.readFileSync(module.id, 'utf-8');
    const fn = vm.compileFunction(script, ['exports'.'require'.'module'.'__filename'.'__dirname']);
    // `(function (exports, require, module, __filename, __dirname) {
    // ${script}
    / /}); `;
    const exports = module.exports; // Default empty object
    const require = myRequire;
    const filename = module.id;
    const dirname = path.dirname(filename);

    Reflect.apply(fn, exports[exports.require.module, filename, dirname]);
  },
  '.json'(module) {
    const jsonStr = fs.readFileSync(module.id, 'utf-8');
    module.exports = JSON.parse(jsonStr); }}; Module._cache =Object.create(null);

Module._resolveFilename = function (id) {
  const filepath = path.resolve(__dirname, id);
  if (fs.existsSync(filepath)) {
    return filepath;
  }
  const exts = Object.keys(Module._extenstions);
  for (let i = 0; i < exts.length; i++) {
    let newPath = filepath + exts[i];
    if (fs.existsSync(newPath)) return newPath;
  }
  throw new Error(`Cannot find module ${id}`);
};

Module.prototype.load = function (filename) {
  // Get the extension
  let ext = path.extname(filename);
  // Load the corresponding policy based on the extension
  Module._extenstions[ext](this);
};

function myRequire(id) {
  // Parse paths, including built-in packages, third-party packages, and other file paths
  const absPath = Module._resolveFilename(id);
  // Get the previous cache
  let existsModule = Module._cache[absPath];
  if (existsModule) {
    return existsModule.exports;
  }
  // Create a module object
  const module = new Module(absPath);
  // Cache files
  Module._cache[absPath] = module;
  // Load the module
  module.load(absPath);
  // Returns the exports property of the Module object
  return module.exports;
}
Copy the code

As you can see from the code, the corresponding module object is created and cached before the module is executed. The process a module performs is actually calculating the variable properties to the module object that need to be exported. As a result, CommonJS modules are already in a fetching state when they start execution, which is a good solution to the problem of module cyclic dependency.

Loop dependencies in CommonJS

The CommonJS module uses depth-first traversal and is load-time, meaning that the script code is completely executed at require. Once a module is “looping”, only the parts that have been executed are printed, and the parts that have not been executed are not printed.

Here’s an official Node example to illustrate how it works

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

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

// main.js
console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
Copy the code

To perform the main js

The code is executed in the following order:

  • In main.js, a.js will be loaded first, and script A will output the done variable with a value of false, and then script B will be loaded. The code of a will stop executing, and the execution will continue after script B completes.

  • When b.js is executed to the third line, it will load A. js, and then cyclic loading occurs, and the system will fetch the value of the corresponding object of THE module A. jS. Because A. jS is not executed, only the executed part can be retrieved from the exports property, and the unexecuted part is not returned, so the retrieved value is not the last value.

  • A. js executed only one line of code, exports. Done = false; So for B. js, require A. js prints only one variable done, with a value of false. Running down, the console prints:

in b, a.done = false

  • B. Js continues with the done variable set to true, console.log(‘b done’); When all the execution is completed, the execution power is returned to A.JS. At this point the console outputs:

b done

  • Console. log(‘in a, b.tone = %j’, b.tone);; The console prints:

n a, b.done = true

  • A.js continues with the done variable set to true until a.js completes execution.

a done

  • The second line in main.js does not execute b.js again and directly prints the cached result. Final console output:

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

The problem of improper use

Since only the parts that have already been executed are printed when the execution is loaded, it is possible to obtain a value of undefined, which is not expected

// a.js
require('./b');
module.exports = {
  obj: {
    name: 'this is a',}};// b.js
const { obj } = require('./a');
console.log(obj.name)
module.exports = 'this is b';
Copy the code

Perform a. s.

ES Module

ES Module is an official modular specification of ES6, implemented with the help of THE JS engine. The JS engine implements the underlying core logic of ES6 Module, and js runtime needs to be adapted in the upper layer.

The processing of ES6 module includes Construction, Instantiation and Evaluation.

  1. The construction stage is mainly to acquire all modules and parse them into Module records, and analyze the dependency between modules.

  • The ES Module does not instantiate or execute any JS code during the build process, which is called static parsing.

Unlike CommonJS, in ES Module, the entire dependency tree is established before any evaluation, which means you can’t use a variable in a Module specifier because it doesn’t have a value yet.

  1. During the instantiation phase, the execution context of the module is created and the module environment record is initialized to manage variables in all modules. Then create a binding for the exported module variable, create a binding for the imported module variable, and initialize the corresponding variable of the submodule (equivalent to the export import statement promotion). Create bindings for var variables and initialize them to undefined. Create bindings for function variables and initialize them to the instantiated value of the function body. Create bindings for other variables (let, const) but do not initialize them.

To instantiate a Module graph, the JS engine does what’s called a depth-first traversal. This means that the JS engine will go to the bottom of the module diagram to find modules that do not depend on any other modules and bind their exports.

When the JS engine has finished binding all exports from a module, it returns one level up to bind the imports from that module. Note that both exports and imports point to the same memory address. Binding the export first ensures that all imports can find the corresponding export.

  1. During the execution phase, the JS engine initializes (assigns) variables in the environment record by executing the uppermost code, which is code other than function.

According to the above introduction, the position of the import and export statements of the ES6 module does not affect the execution result of the module code statements.

console.log(a);

import { a } from 'child.js';
Copy the code

If you are interested in ES Module, please refer to:

  • ES the Module specification

  • ES modules: A cartoon deep-dive

Cyclic dependencies in ES Module

Module processing sequence is as follows:

Problems that are not handled properly

// a.mjs
import { b } from './b.mjs'
console.log(b);
export let a = 'this is a'

// b.mjs
import { a } from './a.mjs';
console.log(a);
export let b = 'this is b';
Copy the code

Perform arjun js

Since the child module is executed before the parent module, when b is executed, module A has not been executed, so variable A has not been initialized, and only the binding is created in the environment record of module A, so we will report an error when we use a in B.

But if we change let a in module A to var a, something changes

// a.mjs
import { b } from './b.mjs'
console.log(b);
export var a = 'this is a'

// b.mjs
import { a } from './a.mjs';
console.log(a);
export let b = 'this is b';
Copy the code

Perform arjun js

Why is that? Remember we mentioned earlier in the instantiation phase that if we encountered a variable declared by var, the binding would be created in the environment record and initialized to undefined, so we would not report an uninitialized error in a submodule. This is the difference between let, const, and VAR. (Let temporary dead zone)

If the execution is asynchronous, there is no problem, because the a variable is already initialized when it is executed asynchronously

// a.mjs
import { b } from './b.mjs'
console.log(b);
export var a = 'this is a'

// b.mjs
import { a } from './a.mjs';
setTimeout(() = > console.log(a));
export let b = 'this is b';
Copy the code

Perform arjun js

Thinking about the problems encountered in the project

After a brief introduction to the improper use of CommonJS and ES Module modularity and cyclic dependency, let’s look at the previous problems. We used ES Module in our project. According to our above analysis, we did not use the var keyword in our project. There should be a ReferenceError, why would there be a undefined error?

When we were developing RN

  • The JSX syntax “sugar” is typically used to describe UI views, however standard JS engines clearly do not support JSX

  • ES6 is usually used. Currently, some JS engines on iOS and Android do not support ES6

  • .

Therefore, we need to package the code we wrote into a code that can be recognized by the JS engine on the mobile phone. Generally, we use Metro to package it into a bundle for native loading. RN bundle is a JS file in essence, which is mainly composed of three parts: Polyfills, module definition, require call

Let’s write a simple example to see what happens when Metro is packaged. The main core code is abbreviated below

// a.js
import { b } from './b'
console.log(b);
export let a = 'this is a'

// b.js
import { a } from './a';
console.log(a);
export let b = 'this is a';
Copy the code

Execute the following command to pack

react-native bundle --entry-file src/a.js --bundle-output metro-dist/ios.bundle --platform ios --assets-dest metro-dist/ios --dev true
Copy the code

Where A. JS is the entry file, the main core code after packaging is written below

global.__d = define;
global.__r = metroRequire

// Use to cache all modules
modules = Object.create(null)

// Define the module
function define(factory, moduleId, dependencyMap) {
  var mod = {
    dependencyMap: dependencyMap,
    factory: factory,
    hasError: false.importedAll: EMPTY,
    importedDefault: EMPTY,
    isInitialized: false.publicModule: {
      exports: {},}}; modules[moduleId] = mod; }function metroImportDefault(moduleId) {
    var exports = metroRequire(moduleId);
    var importedDefault = exports && exports.__esModule ? exports.default : exports;
    return importedDefault;
}

// Reference the module
function metroRequire(moduleId) {
  var module = modules[moduleId];
  // If the module is already initialized
  if (module && module.isInitialized) {
    return module.publicModule.exports;
  }
  
  var moduleObject = module.publicModule;
  moduleObject.id = moduleId;
  factory(
    global,
    metroRequire,
    metroImportDefault,
    moduleObject,
    moduleObject.exports,
    dependencyMap
  );
}

// Define module A
__d(
  function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, module.exports, _dependencyMap) {
    Object.defineProperty(exports.'__esModule', {
      value: true});exports.a = void 0;

    var _b = _$$_REQUIRE(_dependencyMap[0].'./b');

    console.log(_b.b);
    var a = 'this is a';
    exports.a = a;
  },
  0[1].'src/a.js'
);

// Define the b module
__d(
  function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, module.exports, _dependencyMap) {
    Object.defineProperty(exports.'__esModule', {
      value: true});exports.b = void 0;

    var _a = _$$_REQUIRE(_dependencyMap[0].'./a');

    console.log(_a.a);
    var b = 'this is a';
    exports.b = b;
  },
  1[0].'src/b.js'
);

// Import the entry file and start executing the code
__r(0)

Copy the code

As you can see, the packaged result is essentially the CommonJS specification

Execute the packaged bundle

How to avoid or solve circular dependencies?

  • I switched to programming

  • Single function functions as a separate file

    • Files like utils/index.js, common/index.js or shared/index.js should not exist anymore, they are black holes and bogs. Instead of putting many functions into these files, separate functions should be used in a separate file. Otherwise, when you import a function into a file, you introduce additional imports from those files, and you don’t know if those additional imports will re-import your current file, creating a circular dependency.
  • Dependency injection and reverse registration