Tahap 1 (penjelasan)
Pemenang proposal TC39: Daniel Ehrenberg, Yehuda Katz, Jatin Ramanathan, Shay Lewis, Kristen Hewell Garrett, Dominic Gannaway, Preston Sego, Milo M, Rob Eisenberg
Penulis asli: Rob Eisenberg dan Daniel Ehrenberg
Dokumen ini menjelaskan arah umum awal untuk sinyal dalam JavaScript, mirip dengan upaya Promises/A+ yang mendahului Promises yang distandarisasi oleh TC39 di ES2015. Cobalah sendiri, menggunakan polyfill.
Mirip dengan Promises/A+, upaya ini berfokus pada penyelarasan ekosistem JavaScript. Jika penyelarasan ini berhasil, maka sebuah standar dapat muncul, berdasarkan pengalaman tersebut. Beberapa penulis kerangka kerja berkolaborasi di sini dalam model umum yang dapat mendukung inti reaktivitas mereka. Draf saat ini didasarkan pada masukan desain dari penulis/pengelola Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue, Wiz, dan banyak lagi…
Berbeda dengan Promises/A+, kami tidak mencoba memecahkan API permukaan umum yang dapat diakses oleh pengembang, melainkan semantik inti yang tepat dari grafik sinyal yang mendasarinya. Proposal ini mencakup API yang sepenuhnya konkrit, namun API tersebut tidak ditargetkan untuk sebagian besar pengembang aplikasi. Sebaliknya, API sinyal di sini lebih cocok untuk membangun kerangka kerja, memberikan interoperabilitas melalui grafik sinyal umum dan mekanisme pelacakan otomatis.
Rencana dari proposal ini adalah melakukan pembuatan prototipe awal yang signifikan, termasuk integrasi ke dalam beberapa kerangka kerja, sebelum melanjutkan melampaui Tahap 1. Kami hanya tertarik untuk menstandardisasi Sinyal jika cocok untuk digunakan dalam praktik di berbagai kerangka kerja, dan memberikan manfaat nyata dibandingkan kerangka- sinyal yang diberikan. Kami berharap pembuatan prototipe awal yang signifikan akan memberi kami informasi ini. Lihat "Status dan rencana pengembangan" di bawah untuk lebih jelasnya.
Untuk mengembangkan antarmuka pengguna (UI) yang rumit, pengembang aplikasi JavaScript perlu menyimpan, menghitung, membatalkan validasi, menyinkronkan, dan mendorong status ke lapisan tampilan aplikasi dengan cara yang efisien. UI biasanya melibatkan lebih dari sekadar pengelolaan nilai sederhana, namun sering kali melibatkan rendering status terkomputasi yang bergantung pada pohon kompleks berisi nilai atau status lain yang juga dihitung sendiri. Tujuan dari Signals adalah menyediakan infrastruktur untuk mengelola status aplikasi sehingga pengembang dapat fokus pada logika bisnis daripada detail yang berulang-ulang ini.
Konstruksi mirip sinyal juga terbukti berguna dalam konteks non-UI, khususnya dalam sistem pembangunan untuk menghindari pembangunan kembali yang tidak perlu.
Sinyal digunakan dalam pemrograman reaktif untuk menghilangkan kebutuhan mengelola pembaruan dalam aplikasi.
Model pemrograman deklaratif untuk memperbarui berdasarkan perubahan status.
dari Apa itu Reaktivitas? .
Diberikan sebuah variabel, counter
, Anda ingin merender ke dalam DOM apakah penghitungnya genap atau ganjil. Setiap kali counter
berubah, Anda ingin memperbarui DOM dengan paritas terbaru. Di Vanilla JS, Anda mungkin memiliki sesuatu seperti ini:
biarkan penghitung = 0;const setCounter = (nilai) => { penghitung = nilai; render();};const isEven = () => (penghitung & 1) == 0;const parity = () => isEven() ? "genap" : "ganjil";const render = () => element.innerText = parity();// Simulasikan pembaruan eksternal ke counter...setInterval(() => setCounter(counter + 1), 1000);
Ini memiliki sejumlah masalah...
Pengaturan counter
berisik dan berat.
Status counter
terkait erat dengan sistem rendering.
Jika counter
berubah tetapi parity
tidak (misalnya penghitung berubah dari 2 ke 4), maka kita melakukan penghitungan paritas yang tidak perlu dan rendering yang tidak perlu.
Bagaimana jika bagian lain dari UI kita hanya ingin dirender saat counter
diperbarui?
Bagaimana jika bagian lain dari UI kita bergantung pada isEven
atau parity
saja?
Bahkan dalam skenario yang relatif sederhana ini, sejumlah masalah muncul dengan cepat. Kita dapat mencoba mengatasinya dengan memperkenalkan pub/sub untuk counter
. Hal ini akan memungkinkan konsumen counter
tambahan untuk berlangganan untuk menambahkan reaksi mereka sendiri terhadap perubahan negara.
Namun, kami masih mengalami masalah berikut:
Fungsi render, yang hanya bergantung pada parity
harus "mengetahui" bahwa ia benar-benar perlu berlangganan counter
.
Tidak mungkin memperbarui UI berdasarkan isEven
atau parity
saja, tanpa berinteraksi langsung dengan counter
.
Kami telah meningkatkan standar kami. Setiap kali Anda menggunakan sesuatu, ini bukan hanya soal memanggil fungsi atau membaca variabel, melainkan berlangganan dan melakukan pembaruan di sana. Mengelola berhenti berlangganan juga sangat rumit.
Sekarang, kita dapat menyelesaikan beberapa masalah dengan menambahkan pub/sub tidak hanya ke counter
tetapi juga ke isEven
dan parity
. Kita kemudian harus berlangganan isEven
ke counter
, parity
ke isEven
, dan render
ke parity
. Sayangnya, tidak hanya kode boilerplate kami yang meledak, tetapi kami juga terjebak dengan banyak sekali pembukuan langganan, dan potensi bencana kebocoran memori jika kami tidak membersihkan semuanya dengan benar. Jadi, kami telah memecahkan beberapa masalah tetapi menciptakan kategori masalah baru dan banyak kode. Lebih buruk lagi, kita harus melalui seluruh proses ini untuk setiap bagian negara dalam sistem kita.
Abstraksi pengikatan data di UI untuk model dan tampilan telah lama menjadi inti kerangka UI di berbagai bahasa pemrograman, meskipun tidak ada mekanisme seperti itu yang dibangun di JS atau platform web. Dalam kerangka kerja dan pustaka JS, terdapat banyak eksperimen dengan berbagai cara untuk mewakili pengikatan ini, dan pengalaman telah menunjukkan kekuatan aliran data satu arah dalam hubungannya dengan tipe data kelas satu yang mewakili sel keadaan atau komputasi. berasal dari data lain, sekarang sering disebut “Sinyal”. Pendekatan nilai reaktif kelas satu ini tampaknya pertama kali muncul secara populer dalam kerangka web JavaScript sumber terbuka dengan Knockout pada tahun 2010. Bertahun-tahun setelahnya, banyak variasi dan implementasi telah dibuat. Dalam 3-4 tahun terakhir, pendekatan primitif dan terkait Signal telah mendapatkan daya tarik lebih lanjut, dengan hampir setiap perpustakaan atau kerangka kerja JavaScript modern memiliki sesuatu yang serupa, dengan satu nama atau lainnya.
Untuk memahami Signals, mari kita lihat contoh di atas, yang dibayangkan kembali dengan Signal API yang diartikulasikan lebih lanjut di bawah.
const counter = new Signal.State(0);const isEven = new Signal.Computed(() => (counter.get() & 1) == 0);const parity = new Signal.Computed(() => isEven .get() ? "even" : "odd");// Pustaka atau kerangka kerja mendefinisikan efek berdasarkan fungsi fungsi Signal primitif lainnya effect(cb: () => void): (() => void);effect(( ) => element.innerText = parity.get());// Simulasikan pembaruan eksternal ke counter...setInterval(() => counter.set(counter.get() + 1), 1000);
Ada beberapa hal yang bisa kita lihat langsung:
Kita telah menghilangkan boilerplate berisik di sekitar variabel counter
dari contoh sebelumnya.
Ada API terpadu untuk menangani nilai, komputasi, dan efek samping.
Tidak ada masalah referensi melingkar atau ketergantungan terbalik antara counter
dan render
.
Tidak ada langganan manual, juga tidak perlu pembukuan.
Ada cara untuk mengendalikan waktu/penjadwalan efek samping.
Sinyal memberi kita lebih dari apa yang bisa dilihat di permukaan API:
Pelacakan Ketergantungan Otomatis - Sinyal yang dihitung secara otomatis menemukan Sinyal lain yang bergantung padanya, baik Sinyal tersebut berupa nilai sederhana atau perhitungan lainnya.
Evaluasi Malas - Komputasi tidak dievaluasi dengan penuh semangat ketika dideklarasikan, juga tidak segera dievaluasi ketika dependensinya berubah. Mereka hanya dievaluasi ketika nilainya diminta secara eksplisit.
Memoisasi - Sinyal yang Dihitung menyimpan nilai terakhirnya sehingga perhitungan yang tidak mengalami perubahan dalam ketergantungannya tidak perlu dievaluasi ulang, tidak peduli berapa kali mereka diakses.
Setiap implementasi Sinyal memiliki mekanisme pelacakan otomatisnya sendiri, untuk melacak sumber yang ditemui saat mengevaluasi Sinyal yang dihitung. Hal ini membuat sulit untuk berbagi model, komponen, dan perpustakaan antara kerangka kerja yang berbeda--mereka cenderung datang dengan kopling palsu ke mesin tampilan mereka (mengingat bahwa Sinyal biasanya diimplementasikan sebagai bagian dari kerangka JS).
Tujuan dari proposal ini adalah untuk sepenuhnya memisahkan model reaktif dari tampilan rendering, memungkinkan pengembang untuk bermigrasi ke teknologi rendering baru tanpa menulis ulang kode non-UI mereka, atau mengembangkan model reaktif bersama di JS untuk diterapkan dalam konteks berbeda. Sayangnya, karena pembuatan versi dan duplikasi, mencapai tingkat berbagi yang kuat melalui perpustakaan tingkat JS menjadi tidak praktis karena perpustakaan bawaan menawarkan jaminan berbagi yang lebih kuat.
Potensi peningkatan kinerja selalu kecil jika mengirimkan lebih sedikit kode karena perpustakaan yang umum digunakan sudah ada di dalamnya, namun implementasi Sinyal umumnya cukup kecil, jadi kami tidak memperkirakan efek ini akan terlalu besar.
Kami menduga bahwa implementasi C++ asli pada struktur data dan algoritme terkait Signal bisa sedikit lebih efisien dibandingkan apa yang dapat dicapai di JS, dengan faktor konstan. Namun, tidak ada perubahan algoritmik yang diantisipasi vs. apa yang akan ada di polyfill; mesin tidak diharapkan menjadi ajaib di sini, dan algoritme reaktivitasnya sendiri akan terdefinisi dengan baik dan tidak ambigu.
Kelompok juara berharap untuk mengembangkan berbagai implementasi Sinyal, dan menggunakannya untuk menyelidiki kemungkinan kinerja ini.
Dengan pustaka Signal bahasa JS yang ada, mungkin sulit untuk melacak hal-hal seperti:
Callstack di seluruh rantai Sinyal yang dihitung, menunjukkan rantai penyebab kesalahan
Grafik referensi di antara Sinyal, ketika satu sinyal bergantung pada Sinyal lain -- penting saat men-debug penggunaan memori
Sinyal bawaan memungkinkan runtime JS dan DevTools berpotensi meningkatkan dukungan untuk memeriksa Sinyal, terutama untuk proses debug atau analisis kinerja, baik yang terpasang di browser atau melalui ekstensi bersama. Alat yang ada seperti pemeriksa elemen, snapshot kinerja, dan profiler memori dapat diperbarui untuk secara khusus menyorot Sinyal dalam penyajian informasinya.
Secara umum, JavaScript memiliki pustaka standar yang cukup minim, namun tren di TC39 adalah menjadikan JS lebih sebagai bahasa yang "termasuk baterai", dengan serangkaian fungsionalitas bawaan berkualitas tinggi yang tersedia. Misalnya, Temporal menggantikan moment.js, dan sejumlah fitur kecil, misalnya Array.prototype.flat
dan Object.groupBy
menggantikan banyak kasus penggunaan lodash. Keuntungannya mencakup ukuran paket yang lebih kecil, peningkatan stabilitas dan kualitas, lebih sedikit pembelajaran saat bergabung dengan proyek baru, dan kosakata umum yang umum di seluruh pengembang JS.
Pekerjaan saat ini di W3C dan pelaksana browser berupaya menghadirkan templat asli ke HTML (Bagian DOM dan Instansiasi Templat). Selain itu, CG Komponen Web W3C sedang menjajaki kemungkinan memperluas Komponen Web untuk menawarkan API HTML yang sepenuhnya deklaratif. Untuk mencapai kedua tujuan ini, pada akhirnya suatu primitif reaktif akan dibutuhkan oleh HTML. Selain itu, banyak perbaikan ergonomis pada DOM melalui integrasi Sinyal dapat dibayangkan dan diminta oleh komunitas.
Perlu diperhatikan, integrasi ini akan menjadi upaya terpisah yang akan dilakukan kemudian, bukan bagian dari proposal ini sendiri.
Upaya standardisasi terkadang dapat membantu hanya pada tingkat "komunitas", bahkan tanpa perubahan pada browser. Upaya Signals mempertemukan banyak penulis kerangka kerja yang berbeda untuk diskusi mendalam tentang sifat reaktivitas, algoritme, dan interoperabilitas. Ini sudah berguna, dan tidak membenarkan penyertaan dalam mesin dan browser JS; Sinyal hanya boleh ditambahkan ke standar JavaScript jika ada manfaat signifikan di luar ekosistem pertukaran informasi yang diaktifkan.
Ternyata perpustakaan Signal yang ada pada intinya tidak jauh berbeda satu sama lain. Proposal ini bertujuan untuk membangun kesuksesan mereka dengan menerapkan kualitas penting dari banyak perpustakaan tersebut.
Tipe Sinyal yang mewakili keadaan, yaitu Sinyal yang dapat ditulis. Ini adalah nilai yang dapat dibaca orang lain.
Jenis Sinyal yang dihitung/memo/turunan, yang bergantung pada yang lain dan dihitung serta di-cache dengan lambat.
Komputasinya lambat, artinya Sinyal yang dihitung tidak dihitung lagi secara default ketika salah satu dependensinya berubah, melainkan hanya dijalankan jika seseorang benar-benar membacanya.
Perhitungannya "bebas kesalahan", artinya tidak ada perhitungan yang tidak perlu dilakukan. Hal ini menyiratkan bahwa, ketika aplikasi membaca Sinyal yang dihitung, terdapat pengurutan topologi dari bagian grafik yang berpotensi kotor untuk dijalankan, untuk menghilangkan duplikat.
Komputasi di-cache, artinya jika, setelah terakhir kali suatu dependensi berubah, tidak ada dependensi yang berubah, maka Sinyal yang dihitung tidak dihitung ulang saat diakses.
Perbandingan khusus dapat dilakukan untuk Sinyal yang dihitung serta Sinyal negara, untuk mencatat kapan Sinyal yang dihitung lebih lanjut yang bergantung pada sinyal tersebut harus diperbarui.
Reaksi terhadap kondisi ketika Sinyal yang dihitung memiliki salah satu dependensinya (atau dependensi bersarang) menjadi "kotor" dan berubah, artinya nilai Sinyal mungkin sudah ketinggalan jaman.
Reaksi ini dimaksudkan untuk menjadwalkan pekerjaan yang lebih signifikan untuk dilakukan nanti.
Efek diterapkan dalam bentuk reaksi ini, ditambah penjadwalan tingkat kerangka kerja.
Sinyal yang dihitung memerlukan kemampuan untuk bereaksi apakah sinyal tersebut terdaftar sebagai ketergantungan (bersarang) dari salah satu reaksi ini.
Aktifkan kerangka kerja JS untuk melakukan penjadwalannya sendiri. Tidak ada penjadwalan paksa bawaan bergaya Promise.
Reaksi sinkron diperlukan untuk memungkinkan penjadwalan pekerjaan selanjutnya berdasarkan logika kerangka kerja.
Penulisan bersifat sinkron dan langsung berlaku (kerangka kerja yang dapat melakukan penulisan batch di atas).
Dimungkinkan untuk memisahkan pemeriksaan apakah suatu efek mungkin "kotor" dari efek yang sebenarnya dijalankan (mengaktifkan penjadwal efek dua tahap).
Kemampuan membaca Sinyal tanpa memicu ketergantungan untuk direkam ( untrack
)
Aktifkan komposisi basis kode berbeda yang menggunakan Sinyal/reaktivitas, misalnya,
Menggunakan beberapa kerangka kerja secara bersamaan sejauh pelacakan/reaktivitas itu sendiri (penghilangan modulo, lihat di bawah)
Struktur data reaktif yang tidak bergantung pada kerangka kerja (misalnya, proxy penyimpanan yang reaktif secara rekursif, Peta dan Set dan Array yang reaktif, dll.)
Mencegah/melarang penyalahgunaan reaksi sinkron yang naif.
Risiko kesehatan: dapat menyebabkan "gangguan" jika digunakan secara tidak benar: Jika rendering dilakukan segera saat Sinyal disetel, hal ini dapat menyebabkan status aplikasi tidak lengkap kepada pengguna akhir. Oleh karena itu, fitur ini sebaiknya hanya digunakan untuk menjadwalkan pekerjaan secara cerdas untuk nanti, setelah logika aplikasi selesai.
Solusi: Larang membaca dan menulis Sinyal apa pun dari dalam panggilan balik reaksi sinkron
Mencegah untrack
dan menandai sifatnya yang tidak sehat
Risiko kesehatan: memungkinkan pembuatan Sinyal yang dihitung yang nilainya bergantung pada Sinyal lain, namun tidak diperbarui ketika Sinyal tersebut berubah. Ini harus digunakan ketika akses yang tidak terlacak tidak akan mengubah hasil komputasi.
Solusi: API ditandai "tidak aman" pada namanya.
Catatan: Proposal ini mengizinkan sinyal dibaca dan ditulis dari sinyal komputasi dan efek, tanpa membatasi penulisan yang muncul setelah pembacaan, meskipun ada risiko kesehatannya. Keputusan ini diambil untuk menjaga fleksibilitas dan kompatibilitas dalam integrasi dengan kerangka kerja.
Harus menjadi dasar yang kuat bagi berbagai kerangka kerja untuk menerapkan mekanisme Sinyal/reaktivitasnya.
Harus menjadi dasar yang baik untuk proksi penyimpanan rekursif, reaktivitas bidang kelas berbasis dekorator, dan API bergaya .value
dan [state, setState]
.
Semantik mampu mengekspresikan pola valid yang dimungkinkan oleh kerangka kerja berbeda. Misalnya, Sinyal-Sinyal ini dapat menjadi dasar penulisan yang segera direfleksikan atau penulisan yang dikelompokkan dan diterapkan kemudian.
Alangkah baiknya jika API ini bisa digunakan langsung oleh pengembang JavaScript.
Ide: Berikan semua petunjuknya, tetapi sertakan kesalahan jika disalahgunakan jika memungkinkan.
Ide: Letakkan API halus di namespace subtle
, mirip dengan crypto.subtle
, untuk menandai garis antara API yang diperlukan untuk penggunaan lebih lanjut seperti mengimplementasikan kerangka kerja atau membangun alat pengembangan versus penggunaan pengembangan aplikasi sehari-hari seperti memberi contoh sinyal untuk digunakan dengan a kerangka.
Namun, penting untuk tidak membayangi nama yang sama persis!
Jika suatu fitur cocok dengan konsep ekosistem, menggunakan kosakata umum adalah hal yang baik.
Ketegangan antara "kegunaan oleh pengembang JS" dan "menyediakan semua kaitan dengan kerangka kerja"
Dapat diterapkan dan digunakan dengan kinerja yang baik -- API permukaan tidak menyebabkan terlalu banyak overhead
Aktifkan subkelas, sehingga kerangka kerja dapat menambahkan metode dan bidangnya sendiri, termasuk bidang pribadi. Hal ini penting untuk menghindari kebutuhan alokasi tambahan pada tingkat kerangka kerja. Lihat "Manajemen memori" di bawah.
Jika memungkinkan: Sinyal yang dihitung harus dapat dikumpulkan dari sampah jika tidak ada sinyal langsung yang merujuknya untuk kemungkinan pembacaan di masa mendatang, bahkan jika sinyal tersebut dihubungkan ke grafik yang lebih luas yang tetap hidup (misalnya, dengan membaca keadaan yang tetap hidup).
Perhatikan bahwa sebagian besar kerangka kerja saat ini memerlukan pembuangan Sinyal yang dihitung secara eksplisit jika kerangka tersebut memiliki referensi ke atau dari grafik Sinyal lain yang masih aktif.
Hal ini tidak menjadi terlalu buruk ketika masa pakainya terkait dengan masa pakai komponen UI, dan efeknya tetap harus dihilangkan.
Jika terlalu mahal untuk dieksekusi dengan semantik ini, maka kita harus menambahkan penghapusan eksplisit (atau "membatalkan tautan") Sinyal yang dihitung ke API di bawah ini, yang saat ini tidak memilikinya.
Sasaran terkait yang terpisah: Meminimalkan jumlah alokasi, misalnya,
untuk membuat Sinyal yang dapat ditulis (hindari dua penutupan + array terpisah)
untuk menerapkan efek (hindari penutupan untuk setiap reaksi)
Di API untuk mengamati perubahan Sinyal, hindari membuat struktur data sementara tambahan
Solusi: API berbasis kelas memungkinkan penggunaan kembali metode dan bidang yang ditentukan dalam subkelas
Ide awal tentang Signal API ada di bawah. Perhatikan bahwa ini hanyalah draf awal, dan kami mengantisipasi perubahan seiring berjalannya waktu. Mari kita mulai dengan .d.ts
lengkap untuk mendapatkan gambaran tentang bentuk keseluruhannya, lalu kita akan membahas detail tentang arti semua itu.
interface Signal<T> {// Dapatkan nilai signalget(): T;}namespace Signal {// Kelas Signal baca-tulis State<T> mengimplementasikan Signal<T> {// Membuat state Signal dimulai dengan nilai tconstructor(t: T, options?: SignalOptions<T>);// Dapatkan nilai signalget(): T;// Tetapkan nilai sinyal status ke tset(t: T): void;}// A Signal yang merupakan formula berdasarkan yang lain Signalsclass Computed<T = unknown> mengimplementasikan Signal<T> {// Membuat Sinyal yang mengevaluasi nilai yang dikembalikan oleh callback.// Callback dipanggil dengan sinyal ini sebagai this value.constructor(cb: (this: Computed< T>) => T, options?: SignalOptions<T>);// Dapatkan nilai signalget(): T;}// Namespace ini mencakup fitur "lanjutan" yang lebih baik // serahkan pada pembuat kerangka kerja daripada pengembang aplikasi.// Analog dengan `crypto.subtle`namespace halus {// Jalankan panggilan balik dengan semua pelacakan dinonaktifkanfungsi untrack<T>(cb: () => T): T;// Dapatkan sinyal yang dihitung saat ini yaitu melacak sinyal apa pun yang terbaca, jika adafungsi currentComputed(): Computed | null;// Mengembalikan daftar urutan semua sinyal yang direferensikan // selama terakhir kali dievaluasi.// Untuk Pengamat, mencantumkan kumpulan sinyal yang sedang diawasi.fungsi introspectSources(s: Computed | Watcher): (Status | Terhitung)[];// Mengembalikan Pengamat yang memuat sinyal ini, ditambah // Sinyal terkomputasi yang membaca sinyal ini terakhir kali dievaluasi,// jika sinyal yang dihitung tersebut (secara rekursif) diawasi.fungsi introspectSinks(s: State | Computed): (Computed | Watcher)[];// Benar jika sinyal ini "langsung", karena ditonton oleh Watcher,// atau dibaca oleh sinyal Computed yang (secara rekursif) live.function hasSinks(s: State | Computed): boolean;// Benar jika elemen ini "reaktif", karena bergantung// pada sinyal lain. A Computed dengan hasSources bernilai false// akan selalu mengembalikan konstanta yang sama.function hasSources(s: Computed | Watcher): boolean;class Watcher {// Saat sumber Watcher (rekursif) ditulis, panggil callback ini,// jika belum dipanggil sejak panggilan `watch` terakhir.// Tidak ada sinyal yang dapat dibaca atau ditulis selama notify.constructor(notify: (this: Watcher) => void);// Tambahkan sinyal-sinyal ini ke set Watcher, dan setel watcher untuk menjalankan // notify callback saat sinyal apa pun di set Watcher (atau salah satu dependensinya) berubah. // Dapat dipanggil tanpa argumen hanya untuk mengatur ulang "notified" menyatakan, sehingga// panggilan balik notifikasi akan dipanggil lagi.watch(...s: Signal[]): void;// Hapus sinyal-sinyal ini dari set yang diawasi (misalnya, untuk efek yang dibuang)unwatch(. ..s: Sinyal[]): void;// Mengembalikan himpunan sumber dalam himpunan Pengamat yang masih kotor, atau merupakan sinyal yang dihitung// dengan sumber yang kotor atau tertunda dan belum dievaluasi ulanggetPending(): Signal[];} // Kait untuk mengamati sedang ditonton atau tidak lagi ditontonvar ditonton: Simbol;var belum ditonton: Simbol;}antarmuka SignalOptions<T> {// Fungsi perbandingan khusus antara nilai lama dan baru. Default: Object.is.// Sinyal diteruskan sebagai nilai ini untuk konteks.equals?: (ini: Signal<T>, t: T, t2: T) => boolean;// Callback dipanggil ketika isWatched menjadi benar, jika sebelumnya salah[Signal.subtle.watched]?: (ini: Signal<T>) => batal;// Panggilan balik dipanggil setiap kali isWatched menjadi salah, jika sebelumnya true[Signal.subtle.unwatched]?: (ini: Signal<T>) => void;}}
Sinyal mewakili sel data yang dapat berubah seiring waktu. Sinyal dapat berupa "status" (hanya nilai yang ditetapkan secara manual) atau "dihitung" (rumus berdasarkan Sinyal lain).
Sinyal yang Dihitung bekerja dengan secara otomatis melacak Sinyal lain mana yang dibaca selama evaluasinya. Saat komputasi dibaca, ia memeriksa apakah dependensi yang dicatat sebelumnya telah berubah, dan mengevaluasi ulang dirinya sendiri jika demikian. Ketika beberapa Sinyal yang dihitung disarangkan, semua atribusi pelacakan menuju ke sinyal yang paling dalam.
Sinyal yang Dihitung bersifat malas, yaitu berbasis tarikan: sinyal tersebut hanya dievaluasi ulang ketika diakses, bahkan jika salah satu dependensinya diubah sebelumnya.
Callback yang diteruskan ke Sinyal yang dihitung secara umum harus "murni" dalam artian merupakan fungsi deterministik dan bebas efek samping dari Sinyal lain yang diaksesnya. Pada saat yang sama, waktu panggilan balik dipanggil bersifat deterministik, sehingga efek samping dapat digunakan dengan hati-hati.
Sinyal menampilkan caching/memoisasi yang menonjol: baik Sinyal status maupun yang dihitung mengingat nilainya saat ini, dan hanya memicu penghitungan ulang Sinyal yang dihitung yang mereferensikannya jika nilai tersebut benar-benar berubah. Perbandingan berulang antara nilai lama dan nilai baru bahkan tidak diperlukan--perbandingan dilakukan satu kali ketika Sinyal sumber disetel ulang/dievaluasi ulang, dan mekanisme Sinyal melacak hal-hal mana yang merujuk pada Sinyal yang belum diperbarui berdasarkan nilai baru nilai belum. Secara internal, hal ini umumnya direpresentasikan melalui "pewarnaan grafik" seperti yang dijelaskan dalam (postingan blog Milo).
Sinyal yang Dihitung melacak ketergantungannya secara dinamis--setiap kali dijalankan, sinyal tersebut mungkin bergantung pada hal-hal yang berbeda, dan kumpulan ketergantungan yang tepat tersebut tetap diperbarui dalam grafik Sinyal. Ini berarti bahwa jika Anda memiliki ketergantungan yang diperlukan hanya pada satu cabang, dan penghitungan sebelumnya mengambil cabang lainnya, maka perubahan pada nilai yang sementara tidak digunakan tersebut tidak akan menyebabkan Sinyal yang dihitung dihitung ulang, bahkan ketika ditarik.
Tidak seperti Janji JavaScript, semua yang ada di Signals berjalan secara sinkron:
Menetapkan Sinyal ke nilai baru bersifat sinkron, dan ini segera tercermin ketika membaca Sinyal yang dihitung yang bergantung padanya setelahnya. Tidak ada pengelompokan bawaan untuk mutasi ini.
Membaca Sinyal yang dihitung bersifat sinkron--nilainya selalu tersedia.
Callback notify
di Watchers, seperti dijelaskan di bawah, berjalan secara sinkron, selama panggilan .set()
yang memicunya (tetapi setelah pewarnaan grafik selesai).
Seperti Promises, Sinyal dapat mewakili keadaan kesalahan: Jika callback Sinyal yang dihitung muncul, maka kesalahan tersebut akan di-cache seperti nilai lainnya, dan ditampilkan kembali setiap kali Sinyal dibaca.
Sebuah instance Signal
mewakili kemampuan untuk membaca nilai yang berubah secara dinamis dan pembaruannya dilacak dari waktu ke waktu. Hal ini juga secara implisit mencakup kemampuan untuk berlangganan Sinyal, secara implisit melalui akses terlacak dari Sinyal lain yang dihitung.
API di sini dirancang untuk mencocokkan konsensus ekosistem yang sangat kasar di antara sebagian besar perpustakaan Signal dalam penggunaan nama seperti "sinyal", "dihitung", dan "status". Namun, akses ke Sinyal Terkomputasi dan Status dilakukan melalui metode .get()
, yang tidak sesuai dengan semua API Signal populer, yang menggunakan pengakses bergaya .value
, atau sintaksis panggilan signal()
.
API ini dirancang untuk mengurangi jumlah alokasi, untuk membuat Sinyal cocok untuk ditanamkan dalam kerangka kerja JavaScript sambil mencapai kinerja yang sama atau lebih baik daripada Sinyal yang disesuaikan dengan kerangka kerja yang sudah ada. Ini menyiratkan:
Sinyal Status adalah objek tunggal yang dapat ditulis, yang dapat diakses dan diatur dari referensi yang sama. (Lihat implikasinya di bawah pada bagian "Pemisahan kemampuan".)
Baik Sinyal Status maupun Sinyal Terhitung dirancang agar dapat disubklasifikasikan, untuk memfasilitasi kemampuan kerangka kerja untuk menambahkan properti tambahan melalui bidang kelas publik dan privat (serta metode untuk menggunakan status tersebut).
Berbagai callback (misalnya, equals
, callback yang dihitung) dipanggil dengan Signal yang relevan sebagai nilai this
untuk konteksnya, sehingga penutupan baru tidak diperlukan per Signal. Sebaliknya, konteks dapat disimpan dalam properti tambahan dari sinyal itu sendiri.
Beberapa kondisi kesalahan yang diberlakukan oleh API ini:
Merupakan kesalahan untuk membaca perhitungan secara rekursif.
Panggilan balik notify
dari Pengamat tidak dapat membaca atau menulis sinyal apa pun
Jika callback Signal yang dihitung muncul, maka akses Signal selanjutnya akan memunculkan kembali kesalahan yang di-cache tersebut, hingga salah satu dependensi berubah dan dihitung ulang.
Beberapa ketentuan yang tidak ditegakkan:
Sinyal yang Dihitung dapat menulis ke Sinyal lain, secara sinkron dalam panggilan baliknya
Pekerjaan yang diantrekan oleh panggilan balik notify
Watcher dapat membaca atau menulis sinyal, sehingga memungkinkan untuk mereplikasi antipattern React klasik dalam bentuk Sinyal!
Antarmuka Watcher
yang didefinisikan di atas memberikan dasar untuk mengimplementasikan JS API yang umum untuk efek: callback yang dijalankan kembali ketika Sinyal lain berubah, semata-mata karena efek sampingnya. Fungsi effect
yang digunakan di atas pada contoh awal dapat didefinisikan sebagai berikut:
// Fungsi ini biasanya berada di perpustakaan/kerangka kerja, bukan kode aplikasi// CATATAN: Logika penjadwalan ini terlalu mendasar untuk dapat berguna. Jangan salin/tempel.biarkan pending = false;biarkan w = new Signal.subtle.Watcher(() => {if (!pending) {pending = true;queueMicrotask(() => {pending = false;for (biarkan s dari w.getPending()) s.get();w.watch();});}});// Efek efek Sinyal yang mengevaluasi ke cb, yang menjadwalkan pembacaan // dirinya sendiri pada antrian tugas mikro kapan pun salah satu dari ketergantungannya mungkin berubah fungsi ekspor effect(cb) {let destructor;let c = new Signal.Computed(() => { destructor?.(); destructor = cb(); });w.watch(c);c. get();return() => { destruktor?.(); w.tidak menonton(c) };}
Signal API tidak menyertakan fungsi bawaan seperti effect
. Hal ini karena penjadwalan efek tidak kentara dan sering kali terkait dengan siklus rendering kerangka kerja dan status atau strategi khusus kerangka kerja tingkat tinggi lainnya yang tidak dapat diakses oleh JS.
Menelusuri berbagai operasi yang digunakan di sini: Callback notify
yang diteruskan ke konstruktor Watcher
adalah fungsi yang dipanggil ketika Sinyal beralih dari status "bersih" (di mana kita mengetahui cache diinisialisasi dan valid) menjadi "dicentang" atau "kotor " state (di mana cache mungkin valid atau tidak valid karena setidaknya salah satu status yang bergantung secara rekursif telah diubah).
Panggilan untuk notify
pada akhirnya dipicu oleh panggilan ke .set()
pada beberapa status Signal. Panggilan ini sinkron: terjadi sebelum .set
kembali. Namun tidak perlu khawatir callback ini mengamati grafik Sinyal dalam keadaan setengah diproses, karena selama panggilan balik notify
, tidak ada Sinyal yang dapat dibaca atau ditulis, bahkan dalam panggilan untrack
. Karena notify
dipanggil selama .set()
, hal ini mengganggu alur logika lain, yang mungkin tidak lengkap. Untuk membaca atau menulis Sinyal dari notify
, jadwalkan pekerjaan untuk dijalankan nanti, misalnya dengan menuliskan Sinyal ke dalam daftar untuk diakses nanti, atau dengan queueMicrotask
seperti di atas.
Perhatikan bahwa sangat mungkin untuk menggunakan Sinyal secara efektif tanpa Symbol.subtle.Watcher
dengan menjadwalkan polling Sinyal yang dihitung, seperti yang dilakukan Glimmer. Namun, banyak kerangka kerja mendapati bahwa logika penjadwalan ini sering kali berguna untuk dijalankan secara sinkron, sehingga Signals API menyertakannya.
Sinyal yang dihitung dan dinyatakan dikumpulkan dari sampah seperti nilai JS lainnya. Namun Pengamat memiliki cara khusus untuk menjaga agar segala sesuatunya tetap hidup: Sinyal apa pun yang diawasi oleh Pengamat akan tetap hidup selama salah satu status dasarnya dapat dijangkau, karena hal ini dapat memicu panggilan notify
di masa mendatang (dan kemudian .get()
). Untuk alasan ini, ingatlah untuk memanggil Watcher.prototype.unwatch
untuk membersihkan efek.
Signal.subtle.untrack
adalah jalan keluar yang memungkinkan pembacaan Sinyal tanpa melacak pembacaan tersebut. Kemampuan ini tidak aman karena memungkinkan pembuatan Sinyal yang dihitung yang nilainya bergantung pada Sinyal lain, namun tidak diperbarui ketika Sinyal tersebut berubah. Ini harus digunakan ketika akses yang tidak terlacak tidak akan mengubah hasil komputasi.
Fitur-fitur ini mungkin ditambahkan nanti, namun tidak disertakan dalam draf saat ini. Penghilangan kerangka kerja tersebut disebabkan oleh kurangnya konsensus dalam ruang desain antar kerangka kerja, serta kemampuan yang ditunjukkan untuk mengatasi ketidakhadiran kerangka kerja tersebut dengan mekanisme selain gagasan Sinyal yang dijelaskan dalam dokumen ini. Namun, sayangnya, kelalaian tersebut membatasi potensi interoperabilitas antar kerangka kerja. Saat prototipe Sinyal seperti yang dijelaskan dalam dokumen ini diproduksi, akan ada upaya untuk mengkaji ulang apakah kelalaian ini merupakan keputusan yang tepat.
Async : Sinyal selalu tersedia secara sinkron untuk evaluasi, dalam model ini. Namun, sering kali berguna untuk memiliki proses asinkron tertentu yang mengarah pada pengaturan sinyal, dan untuk memahami kapan sinyal masih "dimuat". Salah satu cara sederhana untuk memodelkan status pemuatan adalah dengan pengecualian, dan perilaku cache pengecualian dari sinyal yang dihitung cukup masuk akal dengan teknik ini. Teknik yang lebih baik dibahas di Edisi #30.
Transaksi : Untuk transisi antar tampilan, sering kali berguna untuk mempertahankan status aktif untuk status "dari" dan "ke". Status "ke" ditampilkan di latar belakang, hingga siap untuk ditukar (melakukan transaksi), sedangkan status "dari" tetap interaktif. Mempertahankan kedua status pada saat yang sama memerlukan "forking" status grafik sinyal, dan bahkan mungkin berguna untuk mendukung beberapa transisi yang tertunda sekaligus. Pembahasan di Edisi #73.
Beberapa kemungkinan metode kenyamanan juga dihilangkan.
Proposal ini ada dalam agenda TC39 April 2024 untuk Tahap 1. Saat ini dapat dianggap sebagai "Tahap 0".
Polyfill untuk proposal ini tersedia, dengan beberapa pengujian dasar. Beberapa pembuat kerangka kerja telah mulai bereksperimen dengan mengganti implementasi sinyal ini, namun penggunaan ini masih pada tahap awal.
Para kolaborator dalam proposal Signal ingin bersikap konservatif dalam cara kami memajukan proposal ini, sehingga kami tidak terjebak dalam pengiriman sesuatu yang pada akhirnya kami sesali dan tidak benar-benar digunakan. Rencana kami adalah melakukan tugas tambahan berikut, yang tidak diwajibkan oleh proses TC39, untuk memastikan proposal ini berjalan sesuai rencana:
Sebelum mengusulkan Tahap 2, kami berencana untuk:
Mengembangkan beberapa implementasi polyfill tingkat produksi yang solid, telah teruji dengan baik (misalnya, lulus pengujian dari berbagai kerangka kerja serta pengujian gaya test262), dan kompetitif dalam hal kinerja (sebagaimana diverifikasi dengan kumpulan tolok ukur sinyal/kerangka kerja yang menyeluruh).
Integrasikan Signal API yang diusulkan ke dalam sejumlah besar kerangka JS yang kami anggap cukup representatif, dan beberapa aplikasi besar bekerja dengan basis ini. Uji apakah ini berfungsi secara efisien dan benar dalam konteks ini.
Memiliki pemahaman yang kuat tentang kemungkinan perluasan API, dan telah menyimpulkan hal mana (jika ada) yang harus ditambahkan ke dalam proposal ini.
Bagian ini menjelaskan masing-masing API yang diekspos ke JavaScript, dalam kaitannya dengan algoritme yang diterapkannya. Hal ini dapat dianggap sebagai proto-spesifikasi, dan disertakan pada titik awal ini untuk menentukan satu kemungkinan rangkaian semantik, sekaligus sangat terbuka terhadap perubahan.
Beberapa aspek dari algoritma:
Urutan pembacaan Signals dalam perhitungan adalah signifikan, dan dapat diamati dalam urutan callback tertentu (yang Watcher
dipanggil, equals
, parameter pertama ke new Signal.Computed
, dan callback watched
/ unwatched
) dieksekusi. Ini berarti bahwa sumber Sinyal yang dihitung harus disimpan secara berurutan.
Keempat callback ini mungkin memunculkan pengecualian, dan pengecualian ini disebarkan dengan cara yang dapat diprediksi ke kode JS panggilan. Pengecualian tidak menghentikan eksekusi algoritma ini atau membiarkan grafik dalam keadaan setengah diproses. Untuk kesalahan yang terjadi dalam panggilan balik notify
dari Watcher, pengecualian tersebut dikirim ke panggilan .set()
yang memicunya, menggunakan AggregateError jika beberapa pengecualian dilempar. Yang lainnya (termasuk watched
/ unwatched
?) disimpan dalam nilai Sinyal, untuk dilempar kembali saat dibaca, dan Sinyal yang dilempar ulang tersebut dapat ditandai ~clean~
sama seperti sinyal lainnya dengan nilai normal.
Kehati-hatian dilakukan untuk menghindari sirkularitas dalam kasus sinyal yang dihitung yang tidak "dipantau" (diamati oleh Pengamat mana pun), sehingga sinyal tersebut dapat dikumpulkan secara independen dari bagian lain grafik sinyal. Secara internal, hal ini dapat dilaksanakan dengan sistem nomor pembangkitan yang selalu dikumpulkan; perhatikan bahwa implementasi yang dioptimalkan juga dapat menyertakan nomor pembangkitan per node lokal, atau menghindari pelacakan beberapa nomor pada sinyal yang dipantau.
Algoritme sinyal perlu merujuk pada keadaan global tertentu. Keadaan ini bersifat global untuk keseluruhan thread, atau "agen".
computing
: Sinyal efek atau komputasi terdalam yang saat ini sedang dievaluasi ulang karena panggilan .get
atau .run
, atau null
. Awalnya null
.
frozen
: Boolean yang menunjukkan apakah ada panggilan balik yang sedang dijalankan yang mengharuskan grafik tidak diubah. Awalnya false
.
generation
: Bilangan bulat yang bertambah, dimulai dari 0, digunakan untuk melacak seberapa terkini suatu nilai sambil menghindari sirkularitas.
Signal
Signal
adalah objek biasa yang berfungsi sebagai namespace untuk kelas dan fungsi terkait Signal.
Signal.subtle
adalah objek namespace bagian dalam yang serupa.
Signal.State
Signal.State
slot internal value
: Nilai sinyal keadaan saat ini
equals
: Fungsi perbandingan yang digunakan saat mengubah nilai
watched
: Panggilan balik yang akan dipanggil ketika sinyal diamati oleh suatu efek
unwatched
: Panggilan balik yang akan dipanggil ketika sinyal tidak lagi diamati oleh suatu efek
sinks
: Kumpulan sinyal yang diawasi yang bergantung pada sinyal ini
Signal.State(initialValue, options)
Tetapkan value
Signal ini ke initialValue
.
Tetapkan Sinyal ini equals
dengan opsi?.sama dengan
Setel watched
Signal ini ke opsi?.[Signal.subtle.watched]
Setel Sinyal ini unwatched
ke opsi?.[Signal.subtle.unwatched]
Atur sinks
Sinyal ini ke set kosong
Signal.State.prototype.get()
Jika frozen
itu benar, berikan pengecualian.
Jika computing
tidak undefined
, tambahkan Sinyal ini ke kumpulan sources
computing
.
CATATAN: Kami tidak menambahkan computing
ke set sinks
Sinyal ini sampai ia diawasi oleh Pengamat.
Kembalikan value
Sinyal ini.
Signal.State.prototype.set(newValue)
Jika konteks eksekusi saat ini frozen
, berikan pengecualian.
Jalankan algoritma "set Signal value" dengan Signal ini dan parameter pertama untuk nilainya.
Jika algoritme itu mengembalikan ~clean~
, maka kembalikan tidak terdefinisi.
Atur state
semua sinks
Sinyal ini ke (jika itu adalah Sinyal yang Dihitung) ~dirty~
jika sebelumnya bersih, atau (jika itu adalah Watcher) ~pending~
jika sebelumnya ~watching~
.
Atur state
semua dependensi Computed Signal sink (secara rekursif) ke ~checked~
jika sebelumnya ~clean~
(yaitu, tinggalkan tanda kotor di tempatnya), atau untuk Watchers, ~pending~
jika sebelumnya ~watching~
.
Untuk setiap ~watching~
Pengamat yang ditemui dalam pencarian rekursif itu, lalu dalam urutan pertama yang mendalam,
Setel frozen
ke benar.
Memanggil panggilan balik notify
mereka (mengesampingkan pengecualian apa pun yang dilemparkan, tetapi mengabaikan nilai kembalian notify
).
Kembalikan frozen
ke palsu.
Atur state
Pengamat ke ~waiting~
.
Jika ada pengecualian yang muncul dari callback notify
, sebarkan pengecualian tersebut ke pemanggil setelah semua callback notify
dijalankan. Jika ada beberapa pengecualian, paketkan semuanya menjadi AggregateError dan buang itu.
Kembalikan tidak terdefinisi.
Signal.Computed
Signal.Computed
Mesin Status Terhitung state
Sinyal Terhitung dapat berupa salah satu dari berikut ini:
~clean~
: Nilai Sinyal ada dan diketahui tidak basi.
~checked~
: Sumber (tidak langsung) dari Sinyal ini telah berubah; Sinyal ini mempunyai nilai tetapi mungkin sudah basi. Apakah itu basi atau tidak, hanya akan diketahui jika semua sumber langsung telah dievaluasi.
~computing~
: Panggilan balik Sinyal ini sedang dijalankan sebagai efek samping dari panggilan .get()
.
~dirty~
: Entah Sinyal ini mempunyai nilai yang diketahui sudah basi, atau belum pernah dievaluasi.
Grafik transisinya adalah sebagai berikut:
stateDiagram-v2
[*] --> kotor
kotor --> komputasi: [4]
komputasi --> bersih: [5]
bersih --> kotor: [2]
bersih --> dicentang: [3]
dicentang --> bersih: [6]
diperiksa --> kotor: [1]
MemuatTransisinya adalah:
Nomor | Dari | Ke | Kondisi | Algoritma |
---|---|---|---|---|
1 | ~checked~ | ~dirty~ | Sumber langsung dari sinyal ini, yaitu sinyal yang dihitung, telah dievaluasi, dan nilainya telah berubah. | Algoritma: menghitung ulang Sinyal kotor yang dihitung |
2 | ~clean~ | ~dirty~ | Sumber langsung dari sinyal ini, yaitu suatu Negara, telah ditetapkan, dengan nilai yang tidak sama dengan nilai sebelumnya. | Metode: Signal.State.prototype.set(newValue) |
3 | ~clean~ | ~checked~ | Sumber sinyal ini yang bersifat rekursif, namun tidak langsung, yaitu suatu Keadaan, telah ditetapkan, dengan nilai yang tidak sama dengan nilai sebelumnya. | Metode: Signal.State.prototype.set(newValue) |
4 | ~dirty~ | ~computing~ | Kami akan menjalankan callback . | Algoritma: menghitung ulang Sinyal kotor yang dihitung |
5 | ~computing~ | ~clean~ | callback telah selesai mengevaluasi dan mengembalikan nilai atau memberikan pengecualian. | Algoritma: menghitung ulang Sinyal kotor yang dihitung |
6 | ~checked~ | ~clean~ | Semua sumber langsung dari sinyal ini telah dievaluasi, dan semuanya ditemukan tidak berubah, sehingga kita sekarang diketahui tidak basi. | Algoritma: menghitung ulang Sinyal kotor yang dihitung |
Signal.Computed
internal yang dihitung value
: Nilai Sinyal yang di-cache sebelumnya, atau ~uninitialized~
untuk Sinyal terhitung yang tidak pernah dibaca. Nilainya mungkin merupakan pengecualian yang dimunculkan kembali saat nilainya dibaca. Selalu undefined
untuk sinyal efek.
state
: Mungkin ~clean~
, ~checked~
, ~computing~
, atau ~dirty~
.
sources
: Kumpulan Sinyal yang terurut yang menjadi tempat bergantungnya Sinyal ini.
sinks
: Kumpulan Sinyal terurut yang bergantung pada Sinyal ini.
equals
: Metode sama dengan yang disediakan dalam opsi.
callback
: Panggilan balik yang dipanggil untuk mendapatkan nilai Sinyal yang dihitung. Setel ke parameter pertama yang diteruskan ke konstruktor.
Signal.Computed
Konstruktor TerhitungSet konstruktor
callback
ke parameter pertamanya
equals
berdasarkan opsi, default ke Object.is
jika tidak ada
state
menjadi ~dirty~
value
ke ~uninitialized~
Dengan AsyncContext, callback yang diteruskan ke new Signal.Computed
akan menutup snapshot sejak konstruktor dipanggil, dan memulihkan snapshot ini selama eksekusinya.
Signal.Computed.prototype.get
Jika konteks eksekusi saat ini frozen
atau jika Sinyal ini memiliki status ~computing~
, atau jika sinyal ini adalah Efek dan computing
Sinyal yang dihitung, berikan pengecualian.
Jika computing
bukan null
, tambahkan Signal ini ke kumpulan sources
computing
.
CATATAN: Kami tidak menambahkan computing
ke set sinks
Sinyal ini sampai/kecuali jika diawasi oleh Pengamat.
Jika status Sinyal ini ~dirty~
atau ~checked~
: Ulangi langkah berikut hingga Sinyal ini ~clean~
:
Berulang melalui sources
untuk menemukan sumber rekursif terdalam, paling kiri (yaitu yang paling awal diamati) yang merupakan Sinyal Terhitung yang ditandai ~dirty~
(memotong pencarian ketika mencapai Sinyal Terhitung ~clean~
, dan menyertakan Sinyal Terhitung ini sebagai hal terakhir untuk mencari).
Jalankan algoritme "hitung ulang Sinyal yang dihitung secara kotor" pada Sinyal tersebut.
Pada titik ini, status Sinyal ini akan menjadi ~clean~
, dan tidak ada sumber rekursif yang ~dirty~
atau ~checked~
. Kembalikan value
Sinyal. Jika nilainya merupakan pengecualian, masukkan kembali pengecualian tersebut.
Signal.subtle.Watcher
Signal.subtle.Watcher
state
Pengamat dapat berupa salah satu dari berikut ini:
~waiting~
: Panggilan balik notify
telah dijalankan, atau Pengamat masih baru, tetapi tidak secara aktif mengamati sinyal apa pun.
~watching~
: Pengamat secara aktif mengamati sinyal, namun belum ada perubahan yang memerlukan panggilan balik notify
.
~pending~
: Ketergantungan Watcher telah berubah, namun callback notify
belum dijalankan.
Grafik transisinya adalah sebagai berikut:
stateDiagram-v2
[*] --> menunggu
menunggu --> menonton: [1]
menonton --> menunggu: [2]
menonton --> tertunda: [3]
tertunda --> menunggu: [4]
MemuatTransisinya adalah:
Nomor | Dari | Ke | Kondisi | Algoritma |
---|---|---|---|---|
1 | ~waiting~ | ~watching~ | Metode watch Watcher telah dipanggil. | Metode: Signal.subtle.Watcher.prototype.watch(...signals) |
2 | ~watching~ | ~waiting~ | Metode unwatch Watcher telah dipanggil, dan sinyal yang terakhir ditonton telah dihapus. | Metode: Signal.subtle.Watcher.prototype.unwatch(...signals) |
3 | ~watching~ | ~pending~ | Sinyal yang diawasi mungkin telah berubah nilainya. | Metode: Signal.State.prototype.set(newValue) |
4 | ~pending~ | ~waiting~ | Panggilan balik notify telah dijalankan. | Metode: Signal.State.prototype.set(newValue) |
Signal.subtle.Watcher
state
: Mungkin ~watching~
, ~pending~
atau ~waiting~
signals
: Serangkaian Sinyal yang diurutkan yang sedang diawasi oleh Pengamat ini
notifyCallback
: Panggilan balik yang dipanggil ketika ada perubahan. Setel ke parameter pertama yang diteruskan ke konstruktor.
new Signal.subtle.Watcher(callback)
state
diatur ke ~waiting~
.
Inisialisasi signals
sebagai himpunan kosong.
notifyCallback
diatur ke parameter panggilan balik.
Dengan AsyncContext, callback yang diteruskan ke new Signal.subtle.Watcher
tidak menutup snapshot sejak konstruktor dipanggil, sehingga informasi kontekstual di sekitar penulisan terlihat.
Signal.subtle.Watcher.prototype.watch(...signals)
Jika frozen
itu benar, berikan pengecualian.
Jika salah satu argumen bukan merupakan sinyal, berikan pengecualian.
Tambahkan semua argumen ke akhir dari objek ini signals
.
Untuk setiap sinyal yang baru ditonton, dalam urutan dari kiri ke kanan,
Tambahkan pengamat ini sebagai sink
sinyal itu.
Jika ini adalah sink pertama, maka ulangi sumber untuk menambahkan sinyal tersebut sebagai sink.
Setel frozen
ke benar.
Panggil panggilan balik watched
jika ada.
Kembalikan frozen
ke palsu.
Jika state
Signal adalah ~waiting~
, setel ke ~watching~
.
Signal.subtle.Watcher.prototype.unwatch(...signals)
Jika frozen
itu benar, berikan pengecualian.
Jika salah satu argumen bukan merupakan sinyal, atau tidak diawasi oleh pengamat ini, berikan pengecualian.
Untuk setiap sinyal dalam argumen, dalam urutan dari kiri ke kanan,
Hapus sinyal tersebut dari kumpulan signals
Pengamat ini.
Hapus Pengamat ini dari sink
sinyal itu.
Jika set sink
Sinyal menjadi kosong, hapus Sinyal tersebut sebagai sink dari masing-masing sumbernya.
Setel frozen
ke benar.
Panggil panggilan balik unwatched
jika ada.
Kembalikan frozen
ke palsu.
Jika pengamat sekarang tidak memiliki signals
, dan state
adalah ~watching~
, setel ke ~waiting~
.
Signal.subtle.Watcher.prototype.getPending()
Mengembalikan Array yang berisi subset signals
yang merupakan Sinyal Terhitung dalam status ~dirty~
atau ~pending~
.
Signal.subtle.untrack(cb)
Misalkan c
adalah status computing
konteks eksekusi saat ini.
Setel computing
ke nol.
Hubungi cb
.
Kembalikan computing
ke c
(meskipun cb
memberikan pengecualian).
Mengembalikan nilai kembalian cb
(melempar ulang pengecualian apa pun).
Catatan: untrack tidak membuat Anda keluar dari keadaan frozen
, yang dijaga dengan ketat.
Signal.subtle.currentComputed()
Kembalikan nilai computing
saat ini.
Hapus kumpulan sources
Sinyal ini, dan hapus dari sinks
sumber sinyal tersebut.
Simpan nilai computing
sebelumnya dan atur computing
ke Sinyal ini.
Setel status Sinyal ini ke ~computing~
.
Jalankan callback Signal yang dihitung ini, gunakan Signal ini sebagai nilai ini. Simpan nilai kembalian, dan jika panggilan balik memunculkan pengecualian, simpan nilai tersebut untuk dilempar ulang.
Kembalikan nilai computing
sebelumnya.
Terapkan algoritme "setel nilai Sinyal" ke nilai kembalian panggilan balik.
Setel status Sinyal ini ke ~clean~
.
Jika algoritme tersebut mengembalikan ~dirty~
: tandai semua sink Sinyal ini sebagai ~dirty~
(sebelumnya, sink mungkin merupakan campuran dari yang dicentang dan kotor). (Atau, jika ini tidak diperhatikan, gunakan nomor generasi baru untuk menunjukkan kekotoran, atau semacamnya.)
Jika tidak, algoritme tersebut akan mengembalikan ~clean~
: Dalam hal ini, untuk setiap ~checked~
sink Sinyal ini, jika semua sumber Sinyal tersebut sekarang bersih, maka tandai Sinyal tersebut sebagai ~clean~
juga. Terapkan langkah pembersihan ini ke sink selanjutnya secara rekursif, ke Sinyal baru yang bersih yang telah memeriksa sink. (Atau, jika ini tidak diawasi, tunjukkan hal yang sama, sehingga pembersihan dapat berjalan lambat.)
Jika algoritme ini memberikan nilai (sebagai lawan dari pengecualian untuk pelemparan ulang, dari penghitungan ulang algoritme Sinyal yang dihitung secara kotor):
Panggil fungsi equals
dengan Sinyal ini, dengan meneruskan value
saat ini, nilai baru, dan Sinyal ini sebagai parameter. Jika pengecualian dilempar, simpan pengecualian itu (untuk ditampilkan kembali saat dibaca) sebagai nilai Sinyal dan lanjutkan seolah-olah panggilan balik menghasilkan false.
Jika fungsi itu mengembalikan nilai true, kembalikan ~clean~
.
Tetapkan value
Sinyal ini ke parameter.
Kembali ~dirty~
Q : Bukankah masih terlalu dini untuk melakukan standarisasi sesuatu yang berkaitan dengan Sinyal, padahal mereka baru mulai menjadi hal baru yang populer di tahun 2022? Bukankah kita harus memberi mereka lebih banyak waktu untuk berkembang dan stabil?
J : Status Sinyal dalam kerangka web saat ini adalah hasil pengembangan berkelanjutan selama lebih dari 10 tahun. Seiring dengan meningkatnya investasi, seperti yang terjadi dalam beberapa tahun terakhir, hampir semua kerangka web mendekati model inti Sinyal yang sangat mirip. Proposal ini adalah hasil dari latihan desain bersama antara sejumlah besar pemimpin saat ini dalam kerangka web, dan proposal ini tidak akan diajukan ke standarisasi tanpa validasi dari kelompok pakar domain tersebut dalam berbagai konteks.
T : Apakah Sinyal bawaan dapat digunakan oleh kerangka kerja, mengingat integrasinya yang erat dengan rendering dan kepemilikan?
J : Bagian yang lebih spesifik pada kerangka kerja cenderung berada pada area dampak, penjadwalan, dan kepemilikan/pembuangan, yang tidak ingin diselesaikan oleh proposal ini. Prioritas pertama kami dalam pembuatan prototipe Sinyal jalur standar adalah memvalidasi bahwa sinyal tersebut dapat ditempatkan "di bawah" kerangka kerja yang ada secara kompatibel dan dengan kinerja yang baik.
Q : Apakah Signal API dimaksudkan untuk digunakan langsung oleh pengembang aplikasi, atau dibungkus dengan framework?
J : Meskipun API ini dapat digunakan secara langsung oleh pengembang aplikasi (setidaknya bagian yang tidak berada dalam namespace Signal.subtle
), API ini tidak dirancang secara ergonomis. Sebaliknya, kebutuhan penulis perpustakaan/kerangka kerja adalah prioritas. Sebagian besar kerangka kerja diharapkan untuk membungkus API Signal.State
dan Signal.Computed
dasar dengan sesuatu yang mengekspresikan kemiringan ergonomisnya. Dalam praktiknya, biasanya yang terbaik adalah menggunakan Signals melalui kerangka kerja, yang mengelola fitur-fitur yang lebih rumit (misalnya, Watcher, untrack
), serta mengelola kepemilikan dan pembuangan (misalnya, mencari tahu kapan sinyal harus ditambahkan dan dihapus dari pengamat), dan menjadwalkan rendering ke DOM--proposal ini tidak berupaya menyelesaikan masalah tersebut.
Q : Apakah saya harus menghapus Sinyal yang terkait dengan widget ketika widget tersebut dihancurkan? Apa API untuk itu?
J : Operasi pembongkaran yang relevan di sini adalah Signal.subtle.Watcher.prototype.unwatch
. Hanya Sinyal yang ditonton saja yang perlu dibersihkan (dengan membatalkan penayangannya), sedangkan Sinyal yang belum ditonton dapat dikumpulkan sampahnya secara otomatis.
T : Apakah Sinyal berfungsi dengan VDOM, atau langsung dengan DOM HTML yang mendasarinya?
J : Ya! Sinyal tidak bergantung pada teknologi rendering. Kerangka kerja JavaScript yang ada yang menggunakan konstruksi mirip Signal terintegrasi dengan VDOM (misalnya, Preact), DOM asli (misalnya, Solid) dan kombinasinya (misalnya, Vue). Hal yang sama dapat dilakukan dengan Sinyal bawaan.
T : Apakah penggunaan Signals dalam konteks framework berbasis kelas seperti Angular dan Lit akan menjadi ergonomis? Bagaimana dengan kerangka kerja berbasis kompiler seperti Svelte?
J : Bidang kelas dapat dibuat berbasis Signal dengan dekorator pengakses sederhana, seperti yang ditunjukkan dalam readme polyfill Signal. Sinyal sangat selaras dengan Rune Svelte 5--mudah bagi compiler untuk mengubah rune ke Signal API yang ditentukan di sini, dan faktanya inilah yang dilakukan Svelte 5 secara internal (tetapi dengan pustaka Signalnya sendiri).
T : Apakah Sinyal berfungsi dengan SSR? Hidrasi? Dapat dilanjutkan?
J : Ya. Qwik menggunakan Sinyal untuk memberikan efek yang baik dengan kedua properti ini, dan kerangka kerja lain memiliki pendekatan lain yang dikembangkan dengan baik untuk hidrasi dengan Sinyal dengan pengorbanan yang berbeda. Kami berpendapat bahwa ada kemungkinan untuk memodelkan Sinyal Qwik yang dapat dilanjutkan menggunakan sinyal Status dan Komputasi yang dihubungkan bersama, dan berencana untuk membuktikannya dalam kode.
T : Apakah Signals berfungsi dengan aliran data satu arah seperti yang dilakukan React?
A : Ya, Sinyal adalah mekanisme aliran data satu arah. Framework UI berbasis sinyal memungkinkan Anda mengekspresikan pandangan Anda sebagai fungsi model (saat model tersebut menggabungkan Sinyal). Grafik keadaan dan Sinyal yang dihitung bersifat asiklik berdasarkan konstruksi. Dimungkinkan juga untuk membuat ulang antipola React di dalam Signals (!), misalnya, Signal yang setara dengan setState
di dalam useEffect
adalah dengan menggunakan Watcher untuk menjadwalkan penulisan ke sinyal State.
T : Bagaimana hubungan sinyal dengan sistem manajemen negara seperti Redux? Apakah sinyal mendorong keadaan tidak terstruktur?
J : Sinyal dapat menjadi dasar yang efisien untuk abstraksi pengelolaan keadaan seperti penyimpanan. Pola umum yang ditemukan dalam beberapa kerangka kerja adalah objek berdasarkan Proxy yang secara internal mewakili properti menggunakan Sinyal, misalnya Vue reactive()
, atau Solid store. Sistem ini memungkinkan pengelompokan negara secara fleksibel pada tingkat abstraksi yang tepat untuk aplikasi tertentu.
T : Penawaran Sinyal apa yang saat ini tidak ditangani Proxy
?
J : Proxy dan Sinyal saling melengkapi dan berjalan dengan baik. Proksi memungkinkan Anda mencegat operasi objek dangkal dan sinyal mengoordinasikan grafik ketergantungan (sel). Mendukung Proxy dengan Sinyal adalah cara terbaik untuk membuat struktur reaktif bersarang dengan ergonomis yang baik.
Dalam contoh ini, kita dapat menggunakan proxy untuk membuat sinyal memiliki properti pengambil dan penyetel alih-alih menggunakan metode get
dan set
:
const a = Status Sinyal baru(0);const b = Proksi baru(a, { dapatkan(target, properti, penerima) {if (properti === 'nilai') { kembalikan target.get():} } set(target, properti, nilai, penerima) {if (properti === 'nilai') { target.set(nilai)!} }});// penggunaan dalam konteks reaktif hipotetis:<template> {b.nilai} <tombol onclick={() => {b.value++; }}>ubah</button></template>
saat menggunakan penyaji yang dioptimalkan untuk reaktivitas terperinci, mengeklik tombol akan menyebabkan sel b.value
diperbarui.
Melihat:
contoh struktur reaktif bersarang yang dibuat dengan Sinyal dan Proksi: signal-utils
contoh implementasi sebelumnya yang menunjukkan hubungan antara data reaktif dan proxy: tracked-built-in
diskusi.
Q : Apakah Sinyal berbasis push atau pull?
A : Evaluasi Sinyal yang dihitung berbasis tarikan: Sinyal yang dihitung hanya dievaluasi ketika .get()
dipanggil, meskipun keadaan dasarnya berubah jauh lebih awal. Pada saat yang sama, mengubah sinyal Status dapat segera memicu panggilan balik Watcher, yang "mendorong" notifikasi. Jadi Sinyal dapat dianggap sebagai konstruksi “dorong-tarik”.
T : Apakah Signal memperkenalkan nondeterminisme ke dalam eksekusi JavaScript?
J : Tidak. Pertama, semua operasi Signal memiliki semantik dan urutan yang terdefinisi dengan baik, dan tidak akan berbeda di antara implementasi yang sesuai. Pada tingkat yang lebih tinggi, Sinyal mengikuti serangkaian invarian tertentu, yang berkenaan dengan sinyal tersebut "suara". Sinyal yang dihitung selalu mengamati grafik Sinyal dalam keadaan konsisten, dan eksekusinya tidak terganggu oleh kode mutasi Sinyal lainnya (kecuali untuk hal-hal yang dipanggilnya sendiri). Lihat uraian di atas.
T : Saat saya menulis ke sinyal negara, kapan pembaruan ke sinyal yang dihitung dijadwalkan?
A : Ini tidak dijadwalkan! Sinyal yang dihitung akan menghitung ulang dirinya sendiri saat seseorang membacanya lagi. Secara bersamaan, panggilan balik notify
Watcher dapat dipanggil, memungkinkan kerangka kerja untuk menjadwalkan pembacaan pada waktu yang dirasa tepat.
T : Kapan penulisan ke status Sinyal mulai berlaku? Segera, atau apakah mereka batch?
A : Penulisan ke status Sinyal akan segera direfleksikan--saat berikutnya Sinyal yang dihitung yang bergantung pada status Sinyal dibaca, ia akan menghitung ulang dirinya sendiri jika diperlukan, bahkan jika berada di baris kode berikutnya. Namun, kemalasan yang melekat dalam mekanisme ini (yaitu Sinyal yang dihitung hanya dihitung ketika dibaca) berarti bahwa, dalam praktiknya, penghitungan dapat dilakukan secara batch.
T : Apa artinya Signal mengaktifkan eksekusi "bebas kesalahan"?
J : Model berbasis push sebelumnya untuk reaktivitas menghadapi masalah komputasi yang berlebihan: Jika pembaruan pada suatu keadaan Sinyal menyebabkan Sinyal yang dihitung berjalan dengan cepat, pada akhirnya hal ini dapat mendorong pembaruan ke UI. Namun penulisan ke UI ini mungkin terlalu dini, jika akan ada perubahan lain pada status asal Signal sebelum frame berikutnya. Terkadang, nilai perantara yang tidak akurat bahkan diperlihatkan kepada pengguna akhir karena gangguan tersebut. Sinyal menghindari dinamika ini dengan berbasis tarik, bukan berbasis tekan: Pada saat kerangka kerja menjadwalkan rendering UI, kerangka kerja akan menarik pembaruan yang sesuai, menghindari pekerjaan yang sia-sia baik dalam komputasi maupun penulisan ke DOM.
Q : Apa maksudnya Sinyal menjadi "lossy"?
J : Ini adalah kebalikan dari eksekusi bebas kesalahan: Sinyal mewakili sel data--hanya nilai saat ini (yang dapat berubah), bukan aliran data seiring waktu. Jadi, jika Anda menulis ke suatu keadaan Sinyal dua kali berturut-turut, tanpa melakukan apa pun, penulisan pertama akan "hilang" dan tidak pernah terlihat oleh Sinyal atau efek yang dihitung. Hal ini dipahami sebagai fitur dan bukan bug--konstruksi lain (misalnya, iterable async, observable) lebih sesuai untuk stream.
T : Apakah Sinyal asli akan lebih cepat dibandingkan implementasi Sinyal JS yang sudah ada?
J : Kami berharap demikian (dengan faktor konstan yang kecil), namun hal ini masih harus dibuktikan dalam kode. Mesin JS bukanlah sesuatu yang ajaib, dan pada akhirnya perlu mengimplementasikan jenis algoritme yang sama seperti implementasi JS pada Signals. Lihat bagian di atas tentang kinerja.
T : Mengapa proposal ini tidak menyertakan fungsi effect()
, padahal efek diperlukan untuk penggunaan praktis Sinyal?
J : Efek secara inheren terkait dengan penjadwalan dan pembuangan, yang dikelola oleh kerangka kerja dan di luar cakupan proposal ini. Sebaliknya, proposal ini menyertakan dasar untuk menerapkan efek melalui Signal.subtle.Watcher
API tingkat rendah.
T : Mengapa langganan bersifat otomatis dibandingkan menyediakan antarmuka manual?
J : Pengalaman menunjukkan bahwa antarmuka berlangganan manual untuk reaktivitas tidak ergonomis dan rawan kesalahan. Pelacakan otomatis lebih dapat disusun dan merupakan fitur inti Signal.
T : Mengapa callback Watcher
berjalan secara sinkron, bukan dijadwalkan dalam tugas mikro?
A : Karena callback tidak dapat membaca atau menulis Sinyal, tidak ada ketidaknyamanan yang ditimbulkan dengan memanggilnya secara sinkron. Panggilan balik yang khas akan menambahkan Sinyal ke Array untuk dibaca nanti, atau menandai sedikit di suatu tempat. Tidak perlu dan tidak praktis mahal untuk membuat tugas mikro terpisah untuk semua jenis tindakan ini.
T : API ini tidak memiliki beberapa hal bagus yang disediakan oleh kerangka kerja favorit saya, yang membuatnya lebih mudah untuk memprogram dengan Signals. Bisakah itu ditambahkan ke standar juga?
J : Mungkin. Berbagai perluasan masih dalam pertimbangan. Silakan ajukan masalah untuk meningkatkan diskusi tentang fitur apa pun yang hilang yang menurut Anda penting.
T : Apakah API ini dapat dikurangi ukuran atau kompleksitasnya?
J : Tentu saja tujuannya adalah untuk menjaga API ini tetap minimal, dan kami telah mencoba melakukannya dengan apa yang disajikan di atas. Jika Anda memiliki ide untuk hal lain yang dapat dihapus, silakan ajukan masalah untuk didiskusikan.
T : Bukankah kita sebaiknya memulai pekerjaan standardisasi di bidang ini dengan konsep yang lebih primitif, misalnya observasi?
J : Observable mungkin merupakan ide bagus untuk beberapa hal, namun tidak memecahkan masalah yang ingin dipecahkan oleh Signal. Seperti dijelaskan di atas, mekanisme observasi atau mekanisme terbitkan/langganan lainnya bukanlah solusi lengkap untuk banyak jenis pemrograman UI, antara lain karena terlalu banyak pekerjaan konfigurasi yang rawan kesalahan bagi pengembang, dan pekerjaan yang sia-sia karena kurangnya kemalasan.
T : Mengapa Sinyal diusulkan di TC39 dan bukan DOM, mengingat sebagian besar aplikasinya berbasis web?
J : Beberapa penulis proposal ini tertarik pada lingkungan UI non-web sebagai tujuannya, namun saat ini, tempat mana pun mungkin cocok untuk tujuan tersebut, karena API web lebih sering diterapkan di luar web. Pada akhirnya, Signals tidak perlu bergantung pada DOM API apa pun, jadi cara apa pun bisa digunakan. Jika seseorang mempunyai alasan kuat mengapa grup ini beralih, harap beri tahu kami dalam suatu masalah. Untuk saat ini, seluruh kontributor telah menandatangani perjanjian kekayaan intelektual TC39, dan rencananya akan disampaikan kepada TC39.
Q : Berapa lama waktu yang diperlukan hingga saya dapat menggunakan Sinyal standar?
J : Polyfill sudah tersedia, namun sebaiknya jangan mengandalkan stabilitasnya, karena API ini berkembang selama proses peninjauannya. Dalam beberapa bulan atau satu tahun, polyfill stabil yang berkualitas tinggi dan berkinerja tinggi akan dapat digunakan, namun hal ini masih harus direvisi oleh komite dan belum menjadi standar. Mengikuti jalur umum proposal TC39, diperkirakan diperlukan waktu minimal 2-3 tahun agar Sinyal tersedia secara asli di semua browser sejak beberapa versi, sehingga polyfill tidak diperlukan.
T : Bagaimana kami mencegah standarisasi jenis Sinyal yang salah terlalu cepat, seperti {{Fitur JS/web yang tidak Anda sukai}}?
J : Penulis proposal ini berencana melakukan upaya ekstra dalam pembuatan prototipe dan pembuktian sebelum meminta kemajuan tahapan di TC39. Lihat "Status dan rencana pengembangan" di atas. Jika Anda melihat kesenjangan dalam rencana ini atau peluang untuk perbaikan, silakan ajukan masalah yang menjelaskannya.