Ders 10 / 19
Ağaç Sarsma
Çizgenin modül düzeyinden ad düzeyine indirilmesi; canlı dışa aktarımların yayılımı, yan etkili modülün elenememesi, paket bildirimindeki yan etki bildiriminin üç ayarı ve elemeyi engelleyen kaynak yazımları.
İçindekiler
Bir önceki ders parçaların hangi modülleri taşıyacağını belirledi. Belirlemediği şey, o modüllerin içinde nelerin taşınacağıdır. Bağımlılık çizgesi bir modülün gerekli olduğunu söyler; içindeki hangi adın kullanıldığını söylemez. Kuzey Yamaç Ölçüm İstasyonu uygulamasının birim modülü altı ad dışa aktarıyorsa ve bunların ikisi hiçbir yerden çağrılmıyorsa, çizge bunu göremez.
Ağaç sarsma (tree shaking), çizgeyi modül düzeyinden ad düzeyine indirip erişilemeyen dışa aktarımları çıktıdan düşürme işidir. Modüller, Araçlar ve Ekosistem kursundaki paketleyici dersi bu dönüşümün üç koşulunu saymıştı. Bu ders koşulları çalıştırarak sınar: birinin ihlal edildiği bir kaynak ağaç kurar, elemenin ne zaman durduğunu ölçer ve paketin bunu bildirimle nasıl mümkün kıldığını gösterir.
Ad Düzeyinde Canlılık
Çizgede düğümler modüller değil, modül#ad çiftleridir. Bir çift canlı ise çıktıda
kalır; değilse düşer.
Yayılım giriş noktasından başlar. Giriş noktasının dışa aktarımları koşulsuz canlıdır;
onları isteyen şey derlemenin dışındadır. Canlı bir dışa aktarımın gövdesinde geçen her ad
da canlıdır: gövde bir içe aktarım adına dokunuyorsa, o adın kaynak modüldeki karşılığı
canlanır. Yeniden dışa aktarımlar zinciri uzatır — toplayıcı bir dosyadaki
export { a } from "./x.js" satırı, a istendiğinde x.js içindeki a adını canlandırır,
ötekilere dokunmaz.
Dinamik içe aktarım bu zincirin bir yerinde belirsizlik yaratır. import() çağrısının
sonucu bir ad alanı nesnesidir ve hangi adların kullanılacağı çağrı yerinden her zaman
okunamaz. Temkinli davranış, dinamik hedefin bütün dışa aktarımlarını canlı saymaktır.
Kaynak Ağacın Gerçek Yüzeyi
Şimdiye kadarki kaynak ağaç yalnız kullanılan adları taşıyordu. Gerçek bir yardımcı modül kümesi böyle olmaz: tüketicilerinin hepsinin ihtiyaç duymadığı bir yüzey sunar, adlar tek bir toplayıcı dosyadan dağıtılır ve içlerinden biri en üst düzeyde iş yapar.
Aşağıdaki blok, Modül Paketleme ve Kod Bölme derslerinde kurulan kaynak/ ağacını sürdürür; giriş noktası oradan gelir ve o derslerin kurulum blokları önce çalıştırılmalıdır.
mkdir -p kaynak cat > kaynak/birim.js <<'EOF' export const SICAKLIK_ALT = -60; export const SICAKLIK_UST = 60; export const sinirdaMi = (c) => c >= SICAKLIK_ALT && c <= SICAKLIK_UST; export const fahrenhayt = (c) => c * 9 / 5 + 32; export const milimetreCiva = (hpa) => hpa * 0.75006157584566; export const ruzgarSogumasi = (c, hiz) => 13.12 + 0.6215 * c - 11.37 * hiz ** 0.16 + 0.3965 * c * hiz ** 0.16; EOF cat > kaynak/tarih.js <<'EOF' const iki = (s) => String(s).padStart(2, "0"); export const saatDakika = (d) => iki(new Date(d).getUTCHours()) + ":" + iki(new Date(d).getUTCMinutes()); export const gunAnahtari = (d) => new Date(d).toISOString().slice(0, 10); export const haftaAnahtari = (d) => { const t = new Date(d); const gun = Math.floor((t - Date.UTC(t.getUTCFullYear(), 0, 1)) / 86400000); return t.getUTCFullYear() + "-H" + iki(Math.ceil((gun + 1) / 7)); }; EOF cat > kaynak/tahmin.js <<'EOF' import { SICAKLIK_ALT, SICAKLIK_UST } from "./birim.js"; const gecerli = (o) => o.sicaklik > SICAKLIK_ALT && o.sicaklik < SICAKLIK_UST; export const egilim = (olcumler) => { const dizi = olcumler.filter(gecerli); if (dizi.length < 2) return 0; return (dizi.at(-1).sicaklik - dizi[0].sicaklik) / (dizi.length - 1); }; EOF cat > kaynak/kayit.js <<'EOF' export const bicimciler = new Map(); export const kaydet = (ad, islev) => bicimciler.set(ad, islev); EOF cat > kaynak/olcek-kaydi.js <<'EOF' import { kaydet } from "./kayit.js"; import { fahrenhayt } from "./birim.js"; kaydet("fahrenhayt", fahrenhayt); EOF cat > kaynak/arac.js <<'EOF' import "./olcek-kaydi.js"; export { sinirdaMi, fahrenhayt, milimetreCiva, ruzgarSogumasi } from "./birim.js"; export { saatDakika, gunAnahtari, haftaAnahtari } from "./tarih.js"; export { egilim } from "./tahmin.js"; export { bicimciler } from "./kayit.js"; EOF cat > kaynak/bicim.js <<'EOF' import { saatDakika, sinirdaMi, fahrenhayt, bicimciler } from "./arac.js"; export function olcumSatiri(o) { const uyari = sinirdaMi(o.sicaklik) ? "" : " (sinir disi)"; const ikinci = bicimciler.has("fahrenhayt") ? " / " + bicimciler.get("fahrenhayt")(o.sicaklik).toFixed(1) + " F" : ""; return saatDakika(o.damga) + " " + o.sicaklik.toFixed(1) + " C" + ikinci + " %" + o.nem + uyari; } export const ikiliBirim = (o) => o.sicaklik.toFixed(1) + " C / " + fahrenhayt(o.sicaklik).toFixed(1) + " F"; EOF cat > paket.json <<'EOF' { "name": "kuzey-yamac", "type": "module", "sideEffects": ["./kaynak/olcek-kaydi.js"] } EOF
Üç yapı eklendi. Birim ve tarih modülleri kullanılmayan adlar taşıyor. Toplayıcı dosya adları tek yerden dağıtıyor ve içine bir de çıplak içe aktarım koyuyor. Ölçek kaydı modülü hiçbir şey dışa aktarmıyor; yalnız en üst düzeyde bir kayda yazıyor ve biçimlendirme modülü çıktısını o kayda bakarak değiştiriyor.
Çözümleyici
Aşağıdaki betik modülleri satır satır ayrıştırır, canlılığı yayar ve üç ayrı yan etki politikası altında çıktı boyutunu hesaplar.
// sarsma.mjs — ad duzeyinde canlilik cozumlemesi ve uc yan etki politikasi. import { readFileSync } from "node:fs"; import { dirname, join, relative } from "node:path"; const GIRIS = "kaynak/giris-tarayici.js"; const BASLANGIC = /^(import\b|export\b|const\b|let\b|function\b|class\b)/; function ayristir(yol) { const satirlar = readFileSync(yol, "utf8").split("\n"); const coz = (b) => relative(".", join(dirname(yol), b)); const iceAktarim = new Map(); // yerel ad -> { modul, dis ad } const ciplak = []; // yalniz yan etki icin ice aktarilan moduller const parca = []; // { ad, modul, dis, govde, disaAktarim } for (let i = 0; i < satirlar.length; i++) { const s = satirlar[i]; let e; if ((e = s.match(/^import\s*\{([^}]*)\}\s*from\s*["']([^"']+)["']/))) { for (const p of e[1].split(",").map((x) => x.trim()).filter(Boolean)) { const [dis, yerel = dis] = p.split(/\s+as\s+/); iceAktarim.set(yerel, { modul: coz(e[2]), dis }); } } else if ((e = s.match(/^import\s*["']([^"']+)["']/))) { ciplak.push(coz(e[1])); } else if ((e = s.match(/^export\s*\{([^}]*)\}\s*from\s*["']([^"']+)["']/))) { for (const p of e[1].split(",").map((x) => x.trim()).filter(Boolean)) { parca.push({ ad: p, modul: coz(e[2]), dis: p, govde: "", disaAktarim: true, boyut: Buffer.byteLength(p) + 2 }); } } else if ((e = s.match(/^(?:export\s+)?(?:const|let|function|async function|class)\s+(\w+)/))) { const govde = [s]; while (i + 1 < satirlar.length && !BASLANGIC.test(satirlar[i + 1])) govde.push(satirlar[++i]); const metin = govde.join("\n").replace(/\s+$/, ""); parca.push({ ad: e[1], govde: metin, disaAktarim: s.startsWith("export"), boyut: Buffer.byteLength(metin) + 1 }); } else if (s.trim() && !s.trim().startsWith("//")) { parca.push({ ad: null, govde: s, disaAktarim: false, boyut: Buffer.byteLength(s) + 1 }); } } return { iceAktarim, ciplak, parca, boyut: Buffer.byteLength(readFileSync(yol)) }; } const modul = new Map(); const kuyruk = [GIRIS]; while (kuyruk.length > 0) { const yol = kuyruk.shift(); if (modul.has(yol)) continue; const m = ayristir(yol); modul.set(yol, m); const dinamik = [...readFileSync(yol, "utf8").matchAll(/\bimport\(\s*["']([^"']+)["']\s*\)/g)] .map(([, b]) => relative(".", join(dirname(yol), b))); kuyruk.push(...[...m.iceAktarim.values()].map((i) => i.modul), ...m.ciplak, ...dinamik, ...m.parca.filter((p) => p.modul).map((p) => p.modul)); m.dinamik = dinamik; } function coz(politika) { // politika: "yok" | "yansiz" | "liste" const LISTE = JSON.parse(readFileSync("paket.json", "utf8")).sideEffects; const canli = new Set(); // "modul#ad" const tutulan = new Set(); const is = []; function govdeyiTara(yol, govde) { const m = modul.get(yol); for (const [yerel, kaynak] of m.iceAktarim) if (new RegExp("\\b" + yerel + "\\b").test(govde)) is.push([kaynak.modul, kaynak.dis]); for (const p of m.parca) if (p.ad && new RegExp("\\b" + p.ad + "\\b").test(govde) && !govde.startsWith("export")) is.push([yol, p.ad]); } function tut(yol) { if (tutulan.has(yol)) return; tutulan.add(yol); const m = modul.get(yol); for (const p of m.parca) if (!p.disaAktarim) govdeyiTara(yol, p.govde); for (const c of m.ciplak) { const korunur = politika === "yok" || (politika === "liste" && LISTE.includes("./" + c)); if (korunur) tut(c); } for (const d of m.dinamik) { tut(d); for (const p of modul.get(d).parca) if (p.disaAktarim) is.push([d, p.ad]); } if (politika === "yok") { for (const i of m.iceAktarim.values()) tut(i.modul); for (const p of m.parca) if (p.modul) tut(p.modul); } } function canlandir(yol, ad) { const anahtar = yol + "#" + ad; if (canli.has(anahtar)) return; canli.add(anahtar); tut(yol); const p = modul.get(yol).parca.find((x) => x.ad === ad && x.disaAktarim); if (!p) return; if (p.modul) is.push([p.modul, p.dis]); else govdeyiTara(yol, p.govde); } tut(GIRIS); for (const p of modul.get(GIRIS).parca) if (p.disaAktarim) is.push([GIRIS, p.ad]); while (is.length > 0) canlandir(...is.pop()); let toplam = 0; const dusen = [], oluAd = []; for (const [yol, m] of modul) { if (!tutulan.has(yol)) { dusen.push(yol); continue; } let bayt = m.boyut; for (const p of m.parca) if (p.disaAktarim && !canli.has(yol + "#" + p.ad)) { bayt -= p.boyut; oluAd.push(yol.replace("kaynak/", "") + "#" + p.ad); } toplam += bayt; } return { toplam, dusen, oluAd }; } const ham = [...modul.values()].reduce((t, m) => t + m.boyut, 0); console.log("cizgedeki modul:", modul.size, " ham boyut:", ham, "B"); for (const politika of ["yok", "yansiz", "liste"]) { const r = coz(politika); console.log("-- sideEffects: " + politika + " --"); console.log(" cikti boyutu : " + r.toplam + " B (kazanc " + (ham - r.toplam) + " B)"); console.log(" dusen modul : " + (r.dusen.map((y) => y.replace("kaynak/", "")).join(", ") || "-")); console.log(" olu ad : " + (r.oluAd.sort().join(", ") || "-")); }
$ node sarsma.mjs cizgedeki modul: 13 ham boyut: 4188 B -- sideEffects: yok -- cikti boyutu : 3546 B (kazanc 642 B) dusen modul : - olu ad : arac.js#egilim, arac.js#gunAnahtari, arac.js#haftaAnahtari, arac.js#milimetreCiva, arac.js#ruzgarSogumasi, birim.js#milimetreCiva, birim.js#ruzgarSogumasi, tahmin.js#egilim, tarih.js#haftaAnahtari -- sideEffects: yansiz -- cikti boyutu : 3231 B (kazanc 957 B) dusen modul : olcek-kaydi.js, tahmin.js olu ad : arac.js#egilim, arac.js#gunAnahtari, arac.js#haftaAnahtari, arac.js#milimetreCiva, arac.js#ruzgarSogumasi, birim.js#milimetreCiva, birim.js#ruzgarSogumasi, kayit.js#kaydet, tarih.js#haftaAnahtari -- sideEffects: liste -- cikti boyutu : 3408 B (kazanc 780 B) dusen modul : tahmin.js olu ad : arac.js#egilim, arac.js#gunAnahtari, arac.js#haftaAnahtari, arac.js#milimetreCiva, arac.js#ruzgarSogumasi, birim.js#milimetreCiva, birim.js#ruzgarSogumasi, tarih.js#haftaAnahtari
Üç politika, aynı kaynak ağaçtan üç ayrı çıktı üretiyor. Ölü ad listesi hepsinde ortak bir çekirdek taşıyor: milimetre cıva, rüzgâr soğuması, hafta anahtarı ve eğilim hesabı hiçbir canlı gövdede geçmiyor. Toplayıcı dosyadaki yeniden dışa aktarım satırları da onlarla birlikte düşüyor — adı taşıyan satır, adın kendisi ölünce anlamsızdır.
Yan Etki Engeli
Politikalar arasındaki farkı yaratan tek dosya ölçek kaydıdır. Bu modülün dışa aktarımı yoktur; çizgeye yalnız çıplak bir içe aktarımla girer ve en üst düzeyinde bir işlev çağırır. Ad düzeyi çözümlemesi burada tıkanır: canlanacak bir ad yoktur, ama modülün çalışması bir başkasının davranışını değiştirir.
Sağlaması bir çalıştırmayla yapılır. Aşağıdaki blok, elemenin uygulandığı bir kopya
üretir ve aynı çağrıyı iki çıktı üzerinde koşturur. sed -i seçeneği BSD ve GNU
sürümlerinde farklı davranır; yedek uzantısı vererek her ikisinde de aynı sonuç alınır.
#!/usr/bin/env bash # Yan etkili modul elenmis bir kopya uretilir, ayni cagri iki cikti uzerinde kosturulur. rm -rf elenmis && mkdir elenmis && cp kaynak/*.js elenmis/ sed -i.yedek -e '/^import ".\/olcek-kaydi.js";$/d' -e '/from ".\/tahmin.js"/d' elenmis/arac.js rm -f elenmis/*.yedek elenmis/olcek-kaydi.js elenmis/tahmin.js OLCUM='{ damga: "2026-02-11T06:00:00Z", sicaklik: -4.2, nem: 72 }' node --input-type=module -e " import { olcumSatiri } from './kaynak/bicim.js'; console.log('korunmus:', olcumSatiri(${OLCUM})); " node --input-type=module -e " import { olcumSatiri } from './elenmis/bicim.js'; console.log('elenmis :', olcumSatiri(${OLCUM})); "
korunmus: 06:00 -4.2 C / 24.4 F %72 elenmis : 06:00 -4.2 C %72
İkinci satırda Fahrenhayt sütunu yoktur. Eleme bir hata üretmedi, bir istisna fırlatmadı; çıktıyı sessizce değiştirdi. Bu, ağaç sarsmanın en tehlikeli kusur biçimidir: derleme başarılı görünür, testlerin kapsamadığı bir davranış kaybolur.
Bu yüzden araç, yan etki durumu bildirilmemiş bir modülü kendiliğinden düşürmez.
Bilgiyi paketin bildirimindeki sideEffects alanı verir ve üç ayarı vardır.
Alan yoksa araç temkinli davranır: hiçbir modülü tümüyle düşürmez, yalnız ölü dışa aktarım bildirimlerini siler. Çıktı 3546 bayttır ve doğrudur, ama tahmin modülü içi boşalmış hâlde çıktıda kalır.
Alan false ise yazar, paketteki hiçbir modülün en üst düzeyde iş yapmadığına söz
vermiş olur. Araç ölçek kaydını ve tahmin modülünü düşürür, kayıt modülündeki kaydet
adı da ölür. Çıktı 3231 bayta iner — ve yukarıdaki ikinci satırın davranışını üretir. Bu
paket için false bir yalandır.
Alan bir liste ise yazar hangi dosyaların yan etkili olduğunu tek tek söyler. Ölçek kaydı korunur, tahmin modülü düşer. Çıktı 3408 bayttır: doğruluğu bozmadan elde edilebilen en küçük çıktı.
Alan bu yüzden bir ayar değil, bir taahhüttür. Yanlış doldurulduğunda araç yanlış eleme yapar ve arıza, elemenin yapıldığı yerde değil, kaybolan davranışın kullanıldığı yerde görünür.
Elemeyi Durduran Öteki Yazımlar
Yan etki tek engel değildir. Aşağıdaki yazımların hepsi ad düzeyi izlemeyi kırar ve aracı modülün tamamını tutmaya zorlar.
Ad alanı nesnesinin bütün olarak dolaştırılması. import * as arac from "./arac.js"
yazımından sonra nesne bir işleve verilirse, hangi adların okunacağı statik olarak
bilinemez. Adlar tek tek içe aktarıldığında bu belirsizlik doğmaz.
Hesaplanmış erişim. arac[secilenAd] biçiminde bir okuma, çalışma zamanında
belirlenen bir ada karşılık gelir. Böyle bir erişimi gören araç, o modüldeki bütün adları
canlı saymak zorundadır.
Çağrı tabanlı modül biçimi. CommonJS’te dışa aktarım nesnesi çalışma zamanında değiştirilebilir; hangi adın var olduğu değerlendirme sonuna kadar kesin değildir. Statik ayrıştırmayla elde edilen liste eksik olabileceği için eleme uygulanmaz.
Değerlendirme anında iş yapan bildirimler. En üst düzeydeki bir çağrının sonucu bir sabite bağlanıyorsa, o çağrının yan etkisi olup olmadığı bilinmez. Araçlar bu belirsizliği kaynağa konan saflık açıklamalarıyla aşar; açıklama da bildirim alanı gibi bir taahhüttür.
Toplayıcı dosyalar. Toplayıcının kendisi zararsızdır — örnekteki toplayıcının ölü yeniden dışa aktarımları düştü. Zararlı olan, toplayıcının yan etkili bir modülü çıplak içe aktarımla çizgeye sokmasıdır: o tek satır, toplayıcıyı içe aktaran her tüketiciye o modülü taşır.
Bu liste, kütüphane yazarına dört maddelik somut bir sorumluluk bırakır: en üst düzeyde iş yapmamak, adları doğrudan dışa aktarmak, yan etkili dosyaları bildirimde listelemek ve toplayıcıya çıplak içe aktarım koymamak.
Özet
- Ağaç sarsma çizgeyi
modül#addüzeyine indirir; canlılık giriş noktasının dışa aktarımlarından başlayıp gövdelerde geçen adlar üzerinden yayılır. - Yeniden dışa aktarımlar canlılığı ada göre iletir; dinamik içe aktarım hedefinin bütün dışa aktarımları temkinli olarak canlı sayılır.
- Dışa aktarımı olmayıp en üst düzeyde iş yapan bir modül ad düzeyi çözümlemesiyle elenemez; elenirse arıza sessizdir — örnekte Fahrenhayt sütunu hatasız biçimde kaybolur.
- Paket bildirimindeki yan etki alanı üç sonuç üretir: alan yoksa 3546 bayt ve modül
düşmez,
falseise 3231 bayt ve davranış bozulur, doğru liste ise 3408 bayt ve davranış korunur. - Ad alanı nesnesinin dolaştırılması, hesaplanmış erişim, çağrı tabanlı modül biçimi ve değerlendirme anında iş yapan bildirimler elemeyi durdurur.
Sonraki Adım
Çizge şimdiye kadar yalnız betik dosyalarından kuruldu. Oysa bir arayüzün çıktısı yalnız koddan oluşmaz: ölçüm listesinin biçem dosyası, istasyon simgesi, arayüzün yazı tipi ve tarayıcıya olduğu gibi kopyalanacak birkaç statik dosya da çıktıya girer. Bunların bir kısmı koddan içe aktarılır ve çizgenin düğümü hâline gelir; bir kısmı hiç içe aktarılmaz ama yine de yayımlanır. Sonraki ders bu ikinci akışı ele alır: varlıkların boru hattından geçirilmesi, küçük olanların gövdeye gömülmesi, büyük olanların ayrı dosya olarak yazılması ve kaynaktaki adların çıktıdaki adlara bağlanması.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.