3 điểm bởi GN⁺ 2024-04-01 | 1 bình luận | Chia sẻ qua WhatsApp
  • Đề xuất JavaScript Signals của TC39 là một định hướng ban đầu nhằm chuẩn hóa primitive phản ứng để theo dõi hiệu quả trạng thái UI và trạng thái được tính toán; hiện là bản nháp ở mức Stage 1
  • Đề xuất tập trung vào ngữ nghĩa cốt lõi của đồ thị Signal và cơ chế theo dõi tự động mà các framework có thể dùng chung, hơn là API bề mặt để lập trình viên ứng dụng sử dụng trực tiếp
  • Signal.State, Signal.Computed, Signal.subtle.Watcher là các API cốt lõi; phần tính toán hướng tới lazy evaluation, caching, theo dõi phụ thuộc tự động và thực thi không glitch
  • Mục tiêu của Signals tích hợp sẵn là tăng khả năng tương tác giữa nhiều framework như Angular, Ember, MobX, Preact, Qwik, Solid, Svelte, Vue, đồng thời mở ra khả năng DevTools hỗ trợ debug và phân tích hiệu năng
  • Nhóm đề xuất dự định trước Stage 2 sẽ triển khai nhiều polyfill cấp production, tích hợp framework, kiểm chứng trên ứng dụng quy mô lớn và benchmark hiệu năng; quá trình chuẩn hóa có thể mất ít nhất 2–3 năm hoặc hơn

Vị trí của đề xuất JavaScript Signals

  • JavaScript Signals được giới thiệu là đề xuất Stage 1 trong quy trình TC39
  • Tài liệu hiện tại là một nỗ lực nhằm thống nhất hướng đi chung của hệ sinh thái JavaScript, tương tự Promises/A+ trước khi Promise được chuẩn hóa trong ES2015
  • Có cung cấp polyfill để thử nghiệm trực tiếp
  • Các champion của đề xuất và tác giả gốc đã xây dựng bản nháp hiện tại dựa trên ý kiến thiết kế từ nhiều framework và thư viện

Vấn đề mà việc chuẩn hóa nhắm tới

  • UI phức tạp cần lưu giá trị, tính toán, vô hiệu hóa, đồng bộ và đẩy xuống tầng view; Signals muốn cung cấp hạ tầng quản lý trạng thái cho các công việc lặp lại này
  • Trong ví dụ Vanilla JS, counter, isEven, parity, render bị đan xen trực tiếp, dẫn đến các vấn đề sau
    • Trạng thái và hệ thống render bị ghép nối chặt
    • Phát sinh tính toán và render không cần thiết ngay cả khi parity không đổi, chẳng hạn khi counter thay đổi từ 2 sang 4
    • Khi một phần UI khác muốn subscribe chỉ một số trong counter, isEven, parity, việc quản lý subscribe và hủy subscribe thủ công trở nên phức tạp
    • Nếu thêm pub/sub qua nhiều tầng, boilerplate và việc bookkeeping subscription tăng lên, đồng thời có nguy cơ rò rỉ bộ nhớ
  • Ví dụ dựa trên Signals dùng Signal.StateSignal.Computed để xử lý giá trị, tính toán và side effect theo cùng một cách
    • Không cần subscribe thủ công
    • Signal được tính toán tự động tìm các Signal mà nó phụ thuộc
    • Tính toán chỉ chạy khi giá trị được yêu cầu rõ ràng
    • Signal được tính toán cache giá trị cuối cùng

API cốt lõi của đề xuất

  • Signal<T> được định nghĩa là một giá trị có thể đọc, có get(): T
  • Signal.State<T> là Signal có thể ghi
    • Constructor nhận giá trị ban đầu và các tùy chọn
    • Đọc giá trị bằng get() và thay đổi giá trị bằng set(t)
  • Signal.Computed<T>Signal tính toán dựa trên các Signal khác
    • Được tính bằng giá trị mà callback trả về
    • Tự động theo dõi phụ thuộc
    • Giá trị được tính lazy và được cache
  • Signal.subtle chứa các API nâng cao, gần với nhu cầu của tác giả framework hoặc triển khai DevTools
    • untrack(cb) cho phép đọc Signal mà không theo dõi
    • currentComputed() trả về computed Signal hiện đang được theo dõi
    • introspectSources, introspectSinks, hasSinks, hasSources là các API để quan sát đồ thị
    • Watcher là nền tảng để phát hiện thay đổi Signal và triển khai effect cùng scheduling ở cấp framework
  • SignalOptions<T> hỗ trợ hàm so sánh tùy chỉnh equals và các hook watched / unwatched

Cách hoạt động và mô hình thực thi

  • Signal biểu thị một ô dữ liệu có thể thay đổi theo thời gian, được chia thành state hoặc computed
  • Signal được tính toán tự động ghi lại các Signal đã đọc trong lúc chạy, rồi khi được đọc về sau sẽ kiểm tra xem các phụ thuộc trước đó có thay đổi hay không
  • Tính toán là pull-based
    • Ngay cả khi phụ thuộc thay đổi, nó không tính lại ngay
    • Khi ai đó đọc bằng .get(), nó sẽ tính lại nếu cần
  • Việc ghi vào State Signal được phản ánh đồng bộ
    • Sau .set(), nếu đọc computed Signal phụ thuộc vào giá trị đó, nó sẽ tính lại ngay khi cần
    • Không có batching tích hợp sẵn
  • Callback notify của Watcher có thể được chạy đồng bộ trong lúc .set()
    • Tuy nhiên, trong notify không được đọc hoặc ghi Signal
    • Việc đọc/ghi thực tế phải được schedule sau đó
  • Nếu callback của computed Signal ném exception, exception đó cũng được cache như một giá trị và sẽ được ném lại khi đọc lại Signal

Động lực chuẩn hóa

  • Triển khai Signal của từng framework có cơ chế theo dõi tự động riêng, khiến việc chia sẻ model, component và thư viện giữa các framework trở nên khó khăn
  • Mục tiêu của đề xuất là tách mô hình phản ứng khỏi view render
    • Giúp lập trình viên không phải viết lại mã không liên quan UI khi thay đổi công nghệ render
    • Cho phép tạo các mô hình phản ứng bằng JavaScript để chia sẻ trong nhiều ngữ cảnh
  • Về hiệu năng và bộ nhớ, tài liệu nêu rõ rằng triển khai tích hợp sẵn có thể hiệu quả hơn triển khai JS ở một hệ số hằng số nhỏ, nhưng engine sẽ không thay đổi thuật toán một cách “thần kỳ”
  • Về DevTools, Signals tích hợp sẵn có thể hiển thị tốt hơn các thông tin sau
    • call stack của chuỗi computed Signal
    • đồ thị tham chiếu giữa các Signal
    • quan hệ phụ thuộc cần thiết để debug mức sử dụng bộ nhớ
  • Nếu được đưa vào thư viện chuẩn, cũng có thể kỳ vọng các tác dụng phụ như giảm kích thước bundle, cải thiện độ ổn định và chất lượng, hình thành thuật ngữ chung giữa các dự án

Mục tiêu và ràng buộc thiết kế

  • Chức năng cốt lõi bao gồm Signal có thể ghi, Signal được tính toán, phản ứng với trạng thái dirty, scheduling riêng của framework, untrack và việc kết hợp nhiều codebase
  • Signal được tính toán hướng tới không glitch
    • Để tránh tính toán không cần thiết, nó thực thi bằng cách sắp xếp topo các phần có khả năng dirty trong đồ thị
    • Cố gắng loại bỏ tính toán trùng lặp
  • Không đưa vào cơ chế scheduling bắt buộc tích hợp kiểu Promise, để framework có thể tự scheduling
  • Để ngăn lạm dụng callback phản ứng đồng bộ, việc đọc và ghi Signal bị cấm bên trong notify của Watcher
  • untrack được xem là lối thoát không an toàn
    • Nếu một Signal được đọc mà không theo dõi ảnh hưởng đến kết quả tính toán, computed Signal có thể không được cập nhật khi Signal đó thay đổi
  • API ưu tiên làm nền tảng cho triển khai framework, chứ không được thiết kế đặc biệt ergonomic cho lập trình viên ứng dụng thông thường

effect và Watcher

  • Đề xuất không có hàm tích hợp sẵn như effect()
  • Scheduling effect gắn với vòng đời render của framework, disposal và quản lý ownership, nên API chuẩn JavaScript không trực tiếp giải quyết
  • Thay vào đó, Signal.subtle.Watcher cung cấp nền tảng cấp thấp để triển khai effect
    • Khi phụ thuộc của Signal đang được watch thay đổi, notify sẽ được gọi
    • Có thể kiểm tra các Signal vẫn còn dirty bằng getPending()
    • Effect cần dispose phải được dọn dẹp bằng unwatch
  • Signal mà Watcher đang quan sát có thể tiếp tục sống nếu state nội bộ còn reachable, vì vậy khi dọn dẹp effect cần gọi Watcher.prototype.unwatch

Các tính năng còn thiếu trong bản nháp hiện tại

  • Async hiện chưa được đưa vào mô hình
    • Signals luôn được xem là giá trị có thể đánh giá đồng bộ
    • Có một số cách mô hình hóa trạng thái loading bằng exception; thảo luận cải tiến nằm ở Issue #30
  • Transactions cũng chưa được đưa vào
    • Nếu muốn giữ đồng thời trạng thái “from” và “to” trong chuyển đổi màn hình, sẽ phát sinh vấn đề fork trạng thái đồ thị Signal
    • Thảo luận liên quan nằm ở Issue #73
  • Một số convenience methods cũng chưa có trong bản nháp hiện tại
  • Những tính năng còn thiếu bị loại trừ do chưa có đồng thuận giữa các framework và vì có thể xử lý vòng qua ở tầng cao hơn, nhưng có thể được xem xét lại sau giai đoạn prototype

Kế hoạch phát triển và lịch trình chuẩn hóa

  • Đề xuất này nằm trong chương trình nghị sự TC39 Stage 1 vào tháng 4/2024; tài liệu giải thích rằng hiện tại về mặt văn bản có thể xem như Stage 0
  • Trước khi đề xuất Stage 2, các công việc sau được lên kế hoạch
    • Phát triển nhiều triển khai polyfill cấp production
    • Kiểm thử trên nhiều framework và vượt qua các bài test kiểu test262
    • Kiểm chứng hiệu năng thông qua bộ benchmark signal/framework thorough
    • Tích hợp API đề xuất vào nhiều framework JS tiêu biểu và một số ứng dụng quy mô lớn
    • Tìm hiểu khả năng mở rộng API và quyết định có đưa vào hay không
  • Nhóm đề xuất muốn tiến hành thận trọng để tránh việc chuẩn hóa quá sớm một dạng Signals sai
  • FAQ dự đoán sẽ mất ít nhất 2–3 năm để Signals chuẩn có thể được dùng rộng rãi trên trình duyệt mà không cần polyfill
  • Polyfill hiện tại có thể sử dụng, nhưng tài liệu khuyến nghị không nên phụ thuộc vào tính ổn định vì API có thể thay đổi trong quá trình rà soát

Mô hình sử dụng được tóm tắt trong FAQ

  • Signals tích hợp sẵn độc lập với công nghệ render
    • Tài liệu giải thích rằng các mô hình như Preact dùng VDOM, Solid dùng native DOM, Vue dùng cách hỗn hợp đều có thể khả thi
  • Lập trình viên ứng dụng nhìn chung nên dùng Signals thông qua framework
    • Framework quản lý Watcher, untrack, ownership, disposal và scheduling render DOM
  • Có thể dùng cùng SSR, hydration và resumability
    • Qwik đang dùng Signals cùng các thuộc tính này, và phía đề xuất cho rằng resumable Signals của Qwik có thể được mô hình hóa bằng tổ hợp State và Computed
  • Signals và Proxy bổ trợ cho nhau
    • Proxy chặn các thao tác nông trên object, còn Signals điều phối đồ thị phụ thuộc của các ô dữ liệu
    • Đặt Signals phía sau Proxy có thể giúp tạo nested reactive structure ergonomic hơn
  • Signals là ô biểu thị giá trị hiện tại, không phải stream
    • Nếu ghi liên tiếp hai lần vào State Signal rồi không làm gì thêm, lần ghi đầu tiên có thể không hiển thị với computed Signal hoặc effect
    • Tài liệu xem đây là đặc tính nằm ở mặt đối lập với thực thi không glitch, và cho rằng các cấu trúc khác như async iterable hoặc observable phù hợp hơn với stream

1 bình luận

 
GN⁺ 2024-04-01
Ý kiến trên Hacker News
  • Có phải chỉ mình tôi cảm thấy ví dụ JavaScript thuần lại dễ đọc và dễ xử lý hơn không?
    Tuy nói rằng “phần thiết lập có nhiều nhiễu và boilerplate”, nhưng ví dụ signals cũng trông ồn ào và nhiều boilerplate y như vậy, lại còn thêm một khái niệm mới khó hiểu với người mới bắt đầu.
    Việc “khi counter đổi từ 2 sang 4 thì parity không đổi, nhưng vẫn thực hiện tính toán và render không cần thiết” nghe giống memoization quá sớm.
    Nếu một phần khác của UI muốn render theo cập nhật của counter thì đúng là ví dụ strawman đó không phù hợp, và khi ấy có thể dùng các cách khác như signals, xử lý sự kiện, hay kho trạng thái trung tâm (kiểu Redux).
    Nếu phần khác của UI chỉ phụ thuộc vào isEven hoặc parity, nếu đó là cấu trúc cốt lõi của ứng dụng thì có thể đổi cách tiếp cận, nhưng đa số trường hợp không phải vậy. Việc “render function chỉ phụ thuộc vào parity phải biết rằng nó cần subscribe vào counter” cũng không nhất thiết là một gánh nặng bất công; hàm tính toán thuần có ưu điểm là dễ nắm được đầu vào.

    • Tôi không hiểu vì sao lại xem đây là memoization quá sớm. Đây chỉ là một ví dụ được rút gọn thành một hàm đơn giản, và khó có thể cho rằng người ta bịa ra use case này dù thực tế chưa từng cần đến.
      Việc thử chuẩn hóa signals, một khái niệm ngày càng được dùng nhiều trong phát triển UI, là điều đáng khen. Gác lại các tranh luận chi tiết như mức độ boilerplate ra sao hay có phải tự tạo hệ thống sự kiện hay không, nếu nhiều framework đang dùng signals thì có thể có lý do, và dù mất thời gian cũng đáng thử chuẩn hóa.
    • Đồng ý. Tuy nhiên nếu xem tài liệu signal của Preact thì ngữ cảnh hợp lý hơn nhiều.
      https://preactjs.com/guide/v10/signals
      Trong Preact, khi signal được truyền xuống cây qua props hoặc context, chỉ tham chiếu signal được truyền đi; component nhìn vào signal chứ không phải giá trị, nên việc cập nhật signal có thể không làm component render lại. Trên thực tế, nó có thể đi thẳng đến component trong cây có truy cập .value.
      Ngoài ra signal theo dõi khi nào giá trị được truy cập và khi nào được cập nhật; trong Preact, nếu truy cập .value của signal bên trong component, component sẽ tự động render lại khi giá trị signal đó thay đổi.
    • Vì JavaScript không tích hợp sẵn tính phản ứng, nên khi thêm tính phản ứng thì chắc chắn sẽ có chi phí trừu tượng hóa. Nó là thứ để dùng khi cần, không nhất thiết phải là cách mặc định để xử lý trạng thái.
      Theo kinh nghiệm, lợi ích lớn là có thể mô-đun hóa trạng thái phản ứng. Trong phong cách mệnh lệnh, cần trạng thái bổ sung để theo dõi thay đổi, còn tính mô-đun đạt được bằng trừu tượng hóa. Chỉ dùng khi cần là được.
      Tạo một ví dụ vừa đơn giản vừa áp dụng được là vấn đề cân bằng. Những trường hợp tính phản ứng rõ ràng có lợi thường phức tạp hơn, nên khó trình bày hơn so với ví dụ đơn giản nhưng ít áp dụng được.
    • Cách giải thích điều này còn có thể cải thiện. Trong ví dụ nhỏ, vấn đề không lộ rõ, mà chỉ xuất hiện ở quy mô lớn hơn. Hoan nghênh PR.
    • Khi vượt qua một ngưỡng độ phức tạp nhất định mà phải thay đổi thiết kế thì đó là điều đáng tránh. Cách tiếp cận JS thuần có giới hạn mở rộng xét theo độ phức tạp của đồ thị trạng thái, và vấn đề thật sự không phải là khả năng sử dụng trước hay sau ngưỡng đó, mà là việc khả năng sử dụng thay đổi một cách gián đoạn ngay khi vượt qua ngưỡng.
  • Khi Promises được thêm vào JavaScript, tôi từng có cảm giác phản đối vì sợ sẽ phải viết new Promise khắp nơi.
    Thực tế thì số lần tôi trực tiếp dùng new Promise có thể đếm trên hai bàn tay. Thay vào đó, đặc biệt khi làm việc với thư viện bên thứ ba, tôi dùng .then thường xuyên hơn nhiều.
    Rốt cuộc, tác động thường ngày của việc Promise được thêm vào JavaScript là nó cung cấp một giao diện khá đơn giản, nhìn chung vững chắc và gần như phổ quát cho nhiều hành vi và tính năng đặc thù do thư viện bên thứ ba cung cấp. Dù là đọc file, gọi API hay đầu ra của bước build, chỉ cần dùng .then(res => …) là đã có cảm giác đi được khoảng nửa đường tới một thứ có thể chạy được.
    Nếu đề xuất Signal này có thể đóng vai trò tương tự giữa sự bùng nổ kiểu Cambri của các framework UI phản ứng, thì tôi ủng hộ. Xa hơn nữa, nó cũng có thể giúp mở rộng tính phản ứng ra ngoài UI. Tôi thường hình dung về các cây trạng thái tái tính toán tăng dần cho những thứ không phải trạng thái UI.

    • Tôi cho rằng Promises chủ yếu được đưa vào để thêm async/await, và chính phần đó mới cải thiện chất lượng sống rõ rệt. Trong thực tế, hiếm khi phải trực tiếp viết new Promise.
      .then của Promise thời kỳ đầu là một cải tiến lớn so với delegate lồng nhau và ổn với các chuỗi đơn giản, nhưng nếu phải chain các Promise khác nhau theo điều kiện, hoặc có xử lý lỗi khác nhau cho từng chain cụ thể, hoặc cần trả về sớm, mã có thể trở nên khó đọc và khó xử lý hơn nhiều.
      Với async/await, có thể viết lời gọi như thể không phải Promise, dễ dàng đặt try/catch quanh một lời gọi Promise cụ thể, và việc trả về sớm cũng tự nhiên hơn.
  • Không hiểu vì sao thứ này phải trở thành một phần của ngôn ngữ. Có thể làm bằng thư viện, và đã có những thư viện như vậy rồi. Vì nó nhỏ nên đưa vào mã cũng không phải gánh nặng lớn, và việc thêm vào ngôn ngữ tự thân không nên trở thành mục tiêu
    Nghĩ rằng các thư viện UI JS hiện nay đã thiết kế signals quá tốt nên chúng phải trở thành một phần của ngôn ngữ là kiêu ngạo. signals có nhiều cách triển khai với các đánh đổi khác nhau, và không cái nào trong số đó xứng đáng có vị trí đặc biệt trong đặc tả JavaScript
    Trước khi các thư viện này dùng signals, chúng từng dùng DOM ảo. May mắn là DOM ảo đã không trở thành một phần của JS, vậy signals khác ở điểm nào? Không khác. Lập luận rằng cần chuẩn hóa thậm chí còn yếu hơn so với thời DOM ảo
    Chúng ta định chất hết mọi thứ đang là mốt vào một runtime mà trên thực tế không có cách nào gỡ bỏ các tính năng không còn mong muốn mà không phá vỡ web sao? Khá là thiển cận

    • Có điểm đúng. Không muốn thứ sai, nhưng muốn thứ đúng
      UI phản ứng đã thắng. Ngay cả trong ứng dụng nhỏ, khi quản lý trạng thái thì độ phức tạp bùng nổ, và đó là điểm cốt lõi khiến việc dùng JS thuần trở nên khó khăn. Với tôi, bất kỳ framework phản ứng nào cũng tốt hơn JS thuần, và nếu vậy có thể đang thiếu một thành phần cấu thành nào đó
      Giờ cũng đã khoảng 10 năm rồi, đã đến lúc nghĩ về ranh giới có thể chuẩn hóa. Nếu làm đúng như Promise, nó có thể giảm độ phức tạp cho các trường hợp sử dụng rất phổ biến
      Cách đánh giá tốt hơn là hỏi: “Các framework phản ứng hiện có có dùng đề xuất này không?” Nếu không thì vì sao, thiếu gì, cái gì là không cần thiết, và có thể học gì từ UI và tính phản ứng của các ngôn ngữ khác. Việc chắt lọc những kinh nghiệm rải rác là có giá trị
    • Một lý do tốt để chuẩn hóa Signals là debug trông như ác mộng. Hãy tưởng tượng một cây signal được tính toán rất sâu kích hoạt dây chuyền lẫn nhau, và bạn phải tìm điểm bắt đầu của phản ứng dây chuyền đó. Nếu được chuẩn hóa, công cụ phát triển có thể được xây dựng xung quanh nó
    • Có thể nói điều tương tự với phần lớn thư viện chuẩn. Tuy nhiên, như phần động cơ có nói, đang có xu hướng mở rộng thư viện chuẩn tương đối nhỏ của JS để không phải kéo package về cho mọi tác vụ phổ biến
      Nhu cầu đó có thể tranh luận, nhưng nếu định mở rộng thư viện chuẩn thì nhìn vào những thứ phổ biến là một cách tiếp cận tốt
      signals không phải là thứ thay thế cho DOM ảo
    • Làm tôi nhớ đến đề xuất Observable
  • Khi cần báo hiệu điều gì đó cho toàn bộ ứng dụng, ta dùng event
    window.dispatchEvent(new Event('counterChange'));
    Và bất kỳ phần nào của ứng dụng muốn phản ứng đều có thể subscribe như sau
    window.addEventListener('counterChange', () => { ... do something ... });
    Cách này có vấn đề gì?

    • Về mặt lịch sử, chính ví dụ này là lý do web tiến hóa sang jQuery, rồi từ đó tách thành thế giới Angular và React
      Xử lý event rất dễ trở nên bừa bộn. Muốn nhìn sâu hơn thì hãy xem event bubbling và propagation
      Ứng dụng lớn cần xử lý event vững chắc, và đây là một ưu điểm ngày nay không còn lộ rõ của các framework như Angular, Vue
      Bạn sẽ không muốn dùng nguyên API xử lý event chuẩn mà không có framework. Khi phải xử lý thêm, xóa, sao chép, kích hoạt, gỡ bỏ, kích hoạt một lần... cho nhiều phần tử, có thể phát sinh những tác dụng phụ ngoài ý muốn rất nghiêm trọng
    • Theo bài viết, event emitter/observable gây ra công việc không cần thiết khi được gọi nhiều lần
      Khác biệt với signals nằm ở chỗ giá trị kết quả chỉ được tính khi consumer cuối cùng đọc giá trị. Thời điểm thực sự ghi vào signal được tách khỏi việc lên lịch cập nhật render bất đồng bộ, và chuỗi tính toán do watcher thực hiện chỉ chạy một lần trong lúc render
      Các giá trị trung gian được gửi qua signal sẽ biến mất, nên khó làm được nhiều việc thú vị bên trong đó; thực chất nó gần với một lớp trừu tượng nâng cao để điều phối chu kỳ render
    • Signals rốt cuộc cũng là publish/subscribe, nhưng API dễ dùng hơn. Vì listener được tự động thêm và gỡ bỏ
      Hiệu năng cũng có thể tốt hơn. Ví dụ nếu có một phép tính phụ thuộc vào hai giá trị, trong result = a ? b : 0, nếu a là false thì dù b thay đổi cũng không cần tính lại. Với signals, việc này diễn ra tự động, còn với publish/subscribe truyền thống thì cần khá nhiều mã
    • Tôi đã dùng kiểu pattern này hơn 10 năm. Điểm khó là theo thời gian, một listener nào đó có thể trigger event khác, rồi event khác lại quay về routine đầu tiên và tạo thành vòng lặp listener không kết thúc
      Cũng khó bảo đảm rằng mọi listener sẽ không tạo ra chuỗi trigger như vậy
    • Cách đó có tất cả các nhược điểm của kiến trúc publish/subscribe mà đề xuất đã nhấn mạnh
  • Trong nhiều thập kỷ, tôi vẫn cố hiểu vì sao mọi người lại thấy theo dõi trạng thái và cập nhật DOM khó đến vậy
    Tất nhiên là cần một chút kỷ luật, nhưng tôi thấy nó đơn giản hơn rất nhiều so với các giải pháp cứ vài năm lại xuất hiện. Backbone, Knockout, Angular, React, sửa đổi chính ngôn ngữ, v.v.; có lẽ cách tôi suy nghĩ về cơ bản là khác
    Điều đó cũng thể hiện ngay ở tên hàm. Việc cập nhật innerText được gọi là “render”, nhưng thực ra nó không render. Cùng lắm thì trình duyệt mới là thứ render, và điều đó cũng đúng với mọi việc khác liên quan đến painting. Tôi thật sự bối rối vì cảm giác như đây là một nỗ lực tuyệt vọng để làm phức tạp hóa một trong những hàm DOM đơn giản nhất

    • Với ứng dụng đơn giản thì dễ
      Khi phức tạp hơn thì không còn dễ nữa
    • Từ câu “cần một chút kỷ luật”, tôi có cảm giác mạnh rằng nếu là ngày xưa thì với tư cách lập trình viên ASM, bạn hẳn đã nổi giận với đám lập trình viên C portable điên rồ; rồi sau đó với tư cách lập trình viên C, bạn lại nổi giận với đám lập trình viên Java an toàn bộ nhớ điên rồ
      Có thể xem tiến bộ trong lập trình là quá trình loại bỏ những nghi thức và kỷ luật nghiêm ngặt cần có để đạt kết quả tốt
      Không có nghĩa React là bước tiến hóa tiếp theo, nhưng signals chắc chắn là một bước đúng hướng
    • Vài chục năm là khoảng thời gian dài. Hẳn bạn còn nhớ việc cập nhật DOM từng phức tạp đến mức nào giữa các trình duyệt
      Bản thân việc đồng bộ DOM với trạng thái dữ liệu không quá khó, nhưng làm việc đó thật hiệu năng ở 60fps thì cực kỳ khó. Đặc biệt là khi tạo một API vừa không rò rỉ vừa không quá phiền phức
      Thành thật mà nói, có khi vẽ pixel lên canvas như game còn dễ hơn là chuyển đổi các thay đổi rồi phản ánh chúng lên cây DOM sống
    • Tôi đang xây dựng một ứng dụng giao dịch FX quy mô hàng trăm nghìn dòng. Nó ở mức thay thế một ứng dụng desktop dày, với 20–30 nhà phát triển và nhiều khách hàng cũng có nhà phát triển riêng. Nếu bảo làm mà không có framework thì tôi chỉ có thể chúc may mắn
    • Hoàn toàn đồng ý. Tôi cũng từng phát triển các SPA rất phức tạp và nhiều tương tác, nhưng vẫn chưa gặp vấn đề mà những thứ này nói là sẽ giải quyết
  • Promises là một câu chuyện thành công tốt, nhưng nếu không có async/await thì chưa chắc đã cần chuẩn hóa
    Bản nháp hiện tại được nói là dựa trên thiết kế của các tác giả/người bảo trì Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue, Wiz, v.v.; tôi tò mò các tác giả thư viện hiện có nhìn nhận đề xuất này thế nào. Việc React không có trong danh sách cũng thú vị
    Signals hơi giống channel, nhưng khác ở chỗ là broadcast chứ không phải một bên nhận duy nhất. Sẽ rất hay nếu tận dụng điều này để web worker có thể giao tiếp bằng channel thay vì callback onMessage. Đặc biệt nếu có thể select trên signals/channels/promises như Go, thì về mặt cú pháp sẽ có lợi hơn so với việc quản lý nhiều cơ chế messaging đồng thời bằng callback. Ví dụ như cho phép đưa signals vào Promise.any

    • Tôi phản đối mạnh mẽ ý “nếu không có async/await thì không cần chuẩn hóa”
      x instanceof Promise đơn giản là không hoạt động. Nếu phương thức then trong thư viện của tôi nhận callback catch còn thư viện khác thì không, chúng sẽ âm thầm không tương tác được với nhau và cũng không có cách nào phát hiện. finally chạy khi nào? Có thể kỳ vọng callback được chạy bất đồng bộ đến mức nào?
      Nếu không có chuẩn, mọi thư viện dùng Promise đều phải mang theo polyfill riêng, vì không thể tin thứ đã có sẵn. Và cũng không thể thực sự tiêu thụ Promise của thư viện khác, vì không thể tin nó hành xử theo cách mình kỳ vọng
      Đây không phải suy đoán, mà thực tế đã diễn ra nhiều năm và là địa ngục mà rất nhiều người phải chịu đựng
    • Cũng có lợi ích của chuẩn hóa không liên quan đến async/await. Các engine JavaScript đã có thể tối ưu hiệu năng có lợi cho những ứng dụng dùng Promise nhiều, điều sẽ không thể nếu nó không phải là chuẩn
    • Lý do React không có trong danh sách là vì signals, khác với Preact, không phải là một phần của API lõi React
      Trực giác mơ hồ của tôi là signals quá giống một useEffect() được tổng quát hóa, nên nếu đưa vào React, nó sẽ khiến việc hiểu chuyện gì xảy ra trong chu kỳ render càng rối hơn. Dù tốt hay xấu, React đã chọn một cách cập nhật khác với signals. Dù vậy, tôi cũng có thể sai về khả năng áp dụng của nó
    • Lý do React không có trong danh sách này là vì hiệu ứng của nó mang tính khai báo, không phải mệnh lệnh. Thay đổi props và re-render cũng có thể được xem là khai báo ở một tầng trừu tượng cao hơn. useEffect cô lập gọn gàng các hành vi mệnh lệnh
      Cái này trông rất giống data binding của Ember, và cuối cùng có thể trở thành một cơn ác mộng kiểu mệnh lệnh. Trạng thái mặc định gần như là “khẩu súng tự bắn vào chân”, và để ngăn nó không thành như vậy thì cần gánh nặng nhận thức cùng các meta-pattern khổng lồ
    • Có lẽ cứ gọi cái này là “EventEmitter” là được
      https://nodejs.org/en/learn/asynchronous-work/the-nodejs-eve...
  • Không hiểu ví dụ trong README được liên kết
    // A library or framework defines effects based on other Signal primitives
    declare function effect(cb: () => void): (() => void);
    Thư viện nào? Framework nào? Đến đây thì tôi bị lạc. effect là gì?
    effect(() => element.innerText = parity.get());
    Làm sao effect biết rằng nó phải gọi lambda này mỗi khi parity thay đổi? Nó gọi lambda này mỗi khi signal thay đổi à? Nếu vậy thì tại sao lại nói về caching? Có lẽ không phải vậy
    Dù sao, nếu tôi hiểu đúng điều các tác giả muốn truyền đạt thì bản thân ý tưởng signal có vẻ hợp lý. Nhưng vấn đề lớn của kiểu kiến trúc tách rời này là khi ứng dụng đủ phức tạp, bạn sẽ bị lạc trong lúc lần theo vì sao một sự kiện cụ thể xảy ra. Lý tưởng nhất là signals nên chỉnh sửa stack trace, để khi callback được gọi, nó đã bao gồm sẵn stack trace của đoạn code ban đầu đã trigger signal

    • Có nhiều thư viện export một hàm tên là effect, cho phép chạy code tùy ý để phản ứng với các lần cập nhật signal. Phần nhập môn về signals và effects trong tài liệu Preact khá tốt: https://preactjs.com/guide/v10/signals#effectfn
      Theo tôi hiểu, các hàm effect kiểu này trước hết sẽ chạy callback một lần để xem trong lúc chạy nó đã truy cập những signal nào, rồi gọi lại callback mỗi khi signal mà callback đó phụ thuộc được cập nhật. Nếu việc truy cập signal là đồng bộ và đơn luồng, thì chỉ riêng việc trong lúc chạy callback có truy cập signal đã đủ để biết callback đó nên subscribe signal ấy
      Cũng có thể làm bằng getter. Hàm effect sẽ theo dõi trong getter method đã truy cập thuộc tính nào của signal, và theo tôi biết Vue 2 trước đây từng dùng cách này. Cũng có thể theo dõi truy cập object bằng proxy. Ví dụ trong đề xuất có method get được gọi để truy cập giá trị signal, và thông qua việc method này chạy, có thể theo dõi dependency
      [1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
      [2] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
    • Lời gọi parity.get() đăng ký một dependency cho hàm được truyền vào effect(). Khi parity được cập nhật, hàm đó sẽ được gọi
      Không phải cứ signal thay đổi là gọi, mà chỉ gọi khi signal mà nó phụ thuộc thay đổi
      Trong trường hợp này parity phụ thuộc vào isEven, và isEven phụ thuộc vào counter. Vì vậy khi counter được cập nhật, toàn bộ chuỗi dependency bị vô hiệu hóa, parity bị vô hiệu hóa, và callback được chạy lại
    • Dù tên gọi là gì, các triển khai signals nhìn chung tạo ra một đồ thị dependency động, và khi đọc một node thì cạnh được tạo ra. Trong một tracking context như effect giả định này, thao tác đọc tạo một cạnh giữa node trạng thái của signal và node tính toán của effect, trên thực tế khiến node sau subscribe các lần ghi về sau của node trước để quyết định khi nào chạy lại phép tính
    • effect là một hàm tùy ý mà bạn muốn gọi
      Trong signals, cơ chế theo dõi dependency biết giá trị nào cần được tính lại, và kết quả là hệ thống cũng biết hàm nào cần được gọi lại
    • Có vẻ watcher sẽ cần thiết cho việc triển khai effect
  • Liên quan thì có S.js: https://github.com/adamhaile/s
    Tôi thích signals. Khi xây UI, tôi ưa chúng hơn bất kỳ primitive nào khác, có lẽ ngoại lệ duy nhất là thuật toán ràng buộc cassowary. Tôi cố bắt chước signals trong mọi ngôn ngữ tôi dùng cho vui
    Nhưng tôi hoàn toàn không nghĩ đây là thứ nên đưa vào bản thân ngôn ngữ JavaScript. Mong là họ để yên ngôn ngữ này một thời gian. Mọi người đã khó theo kịp rồi, và TC-39 thì đã khiến người ta sợ mà tránh xa ngôn ngữ này

  • Cái này trông rất giống MobX, hệ thống effect JS tôi thích nhất
    Phiên bản MobX sẽ như sau
    import { observable, computed, autorun } from 'mobx';
    const counter = observable.box(0);
    const isEven = computed(() => (counter.get() & 1) === 0);
    const parity = computed(() => isEven.get() ? "even" : "odd");
    autorun(() => { element.innerText = parity.get(); });
    setInterval(() => counter.set(counter.get() + 1), 1000);

    • MobX đúng là signals. Chỉ có điều đó là signals theo dõi dependency một cách ngầm định thông qua đối tượng proxy, thay vì theo dõi tường minh bằng getter
  • Cảm giác như “hãy nhúng framework tôi đang dùng dạo này vào standard library!”
    Giống như xăm tên bạn gái lên người vậy

    • Không phải như thế
      Đây là đưa vào standard library một thành phần cấu thành mà phần lớn framework đã hội tụ để sử dụng
      Promises cũng được dùng rộng rãi rồi mới vào standard library, và cái này cũng tương tự