Uygulama yapısı
Bellek Yönetimi ve Serbest Bırakma
Kendiliğinden bırakılanlar, motif.setDisposable/register, serbest bırakma metotları, detach/silentDetach/silentUnlink, DisposableStore, using/doWork, sızıntı izleme ve canlı bileşen sayımı.
MotifJS'te her eleman canlı bir bileşendir: kendi olay dinleyicilerini, bağlarını, reaktif aboneliklerini ve çocuklarını tutar. Bileşen serbest bırakılınca bunlar onunla birlikte bırakılır. Bileşenin dışında açılan kaynakları (zamanlayıcı, belgeye eklenen dinleyici, gözlemci, bağlantı) ise bileşene siz bağlarsınız.
Kendiliğinden bırakılanlar
Bir bileşen serbest bırakılırken MotifJS şunları kendisi yapar:
motif.onile bağlanan DOM dinleyicilerini kaldırır.- Bağları (
bindings: metin, öznitelik,watch,method…) durdurur. context.onvecontext.onRouterChangedaboneliklerini bırakır.motif.setDisposableile kaydedilen fonksiyonları çalıştırır,motif.registerile bağlanan nesneleri serbest bırakır.- Çocuk bileşenleri aynı yolla serbest bırakır.
- Elemanı belgeden söker; çıkış geçişi tanımlıysa geçiş bittikten sonra.
- Bileşenin alanlarını boşaltır:
element,props,class,attrve bileşene sizin eklediğiniz alanlarundefinedolur. Çocuklarda bu adımdispose({ deep: false })ile atlanır.
Bileşenin kendi kurduğu olaylar, bağlar ve çocuklar için elle temizlik gerekmez.
Elle bağlanan kaynaklar
Bileşenin dışında açılan bir kaynağı bileşen tanımaz: setInterval, document ya da window dinleyicisi, ResizeObserver, WebSocket. Bunlar bileşenin ömrüne motif ad alanıyla bağlanır; sınıfta ve Options'ta this.motif, fonksiyon bileşeninde kökün motif'i.
motif.setDisposable(fn) ve motif.register(nesne)
Not satırının tarihi formatDay(note.updatedAt, now) ile yazılır: bugün saat, dün "Yesterday", daha eskisi gün. Örnekteki bileşen, gün dönünce yazı değişsin diye now'u dakikada bir yeniler; zamanlayıcı bileşen serbest bırakılınca durur:
import { Component, reactive } from "@motifx/core";
import { formatDay } from "../format";
import type { Note } from "../services/notes";
export class NoteTime extends Component<HTMLTimeElement, { note: Note }> {
state = reactive({ now: Date.now() });
override onConfigured() {
const timer = setInterval(() => (this.state.now = Date.now()), 60_000);
this.motif.setDisposable(() => clearInterval(timer));
}
override view() {
return <>{() => formatDay(this.props.note.updatedAt, this.state.now)}</>;
}
}
import { Component, ComponentBase, motifComponent, reactive } from "@motifx/core";
import { formatDay } from "../format";
import type { Note } from "../services/notes";
export class NoteTime extends Component<HTMLTimeElement, { note: Note }> {
static override elementTag = "time";
state = reactive({ now: Date.now() });
override onConfigured() {
const timer = setInterval(() => (this.state.now = Date.now()), 60_000);
this.motif.setDisposable(() => clearInterval(timer));
}
override view() {
return motifComponent("text", (node: ComponentBase) => node.bindings.text(() => formatDay(this.props.note.updatedAt, this.state.now)));
}
}
import { reactive, type ComponentBase } from "@motifx/core";
import { formatDay } from "../format";
import type { Note } from "../services/notes";
export function NoteTime(props: { note: Note }) {
const state = reactive({ now: Date.now() });
const start = (time: ComponentBase) => {
const timer = setInterval(() => (state.now = Date.now()), 60_000);
time.motif.setDisposable(() => clearInterval(timer));
};
return <time onConfig={start}>{() => formatDay(props.note.updatedAt, state.now)}</time>;
}
import { motifComponent, reactive, type ComponentBase } from "@motifx/core";
import { formatDay } from "../format";
import type { Note } from "../services/notes";
export function NoteTime(props: { note: Note }) {
const state = reactive({ now: Date.now() });
return motifComponent("time", (time: ComponentBase) => {
const timer = setInterval(() => (state.now = Date.now()), 60_000);
time.motif.setDisposable(() => clearInterval(timer));
time.bindings.text(() => formatDay(props.note.updatedAt, state.now));
});
}
import { motifOptions, reactive } from "@motifx/core";
import { formatDay } from "../format";
import type { Note } from "../services/notes";
export const NoteTime = (props: { note: Note }) => motifOptions({
el: "time",
state: reactive({ now: Date.now() }),
onConfigured() {
const timer = setInterval(() => (this.state.now = Date.now()), 60_000);
this.motif.setDisposable(() => clearInterval(timer));
},
view() {
return <>{() => formatDay(props.note.updatedAt, this.state.now)}</>;
},
});
import { ComponentBase, motifComponent, motifOptions, reactive } from "@motifx/core";
import { formatDay } from "../format";
import type { Note } from "../services/notes";
export const NoteTime = (props: { note: Note }) => motifOptions({
el: "time",
state: reactive({ now: Date.now() }),
onConfigured() {
const timer = setInterval(() => (this.state.now = Date.now()), 60_000);
this.motif.setDisposable(() => clearInterval(timer));
},
view() {
return motifComponent("text", (node: ComponentBase) => node.bindings.text(() => formatDay(props.note.updatedAt, this.state.now)));
},
});
import { Component, reactive } from "@motifx/core";
import { formatDay } from "../format";
export class NoteTime extends Component {
static elementTag = "time";
state = reactive({ now: Date.now() });
onConfigured() {
const timer = setInterval(() => (this.state.now = Date.now()), 60_000);
this.motif.setDisposable(() => clearInterval(timer));
}
view() {
return <>{() => formatDay(this.props.note.updatedAt, this.state.now)}</>;
}
}
import { Component, motifComponent, reactive } from "@motifx/core";
import { formatDay } from "../format";
export class NoteTime extends Component {
static elementTag = "time";
state = reactive({ now: Date.now() });
onConfigured() {
const timer = setInterval(() => (this.state.now = Date.now()), 60_000);
this.motif.setDisposable(() => clearInterval(timer));
}
view() {
return motifComponent("text", (node) => node.bindings.text(() => formatDay(this.props.note.updatedAt, this.state.now)));
}
}
import { reactive } from "@motifx/core";
import { formatDay } from "../format";
export function NoteTime(props) {
const state = reactive({ now: Date.now() });
const start = (time) => {
const timer = setInterval(() => (state.now = Date.now()), 60_000);
time.motif.setDisposable(() => clearInterval(timer));
};
return <time onConfig={start}>{() => formatDay(props.note.updatedAt, state.now)}</time>;
}
import { motifComponent, reactive } from "@motifx/core";
import { formatDay } from "../format";
export function NoteTime(props) {
const state = reactive({ now: Date.now() });
return motifComponent("time", (time) => {
const timer = setInterval(() => (state.now = Date.now()), 60_000);
time.motif.setDisposable(() => clearInterval(timer));
time.bindings.text(() => formatDay(props.note.updatedAt, state.now));
});
}
import { motifOptions, reactive } from "@motifx/core";
import { formatDay } from "../format";
export const NoteTime = (props) => motifOptions({
el: "time",
state: reactive({ now: Date.now() }),
onConfigured() {
const timer = setInterval(() => (this.state.now = Date.now()), 60_000);
this.motif.setDisposable(() => clearInterval(timer));
},
view() {
return <>{() => formatDay(props.note.updatedAt, this.state.now)}</>;
},
});
import { motifComponent, motifOptions, reactive } from "@motifx/core";
import { formatDay } from "../format";
export const NoteTime = (props) => motifOptions({
el: "time",
state: reactive({ now: Date.now() }),
onConfigured() {
const timer = setInterval(() => (this.state.now = Date.now()), 60_000);
this.motif.setDisposable(() => clearInterval(timer));
},
view() {
return motifComponent("text", (node) => node.bindings.text(() => formatDay(props.note.updatedAt, this.state.now)));
},
});
Satır tarihini <NoteTime note={note} /> ile yerleştirir.
Satır tarihini new NoteTime({ note }) ile yerleştirir.
Satır tarihini motifComponent(NoteTime, { note }) ile yerleştirir.
motif.setDisposable(fn):fnbileşen serbest bırakılırken bir kez çalışır.motif.register(nesne):dispose()metodu olan bir nesneyi (IDisposable) bileşenin ömrüne bağlar; nesne bileşenle birlikte serbest bırakılır. Bir fonksiyonu böyle bir nesneyedisposableCore.toDisposable(fn)çevirir:motif.register(disposableCore.toDisposable(() => clearInterval(timer))),setDisposableile aynı işi görür.
Uygulama olayı aboneliği
Bileşenin context.on(...) ile açtığı abonelik bileşenin ömrüne kendiliğinden bağlanır; bileşen serbest bırakılınca kalkar. Daha önce bırakmak için döndürdüğü fonksiyon çağrılır:
const off = this.context.on("notes-imported", () => (this.state.query = ""));
off();
const off = list.context.on("notes-imported", () => (state.query = ""));
off();
const off = this.context.on("notes-imported", () => (this.state.query = ""));
off();
const off = list.context.on("notes-imported", () => (state.query = ""));
off();
Fonksiyon bileşeninde bu sayfadaki çağrılar bileşenin kendisi üzerindedir (örneklerde list): JSX'te kökün onConfig kancasına ya da ref'ine gelen bileşen, JSX'siz yazımda kökü kuran motifComponent fonksiyonunun aldığı bileşen.
Bileşenin dışında, uygulama üzerinden açılan abonelik (app.on(...), Application.main.on(...)) hiçbir bileşene bağlanmaz; onun döndürdüğü fonksiyonu bir bileşene motif.setDisposable ile verin ya da kendiniz çağırın.
effect temizliği
effect(...) hiçbir bileşene bağlanmaz; durdurmak için döndürdüğü fonksiyon çağrılır. Bileşene ait bir izleyicide aynı işi bindings.watch görür ve bileşenle birlikte durur:
this.bindings.watch(() => localStorage.setItem("notes.query", this.state.query));
const stop = effect(() => localStorage.setItem("notes.query", this.state.query));
this.motif.setDisposable(stop);
list.bindings.watch(() => localStorage.setItem("notes.query", state.query));
const stop = effect(() => localStorage.setItem("notes.query", state.query));
list.motif.setDisposable(stop);
this.bindings.watch(() => localStorage.setItem("notes.query", this.state.query));
const stop = effect(() => localStorage.setItem("notes.query", this.state.query));
this.motif.setDisposable(stop);
list.bindings.watch(() => localStorage.setItem("notes.query", state.query));
const stop = effect(() => localStorage.setItem("notes.query", state.query));
list.motif.setDisposable(stop);
İki yazım aynı sonucu verir: ilkinde izleyiciyi bileşen bırakır, ikincisinde durdurucuyu bileşene siz verirsiniz.
Serbest bırakma metotları
await this.dispose(); // çıkış geçişiyle; alt ağacın başvuruları da temizlenir
await this.dispose({ skipLeaveTransition: true }); // çıkış geçişi oynamadan
await this.dispose({ deep: false }); // çocukların başvuru temizliği atlanır
await this.disposeAsync(); // çıkış geçişi oynatılmaz, süren animasyonlar durdurulur
await list.dispose(); // çıkış geçişiyle; alt ağacın başvuruları da temizlenir
await list.dispose({ skipLeaveTransition: true }); // çıkış geçişi oynamadan
await list.dispose({ deep: false }); // çocukların başvuru temizliği atlanır
await list.disposeAsync(); // çıkış geçişi oynatılmaz, süren animasyonlar durdurulur
await this.dispose(); // çıkış geçişiyle; alt ağacın başvuruları da temizlenir
await this.dispose({ skipLeaveTransition: true }); // çıkış geçişi oynamadan
await this.dispose({ deep: false }); // çocukların başvuru temizliği atlanır
await this.disposeAsync(); // çıkış geçişi oynatılmaz, süren animasyonlar durdurulur
await list.dispose(); // çıkış geçişiyle; alt ağacın başvuruları da temizlenir
await list.dispose({ skipLeaveTransition: true }); // çıkış geçişi oynamadan
await list.dispose({ deep: false }); // çocukların başvuru temizliği atlanır
await list.disposeAsync(); // çıkış geçişi oynatılmaz, süren animasyonlar durdurulur
Seçeneklerin ayrıntısı için bkz. Serbest bırakma. isDisposed bileşenin serbest bırakılıp bırakılmadığını söyler; serbest bırakılmış bileşende dispose() yeniden çağrılırsa bir şey yapmaz, motif çağrıları hata fırlatmaz.
Çocuklar controls üzerinden de serbest bırakılır:
this.controls.remove(child); // child'ı çıkarır ve serbest bırakır
this.controls.clear(); // bütün çocukları serbest bırakır
await this.motif.clear(); // bütün çocukları serbest bırakır, hepsi bitince çözülür
list.controls.remove(child); // child'ı çıkarır ve serbest bırakır
list.controls.clear(); // bütün çocukları serbest bırakır
await list.motif.clear(); // bütün çocukları serbest bırakır, hepsi bitince çözülür
this.controls.remove(child); // child'ı çıkarır ve serbest bırakır
this.controls.clear(); // bütün çocukları serbest bırakır
await this.motif.clear(); // bütün çocukları serbest bırakır, hepsi bitince çözülür
list.controls.remove(child); // child'ı çıkarır ve serbest bırakır
list.controls.clear(); // bütün çocukları serbest bırakır
await list.motif.clear(); // bütün çocukları serbest bırakır, hepsi bitince çözülür
motif.clear(), motif.options.disableDisposal açıksa hiçbir şey yapmaz; controls.clear() bu ayara bakmaz.
Serbest bırakmadan taşıma
remove() ve clear() çocuğu serbest bırakır: bağları, izleyicileri, dinleyicileri ve DOM'u gider. Bir bileşeni serbest bırakmadan başka bir yere taşımak ya da sonra yeniden kullanmak için şu üçünden biri kullanılır:
| Metot | Koleksiyon | Çıkış animasyonu | DOM | Bildirim |
|---|---|---|---|---|
detach(child) |
çıkarır | oynatır: kök element ise kendi leave'i; kök fragment ise görünür element çocuklarının (iç içe fragment'lar dâhil) leave'leri birlikte | leave bitince söker (fragment köklüyse açılış..kapanış aralığının tamamı; gizliyse yer tutucu); geçiş tanımlı değilse senkron | leave bitip DOM söküldükten sonra ebeveynde controlremoved |
silentDetach(child) |
çıkarır | oynatmaz | senkron söker | yok |
silentUnlink(child) |
çıkarır | oynatmaz | dokunmaz | yok |
Üçü de bileşeni ve çocuklarını canlı bırakır. detach ve silentDetach DOM söküldüğünde bileşene ve görünür alt ağacına onDeactivated, bileşen controls.add ile yeniden yerleşince onActivated gönderir; silentUnlink DOM'a dokunmadığı için bildirim göndermez (Yaşam Döngüsü). detach bir Promise döndürür; geçiş varsa söküm bittiğinde çözülür, yoksa çözülmüş gelir:
await this.controls.detach(child); // leave oynadı, DOM söküldü
other.controls.add(child); // yeni yere taşındı
await list.controls.detach(child); // leave oynadı, DOM söküldü
other.controls.add(child); // yeni yere taşındı
await this.controls.detach(child); // leave oynadı, DOM söküldü
other.controls.add(child); // yeni yere taşındı
await list.controls.detach(child); // leave oynadı, DOM söküldü
other.controls.add(child); // yeni yere taşındı
Beklemeden other.controls.add(child) yazmak da güvenlidir: eski ebeveynden detach kendiliğinden yapılır, koleksiyon ve parent hemen güncellenir, DOM ise leave bitince yeni ebeveyne bağlanır. Animasyonlu kaldırmanın diğer yolları motif.hide() ve dispose()'dur.
DisposableStore — grup temizliği
Birden çok kaynak bir DisposableStore'da toplanıp birlikte serbest bırakılır:
import { DisposableStore, disposableCore } from "@motifx/core";
const store = new DisposableStore();
const timer = setInterval(() => (state.now = Date.now()), 60_000);
const tick = store.add(disposableCore.toDisposable(() => clearInterval(timer))); // add eklediğini döndürür
const observer = new ResizeObserver(() => autosize(area));
store.add(disposableCore.toDisposable(() => observer.disconnect()));
store.delete(tick); // tekini çıkarır ve serbest bırakır
store.detach(tick); // tekini çıkarır, serbest bırakmaz
store.clear(); // hepsini serbest bırakır, depo açık kalır
store.dispose(); // hepsini serbest bırakır ve depoyu kapatır
store.isDisposed; // true
import { DisposableStore, disposableCore } from "@motifx/core";
const store = new DisposableStore();
const timer = setInterval(() => (state.now = Date.now()), 60_000);
const tick = store.add(disposableCore.toDisposable(() => clearInterval(timer))); // add eklediğini döndürür
const observer = new ResizeObserver(() => autosize(area));
store.add(disposableCore.toDisposable(() => observer.disconnect()));
store.delete(tick); // tekini çıkarır ve serbest bırakır
store.detach(tick); // tekini çıkarır, serbest bırakmaz
store.clear(); // hepsini serbest bırakır, depo açık kalır
store.dispose(); // hepsini serbest bırakır ve depoyu kapatır
store.isDisposed; // true
Kapatılmış depoya add yapılırsa eklenen nesne depoya girmez ve serbest bırakılmaz (sızar); geliştirme modunda MJX504 uyarısı yazılır. clear/dispose sırasında birden çok öğe hata fırlatırsa hepsi tek bir MJX503 hatasının cause'undaki AggregateError içinde toplanır; tek hata olduğu gibi fırlatılır.
Depo bir bileşene motif.register(store) ile bağlanır.
Serbest bırakma sonrası güvenlik: using ve doWork
Asenkron bir iş bittiğinde bileşen serbest bırakılmış, alanları da boşaltılmış olabilir. using(söz, onfulfilled?, onrejected?) sonucu yalnız bileşen yaşıyorsa işleyicilere verir, değer döndürmez (bkz. using ile Asenkron Sonuç). doWork(söz) bir söz döndürür; bileşen serbest bırakılmışsa değerin ya da reddin yerine MJX108 kodlu bir MotifError gelir (bkz. doWork ile Asenkron Sonuç).
Sızıntı izleme
app.useReactiveMonitor({ enabled: true, threshold: 200, name: "notes" });
app.useReactiveMonitor({ enabled: true, threshold: 200, name: "notes" });
Bir reaktif nesnenin tek bir alanını izleyen efekt sayısı threshold'a (verilmezse 200) ulaşınca MJX507 uyarısı yazılır; uyarı en sık görülen kurulum yığınını da gösterir. Bu çoğunlukla serbest bırakılmayan bileşenlerin bağlarının biriktiğini gösterir. Uyarı yalnız geliştirme modunda yazılır. İzleme varsayılan olarak kapalıdır; geliştirme sırasında açın.
Canlı bileşen sayımı — disposableCore.disposableTracker
Her bileşen bir Disposable'dır: yapıcısı trackDisposable, serbest bırakma zinciri markAsDisposed çağırır. disposableCore.disposableTracker'a bir izleyici takılarak canlı bileşen sayısı ölçülür; tur testlerinde ("N kez kur-sök sonrası sayaç başladığı değere döndü mü") kullanılır:
import { disposableCore, ComponentBase } from "@motifx/core";
let alive = 0;
disposableCore.disposableTracker = {
trackDisposable(x) { if (x instanceof ComponentBase) alive++; },
markAsDisposed(x) { if (x instanceof ComponentBase) alive--; },
setParent() { },
markAsSingleton() { },
};
import { disposableCore, ComponentBase } from "@motifx/core";
let alive = 0;
disposableCore.disposableTracker = {
trackDisposable(x) { if (x instanceof ComponentBase) alive++; },
markAsDisposed(x) { if (x instanceof ComponentBase) alive--; },
setParent() { },
markAsSingleton() { },
};
İzleyiciye bileşenlerin yanında depolar ve toDisposable nesneleri de gelir; örnekteki instanceof denetimi yalnız bileşenleri sayar. markAsDisposed her nesne için bir kez gelir; dispose() yeniden çağrılsa da tekrar bildirilmez. Reaktif nesnelerin bağımlılık haritalarını incelemek için @motifx/core/devtools alt yolundaki debugGetDeps(hedef) ve debugGetDepMap() kullanılır (import { debugGetDeps } from "@motifx/core/devtools"). Geliştirme modunda __MOTIF_DEVTOOLS__.getCounters().components.notInTree, yaşayan ama ağaçta olmayan bileşenleri sayar; sayfadan çıkıldıktan sonra düşmüyorsa o bileşenler bir yerde tutuluyordur (DevTools sözleşmesi).
En iyi uygulamalar
- Bileşenin kendi kurduğu olay, bağ ve çocuklar kendiliğinden bırakılır; onlar için temizlik yazılmaz.
- Bileşenin dışında açılan her kaynak (zamanlayıcı, belge ya da pencere dinleyicisi, gözlemci, bağlantı)
motif.setDisposableya damotif.registerile bileşene bağlanır. - Bileşene ait reaktif izleyicide
effectyerinebindings.watchkullanılır. - Asenkron sonuç using ya da doWork ile işlenir.
- Elle
controls.addile eklenen bileşen kapsayıcıyla birlikte serbest bırakılır; daha önce kaldırmak içincontrols.removekullanılır.
Sonraki adım
Hataların nasıl fırlatıldığını, raporlandığını ve yakalandığını öğrenmek için Hata Yönetimi bölümüne geçin.