¿Qué hace `require` internamente en CommonJS? ¿Qué pasa si haces `require` del mismo archivo dos veces?
Ver respuesta — intenta responderla en voz alta primero
require hace cuatro cosas: resuelve la ruta del módulo hasta un archivo concreto, ejecuta ese archivo una sola vez, envuelve su código en una función (el module wrapper) que le da variables como module, exports, require, __dirname y __filename, y cachea el resultado (module.exports). Por eso los módulos son singletons: si haces require del mismo archivo dos veces, Node no vuelve a ejecutarlo; te devuelve exactamente el mismo objeto guardado en caché.
Cuando escribes require('./mi-modulo'), Node hace estos pasos en orden:
Resuelve la ruta (resolution): convierte lo que le pasaste en la ruta absoluta de un archivo real. Si es
./mi-modulo, buscami-modulo.js, luego.json, luego.node, o una carpeta conindex.js. Si es un nombre pelado comoexpress, lo busca ennode_modules.Revisa la caché (
require.cache): si ese archivo ya se cargó antes, devuelve el resultado guardado y no ejecuta nada más. Aquí es donde nace el comportamiento de singleton.Envuelve el código (module wrapper): si no estaba en caché, Node envuelve todo el contenido del archivo dentro de una función, más o menos así:
(function (exports, require, module, __filename, __dirname) { // ...aqui va el codigo de tu archivo... });Por eso dentro de cada módulo tienes disponibles esas cinco variables sin haberlas declarado, y por eso las variables que declaras en un módulo no contaminan el ámbito global: viven dentro de esa función.
Ejecuta la función una vez y guarda lo que quedó en
module.exportsdentro de la caché. Ese objeto es lo que recibe quien hizorequire.
La consecuencia más importante: un módulo se ejecuta una sola vez por proceso. Si diez archivos distintos hacen require('./config'), el código de config.js corre una vez, y los diez reciben el mismo objeto compartido.
- Resuelve -> encuentra el archivo exacto.
- Cachea -> si ya se cargó, lo reutiliza (singleton).
- Envuelve -> le da
module,exports,require,__dirname,__filename. - Ejecuta una vez -> corre el archivo y guarda
module.exports.
Un contador que vive dentro de un módulo y persiste entre requires, demostrando que el módulo es un singleton.
// archivo: contador.js
let cuenta = 0; // esta linea SOLO corre la primera vez que se hace require
function incrementar() {
cuenta += 1;
return cuenta;
}
console.log("contador.js se esta EJECUTANDO"); // se imprime una unica vez
module.exports = { incrementar };
// archivo: main.js
// Dos requires del MISMO archivo:
const a = require("./contador");
const b = require("./contador");
// El codigo de contador.js corrio una sola vez,
// asi que "contador.js se esta EJECUTANDO" aparece UNA vez.
console.log(a === b); // true -> es exactamente el mismo objeto (singleton)
console.log(a.incrementar()); // 1
console.log(a.incrementar()); // 2
console.log(b.incrementar()); // 3 <-- usa el MISMO estado que 'a', no reinicia
// Salida:
// contador.js se esta EJECUTANDO
// true
// 1
// 2
// 3
Fíjate que b.incrementar() devuelve 3 y no 1: aunque b vino de un segundo require, comparte el mismo cuenta porque Node entregó el objeto cacheado, no una copia nueva.
Creer que cada require "reinicia" el módulo o crea una instancia nueva. No es así: como el resultado se cachea, todos comparten el mismo estado. Esto es útil para cosas como una conexión a la base de datos o la configuración (quieres una sola), pero es una trampa si guardas estado mutable pensando que cada consumidor tiene el suyo. Otro error relacionado: asignar propiedades a la variable exports y luego reasignar module.exports a otra cosa (module.exports = ...), lo cual descarta todo lo que pusiste en exports, porque al final Node devuelve module.exports, no exports.
"Cuando llamas a require, Node primero resuelve la ruta hasta un archivo concreto, luego revisa la caché: si ese archivo ya se cargó, devuelve el mismo objeto sin volver a ejecutarlo. Si no estaba cacheado, envuelve el código del archivo en una función, el module wrapper, que le inyecta module, exports, require, __dirname y __filename, lo ejecuta una sola vez y guarda module.exports en la caché. Por eso los módulos en CommonJS son singletons: dos requires del mismo archivo devuelven exactamente el mismo objeto y comparten estado. Eso es genial para configuración o una conexión única, pero es un error confiar en que cada require te da una instancia fresca."
¿Qué imprime este programa?
// logger.js
let llamadas = 0;
module.exports = () => {
llamadas += 1;
console.log("llamada numero", llamadas);
};
// app.js
const log1 = require("./logger");
const log2 = require("./logger");
log1();
log2();
log1();
Ver respuesta
Imprime:
llamada numero 1
llamada numero 2
llamada numero 3
Aunque log1 y log2 vienen de dos require distintos, apuntan a la misma función cacheada y comparten la variable llamadas del módulo. Por eso el contador sube 1, 2, 3 en lugar de reiniciarse. log1 === log2 sería true.