Temel kavramlar
Reaktivite
reactive, createSignal, createComputed, createLazyComputed, effect, untracked ve bağların DOM'la buluşması; JSX ile ve JSX'siz.
MotifJS'in reaktivite sistemi noktasaldır (fine-grained). Bir modelin bir alanı değiştiğinde yalnızca o alanı okuyan bağlar ve efektler yeniden çalışır; bileşen yeniden render edilmez.
Durum tutmak için birden çok araç vardır ve her biri ayrı bir iş için tasarlanmıştır; seçimi belirleyen, tutulan şeyin biçimidir. Bir not başlığı, metni, bayrakları ve içindeki listeleriyle bir modeldir: reactive onu olduğu gibi sarar, her alanı ayrı izler, iç içe nesneleri ve dizileri de reaktif yapar; değişen alan yalnız onu okuyan bağı uyandırır. Durum tek bir değerse, örneğin seçili notun kimliği ya da bir kaydırma konumu, onu bir nesneye sarmaya gerek yoktur: createSignal o değeri tutar, değer bütün olarak yazılır ve istenirse kendi eşitlik kuralıyla karşılaştırılır. Bu kimliği yüzlerce satır kendi kimliğiyle karşılaştırıyorsa createSelection seçim değişince yalnız eski ve yeni seçili satırı uyandırır (bkz. Seçili satır). Başka değerlerden türeyen bir değeri elle güncel tutmak yerine createComputed hesaplar; hesap pahalıysa ve değer seyrek okunuyorsa createLazyComputed okunana kadar hesap yapmaz. Değişikliğe ekran dışında bir işle karşılık vermek gerektiğinde effect yazılır. Hepsi aynı izleme düzeneğinin üzerinde çalışır, birbirini okuyabilir ve aynı bağlarla ekrana yansır; aynı bileşende bir reactive model ile bir signal yan yana durabilir.
| Araç | Ne için |
|---|---|
reactive(model) |
Alanları olan bir model: iç içe nesneler, diziler, Map ve Set. Her alan ayrı izlenir; bileşenin ve servisin durumu çoğunlukla budur. useModel aynı işi yapar. |
createSignal(value) |
Tek bir değer: sayı, dizge, seçili kimlik. Değer .value ile bütün olarak yazılır; içindeki bir nesneyi yerinde değiştirmek bildirim yapmaz, mutate() ile bildirilir. Eşitlik karşılaştırıcısı verilebilir. |
createComputed(getter) |
Başka değerlerden türeyen ve sürekli okunan değer. Hazır tutulur; bağımlılık değişince bir sonraki mikro görevde yeniden hesaplanır. |
createLazyComputed(getter) |
Pahalı ve yazıldığından seyrek okunan türetilmiş değer. Okunana kadar hesaplanmaz. |
effect(fn) |
Değişikliğe yan etkiyle karşılık vermek: belge başlığı, localStorage. Bileşen içinde bindings.watch kullanılır, bileşenle birlikte durur. |
Örnekler başlangıç şablonundaki not uygulamasının kesitleridir. Note ve NotesService Bileşenler sayfasındaki src/services/notes.ts, SettingsService Başlarken sayfasındaki src/services/settings.ts dosyasındakilerdir; notes ve settings bu servislerin örnekleridir.
reactive(model)
Bir nesneyi (veya diziyi) reaktif bir proxy'ye dönüştürür. Alanları normal nesne gibi okur/yazarsınız; okumalar izlenir, yazmalar bağımlıları tetikler. İlkel bir değer (sayı, dizge, boolean) verilirse { value } biçiminde bir nesneye sarılır: reactive("") bir { value: "" } proxy'si döndürür, değer .value ile okunup yazılır.
import { reactive } from "@motifx/core";
import type { Note } from "./services/notes";
const state = reactive({ items: [] as Note[], saved: false });
state.saved = true; // bu alana bağlı bağları tetikler
state.items.unshift(note); // dizi metotları izlenir
state.items[0].title = "Toplantı notları"; // iç içe nesneler de reaktiftir
state.items[state.items.length] = draft; // indeksle ekleme de indeksi ve length'i tetikler
state.items.length = 0; // kısaltma, düşen indekslerin okuyucularını uyandırır
import { reactive } from "@motifx/core";
const state = reactive({ items: [], saved: false });
state.saved = true; // bu alana bağlı bağları tetikler
state.items.unshift(note); // dizi metotları izlenir
state.items[0].title = "Toplantı notları"; // iç içe nesneler de reaktiftir
state.items[state.items.length] = draft; // indeksle ekleme de indeksi ve length'i tetikler
state.items.length = 0; // kısaltma, düşen indekslerin okuyucularını uyandırır
NotesService notları böyle bir state.items dizisinde tutar; create() yeni notu unshift ile başa ekler.
Değeri değiştirmeyen bir yazma kimseyi uyandırmaz: bir alana aynı değeri yazmak, bir dizi elemanını kendisiyle değiştirmek ya da uzunluğu aynı sayıya kurmak bir şey tetiklemez; karşılaştırma Object.is iledir. Reaktif bir nesne başka bir reaktif alana ya da diziye konursa ham nesnesi saklanır (state.current = state.items[0]); okurken yine aynı reaktif nesne gelir, ham veri proxy taşımaz.
Dizinin okuma metotları (find, findIndex, some, every, filter, map, forEach, reduce, join, slice, includes, indexOf), for...of ve yayma ([...state.items]) diziye indeks indeks değil tek bir anahtarla abone olur: değeri değiştiren bir indeks yazımı, uzunluk değişimi ya da push, splice, sort gibi yapısal bir metot onları bir kez uyandırır. Eşleşmede duran arama (find, findIndex, some, every, indexOf, includes) yalnız gezdiği ön eke bağlanır: durduğu indeksin ötesindeki bir yazım, araya ekleme ya da sona push onu uyandırmaz; eşleşmesiz tamamlanan arama ile findLast/findLastIndex bütün listeye bağlıdır. Geri çağrının elemanlarda okuduğu alanlar (note.title) ayrıca o elemanlara abone eder; find ve filter'ın döndürdüğü elemanlar da reaktiftir. Doğrudan indeks okuması (state.items[3]) yalnız o indeksi izler.
Proxy asıl nesnenin üzerine kurulur, kopya alınmaz; aynı nesne için aynı proxy döner (reactive(a) === reactive(a)), proxy'nin kendisi verilirse o döner. clearModel(a) bu kaydı düşürür; ondan sonra reactive(a) yeni bir proxy verir (bkz. Yardımcılar). Düz nesneler, diziler, sınıf örnekleri ([object Object]) ve Map, Set, WeakMap, WeakSet reaktif olur (koleksiyonlar). Proxy üzerinden çağrılan getter ve metotlarda this proxy'dir; bu yüzden sınıfın # ile yazılan özel alanına dokunan getter ve metot TypeError fırlatır, örnek ister doğrudan sarılsın ister reaktif bir nesnenin içinden okunsun. Böyle bir sınıfın reaktif durumunu sınıfın içindeki bir reactive({ … }) alanında tutun; TypeScript'in private sözcüğüyle yazılan alan sıradan bir alandır, proxy üzerinden çalışır ve reaktiftir. Bileşen, Application ve ServiceProvider örnekleri ile app.router olduğu gibi döner; reaktif bir nesnenin alanına konsalar da sarılmaz. Date, RegExp gibi nesneler ve genişletilemez (Object.freeze) nesneler olduğu gibi döner, içlerindeki değişiklik bağları tetiklemez. RegExp proxy'lenmez: reaktif nesnenin içinde de test, exec, replace her zamanki gibi çalışır. Deseni yerinde değişmez; yeni desen alana yeni bir RegExp olarak yazılır (highlight.pattern = /yeni/) ve bu yazma bağları tetikler. Date değiştiğinde alanı yeni bir referansla yazın (reminder.due = new Date(reminder.due.getTime() + 24 * 60 * 60 * 1000)).
Bileşende durum
Durum sınıfta bir alan, fonksiyonda bir kapanış değişkeni, Options API'de data alanıdır; üçü de aynı reactive nesnesidir ve görünümdeki getter'lar onu okur. FilterBar bir arama kutusu ve bir "yalnız sabitler" seçeneği tutar; eşleşen not sayısı iki alandan türetilir:
import { Component, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export class FilterBar extends Component {
notes = this.getService(NotesService)!;
state = reactive({ query: "", onlyPinned: false });
matches() {
return this.notes.list("notes", this.state.query).filter((note) => !this.state.onlyPinned || note.pinned);
}
override view() {
return (
<div class="filter">
<input type="search" placeholder="Notlarda ara" value={() => this.state.query} oninput={(e) => (this.state.query = (e.target as HTMLInputElement).value)} />
<label>
<input type="checkbox" checked={() => this.state.onlyPinned} onchange={(e) => (this.state.onlyPinned = (e.target as HTMLInputElement).checked)} />
Yalnız sabitler
</label>
<small>{() => `${this.matches().length} not`}</small>
</div>
);
}
}
import { FromService, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export function FilterBar() {
const notes = FromService(NotesService)!;
const state = reactive({ query: "", onlyPinned: false });
const matches = () => notes.list("notes", state.query).filter((note) => !state.onlyPinned || note.pinned);
return (
<div class="filter">
<input type="search" placeholder="Notlarda ara" value={() => state.query} oninput={(e) => (state.query = (e.target as HTMLInputElement).value)} />
<label>
<input type="checkbox" checked={() => state.onlyPinned} onchange={(e) => (state.onlyPinned = (e.target as HTMLInputElement).checked)} />
Yalnız sabitler
</label>
<small>{() => `${matches().length} not`}</small>
</div>
);
}
import { FromService, motifOptions, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export const FilterBar = () => motifOptions({
el: "div",
data: reactive({ query: "", onlyPinned: false }),
view() {
const notes = FromService(NotesService)!;
const state = this.data;
const matches = () => notes.list("notes", state.query).filter((note) => !state.onlyPinned || note.pinned);
this.class.add("filter");
return (
<>
<input type="search" placeholder="Notlarda ara" value={() => state.query} oninput={(e) => (state.query = (e.target as HTMLInputElement).value)} />
<label>
<input type="checkbox" checked={() => state.onlyPinned} onchange={(e) => (state.onlyPinned = (e.target as HTMLInputElement).checked)} />
Yalnız sabitler
</label>
<small>{() => `${matches().length} not`}</small>
</>
);
},
});
import { Component, ComponentBase, motifComponent, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export class FilterBar extends Component {
notes = this.getService(NotesService)!;
state = reactive({ query: "", onlyPinned: false });
matches() {
return this.notes.list("notes", this.state.query).filter((note) => !this.state.onlyPinned || note.pinned);
}
override view() {
return motifComponent("div", (bar: ComponentBase) => {
bar.class.add("filter");
const field = motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "search", placeholder: "Notlarda ara" });
input.bindings.add("value", () => this.state.query);
input.motif.on("input", (_sender: ComponentBase, e: Event) => (this.state.query = (e.target as HTMLInputElement).value));
});
const label = motifComponent("label", (label: ComponentBase) => {
const box = motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "checkbox" });
input.bindings.add("checked", () => this.state.onlyPinned);
input.motif.on("change", (_sender: ComponentBase, e: Event) => (this.state.onlyPinned = (e.target as HTMLInputElement).checked));
});
label.controls.add(box, motifComponent("text", (node: ComponentBase) => node.setText("Yalnız sabitler")));
});
const count = motifComponent("small", (small: ComponentBase) => small.bindings.text(() => `${this.matches().length} not`));
bar.controls.add(field, label, count);
});
}
}
import { ComponentBase, FromService, motifComponent, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export function FilterBar() {
const notes = FromService(NotesService)!;
const state = reactive({ query: "", onlyPinned: false });
const matches = () => notes.list("notes", state.query).filter((note) => !state.onlyPinned || note.pinned);
return motifComponent("div", (bar: ComponentBase) => {
bar.class.add("filter");
const field = motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "search", placeholder: "Notlarda ara" });
input.bindings.add("value", () => state.query);
input.motif.on("input", (_sender: ComponentBase, e: Event) => (state.query = (e.target as HTMLInputElement).value));
});
const label = motifComponent("label", (label: ComponentBase) => {
const box = motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "checkbox" });
input.bindings.add("checked", () => state.onlyPinned);
input.motif.on("change", (_sender: ComponentBase, e: Event) => (state.onlyPinned = (e.target as HTMLInputElement).checked));
});
label.controls.add(box, motifComponent("text", (node: ComponentBase) => node.setText("Yalnız sabitler")));
});
const count = motifComponent("small", (small: ComponentBase) => small.bindings.text(() => `${matches().length} not`));
bar.controls.add(field, label, count);
});
}
import { ComponentBase, FromService, motifComponent, motifOptions, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export const FilterBar = () => motifOptions({
el: "div",
data: reactive({ query: "", onlyPinned: false }),
view() {
const notes = FromService(NotesService)!;
const state = this.data;
const matches = () => notes.list("notes", state.query).filter((note) => !state.onlyPinned || note.pinned);
this.class.add("filter");
const field = motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "search", placeholder: "Notlarda ara" });
input.bindings.add("value", () => state.query);
input.motif.on("input", (_sender: ComponentBase, e: Event) => (state.query = (e.target as HTMLInputElement).value));
});
const label = motifComponent("label", (label: ComponentBase) => {
const box = motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "checkbox" });
input.bindings.add("checked", () => state.onlyPinned);
input.motif.on("change", (_sender: ComponentBase, e: Event) => (state.onlyPinned = (e.target as HTMLInputElement).checked));
});
label.controls.add(box, motifComponent("text", (node: ComponentBase) => node.setText("Yalnız sabitler")));
});
const count = motifComponent("small", (small: ComponentBase) => small.bindings.text(() => `${matches().length} not`));
return [field, label, count];
},
});
import { Component, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export class FilterBar extends Component {
notes = this.getService(NotesService);
state = reactive({ query: "", onlyPinned: false });
matches() {
return this.notes.list("notes", this.state.query).filter((note) => !this.state.onlyPinned || note.pinned);
}
view() {
return (
<div class="filter">
<input type="search" placeholder="Notlarda ara" value={() => this.state.query} oninput={(e) => (this.state.query = e.target.value)} />
<label>
<input type="checkbox" checked={() => this.state.onlyPinned} onchange={(e) => (this.state.onlyPinned = e.target.checked)} />
Yalnız sabitler
</label>
<small>{() => `${this.matches().length} not`}</small>
</div>
);
}
}
import { FromService, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export function FilterBar() {
const notes = FromService(NotesService);
const state = reactive({ query: "", onlyPinned: false });
const matches = () => notes.list("notes", state.query).filter((note) => !state.onlyPinned || note.pinned);
return (
<div class="filter">
<input type="search" placeholder="Notlarda ara" value={() => state.query} oninput={(e) => (state.query = e.target.value)} />
<label>
<input type="checkbox" checked={() => state.onlyPinned} onchange={(e) => (state.onlyPinned = e.target.checked)} />
Yalnız sabitler
</label>
<small>{() => `${matches().length} not`}</small>
</div>
);
}
import { FromService, motifOptions, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export const FilterBar = () => motifOptions({
el: "div",
data: reactive({ query: "", onlyPinned: false }),
view() {
const notes = FromService(NotesService);
const state = this.data;
const matches = () => notes.list("notes", state.query).filter((note) => !state.onlyPinned || note.pinned);
this.class.add("filter");
return (
<>
<input type="search" placeholder="Notlarda ara" value={() => state.query} oninput={(e) => (state.query = e.target.value)} />
<label>
<input type="checkbox" checked={() => state.onlyPinned} onchange={(e) => (state.onlyPinned = e.target.checked)} />
Yalnız sabitler
</label>
<small>{() => `${matches().length} not`}</small>
</>
);
},
});
import { Component, motifComponent, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export class FilterBar extends Component {
notes = this.getService(NotesService);
state = reactive({ query: "", onlyPinned: false });
matches() {
return this.notes.list("notes", this.state.query).filter((note) => !this.state.onlyPinned || note.pinned);
}
view() {
return motifComponent("div", (bar) => {
bar.class.add("filter");
const field = motifComponent("input", (input) => {
input.attr.add({ type: "search", placeholder: "Notlarda ara" });
input.bindings.add("value", () => this.state.query);
input.motif.on("input", (_sender, e) => (this.state.query = e.target.value));
});
const label = motifComponent("label", (label) => {
const box = motifComponent("input", (input) => {
input.attr.add({ type: "checkbox" });
input.bindings.add("checked", () => this.state.onlyPinned);
input.motif.on("change", (_sender, e) => (this.state.onlyPinned = e.target.checked));
});
label.controls.add(box, motifComponent("text", (node) => node.setText("Yalnız sabitler")));
});
const count = motifComponent("small", (small) => small.bindings.text(() => `${this.matches().length} not`));
bar.controls.add(field, label, count);
});
}
}
import { FromService, motifComponent, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export function FilterBar() {
const notes = FromService(NotesService);
const state = reactive({ query: "", onlyPinned: false });
const matches = () => notes.list("notes", state.query).filter((note) => !state.onlyPinned || note.pinned);
return motifComponent("div", (bar) => {
bar.class.add("filter");
const field = motifComponent("input", (input) => {
input.attr.add({ type: "search", placeholder: "Notlarda ara" });
input.bindings.add("value", () => state.query);
input.motif.on("input", (_sender, e) => (state.query = e.target.value));
});
const label = motifComponent("label", (label) => {
const box = motifComponent("input", (input) => {
input.attr.add({ type: "checkbox" });
input.bindings.add("checked", () => state.onlyPinned);
input.motif.on("change", (_sender, e) => (state.onlyPinned = e.target.checked));
});
label.controls.add(box, motifComponent("text", (node) => node.setText("Yalnız sabitler")));
});
const count = motifComponent("small", (small) => small.bindings.text(() => `${matches().length} not`));
bar.controls.add(field, label, count);
});
}
import { FromService, motifComponent, motifOptions, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export const FilterBar = () => motifOptions({
el: "div",
data: reactive({ query: "", onlyPinned: false }),
view() {
const notes = FromService(NotesService);
const state = this.data;
const matches = () => notes.list("notes", state.query).filter((note) => !state.onlyPinned || note.pinned);
this.class.add("filter");
const field = motifComponent("input", (input) => {
input.attr.add({ type: "search", placeholder: "Notlarda ara" });
input.bindings.add("value", () => state.query);
input.motif.on("input", (_sender, e) => (state.query = e.target.value));
});
const label = motifComponent("label", (label) => {
const box = motifComponent("input", (input) => {
input.attr.add({ type: "checkbox" });
input.bindings.add("checked", () => state.onlyPinned);
input.motif.on("change", (_sender, e) => (state.onlyPinned = e.target.checked));
});
label.controls.add(box, motifComponent("text", (node) => node.setText("Yalnız sabitler")));
});
const count = motifComponent("small", (small) => small.bindings.text(() => `${matches().length} not`));
return [field, label, count];
},
});
matches() sıradan bir metottur; <small> içindeki getter onu çağırırken state.query, state.onlyPinned ve servisteki notların archived, title, pinned alanlarını okur. Bu alanlardan hangisi değişirse değişsin yalnız o metin yeniden hesaplanır.
Durumu yalnız metotlarla değiştirmek
Bir durumun nereden değiştiğini bilmek, değişikliği doğrulamak ya da kaydını tutmak istediğinizde reaktif nesneyi dışarı vermeyin. Durum bir modülün ya da servisin içinde kalır; dışarıya okumak için getter'lar, değiştirmek için metotlar verilir. Getter reaktif nesneyi okuduğu için bağlar ve effect'ler onu izler. Her değişiklik bir metottan geçtiği için kural, doğrulama ya da kayıt tek yerde durur. Not listesinde toplu işlem için seçilen notlar böyle bir durumdur.
DI kullanmadan, bir modülde:
import { reactive } from "@motifx/core";
const state = reactive({ ids: [] as number[] });
export const selection = {
get ids(): readonly number[] {
return state.ids;
},
get count() {
return state.ids.length;
},
has(id: number) {
return state.ids.includes(id);
},
toggle(id: number) {
const index = state.ids.indexOf(id);
if (index < 0) state.ids.push(id);
else state.ids.splice(index, 1);
},
clear() {
state.ids.length = 0;
},
};
import { reactive } from "@motifx/core";
const state = reactive({ ids: [] });
export const selection = {
get ids() {
return state.ids;
},
get count() {
return state.ids.length;
},
has(id) {
return state.ids.includes(id);
},
toggle(id) {
const index = state.ids.indexOf(id);
if (index < 0) state.ids.push(id);
else state.ids.splice(index, 1);
},
clear() {
state.ids.length = 0;
},
};
state modülün dışına çıkmaz; ona yalnız selection'ın metotları yazar. Bileşen selection'ı içe aktarıp kullanır:
<p>{() => `${selection.count} not seçili`}</p>
<input type="checkbox" checked={() => selection.has(note.id)} onchange={() => selection.toggle(note.id)} />
<button type="button" disabled={() => selection.count === 0} onclick={() => selection.clear()}>Seçimi temizle</button>
const count = motifComponent("p", (p: ComponentBase) => p.bindings.text(() => `${selection.count} not seçili`));
const box = motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "checkbox" });
input.bindings.add("checked", () => selection.has(note.id));
input.motif.on("change", () => selection.toggle(note.id));
});
const clear = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "button", disabled: () => selection.count === 0 });
button.motif.on("click", () => selection.clear());
button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Seçimi temizle")));
});
<p>{() => `${selection.count} not seçili`}</p>
<input type="checkbox" checked={() => selection.has(note.id)} onchange={() => selection.toggle(note.id)} />
<button type="button" disabled={() => selection.count === 0} onclick={() => selection.clear()}>Seçimi temizle</button>
const count = motifComponent("p", (p) => p.bindings.text(() => `${selection.count} not seçili`));
const box = motifComponent("input", (input) => {
input.attr.add({ type: "checkbox" });
input.bindings.add("checked", () => selection.has(note.id));
input.motif.on("change", () => selection.toggle(note.id));
});
const clear = motifComponent("button", (button) => {
button.attr.add({ type: "button", disabled: () => selection.count === 0 });
button.motif.on("click", () => selection.clear());
button.controls.add(motifComponent("text", (node) => node.setText("Seçimi temizle")));
});
DI ile, bir serviste:
import { reactive } from "@motifx/core";
export class SelectionService {
private readonly state = reactive({ ids: [] as number[] });
get ids(): readonly number[] {
return this.state.ids;
}
get count() {
return this.state.ids.length;
}
has(id: number) {
return this.state.ids.includes(id);
}
toggle(id: number) {
const index = this.state.ids.indexOf(id);
if (index < 0) this.state.ids.push(id);
else this.state.ids.splice(index, 1);
}
clear() {
this.state.ids.length = 0;
}
}
import { reactive } from "@motifx/core";
export class SelectionService {
state = reactive({ ids: [] });
get ids() {
return this.state.ids;
}
get count() {
return this.state.ids.length;
}
has(id) {
return this.state.ids.includes(id);
}
toggle(id) {
const index = this.state.ids.indexOf(id);
if (index < 0) this.state.ids.push(id);
else this.state.ids.splice(index, 1);
}
clear() {
this.state.ids.length = 0;
}
}
builder.services.addSingleton(SelectionService, { useClass: SelectionService });
builder.services.addSingleton(SelectionService, { useClass: SelectionService });
Bileşen servisi sınıfta this.getService(SelectionService)!, fonksiyon ve Options bileşeninde FromService(SelectionService)! ile alır; kullanım yukarıdakiyle aynıdır. Servis yolu DI'ın kapsamını da getirir: seçimin sayfa değişince sıfırlanması isteniyorsa servis addScoped ile kaydedilir ve her gezinme kendi örneğini alır (bkz. scoped).
İki yolda da selection.count = 3 ve selection.ids.push(…) TypeScript'te derlenmez; getter'ın dönüş tipi readonly number[] olduğu için dizi de dışarıdan değiştirilemez. Servisin state alanı private olduğu için dışarıdan erişim derlenmez. Getter'ı olan bir alana atama ES modülünde çalışma anında da TypeError verir. TypeScript'in readonly ve private korumaları derleme anındadır; modüldeki state ise çalışma anında da dışarıdan görünmez. Bir metot birden çok alan yazsa da onu izleyen bağlar bir kez çalışır.
Çalışma anında da kapatmak için dışarıya asReadonly ile bir görünüm verin. Görünüm aynı veriyi okur ve değişikliği izler; ona yazmak, ondan silmek ya da push gibi değiştiren bir metot çağırmak MJX212 hatası fırlatır:
import { reactive, asReadonly } from "@motifx/core";
const state = reactive({ ids: [] as number[] });
const view = asReadonly(state);
export const selection = {
get ids() {
return view.ids;
},
toggle(id: number) {
const index = state.ids.indexOf(id);
if (index < 0) state.ids.push(id);
else state.ids.splice(index, 1);
},
};
import { reactive, asReadonly } from "@motifx/core";
const state = reactive({ ids: [] });
const view = asReadonly(state);
export const selection = {
get ids() {
return view.ids;
},
toggle(id) {
const index = state.ids.indexOf(id);
if (index < 0) state.ids.push(id);
else state.ids.splice(index, 1);
},
};
selection.ids.push(4) artık çalışma anında da reddedilir; getter'ın dönüş tipi kendiliğinden salt-okunurdur, ayrıca readonly number[] yazmak gerekmez.
asReadonly(obj) — salt-okunur görünüm
Reaktif bir nesnenin, dizinin, Map'in ya da Set'in canlı, salt-okunur görünümünü verir. Kopya değildir: aynı veriye bakar, asıl nesne üzerinden yapılan her değişiklik görünümde de görünür ve görünümü okuyan bağlar uyanır.
import { reactive, asReadonly, isReadonly } from "@motifx/core";
import type { Note } from "../services/notes";
const state = reactive({ items: [] as Note[], saving: false });
export const notes = asReadonly(state);
notes.saving; // okunur ve izlenir
state.saving = true; // notes.saving'i okuyan bağlar uyanır
isReadonly(notes); // true; notes.items ve elemanları da salt-okunurdur
// notes.saving = false; → MJX212 (TypeScript'te derlenmez de)
// notes.items.push(draft); → MJX212
import { reactive, asReadonly, isReadonly } from "@motifx/core";
const state = reactive({ items: [], saving: false });
export const notes = asReadonly(state);
notes.saving; // okunur ve izlenir
state.saving = true; // notes.saving'i okuyan bağlar uyanır
isReadonly(notes); // true; notes.items ve elemanları da salt-okunurdur
// notes.saving = false; → MJX212
// notes.items.push(draft); → MJX212
- Görünümden okunan iç nesneler, dizi elemanları,
Mapdeğerleri veSetelemanları da salt-okunurdur. - Dizinin okuma metotları (
map,filter,find,for...of) çalışır ve izlenir. - Yazma,
delete, diziyi değiştiren metotlar (push,splice,sort,reverse) veMap/Setdeğişiklikleri (set,add,delete,clear)MJX212fırlatır; veri değişmez. Hata strict modda da, değilken de aynıdır. - Aynı nesne için hep aynı görünüm döner.
reactive(view)görünümün kendisini verir; görünümden yazılabilir bir proxy elde edilmez. isReadonly(x)yalnızasReadonlyile alınmış görünümlerdetrue'dur. Dondurulmuş bir nesne (Object.freeze) zaten değiştirilemediği için olduğu gibi döner.- Dönüş tipi
DeepReadonly<T>'dir; atamalar ve değiştiren metotlar derlemede de yakalanır.
useModel(model)
reactive ile aynı işi yapan bir eş addır (alias). Her bileşende aynı işi yapan useModel(model) metodu da vardır.
const state = useModel({ open: false });
const state = useModel({ open: false });
Map, Set, WeakMap, WeakSet
Reaktif bir modelin içindeki koleksiyonlar da reaktiftir. Metotlarını her zamanki gibi çağırırsınız; okuyan bağlar ve efektler yalnızca ilgili değişiklikte yeniden çalışır:
import { reactive } from "@motifx/core";
import type { Note } from "./services/notes";
const board = reactive({
notesById: new Map<number, Note>(),
selected: new Set<number>(),
});
board.notesById.set(note.id, note);
board.selected.add(note.id);
board.notesById.get(note.id)!.title = "Toplantı"; // içerideki nesneler de reaktiftir
board.selected.delete(note.id);
board.notesById.clear();
import { reactive } from "@motifx/core";
const board = reactive({
notesById: new Map(),
selected: new Set(),
});
board.notesById.set(note.id, note);
board.selected.add(note.id);
board.notesById.get(note.id).title = "Toplantı"; // içerideki nesneler de reaktiftir
board.selected.delete(note.id);
board.notesById.clear();
Liste ve seçim, dizi gibi çizilir:
<ul>
{[...board.notesById.values()].map((note) => (
<li key={note.id} class={board.selected.has(note.id) ? "selected" : ""}>{note.title}</li>
))}
</ul>
motifComponent("ul", (ul: ComponentBase) => {
ul.controls.add(motifComponent({
initializeComponent: (holder: ComponentBase) => holder.bindings.list(() => [...board.notesById.values()], (note: Note) =>
motifComponent("li", {
indexkey: () => note.id,
initializeComponent: (li: ComponentBase) => {
li.class.add(() => (board.selected.has(note.id) ? "selected" : ""));
li.controls.add(motifComponent("text", (node: ComponentBase) => node.bindings.add("textContent", note, "title")));
},
})),
}));
});
<ul>
{[...board.notesById.values()].map((note) => (
<li key={note.id} class={board.selected.has(note.id) ? "selected" : ""}>{note.title}</li>
))}
</ul>
motifComponent("ul", (ul) => {
ul.controls.add(motifComponent({
initializeComponent: (holder) => holder.bindings.list(() => [...board.notesById.values()], (note) =>
motifComponent("li", {
indexkey: () => note.id,
initializeComponent: (li) => {
li.class.add(() => (board.selected.has(note.id) ? "selected" : ""));
li.controls.add(motifComponent("text", (node) => node.bindings.add("textContent", note, "title")));
},
})),
}));
});
| Okuma | Yeniden çalıştıran yazma |
|---|---|
get(k), has(k) |
Yalnızca o anahtarı değiştiren set, add, delete, clear |
size, forEach, values(), entries(), for…of, [...x] |
İçeriği değiştiren her yazma |
Map üzerinde keys() |
Yalnızca anahtar eklenmesi ya da silinmesi; var olan anahtarın değerinin değişmesi tetiklemez |
Gerçekte bir şey değişmeyen yazmalar hiçbir şeyi tetiklemez: aynı değerle set (Object.is), var olan elemanı yeniden add, olmayan anahtarı delete, boş koleksiyonu clear.
- İçerideki nesneler:
Mapdeğerleri veSetelemanları okunurken reaktif proxy olarak gelir; alanlarındaki değişiklik izlenir.Mapanahtarları dolaşmada ham döner. - Ham saklama: koleksiyona yazılan proxy'ler ham hâliyle konur. Bir nesne anahtarını ham ya da reaktif vermeniz fark etmez, ikisi de aynı kayda ulaşır.
- Kimlik:
reactive(new Map())doğrudan da kullanılabilir. Aynı koleksiyon için hep aynı proxy döner;instanceof Map, zincirleme (map.set(a, 1).set(b, 2)) veObject.prototype.toStringyerel davranışı korur. WeakMap/WeakSet: yalnızca kendi metotları (get,set,has,add,delete) vardır ve aynı kurallarla izlenir.
Türetilmiş (computed) alanlar — nesne içinde fonksiyon
Reaktif nesne içinde bir fonksiyon alan tanımlarsanız, türetilmiş reaktif değer gibi davranır:
const filters = reactive({
query: "",
onlyPinned: false,
label: () => (filters.onlyPinned ? `${filters.query} (sabitler)` : filters.query), // türetilmiş
});
const filters = reactive({
query: "",
onlyPinned: false,
label: () => (filters.onlyPinned ? `${filters.query} (sabitler)` : filters.query), // türetilmiş
});
<span>{() => filters.label()}</span>
const label = motifComponent("span", (span: ComponentBase) => span.bindings.text(() => filters.label()));
<span>{() => filters.label()}</span>
const label = motifComponent("span", (span) => span.bindings.text(() => filters.label()));
Şablon yuvası fonksiyon değerini argümansız çağırır ({filters.label}, x-text={filters.label}, title={filters.label}) ve okuduğu alanlar değiştikçe yeniden çağırır. {filters.label} ile title={filters.label} fonksiyonu this olmadan, x-text={filters.label} ise filters üzerinden çağırır; bu yüzden türetilmiş alan this yerine filters'ı okur. this kullanan bir metot yuvaya {() => filters.metot()} biçiminde verilir. Bağ kurulurken ve okuduğu alanlar değiştikçe çalışmaması gereken bir fonksiyon (ör. bir Promise çözücüsü) şablon yuvasına verilmez; kendi kodunuzdan çağrılır.
Signal ve Computed
Tek bir değeri saran, en ince taneli ilkellerdir. Bileşen ağacından bağımsız reaktif durum tutmak için uygundur.
createSignal(initialValue)
import { createSignal } from "@motifx/core";
const draft = createSignal("");
draft.value; // oku (izlenir)
draft.value = "Toplantı notları"; // yaz (bağımlıları tetikler)
draft.peek(); // izlemeden oku
draft.update((text) => text.trim()); // fonksiyonla güncelle
draft.mutate((text) => { /* referans korunarak mutasyon */ });
draft.notify(); // referans değişmese de dinleyicileri uyar
draft.asReadonly(); // salt-okunur görünüm
draft.dispose(); // serbest bırak
import { createSignal } from "@motifx/core";
const draft = createSignal("");
draft.value; // oku (izlenir)
draft.value = "Toplantı notları"; // yaz (bağımlıları tetikler)
draft.peek(); // izlemeden oku
draft.update((text) => text.trim()); // fonksiyonla güncelle
draft.mutate((text) => { /* referans korunarak mutasyon */ });
draft.notify(); // referans değişmese de dinleyicileri uyar
draft.asReadonly(); // salt-okunur görünüm
draft.dispose(); // serbest bırak
Signal sınıfının özeti:
| Üye | Açıklama |
|---|---|
get value / set value |
İzlenen okuma / tetikleyen yazma. |
peek() |
İzlemeden oku. |
update(fn) |
Önceki değere göre güncelle. |
mutate(fn) |
İç mutasyon sonrası bildir (referans aynı kaldığında). |
notify() |
Manuel bildirim. |
asReadonly() |
ReadonlySignal<T> görünümü (value, peek()). |
dispose() |
Sonraki işlemleri no-op yapar. |
valueOf() / toString() |
İzlenen okuma; şablon dizesinde (`${draft}`) ya da dize birleştirmede değeri verir. |
Eşitlik karşılaştırıcısı özelleştirilebilir (varsayılan Object.is); bunun için Signal sınıfını doğrudan kurun ya da Signal.create(value, equals) çağırın:
import { Signal } from "@motifx/core";
const scrollTop = new Signal(0, (a, b) => Math.abs(a - b) < 1); // 1 px altı fark yazma sayılmaz
import { Signal } from "@motifx/core";
const scrollTop = new Signal(0, (a, b) => Math.abs(a - b) < 1); // 1 px altı fark yazma sayılmaz
createComputed(getter)
Bağımlılıklarından otomatik türetilen, önbelleklenen değer. Kurulumda hemen hesaplanır; bağımlılıklarından biri değişince kimse okumasa da bir sonraki mikro görevde yeniden hesaplanır ve değer değiştiyse kendisine bağlı effect'leri tetikler. .value önbellekteki değeri verir, okuma sırasında hesap yapmaz; bu yüzden yazmadan hemen sonraki senkron okuma bir önceki değeri verebilir:
import { createSignal, createComputed } from "@motifx/core";
const title = createSignal("");
const body = createSignal("");
const preview = createComputed(() => title.value.trim() || body.value.slice(0, 40) || "Adsız");
preview.value; // "Adsız" — bağımlılıklar değişince yeniden hesaplanır
preview.peek(); // izlemeden oku
preview.dispose();
import { createSignal, createComputed } from "@motifx/core";
const title = createSignal("");
const body = createSignal("");
const preview = createComputed(() => title.value.trim() || body.value.slice(0, 40) || "Adsız");
preview.value; // "Adsız" — bağımlılıklar değişince yeniden hesaplanır
preview.peek(); // izlemeden oku
preview.dispose();
createLazyComputed(getter) — okunana kadar hesaplama
createComputed'ın tembel karşılığı. Kurulumda hesap yapmaz; bir bağımlılık değişince yalnızca kirli işaretlenir ve kendisine bağlı effect'ler uyandırılır. Hesap, .value ilk okunduğunda ve her kirli okumada yapılır; okunmayan bir değer hiç hesaplanmaz. İşaretleme yazma anında senkrondur: items.value = … satırından hemen sonra index.value taze değeri verir.
import { createSignal, createLazyComputed } from "@motifx/core";
import type { Note } from "./services/notes";
const items = createSignal<Note[]>([]);
const index = createLazyComputed(() => buildSearchIndex(items.value));
items.value = await loadNotes(); // hesap yok, yalnızca kirli işareti
index.isDirty; // true
index.value; // şimdi hesaplanır ve önbelleğe alınır
index.value; // önbellekten
index.peek(); // izlemeden oku (kirliyse yine hesaplar)
index.dispose(); // izlemeyi bırakır; sonraki okumalar son değeri verir
import { createSignal, createLazyComputed } from "@motifx/core";
const items = createSignal([]);
const index = createLazyComputed(() => buildSearchIndex(items.value));
items.value = await loadNotes(); // hesap yok, yalnızca kirli işareti
index.isDirty; // true
index.value; // şimdi hesaplanır ve önbelleğe alınır
index.value; // önbellekten
index.peek(); // izlemeden oku (kirliyse yine hesaplar)
index.dispose(); // izlemeyi bırakır; sonraki okumalar son değeri verir
Ne zaman hangisi: değer sürekli ekranda okunuyorsa createComputed (her değişimde hazır); pahalıysa ve yazıldığından seyrek okunuyorsa createLazyComputed (iki okuma arasındaki N yazma → 1 hesap). Aynı senkron bloktaki yazmalar createComputed'da da tek hesaba iner (bkz. Ardışık yazmalar). Ödün: tembel değer değişmese de bağımlı effect'ler uyanır, çünkü kirli işaretlenirken hesap yapılmaz ve eşitlik bilinemez.
Senkron tazelik zincirin tamamı reactive/Signal/LazyComputed'dan oluştuğunda geçerlidir. Zincirde bir createComputed varsa onun yenilenmesi mikro görevde olduğu için ondan beslenen tembel değer de yazmadan hemen sonra değil, akıtmadan sonra tazelenir.
createSelection(source) — seçili satır
Listenin her satırı seçili kimliği kendi kimliğiyle karşılaştırıyorsa (() => state.selected === note.id), seçim değişince her satırın bağı uyanır ve karşılaştırmasını yeniden yapar; oysa değişen yalnız iki satırdır. createSelection karşılaştırmayı anahtara göre tutar: isSelected(key) onu çağıran bağı yalnız o anahtara abone eder, kaynak değişince yalnız eski ve yeni değerin bağları uyanır.
import { reactive, createSelection } from "@motifx/core";
import type { Note } from "../services/notes";
export const state = reactive({ items: [] as Note[], selected: 0 });
export const isSelected = createSelection(() => state.selected);
isSelected(3); // state.selected === 3; bir bağın içinde yalnız 3'e abone olur
isSelected.dispose(); // kaynağı izlemeyi bırakır
import { reactive, createSelection } from "@motifx/core";
export const state = reactive({ items: [], selected: 0 });
export const isSelected = createSelection(() => state.selected);
isSelected(3); // state.selected === 3; bir bağın içinde yalnız 3'e abone olur
isSelected.dispose(); // kaynağı izlemeyi bırakır
Satır seçili sınıfını bu fonksiyonla alır:
<li class={{ "is-selected": () => isSelected(note.id) }} onclick={() => { state.selected = note.id; }}>{() => note.title}</li>
const row = motifComponent("li", (li: ComponentBase) => {
li.class.add({ "is-selected": () => isSelected(note.id) });
li.motif.on("click", () => { state.selected = note.id; });
li.bindings.text(() => note.title);
});
<li class={{ "is-selected": () => isSelected(note.id) }} onclick={() => { state.selected = note.id; }}>{() => note.title}</li>
const row = motifComponent("li", (li) => {
li.class.add({ "is-selected": () => isSelected(note.id) });
li.motif.on("click", () => { state.selected = note.id; });
li.bindings.text(() => note.title);
});
- Karşılaştırma
Object.isiledir. Reaktif nesneler karşılaştırmadan önce ham nesneye indirilir;isSelected(note)satıra hangi biçimde gelirse gelsin seçili nesneyle eşleşir. - Bir bağın dışında çağrılırsa yalnız karşılaştırır, abonelik kurmaz.
- Yalnız eşitlik içindir;
state.selected > note.idgibi bir karşılaştırma anahtara bölünemez, o durumda düz getter kalır. dispose()kaynağı izlemeyi bırakır; modülde verinin yanında oluşturulan bir seçim veriyle birlikte yaşar.- Ölçüm, 60 000 satırlık liste: seçim değişimi 49 ms'den 0,2 ms'ye iner, satır başı bellek aynı kalır.
effect(fn) — yan etkiler ve izleyiciler
Bir efekt kurulduğu anda senkron olarak bir kez çalışır, içinde okunan reaktif değerlere abone olur ve bunlar değiştikçe yeniden çalışır. effect bir durdurucu fonksiyon döndürür:
import { effect } from "@motifx/core";
const stop = effect(() => {
document.title = `${notes.count("notes")} not · Notlar`;
});
// aboneliği bitir
stop();
import { effect } from "@motifx/core";
const stop = effect(() => {
document.title = `${notes.count("notes")} not · Notlar`;
});
// aboneliği bitir
stop();
İkinci argüman isteğe bağlı bir sonuç geri çağrısıdır: effect(getter, onValue) her koşudan sonra getter'ın dönüş değerini onValue'ya verir ve onValue içindeki okumalar izlenmez. İlk onValue çağrısı effect(...) çağrısının içinde senkron koşar; efekt başka bir efektin koşusu sırasında kurulursa bu ilk çağrıdaki okumalar o dıştaki efekte yazılır. Bağımlılık yüzeyini yalnız getter'la sınırlamak için kullanılır; çerçevenin when/ternary/switchCase bağları böyle kuruludur. getter hata fırlatırsa o koşunun değeri yoktur: onValue çağrılmaz, hata MJX208 olarak raporlanır ve efekt izlemeye devam eder; bir sonraki sağlıklı değer yine onValue'ya gelir (bu yüzden when/ternary/switchCase dalı hata anında olduğu gibi kalır):
const stop = effect(() => settings.state.theme, (theme) => {
// yalnız tema değişince koşar; burada okunan settings.state.density izlenmez
const root = document.documentElement;
if (theme === "system") delete root.dataset.theme;
else root.dataset.theme = theme;
localStorage.setItem("settings", JSON.stringify({ theme, density: settings.state.density }));
});
const stop = effect(() => settings.state.theme, (theme) => {
// yalnız tema değişince koşar; burada okunan settings.state.density izlenmez
const root = document.documentElement;
if (theme === "system") delete root.dataset.theme;
else root.dataset.theme = theme;
localStorage.setItem("settings", JSON.stringify({ theme, density: settings.state.density }));
});
Bileşen içinde bir efekt kurarken bindings.watch kullanın (bkz. aşağıda): bileşen serbest bırakıldığında efekt de durur, elle stop() gerekmez.
await sonrası okumalar
İzleme, efektin senkron parçasıyla sınırlıdır: async bir getter/efekt içinde ilk await'ten sonra okunan reaktif alanlar bağımlılık olarak kaydedilmez. Asenkron bir işin sonucuna göre güncellenmesi gereken durumu, sonucu reaktif bir alana yazıp o alanı okuyan ayrı bir bağla gösterin. Tek istisna Virtualization'ın dataRequest prop'udur: derleyici <Virtualization> etiketine satır içi yazılan async fonksiyonun kendi await'lerini sarar ve orada await sonrası okumalar da izlenir; adıyla verilen (dataRequest={loadPage}) ya da async olmayan fonksiyon sarılmaz (bkz. Sanal Listeler). Bu sarmanın çalışma zamanı yarısı asyncTracking dışa aktarımıdır; elle çağrılmaz.
İzlemesiz okuma — untracked
Bir efektin içinde bir değeri okumak ama ona abone olmamak istediğinizde untracked(fn) kullanın; fn'in dönüş değerini verir. İçeride kurulan effect'ler kendi bağımlılıklarını normal izler.
import { effect, untracked } from "@motifx/core";
effect(() => {
const query = filters.query; // izlenir
const density = untracked(() => settings.state.density); // izlenmez: yoğunluk değişince bu efekt koşmaz
localStorage.setItem("lastSearch", JSON.stringify({ query, density }));
});
import { effect, untracked } from "@motifx/core";
effect(() => {
const query = filters.query; // izlenir
const density = untracked(() => settings.state.density); // izlenmez: yoğunluk değişince bu efekt koşmaz
localStorage.setItem("lastSearch", JSON.stringify({ query, density }));
});
Çekirdek bunu liste bağında (bindings.list) kullanır: satır bileşenleri liste effect'inin içinde üretilir, ancak satır şablonundaki ve satırın kurulumundaki okumalar listeye değil satırın kendi bağlarına yazılır; ilk çizimden sonra eklenen satırlar da böyle kurulur. Böylece bir satırın okuduğu değer değişince liste bağı boşuna yeniden koşmaz.
Kural: bir efekt kendi okuduğu değeri yazmamalı.
stats.runs++hem okuma hem yazmadır; efekt kendini yeniden tetikler ve döner. Sayaç/günlük gibi yan kayıtlarıuntrackediçine alın:effect(() => { const count = notes.count("notes"); // izlenen okuma untracked(() => { // okumalar izlenmez → kendini tetiklemez stats.runs++; stats.log.unshift(`${count} not`); }); });
untrackedyalnız okumaların izlenmesini kapatır; içerideki yazmalar bağımlılarını yine tetikler:stats.runs'u gösteren bir bağ güncellenir.Çerçeve bunu yakalar: bir efekt tek akıtmada 50 kez koşarsa o akıtma boyunca atlanır ve geliştirme modunda
MJX203uyarısı verilir. Kuyruktaki diğer efektler etkilenmez.Dizi mutatörleri (
push,pop,shift,unshift,splice,reverse) bu kuralın dışındadır: çağrının kendi iç okumaları (length, indeksler) izlenmez, yani bir efekt içindestats.log.push(x)yazmak efekti diziye abone etmez. Efekt diziyi ayrıca okuyorsa (stats.log.lengthgibi) abonelik yine kurulur.sortbu istisnaya girmez: diziyi okuyup yerinde yazdığı için efekt içinde çağrılırsa efekt kendini tetikler; sıralamayıuntrackediçinde ya da bir kopyada ([...stats.log].sort()) yapın.
Bir efekt tek koşuda 1000'den çok bağımlılık toplarsa geliştirme kipinde MJX209 uyarısı verir; efekt başına bir kez, oturumda en çok on kez. DevTools açıkken uyarı sahibi bileşeni ve bağı söyler (StoreRow at [0, 2, 5] (binding:textContent)). Bu çoğu zaman reaktif bir listenin her satırda baştan sona taranmasıdır: elemanı anahtarla bulun ya da listeyi untracked ile okuyun. Eşik app.useReactiveMonitor({ dependencies: 2000 }) ile değişir, 0 kapatır. İkinci sinyal MJX210: aynı reaktif listeyi aynı bileşen türünün iki ya da daha çok örneği find/some gibi bir arama metoduyla tarıyorsa ve liste en az örnek sayısı kadar öğeliyse (satır listesinin kendisi gibi), liste props'tan ya da başka dosyadan da gelse, geliştirme kipi liste başına bir kez uyarır. Üç öğelik bir ayar listesini yüz satırın taraması uyarmaz; sahipsiz effect'lerde (createComputed, düz effect) eşik sekizdir; DevTools kapalıyken sahip bilinmediği için denetim çalışmaz. scans: false kapatır.
Zincirli (türetilmiş) durum
Bir efekt, başka bir efektin yazdığı durumu okuyabilir. Zincirin tamamı aynı mikro görevde oturur; son değer efektlerin kurulma sırasına bağlı değildir ve ara adımları beklemek gerekmez:
effect(() => { results.items = notes.list("notes", filters.query); }); // yazar
effect(() => { summary.text = `${results.items.length} not`; }); // okur ve yazar
filters.query = "iş";
// bir sonraki mikro görevde summary.text, "iş" ile eşleşen not sayısını taşır
effect(() => { results.items = notes.list("notes", filters.query); }); // yazar
effect(() => { summary.text = `${results.items.length} not`; }); // okur ve yazar
filters.query = "iş";
// bir sonraki mikro görevde summary.text, "iş" ile eşleşen not sayısını taşır
Bir efekt (ya da bir bağın getter'ı) hata fırlatırsa yalnızca o efekt etkilenir; aynı akıtmadaki diğerleri çalışmaya devam eder. Hata, asıl hatayı cause alanında taşıyan MJX208 kodlu bir MotifError olarak merkezî hata yöneticisine bildirilir: konsola yazılır ve errorHandler.addListener(fn) dinleyicilerine ulaşır. Getter'ı fırlatan bağ DOM'daki son değeri (metin, öznitelik, sınıf, stil) olduğu gibi bırakır.
Bileşene bağlı izleyici — bindings.watch / x-watch
İzleyici bileşene kaydedilir; bileşen serbest bırakılınca kendiliğinden durur. İlk koşu kayıt anında, senkron yapılır: sınıfın onConfig'inde ve Options'ın onConfigured'ında kurulan izleyici onBuilding'den önce, etiketteki x-watch ve kurulum fonksiyonundaki bindings.watch ise o etiket kurulurken (initializeComponent içinde) ilk kez çalışır. QueryField arama kutusudur; son sorguyu açılışta localStorage'dan okur ve her değişimde oraya yazar. Sınıfta onConfig içinde this.bindings.watch, fonksiyonda etikete x-watch, Options'ta onConfigured içinde this.bindings.watch; JSX'siz yazımda hepsi bindings.watch çağrısıdır:
import { Component, reactive } from "@motifx/core";
export class QueryField extends Component {
state = reactive({ query: localStorage.getItem("lastQuery") ?? "" });
override onConfig() {
this.bindings.watch(() => localStorage.setItem("lastQuery", this.state.query));
}
override view() {
return <input type="search" placeholder="Notlarda ara" value={() => this.state.query} oninput={(e) => (this.state.query = (e.target as HTMLInputElement).value)} />;
}
}
import { reactive } from "@motifx/core";
export function QueryField() {
const state = reactive({ query: localStorage.getItem("lastQuery") ?? "" });
return (
<input
type="search"
placeholder="Notlarda ara"
value={() => state.query}
oninput={(e) => (state.query = (e.target as HTMLInputElement).value)}
x-watch={() => localStorage.setItem("lastQuery", state.query)}
/>
);
}
import { ComponentBase, motifOptions, reactive } from "@motifx/core";
export const QueryField = () => motifOptions({
el: "input",
data: reactive({ query: localStorage.getItem("lastQuery") ?? "" }),
onConfigured() {
const state = this.data;
this.attr.add({ type: "search", placeholder: "Notlarda ara" });
this.bindings.add("value", () => state.query);
this.motif.on("input", (_sender: ComponentBase, e: Event) => (state.query = (e.target as HTMLInputElement).value));
this.bindings.watch(() => localStorage.setItem("lastQuery", state.query));
},
});
import { Component, ComponentBase, motifComponent, reactive } from "@motifx/core";
export class QueryField extends Component {
state = reactive({ query: localStorage.getItem("lastQuery") ?? "" });
override onConfig() {
this.bindings.watch(() => localStorage.setItem("lastQuery", this.state.query));
}
override view() {
return motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "search", placeholder: "Notlarda ara" });
input.bindings.add("value", () => this.state.query);
input.motif.on("input", (_sender: ComponentBase, e: Event) => (this.state.query = (e.target as HTMLInputElement).value));
});
}
}
import { ComponentBase, motifComponent, reactive } from "@motifx/core";
export function QueryField() {
const state = reactive({ query: localStorage.getItem("lastQuery") ?? "" });
return motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "search", placeholder: "Notlarda ara" });
input.bindings.add("value", () => state.query);
input.motif.on("input", (_sender: ComponentBase, e: Event) => (state.query = (e.target as HTMLInputElement).value));
input.bindings.watch(() => localStorage.setItem("lastQuery", state.query));
});
}
import { ComponentBase, motifOptions, reactive } from "@motifx/core";
export const QueryField = () => motifOptions({
el: "input",
data: reactive({ query: localStorage.getItem("lastQuery") ?? "" }),
onConfigured() {
const state = this.data;
this.attr.add({ type: "search", placeholder: "Notlarda ara" });
this.bindings.add("value", () => state.query);
this.motif.on("input", (_sender: ComponentBase, e: Event) => (state.query = (e.target as HTMLInputElement).value));
this.bindings.watch(() => localStorage.setItem("lastQuery", state.query));
},
});
import { Component, reactive } from "@motifx/core";
export class QueryField extends Component {
state = reactive({ query: localStorage.getItem("lastQuery") ?? "" });
onConfig() {
this.bindings.watch(() => localStorage.setItem("lastQuery", this.state.query));
}
view() {
return <input type="search" placeholder="Notlarda ara" value={() => this.state.query} oninput={(e) => (this.state.query = e.target.value)} />;
}
}
import { reactive } from "@motifx/core";
export function QueryField() {
const state = reactive({ query: localStorage.getItem("lastQuery") ?? "" });
return (
<input
type="search"
placeholder="Notlarda ara"
value={() => state.query}
oninput={(e) => (state.query = e.target.value)}
x-watch={() => localStorage.setItem("lastQuery", state.query)}
/>
);
}
import { motifOptions, reactive } from "@motifx/core";
export const QueryField = () => motifOptions({
el: "input",
data: reactive({ query: localStorage.getItem("lastQuery") ?? "" }),
onConfigured() {
const state = this.data;
this.attr.add({ type: "search", placeholder: "Notlarda ara" });
this.bindings.add("value", () => state.query);
this.motif.on("input", (_sender, e) => (state.query = e.target.value));
this.bindings.watch(() => localStorage.setItem("lastQuery", state.query));
},
});
import { Component, motifComponent, reactive } from "@motifx/core";
export class QueryField extends Component {
state = reactive({ query: localStorage.getItem("lastQuery") ?? "" });
onConfig() {
this.bindings.watch(() => localStorage.setItem("lastQuery", this.state.query));
}
view() {
return motifComponent("input", (input) => {
input.attr.add({ type: "search", placeholder: "Notlarda ara" });
input.bindings.add("value", () => this.state.query);
input.motif.on("input", (_sender, e) => (this.state.query = e.target.value));
});
}
}
import { motifComponent, reactive } from "@motifx/core";
export function QueryField() {
const state = reactive({ query: localStorage.getItem("lastQuery") ?? "" });
return motifComponent("input", (input) => {
input.attr.add({ type: "search", placeholder: "Notlarda ara" });
input.bindings.add("value", () => state.query);
input.motif.on("input", (_sender, e) => (state.query = e.target.value));
input.bindings.watch(() => localStorage.setItem("lastQuery", state.query));
});
}
import { motifOptions, reactive } from "@motifx/core";
export const QueryField = () => motifOptions({
el: "input",
data: reactive({ query: localStorage.getItem("lastQuery") ?? "" }),
onConfigured() {
const state = this.data;
this.attr.add({ type: "search", placeholder: "Notlarda ara" });
this.bindings.add("value", () => state.query);
this.motif.on("input", (_sender, e) => (state.query = e.target.value));
this.bindings.watch(() => localStorage.setItem("lastQuery", state.query));
},
});
Her satırın listeyi taradığı tuzak
Tuzak: reaktif listeyi her satırda tarayan getter. Bir satır bileşeni elinde yalnız kimlik tutup elemanını
notes.items.find((n) => n.id === this.props.id)gibi bir getter'la arıyorsa, o getter eşleşmeye kadar her elemanınidalanını okur ve hepsine abone olur. Tek satırda bu n abonelik, n satırda n² abonelik demektir. Biriddeğiştiğinde onu geçmiş bütün satırlar, liste yenisiyle değiştirildiğinde bütün satırlar yeniden koşar ve her biri listeyi baştan tarar: n² okuma, n² yeniden abonelik; sona ekleme yalnız eşleşmesini bulamamış satırları uyandırır. Ölçüm, 2000 satır ve 2000 elemanlı liste: 2 milyon abonelik, ilk kuruluş 1,2 s, biriddeğişimi 0,8 s, listeyi değiştirmek 4 s.
Sebep reaktivitenin ince taneli olmasıdır: okunan her alan bir aboneliktir, taranan her eleman okunmuş bir alandır. Taramanın kendisi ucuzdur; pahalı olan her satırın her elemana abone olmasıdır. includes ve indexOf eleman alanı okumadığı için bu tuzağa girmez; filter(...).length gibi "tam gezme" ise tuzağı büyütür.
Çerçeve bunu dört yerde söyler: derleyici, reaktif getter içinde find/findIndex/some/every ve x.alan === değer biçiminde bir yüklem görünce MJX004 uyarır; çalışma anında aynı listeyi aynı bileşen türünün iki ya da daha çok örneği bir arama metoduyla tarıyorsa ve liste en az örnek sayısı kadar öğeliyse, props'tan ya da başka dosyadan da gelse, MJX210 ile bildirilir; tek koşuda 1000'den çok bağımlılık toplayan effect, DevTools açıkken sahibi bileşen ve bağla birlikte MJX209 ile bildirilir; DevTools'ta bileşenin effect listesi her bağın bağımlılık sayısını gösterir.
Kaçınma yolları, tercih sırasıyla:
- Satıra kimliği değil elemanı verin. Liste bağı zaten bunu yapar:
{notes.items.map((note) => <NoteRow note={note} />)}; satırthis.props.note.titleokur ve yalnız o nesnenin o alanına abone olur (bkz. Liste gösterimi). - Elde yalnız kimlik varsa anahtarlı dizin tutun. Listeyi tutan store elemanları bir
Map'te de tutar; satırnotes.byId(id)okur ve yalnız o anahtara abone olur, liste değişince yalnız o anahtarın okuyucusu uyanır:
import { reactive } from "@motifx/core";
import type { Note } from "../services/notes";
const state = reactive({ items: [] as Note[], byId: new Map<number, Note>() });
export const notes = {
get items(): readonly Note[] {
return state.items;
},
byId(id: number): Note | undefined {
return state.byId.get(id);
},
add(note: Note): void {
state.items.push(note);
state.byId.set(note.id, note);
},
remove(id: number): void {
const index = state.items.findIndex((note) => note.id === id);
if (index >= 0) state.items.splice(index, 1);
state.byId.delete(id);
},
};
import { reactive } from "@motifx/core";
const state = reactive({ items: [], byId: new Map() });
export const notes = {
get items() {
return state.items;
},
byId(id) {
return state.byId.get(id);
},
add(note) {
state.items.push(note);
state.byId.set(note.id, note);
},
remove(id) {
const index = state.items.findIndex((note) => note.id === id);
if (index >= 0) state.items.splice(index, 1);
state.byId.delete(id);
},
};
Liste seyrek değişiyorsa dizin bir computed da olabilir: const index = createComputed(() => new Map(state.items.map((n) => [n.id, n]))). Tarama bir kez yapılır, satırlar index.value.get(id) okur; liste her değiştiğinde bütün satırlar bir kez yeniden koşar ama her koşu tek okumadır.
3. Abonelik gerekmiyorsa untracked. Yalnız o anki değeri isteyen bir getter listeyi untracked(() => notes.items.find((n) => n.id === id)) ile okur; liste değişince bu getter yenilenmez, onu başka bir bağımlılığı yeniden koşturur.
Ardışık yazmalar kendiliğinden toplanır
Aynı senkron blokta yapılan yazmalar için ayrı bir toplama çağrısı gerekmez. Yazmalar effect'leri bir kuyruğa ekler; kuyruk bir sonraki mikro görevde boşaltılır ve her effect bir kez çalışır. Ara durumları hiçbir effect görmez:
filters.query = "iş";
filters.onlyPinned = true;
settings.state.density = "compact";
// üçünü de okuyan bir effect, mikro görevde yalnızca bir kez koşar
filters.query = "iş";
filters.onlyPinned = true;
settings.state.density = "compact";
// üçünü de okuyan bir effect, mikro görevde yalnızca bir kez koşar
Akıtma turlarla yapılır: bir effect, aynı akıtmada başka bir effect'in ya da createComputed'ın yazdığı bir değeri de okuyorsa bir tur sonra, aynı mikro görevde yeniden çalışır. Kurulma sırasına göre ilk koşusunda o değerin eski hâlini görebilir; son koşusu güncel değerleri görür.
Yardımcılar
| Fonksiyon | Açıklama |
|---|---|
deepClone(value) |
Değerin reaktif olmayan derin kopyasını üretir; proxy verilirse ham hâli kopyalanır. Düz nesne, dizi, Map ve Set iç içe kopyalanır; Date ve RegExp yeni örnek olur; sınıf örneği prototipini korur, getter ve metotları kopyada çalışır; döngüsel referanslar kopyada aynı döngüyü kurar. Fonksiyonlar, WeakMap ve WeakSet aynı referansla geçer, DOM düğümü cloneNode(true) ile kopyalanır. Nesnelerin kendi sayılabilir alanları kopyalanır. |
clearModel(model) |
Nesnenin ve proxy'si oluşmuş iç nesnelerinin proxy kaydını siler; değerleri sıfırlamaz, kurulu bağları ve efektleri durdurmaz. Çağrıdan sonra reactive(model) yeni bir proxy verir ve onun üzerinden yazmak var olan bağları yine günceller. Çağrıdan önce alınmış bir proxy'nin kendi alanlarına yazmak değeri değiştirir ama bildirmez; o proxy'den sonradan okunan iç nesneler yeni proxy olarak gelir ve bildirir. |
read(value) / toGetter(value) |
Bind<T> (bir değer ya da onu döndüren getter) tipindeki değeri okur: read değer fonksiyonsa çağırıp sonucunu, değilse değerin kendisini verir; toGetter her durumda bir getter verir. Prop'u getter olarak da alabilen bileşenlerde kullanılır (bkz. Şablon sonradan değişecekse getter yazın). |
configureReactivityLeakMonitor({ enabled, threshold, name }) |
Tek bir alanı izleyen efekt sayısı threshold'a (açıkken varsayılan 200) ulaşınca MJX507 uyarısı verir; uygulamada app.useReactiveMonitor(...) aynı çağrıdır (bkz. Sızıntı izleme). |
Reaktivite ile DOM'un buluşması
Reaktif alanlar DOM'a bağlar (bindings) aracılığıyla yansır. JSX yazdığınızda derleyici bunları kurar; JSX'siz yazımda aynı çağrıları siz yazarsınız:
<span>{note.title}</span>
<li class={() => (note.pinned ? "note pinned" : "note")} />
<input value={() => filters.query} />
const title = motifComponent("span", (span: ComponentBase) => span.controls.add(motifComponent("text", (node: ComponentBase) => node.bindings.add("textContent", note, "title"))));
const row = motifComponent("li", (li: ComponentBase) => li.class.add(() => (note.pinned ? "note pinned" : "note")));
const field = motifComponent("input", (input: ComponentBase) => input.bindings.add("value", () => filters.query));
<span>{note.title}</span>
<li class={() => (note.pinned ? "note pinned" : "note")} />
<input value={() => filters.query} />
const title = motifComponent("span", (span) => span.controls.add(motifComponent("text", (node) => node.bindings.add("textContent", note, "title"))));
const row = motifComponent("li", (li) => li.class.add(() => (note.pinned ? "note pinned" : "note")));
const field = motifComponent("input", (input) => input.bindings.add("value", () => filters.query));
setState() / reState() bir bileşenin ve bütün çocuklarının bağlarını yeniden değerlendirir: metin ve öznitelik yolları, sınıf ve stil getter'ları, x-text, x-html, value/checked/x-model, x-display/x-wait, {() => …}, şablon dizesi ve üye çağrısı yuvaları, &&/ternary/switch dalları ve x-watch/bindings.watch izleyicileri yeniden koşar; listeler, reaktif güncellemede olduğu gibi satırlarını öğelerle yeniden eşler: listede kalan öğenin satırı yerinde kalır ve kendi bağlarını yeniden okur, yeni öğeye bir satır kurulur, çıkan öğenin satırı serbest bırakılır, sıra değiştiyse satırlar yer değiştirir. Yalnız sıra farklıdır: setState() önce çocukları, sonra bileşenin kendisini, reState() önce bileşenin kendisini, sonra çocukları işler.
this.setState(); // bu bileşen ve çocuklarının bağlarını yeniden değerlendir
list.setState(); // bu bileşen ve çocuklarının bağlarını yeniden değerlendir
this.setState(); // bu bileşen ve çocuklarının bağlarını yeniden değerlendir
list.setState(); // bu bileşen ve çocuklarının bağlarını yeniden değerlendir
Fonksiyon bileşeninde bu çağrılar bileşenin kendisi üzerindedir (örnekte 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.
reactive ile çalışırken setState'e gerek kalmaz; noktasal reaktivite güncellemeyi kendi yapar. setState, reaktif olmayan düz veriyi elle güncellediğiniz durumlar içindir; örneğin düz bir dil sözlüğünün alanları yerinde değiştirildiğinde bütün yuvaları yeniden okutmak. Sözlük nesnesini başka bir nesneyle değiştirirseniz doğrudan yazılan yol ({i18n.dict.title}) kurulumda okunan eski nesnede kalır; getter yuvası ({() => i18n.dict.title}) yeni nesneyi okur.
Sonraki adım
Koşullu ve liste tabanlı gösterimi öğrenmek için Koşullu Gösterim ve Listeler bölümüne geçin.