İçeriğe geç
academia.sh

Ders 03 / 14

CommonJS

Fonksiyon çağrısıyla yükleme modeli, modül sarmalayıcısı ve önbellek, değer kopyası ile canlı bağın farkı, dairesel bağımlılıkta iki sistemin davranışı ve birlikte çalışabilirlik.

İçindekiler

Önceki ders, içe aktarmanın bir deyim olduğu ve bağlamanın değerlendirmeden önce tamamlandığı bir modeli kurdu. Dilin bundan önce yaygınlaşmış ikinci bir modül sistemi vardır ve bu sistem hâlâ çalışan kodun büyük bölümünü taşır.

İkisinin bir arada bulunması bir geçiş kazası değil, kalıcı bir durumdur: bir projede iki sistemin dosyaları yan yana durur ve birbirini çağırır. Bu dersin sorusu, ikinci sistemin neyi farklı yaptığı ve iki modelin sınırının nerede olduğudur.

Yükleme Bir Fonksiyon Çağrısıdır

Bu sistemde bir modülü almak, require adlı bir fonksiyonu çağırmaktır; dışa aktarmak ise module.exports adlı bir nesneye yazmaktır. İkisi de sözdizim değil, sıradan çalışma zamanı işlemleridir.

Bu, kaynak metnin doğrudan çalıştırılmadığı anlamına gelir. Çalışma zamanı, dosyanın içeriğini bir fonksiyon gövdesine sarar ve o fonksiyonu beş argümanla çağırır:

// dosya: sarmalayici.cjs
console.log(typeof require, typeof module, typeof exports);
console.log(__filename.endsWith('sarmalayici.cjs'), typeof __dirname);
console.log(module.exports === exports);
$ node sarmalayici.cjs
function object object
true string
true

Bu beş ad dilin bir parçası değildir; modül sarmalayıcısı (module wrapper) tarafından sağlanır. Modül kapsamı, önceki derste ayrıştırma düzeyinde elde edilen yalıtımın burada sıradan bir fonksiyon kapsamıyla elde edilmiş hâlidir. Sonuç aynıdır: en üst düzey bildirimler genel kapsama sızmaz.

Son satır ayrıca bir tuzağı gösterir: exports başlangıçta module.exports ile aynı nesnedir. exports.ad = ... yazmak ikisini de etkiler, ancak exports = ... yazmak yalnızca yerel adı değiştirir ve dışa aktarım yüzeyine dokunmaz. Yüzeyi tümüyle değiştirmek için module.exports atanmalıdır.

Çağrı tabanlı olmanın doğrudan sonucu, yüklemenin koşullu ve hesaplanmış olabilmesidir:

// dosya: bicim-duz.cjs
module.exports = (olcum) => `sözcük: ${olcum.sozcukSayisi}`;
// dosya: secim.cjs — bicim-duz.cjs yukarıdaki dosyadır
const kip = process.argv[2] ?? 'duz';
const bicimlendir = require(`./bicim-${kip}.cjs`);

console.log(bicimlendir({ sozcukSayisi: 8, cumleSayisi: 2 }));
$ node secim.cjs
sözcük: 8

Belirteç çalışma anında hesaplandığı için hangi dosyanın yükleneceği kaynağa bakılarak bilinemez. Esnekliğin bedeli budur: statik çözümleme yapan araçlar bu bağımlılığı göremez, ağaç sarsma uygulanamaz.

Önbellek ve Tek Değerlendirme

Aynı dosya birden çok kez istendiğinde gövdesi bir kez çalışır. Çalışma zamanı, çözülmüş dosya yolunu anahtar alan bir önbellek tutar:

// dosya: sayac.cjs
console.log('sayac.cjs gövdesi çalıştı');
let sayi = 0;
module.exports = {
  artir() { sayi += 1; return sayi; },
  get deger() { return sayi; },
};
// dosya: onbellek.cjs — sayac.cjs yukarıdaki dosyadır
const a = require('./sayac.cjs');
const b = require('./sayac.cjs');

console.log('aynı nesne mi:', a === b);
a.artir();
console.log('b üzerinden okunan:', b.deger);
console.log('önbellekte kayıtlı mı:', require.resolve('./sayac.cjs') in require.cache);
$ node onbellek.cjs
sayac.cjs gövdesi çalıştı
aynı nesne mi: true
b üzerinden okunan: 1
önbellekte kayıtlı mı: true

Gövde bir kez çalıştı ve iki çağrı aynı nesneyi verdi. Önceki derste sayılan dört güvenceden dördüncüsü — tek değerlendirme — burada gözlenebilir bir tabloyla gerçekleştirilmiştir. Önbelleğin anahtarı çözülmüş mutlak yoldur; aynı dosyaya farklı yollardan ulaşılırsa, kayıt da farklı olur.

Kopya ve Canlı Bağ

module.exports sıradan bir nesnedir; içe aktarma da sıradan bir okuma. Bu, önceki dersin canlı bağ davranışının burada bulunmadığı anlamına gelir:

// dosya: kayit.cjs
let cagriSayisi = 0;
function say() { cagriSayisi += 1; }

module.exports = { cagriSayisi, say };
// dosya: kopya.cjs — kayit.cjs yukarıdaki dosyadır
const { cagriSayisi, say } = require('./kayit.cjs');
const tumu = require('./kayit.cjs');

say();
say();
console.log('yıkımla alınan:', cagriSayisi);
console.log('nesne üzerinden:', tumu.cagriSayisi);
$ node kopya.cjs
yıkımla alınan: 0
nesne üzerinden: 0

İki okuma da sıfır verdi. module.exports nesnesi kurulurken cagriSayisi değişkeninin o anki değeri nesneye kopyalandı; sonraki artırmalar yerel değişkeni değiştirdi ama nesneyi değiştirmedi. Aynı senaryo standart modül sözdiziminde 2 vermişti.

Bu fark, iki sistemin arasında taşınan kodun sessiz hata kaynağıdır. Değişen bir sayaç ya da bayrak dışa aktarılıyorsa, çağrı tabanlı sistemde okuma anında hesaplanan bir erişimci kullanılmalıdır — sayac.cjs örneğindeki get deger tanımı bunun için vardı.

Dairesel Bağımlılık

İki modül birbirini içe aktardığında çizge döngü içerir. Hiçbir sistem bu döngüyü çözemez; yapabilecekleri, döngüyü belirli bir noktada kırmaktır. Kırma noktası iki sistemde farklıdır.

Çağrı tabanlı sistemde require, önbellekte kısmen dolmuş bir dışa aktarım nesnesi bulur ve onu döndürür:

// dosya: a.cjs
console.log('a: gövde başladı');
exports.ad = 'A';
const b = require('./b.cjs');
console.log('a: b.ad =', b.ad, '| b.tanim =', typeof b.tanim);
exports.tanim = function () { return 'A tanımı'; };
console.log('a: gövde bitti');
// dosya: b.cjs
console.log('b: gövde başladı');
const a = require('./a.cjs');
console.log('b: a.ad =', a.ad, '| a.tanim =', typeof a.tanim);
exports.ad = 'B';
exports.tanim = function () { return 'B tanımı'; };
console.log('b: gövde bitti');
$ node a.cjs
a: gövde başladı
b: gövde başladı
b: a.ad = A | a.tanim = undefined
b: gövde bitti
a: b.ad = B | b.tanim = function
a: gövde bitti
(node:4526) Warning: Accessing non-existent property 'tanim' of module exports inside circular dependency
(Use `node --trace-warnings ...` to show where the warning was created)

b.cjs, a.cjs dosyasının yarısı çalışmış hâlini gördü: ad tanımlıydı, tanim tanımsızdı. Program çöktürmedi, eksik bir değer verdi. Uyarı satırındaki süreç numarası her çalıştırmada değişir.

Standart modül sözdiziminde bağlama değerlendirmeden önce tamamlandığı için adlar zaten vardır; sorun, bir adın değerinin henüz atanmamış olmasıdır. Fonksiyon bildirimleri gövde çalışmadan hazır olduğundan sorunsuz kullanılır:

// dosya: a.mjs
import { bAdi, bTanim } from './b.mjs';

console.log('a: gövde başladı');
export const aAdi = 'A';
export function aTanim() { return 'A tanımı'; }
console.log('a: b.bAdi =', bAdi, '| bTanim() =', bTanim());
// dosya: b.mjs
import { aAdi, aTanim } from './a.mjs';

console.log('b: gövde başladı');
console.log('b: aTanim() =', aTanim());
export const bAdi = 'B';
export function bTanim() { return 'B tanımı'; }
$ node a.mjs
b: gövde başladı
b: aTanim() = A tanımı
a: gövde başladı
a: b.bAdi = B | bTanim() = B tanımı

b.mjs, henüz gövdesi çalışmamış olan a.mjs dosyasının fonksiyonunu çağırabildi. Aynı düzenekte const ile tanımlı bir değere erken erişilirse sonuç sessiz bir tanımsız değil, hatadır:

// dosya: a2.mjs
import { bAdi } from './b2.mjs';

export const aAdi = 'A';
console.log('a2: bAdi =', bAdi);
// dosya: b2.mjs
import { aAdi } from './a2.mjs';

console.log('b2: aAdi =', aAdi);
export const bAdi = 'B';
$ node a2.mjs 2>&1 | grep '^ReferenceError'
ReferenceError: Cannot access 'aAdi' before initialization

Ayrım özetlenebilir: biri eksik veriyi sessizce geçirir, diğeri erişimi durdurur. İkisinde de asıl çözüm aynıdır — dairesel bağımlılık bir tasarım kokusudur ve ortak parçanın üçüncü bir modüle çıkarılmasıyla giderilir.

Birlikte Çalışabilirlik

İki sistem aynı projede bulunduğunda hangi dosyanın hangi kurala göre okunacağı bir işaretle belirlenir: .mjs uzantısı standart modülü, .cjs uzantısı çağrı tabanlı modülü zorlar; uzantısı .js olan dosyalar için en yakın bildirim dosyasındaki tip alanına bakılır.

Standart modülden çağrı tabanlı modüle erişim doğrudandır:

// dosya: eski-bicim.cjs
function bicimle(sayi) { return sayi.toFixed(2); }

module.exports = { bicimle, surum: '1.0.0' };
// dosya: tuketici.mjs — eski-bicim.cjs yukarıdaki dosyadır
import eski from './eski-bicim.cjs';
import { bicimle } from './eski-bicim.cjs';
import { createRequire } from 'node:module';

const require = createRequire(import.meta.url);
const elle = require('./eski-bicim.cjs');

console.log('varsayılan dışa aktarım:', eski);
console.log('adlandırılmış içe aktarım:', bicimle(3.14159));
console.log('createRequire ile:', elle === eski);
$ node tuketici.mjs
varsayılan dışa aktarım: { bicimle: [Function: bicimle], surum: '1.0.0' }
adlandırılmış içe aktarım: 3.14
createRequire ile: true

module.exports nesnesi varsayılan dışa aktarım olarak gelir. Adlandırılmış içe aktarımın çalışması ise bir çıkarıma dayanır: çalışma zamanı kaynağı tarayarak hangi adların atandığını tahmin eder. Atamalar bir döngü içinde veya hesaplanmış anahtarlarla yapılıyorsa bu tahmin tutmaz ve yalnızca varsayılan dışa aktarım kullanılabilir.

Ters yön daha kısıtlıdır. require eşzamanlı bir çağrıdır: değeri hemen döndürmek zorundadır. Standart modül çizgesi ise üst düzey await (top-level await) içerebilir, yani değerlendirmesi eşzamansız olabilir. Böyle bir çizge eşzamanlı çağrıyla yüklenemez:

// dosya: bekleyen.mjs
const gecikme = await new Promise((coz) => setTimeout(() => coz('hazır'), 10));
export const durum = gecikme;
// dosya: bekleyen-cagri.cjs — bekleyen.mjs yukarıdaki dosyadır
const m = require('./bekleyen.mjs');
console.log(m.durum);
$ node bekleyen-cagri.cjs 2>&1 | grep '^Error'
Error [ERR_REQUIRE_ASYNC_MODULE]: require() cannot be used on an ESM graph with top-level await. Use import() instead. To see where the top-level await comes from, use --experimental-print-required-tla.

Sınır teknik bir zorunluluktur, bir eksiklik değil: eşzamanlı bir çağrı, tamamlanması zamana yayılan bir işi bekleyemez. Hata iletisinin işaret ettiği çözüm, sonraki dersin konusudur.

Özet

  • Çağrı tabanlı sistemde içe aktarma bir fonksiyon çağrısı, dışa aktarma bir nesneye yazmadır; belirteç çalışma anında hesaplanabilir, bu yüzden statik çözümleme yapılamaz.
  • Modül sarmalayıcısı, kapsam yalıtımını sıradan bir fonksiyon kapsamıyla sağlar ve require, module, exports adlarını dışarıdan verir.
  • Yüklenen modüller çözülmüş yola göre önbelleğe alınır; gövde bir kez çalışır ve tüm çağrılar aynı nesneyi alır.
  • Dışa aktarılan ilkel değerler kopyadır; canlı bağ davranışı için erişimci tanımlanmalıdır.
  • Dairesel bağımlılıkta çağrı tabanlı sistem kısmen dolmuş bir nesne verir, standart sistem erken erişimi hata ile durdurur.
  • Uzantı ve tip alanı hangi kuralın uygulanacağını belirler; eşzamanlı çağrı, üst düzey await içeren bir çizgeyi yükleyemez.

Sonraki Adım

Eşzamanlı çağrının yapamadığını yapan bir biçim vardır: söz döndüren, çalışma anında çağrılabilen ve belirteci hesaplanabilen bir içe aktarma. Sonraki ders bu biçimi, kod bölme ve gecikmeli yükleme kalıplarını ve modül kaydının bu kalıplardaki rolünü ele alır.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat