ร่างข้อเสนอมาตรฐาน JavaScript Signals
- เป็นเอกสารที่อธิบายทิศทางร่วมเบื้องต้นเกี่ยวกับ signals ใน JavaScript โดยมีลักษณะคล้ายกับความพยายามของ Promises/A+ ก่อนที่ Promises จะถูกทำให้เป็นมาตรฐานโดย TC39 ใน ES2015
- ความพยายามนี้มุ่งเน้นไปที่การประสานระบบนิเวศของ JavaScript และหากการประสานนี้ประสบความสำเร็จ ก็อาจมีมาตรฐานเกิดขึ้นตามมาโดยอาศัยประสบการณ์ดังกล่าว
- ผู้สร้างเฟรมเวิร์กหลายรายกำลังร่วมมือกันในโมเดลร่วมที่สามารถรองรับ reactive core ได้
- ร่างปัจจุบันอิงจากข้อเสนอด้านการออกแบบจากผู้เขียน/ผู้ดูแลของ Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue และ Wiz
ภูมิหลัง: ทำไมต้อง signals?
- เพื่อพัฒนา user interface (UI) ที่ซับซ้อน นักพัฒนาแอปพลิเคชัน JavaScript จำเป็นต้องเก็บ state, คำนวณ, ทำให้ใช้ไม่ได้, ซิงก์ และส่งต่อไปยัง view layer ของแอปพลิเคชันอย่างมีประสิทธิภาพ
- UI มักไม่ได้เกี่ยวข้องแค่การจัดการค่าธรรมดาเท่านั้น แต่ยังรวมถึงการเรนเดอร์ state ที่คำนวณขึ้นซึ่งขึ้นอยู่กับค่าอื่นหรือ state อื่นด้วย
- เป้าหมายของ signals คือการจัดเตรียมโครงสร้างพื้นฐานสำหรับการจัดการ state ของแอปพลิเคชันเหล่านี้ เพื่อให้นักพัฒนาสามารถโฟกัสกับ business logic ได้มากกว่ารายละเอียดที่ซ้ำซาก
ตัวอย่าง - ตัวนับแบบ VanillaJS
- มีตัวแปรชื่อ
counter และต้องการอัปเดตใน DOM ว่าตัวนับเป็นเลขคู่หรือไม่ทุกครั้งที่ค่าของมันเปลี่ยน
- ใน Vanilla JS อาจเขียนโค้ดได้ดังนี้:
let counter = 0;
const setCounter = (value) => {
counter = value;
render();
};
const isEven = () => (counter & 1) == 0;
const parity = () => isEven() ? "even" : "odd";
const render = () => element.innerText = parity();
// Simulate external updates to counter...
setInterval(() => setCounter(counter + 1), 1000);
- โค้ดนี้มีปัญหาหลายอย่าง:
- การตั้งค่า
counter มีความรกและมี boilerplate มาก
- state ของ
counter ถูกผูกติดแน่นกับระบบเรนเดอร์
- ในกรณีที่
counter เปลี่ยนแต่ parity ไม่เปลี่ยน (เช่น เปลี่ยนจาก 2 เป็น 4) ก็ยังมีการคำนวณและเรนเดอร์โดยไม่จำเป็น
- หากส่วนอื่นของ UI ต้องการเรนเดอร์เฉพาะเมื่อ
counter อัปเดต
- ส่วนอื่นของ UI ที่พึ่งพาเพียง
isEven หรือ parity จะอัปเดตไม่ได้หากไม่โต้ตอบกับ counter โดยตรง
แนะนำ signals
- abstraction สำหรับ data binding ระหว่าง model กับ view เป็นแกนหลักของ UI framework มานานแล้ว แม้ว่า JS หรือเว็บแพลตฟอร์มจะไม่ได้มี mechanism ลักษณะนี้ในตัวก็ตาม
- ภายใน JS framework และไลบรารี มีการทดลองมากมายกับวิธีต่าง ๆ ในการแทน binding เหล่านี้ และได้พิสูจน์ให้เห็นถึงพลังของแนวทาง reactive value แบบ first-class ที่ใช้แทน state หรือการคำนวณที่ได้มาจากข้อมูลอื่น ซึ่งมักถูกเรียกว่า "Signals"
- หากนำตัวอย่างข้างต้นมาคิดใหม่โดยใช้ API ของ signals จะได้ดังนี้:
const counter = new Signal.State(0);
const isEven = new Signal.Computed(() => (counter.get() & 1) == 0);
const parity = new Signal.Computed(() => isEven.get() ? "even" : "odd");
// A library or framework defines effects based on other Signal primitives
declare function effect(cb: () => void): (() => void);
effect(() => element.innerText = parity.get());
// Simulate external updates to counter...
setInterval(() => counter.set(counter.get() + 1), 1000);
แรงจูงใจในการทำ signals ให้เป็นมาตรฐาน
การทำงานร่วมกันได้
- การติดตั้งใช้งาน signals แต่ละแบบมีกลไก automatic tracking เป็นของตัวเอง ทำให้แชร์โมเดล คอมโพเนนต์ และไลบรารีข้ามเฟรมเวิร์กได้ยาก
- เป้าหมายของข้อเสนอนี้คือแยก reactive model ออกจาก rendering view อย่างสมบูรณ์ เพื่อให้นักพัฒนาไม่ต้องเขียนโค้ดส่วนที่ไม่ใช่ UI ใหม่เมื่อย้ายไปใช้เทคโนโลยีการเรนเดอร์แบบใหม่ หรือสามารถพัฒนา reactive model แบบใช้ร่วมกันซึ่งนำไป deploy ในบริบทอื่นได้ด้วย JS
ประสิทธิภาพ/การใช้หน่วยความจำ
- โดยทั่วไป การที่มีไลบรารีที่ใช้กันอย่างแพร่หลายถูกรวมมาในตัวจะช่วยลดปริมาณโค้ดที่ต้องส่ง และอาจให้ประโยชน์ด้านประสิทธิภาพเล็กน้อยอยู่เสมอ แต่ไม่ได้คาดว่าผลนี้จะใหญ่มากนัก เพราะการติดตั้งใช้งาน signals มักมีขนาดค่อนข้างเล็กอยู่แล้ว
เครื่องมือสำหรับนักพัฒนา
- เมื่อใช้ไลบรารี signals ในภาษา JS ที่มีอยู่เดิม มักติดตาม call stack ผ่านสายโซ่ของ computed signals หรือกราฟอ้างอิงระหว่าง signals ได้ยาก
- signals ที่มีมาในตัวจะช่วยให้ JS runtime และเครื่องมือสำหรับนักพัฒนาสามารถรองรับการตรวจสอบ signals ได้ดีขึ้น
ประโยชน์เพิ่มเติม
ข้อดีของ standard library
- โดยทั่วไป JavaScript มี standard library ที่ค่อนข้างมินิมอล แต่แนวโน้มของ TC39 คือทำให้ JS เป็นภาษาที่ "มีแบตเตอรี่มาพร้อม" พร้อมชุดความสามารถ built-in คุณภาพสูง
การผสานกับ HTML/DOM (ความเป็นไปได้ในอนาคต)
- W3C และผู้พัฒนาเบราว์เซอร์กำลังดำเนินงานเพื่อนำ native template เข้าสู่ HTML
- เพื่อให้บรรลุเป้าหมายเหล่านี้ ในท้ายที่สุด HTML จะต้องมี reactive primitive
เป้าหมายในการออกแบบ signals
- ไลบรารี signals ที่มีอยู่ไม่ได้แตกต่างกันมากนักในส่วนแกนหลัก
- ข้อเสนอนี้ต้องการต่อยอดจากความสำเร็จของพวกมันด้วยการนำคุณสมบัติสำคัญของหลายไลบรารีมาใช้งาน
ความสามารถหลัก
- Signal type สำหรับแทน state หรือก็คือ Signal ที่เขียนได้
- Signal type สำหรับค่าที่คำนวณ/memo/derived ซึ่งขึ้นอยู่กับ signal อื่น คำนวณแบบ lazy และมีการ cache
- เปิดให้ JS framework จัดการ scheduling ของตัวเองได้
ภาพร่าง API
- แนวคิด API ของ signals เบื้องต้นเป็นดังนี้ นี่เป็นเพียงร่างเริ่มต้น และคาดว่าจะเปลี่ยนแปลงไปตามเวลา
namespace Signal {
// A read-write Signal
class State<T> implements Signal<T> {
// Create a state Signal starting with the value t
constructor(t: T, options?: SignalOptions<T>);
// Get the value of the signal
get(): T;
// Set the state Signal value to t
set(t: T): void;
}
// A Signal which is a formula based on other Signals
class Computed<T> implements Signal<T> {
// Create a Signal which evaluates to the value returned by the callback.
// Callback is called with this signal as the this value.
constructor(cb: (this: Computed<T>) => T, options?: SignalOptions<T>);
// Get the value of the signal
get(): T;
}
// This namespace includes "advanced" features that are better to
// leave for framework authors rather than application developers.
// Analogous to `crypto.subtle`
namespace subtle {
// Run a callback with all tracking disabled (even for nested computed).
function untrack<T>(cb: () => T): T;
// Get the current computed signal which is tracking any signal reads, if any
function currentComputed(): Computed | null;
// Returns ordered list of all signals which this one referenced
// during the last time it was evaluated.
// For a Watcher, lists the set of signals which it is watching.
function introspectSources(s: Computed | Watcher): (State | Computed)[];
// Returns the Watchers that this signal is contained in, plus any
// Computed signals which read this signal last time they were evaluated,
// if that computed signal is (recursively) watched.
function introspectSinks(s: State | Computed): (Computed | Watcher)[];
// True if this signal is "live", in that it is watched by a Watcher,
// or it is read by a Computed signal which is (recursively) live.
function hasSinks(s: State | Computed): boolean;
// True if this element is "reactive", in that it depends
// on some other signal. A Computed where hasSources is false
// will always return the same constant.
function hasSources(s: Computed | Watcher): boolean;
class Watcher {
// When a (recursive) source of Watcher is written to, call this callback,
// if it hasn't already been called since the last `watch` call.
// No signals may be read or written during the notify.
constructor(notify: (this: Watcher) => void);
// Add these signals to the Watcher's set, and set the watcher to run its
// notify callback next time any signal in the set (or one of its dependencies) changes.
// Can be called with no arguments just to reset the "notified" state, so that
// the notify callback will be invoked again.
watch(...s: Signal[]): void;
// Remove these signals from the watched set (e.g., for an effect which is disposed)
unwatch(...s: Signal[]): void;
// Returns the set of sources in the Watcher's set which are still dirty, or is a computed signal
// with a source which is dirty or pending and hasn't yet been re-evaluated
getPending(): Signal[];
}
// Hooks to observe being watched or no longer watched
var watched: Symbol;
var unwatched: Symbol;
}
interface Options<T> {
// Custom comparison function between old and new value. Default: Object.is.
// The signal is passed in as the this value for context.
equals?: (this: Signal<T>, t: T, t2: T) => boolean;
// Callback called when isWatched becomes true, if it was previously false
[Signal.subtle.watched]?: (this: Signal<T>) => void;
// Callback called whenever isWatched becomes false, if it was previously true
[Signal.subtle.unwatched]?: (this: Signal<T>) => void;
}
}
อัลกอริทึมของ signals
- อธิบายอัลกอริทึมที่ใช้ติดตั้งสำหรับ API แต่ละตัวที่เปิดเผยต่อ JavaScript
- สิ่งนี้อาจมองได้ว่าเป็นสเปกเบื้องต้น และมีเป้าหมายเพื่อกำหนดความหมายชุดหนึ่งให้ชัดเจนที่สุดเท่าที่จะทำได้ ท่ามกลางความเป็นไปได้ในการเปลี่ยนแปลงที่ยังเปิดกว้างมาก
ความเห็นของ GN⁺
- ข้อเสนอมาตรฐาน JavaScript Signals มีเป้าหมายเพื่อยกระดับการทำงานร่วมกันระหว่างเฟรมเวิร์ก และช่วยให้นักพัฒนาสามารถนำ reactive programming ไปใช้งานได้ง่ายขึ้น
- ข้อเสนอนี้เป็นความพยายามในการทำให้ความสามารถหลักของไลบรารี signals หลายตัวที่มีอยู่เดิมกลายเป็นมาตรฐาน ซึ่งอาจมอบ programming model ที่สอดคล้องกันให้แก่นักพัฒนา
- แนวคิดของ signals ไม่ได้มีประโยชน์เฉพาะในการพัฒนา UI เท่านั้น แต่ยังนำไปใช้ได้ดีในบริบทที่ไม่ใช่ UI ด้วย โดยเฉพาะในระบบ build ที่ช่วยหลีกเลี่ยงการ rebuild ที่ไม่จำเป็น
- API ที่เสนอจะมอบเครื่องมือที่มีประโยชน์ให้กับนักพัฒนาเฟรมเวิร์ก และคาดว่าจะช่วยให้บรรลุทั้งประสิทธิภาพและการจัดการหน่วยความจำที่ดีขึ้น
- อย่างไรก็ตาม หากต้องการให้เทคโนโลยีนี้ได้รับการยอมรับอย่างแพร่หลาย ยังจำเป็นต้องมีการทำ prototype เพิ่มเติมและรับฟัง feedback จากชุมชน รวมถึงต้องพิสูจน์ประสิทธิผลผ่านการผสานเข้ากับแอปพลิเคชันจริง
- ปัจจุบันเฟรมเวิร์กอย่าง React, Vue และ Svelte ต่างก็มีระบบ reactive ของตัวเองอยู่แล้ว ดังนั้นความเข้ากันได้หรือกลยุทธ์การผสานกับเฟรมเวิร์กเหล่านี้จึงเป็นประเด็นสำคัญที่ต้องพิจารณา
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ตัวอย่าง Vanilla JS เทียบกับ Signals
isEvenหรือ parity ก็อาจจำเป็นต้องเปลี่ยนแนวทางทั้งหมดPromises กับการเปลี่ยนแปลงของ JavaScript
new Promiseบ่อยไหม แต่ในความเป็นจริงแทบไม่ได้ใช้เลย.thenเยอะมาก และมันช่วยทำให้อินเทอร์เฟซกับไลบรารีของบุคคลที่สามหลากหลายตัวง่ายขึ้นSignals ในฐานะส่วนหนึ่งของภาษา
การใช้อีเวนต์ในแอปพลิเคชัน
window.dispatchEventและwindow.addEventListenerความยากของการจัดการสถานะ DOM และการอัปเดต
Promises กับการเขียนโปรแกรมแบบอะซิงก์
S.js กับ Signals
Signals ที่คล้ายกับ MobX
การเพิ่มเฟรมเวิร์กลงใน standard library
ความเข้าใจและปัญหาของข้อเสนอ Signal
effectตรวจจับการเปลี่ยนแปลงของ parity ได้อย่างไร และมันเรียก lambda นี้เมื่อมีการเปลี่ยนแปลงของ signal ใด ๆ หรือไม่