3 คะแนน โดย GN⁺ 2024-04-01 | 1 ความคิดเห็น | แชร์ทาง WhatsApp

ร่างข้อเสนอมาตรฐาน 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 ความคิดเห็น

 
GN⁺ 2024-04-01
ความคิดเห็นจาก Hacker News
  • ตัวอย่าง Vanilla JS เทียบกับ Signals

    • มีแค่ฉันคนเดียวหรือเปล่าที่รู้สึกว่าตัวอย่าง Vanilla JS อ่านง่ายกว่าและทำงานด้วยสะดวกกว่า?
      • ดูเหมือนว่าการตั้งค่าจะซับซ้อนและมีโค้ด boilerplate เยอะ
      • เมื่อค่าเคาน์เตอร์เปลี่ยน อาจเกิดการคำนวณและการเรนเดอร์ที่ไม่จำเป็น
      • หากต้องการให้ส่วนอื่นของ UI เรนเดอร์เฉพาะตอนที่เคาน์เตอร์อัปเดต ก็อาจต้องเปลี่ยนวิธีจัดการสถานะ
      • หากส่วนอื่นของ UI พึ่งพาแค่ isEven หรือ parity ก็อาจจำเป็นต้องเปลี่ยนแนวทางทั้งหมด
  • Promises กับการเปลี่ยนแปลงของ JavaScript

    • ตอนแรกกังวลว่าจะต้องเขียน new Promise บ่อยไหม แต่ในความเป็นจริงแทบไม่ได้ใช้เลย
    • กลับกลายเป็นว่าได้ใช้ .then เยอะมาก และมันช่วยทำให้อินเทอร์เฟซกับไลบรารีของบุคคลที่สามหลากหลายตัวง่ายขึ้น
    • ถ้าข้อเสนอ Signal จะให้ผลลัพธ์คล้ายกันกับเฟรมเวิร์ก UI แบบ reactive ก็เห็นด้วย
  • Signals ในฐานะส่วนหนึ่งของภาษา

    • Signals ไม่จำเป็นต้องเป็นส่วนหนึ่งของภาษา เพราะทำเป็นไลบรารีก็เพียงพอ
    • การคิดว่า Signals ที่ไลบรารี UI ของ JS ปัจจุบันออกแบบไว้ดีพอจะกลายเป็นส่วนหนึ่งของภาษานั้นดูหยิ่งเกินไป
    • การเพิ่มทุกกระแสนิยมเข้าไปใน language runtime ดูเป็นมุมมองระยะสั้น
  • การใช้อีเวนต์ในแอปพลิเคชัน

    • ใช้อีเวนต์ทั่วทั้งแอปพลิเคชันเพื่อส่งสัญญาณ
    • สร้างและ subscribe อีเวนต์ผ่าน window.dispatchEvent และ window.addEventListener
  • ความยากของการจัดการสถานะ DOM และการอัปเดต

    • พยายามทำความเข้าใจว่าทำไมตลอดหลายสิบปีผู้คนถึงมองว่าการจัดการสถานะและการอัปเดต DOM เป็นเรื่องยาก
    • รู้สึกแปลกใจเพราะมันเหมือนทำให้ฟังก์ชัน DOM ที่เรียบง่ายกลายเป็นเรื่องซับซ้อน
  • Promises กับการเขียนโปรแกรมแบบอะซิงก์

    • Promises เป็นกรณีตัวอย่างที่ประสบความสำเร็จ แต่ถ้าไม่มี async/await ก็อาจไม่จำเป็นต้องทำให้เป็นมาตรฐาน
    • อยากรู้ว่านักเขียนไลบรารีหลาย ๆ คนคิดอย่างไรกับข้อเสนอนี้
  • S.js กับ Signals

    • ชอบ Signals และเวลาเขียน UI ก็ชอบมากกว่า primitive แบบอื่น
    • แต่ก็ไม่ได้คิดว่ามันควรถูกรวมเข้าไปในภาษา JavaScript
  • Signals ที่คล้ายกับ MobX

    • MobX คือระบบ effect ของ JS ที่ชอบที่สุด
    • มีการยกตัวอย่างโค้ดเวอร์ชันของ MobX
  • การเพิ่มเฟรมเวิร์กลงใน standard library

    • คล้ายกับการเสนอให้นำเฟรมเวิร์กที่ตัวเองชอบอยู่ตอนนี้ไปใส่ใน standard library
  • ความเข้าใจและปัญหาของข้อเสนอ Signal

    • มีปัญหาในการทำความเข้าใจตัวอย่างของข้อเสนอ Signal
    • มีคำถามว่า effect ตรวจจับการเปลี่ยนแปลงของ parity ได้อย่างไร และมันเรียก lambda นี้เมื่อมีการเปลี่ยนแปลงของ signal ใด ๆ หรือไม่
    • แนวคิดของ Signal ฟังดูสมเหตุสมผล แต่ในแอปพลิเคชันที่ซับซ้อน การติดตามอีเวนต์อาจทำได้ยากขึ้น