Modularity lets us create code that spans multiple modules (technically: files).
But how it works under the hood? How do import and export actually work in a functional way? How it is possible to have cyclic dependencies between modules?
We are going to build a short example of three modules: A, B and Main. Modules A and B will depend on each other.
Let's start with the code:
// ==========================================
// Infrastructure
// ==========================================
const registry = {};
function registerModule(id, factoryFn) {
registry[id] = {
id,
factoryFn,
exports: {},
isEvaluated: false
};
}
function evaluateModule(id) {
const mod = registry[id];
if (mod.isEvaluated) {
return mod.exports;
}
mod.isEvaluated = true;
const _import = (dependencyName) => {
return evaluateModule(dependencyName);
};
const _export = (name, fn) => {
mod.exports[name] = fn;
};
mod.factoryFn(_import, _export);
return mod.exports;
}
// ==========================================
// Example use
// ==========================================
// Module A (a.js) - cyclic dependency to B
registerModule('./a.js', (_import, _export) => {
const _b = _import('./b.js');
let valueA = "Wartość z A";
_export('getA', () => valueA);
_export('callB', () => "A calls B -> " + _b.getB() );
});
// Module B (b.js) - cyclic dependency to A
registerModule('./b.js', (_import, _export) => {
const _a = _import('./a.js');
let valueB = "Wartość z B";
_export('getB', () => valueB);
_export('callA', () => "B calls A -> " + _a.getA() );
});
// main module (main.js)
registerModule('./main.js', (_import, _export) => {
const _a = _import('./a.js');
const _b = _import('./b.js');
console.log(_a.callB());
console.log(_b.callA());
});
evaluateModule('./main.js');
Look how simple it is. We have a global registry of modules. Our register function just adds a module to the registry.
The only non trivial function here is the evaluation function. The function executes module's factory function.
Whenever we execute the import, we just recursively call the evaluation function. And this is where a problem with cyclic dependencies could possibly occur.
How do we prevent it?
...
if (mod.isEvaluated) {
return mod.exports;
}
mod.isEvaluated = true;
We check if module is already evaluated and if it is, we just return its exports - but the actual list of module's exports can possibly be empty at this point! In fact, the list of module's exports can be available only after the module initialization is complete!
That's why we only export functions and we call the evaluation of the main module only when the dependency graph is fully evaluated (all exports are available). If a module is imported multiple times (by other modules), its factory function is evaluated only once, all subsequent initializations terminate early.