Этап 1 (объяснение)
Чемпионы предложений TC39: Дэниел Эренберг, Иегуда Кац, Джатин Раманатан, Шей Льюис, Кристен Хьюэлл Гарретт, Доминик Ганнауэй, Престон Сего, Майло М, Роб Айзенберг
Оригинальные авторы: Роб Айзенберг и Дэниел Эренберг.
В этом документе описывается раннее общее направление для сигналов в JavaScript, аналогичное усилиям Promises/A+, которые предшествовали Promises, стандартизированным TC39 в ES2015. Попробуйте сами, используя полифилл.
Как и в случае с Promises/A+, эти усилия направлены на согласование экосистемы JavaScript. Если такое согласование окажется успешным, то на основе этого опыта может появиться стандарт. Несколько авторов фреймворка сотрудничают здесь над общей моделью, которая могла бы поддержать их ядро реактивности. Текущий проект основан на предложениях авторов/сопровождающих Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue, Wiz и других…
В отличие от Promises/A+, мы пытаемся найти не общий API-интерфейс, ориентированный на разработчиков, а скорее точную базовую семантику базового графа сигналов. Это предложение включает в себя полностью конкретный API, но этот API не предназначен для большинства разработчиков приложений. Вместо этого API сигналов здесь лучше подходит для построения поверх него платформ, обеспечивая совместимость посредством общего графа сигналов и механизма автоматического отслеживания.
План этого предложения состоит в том, чтобы провести значительное раннее прототипирование, включая интеграцию в несколько фреймворков, прежде чем переходить к этапу 1. Мы заинтересованы в стандартизации сигналов только в том случае, если они подходят для использования на практике в нескольких фреймворках и обеспечивают реальные преимущества по сравнению с фреймворками. подаваемые сигналы. Мы надеемся, что значительное раннее прототипирование даст нам эту информацию. Более подробную информацию см. в разделе «Состояние и план развития» ниже.
Чтобы разработать сложный пользовательский интерфейс (UI), разработчикам приложений JavaScript необходимо эффективно хранить, вычислять, аннулировать, синхронизировать и передавать состояние на уровень представления приложения. Пользовательские интерфейсы обычно включают в себя нечто большее, чем просто управление простыми значениями, но часто включают в себя рендеринг вычисленного состояния, которое зависит от сложного дерева других значений или состояния, которое также вычисляется само по себе. Цель Signals — предоставить инфраструктуру для управления таким состоянием приложения, чтобы разработчики могли сосредоточиться на бизнес-логике, а не на этих повторяющихся деталях.
Независимо было обнаружено, что конструкции, подобные сигналу, полезны и в контекстах, не связанных с пользовательским интерфейсом, особенно в системах сборки, позволяющих избежать ненужных перестроений.
Сигналы используются в реактивном программировании, чтобы исключить необходимость управления обновлениями в приложениях.
Модель декларативного программирования для обновления на основе изменений состояния.
из Что такое реактивность? .
Учитывая переменную counter
, вы хотите отобразить в DOM, является ли счетчик четным или нечетным. Всякий раз, когда counter
изменяется, вы хотите обновить DOM с использованием последней четности. В Vanilla JS у вас может быть что-то вроде этого:
let counter = 0;const setCounter = (значение) => { счетчик = значение; render();};const isEven = () => (счетчик & 1) == 0;const parity = () => isEven() ? "even" : "odd";const render = () => element.innerText = parity();// Имитируем внешние обновления счетчика...setInterval(() => setCounter(counter + 1), 1000);
Это имеет ряд проблем...
Настройка counter
шумная и громоздкая.
Состояние counter
тесно связано с системой рендеринга.
Если counter
изменяется, а parity
нет (например, счетчик меняется с 2 на 4), то мы выполняем ненужные вычисления четности и ненужный рендеринг.
Что, если другая часть нашего пользовательского интерфейса просто хочет отобразиться при обновлении counter
?
Что, если другая часть нашего пользовательского интерфейса зависит только от isEven
или parity
?
Даже в этом относительно простом сценарии быстро возникает ряд проблем. Мы могли бы попытаться обойти эту проблему, введя pub/sub для counter
. Это позволит дополнительным потребителям counter
подписаться на добавление собственных реакций на изменения состояния.
Однако нас по-прежнему беспокоят следующие проблемы:
Функция рендеринга, которая зависит только от parity
, вместо этого должна «знать», что ей действительно необходимо подписаться на counter
.
Невозможно обновить пользовательский интерфейс на основе только isEven
или parity
без прямого взаимодействия с counter
.
Мы увеличили наш шаблон. Каждый раз, когда вы что-то используете, это не просто вызов функции или чтение переменной, а подписка и выполнение обновлений. Управление отпиской также особенно сложно.
Теперь мы могли бы решить пару проблем, добавив pub/sub не только для counter
, но также для isEven
и parity
. Тогда нам пришлось бы подписаться на isEven
на counter
, parity
на isEven
и render
на parity
. К сожалению, наш шаблонный код не только взорвался, но мы застряли в тонне учета подписок и потенциальной утечке памяти, если мы не очистим все должным образом и должным образом. Итак, мы решили некоторые проблемы, но создали совершенно новую категорию проблем и много кода. Что еще хуже, нам придется пройти весь этот процесс для каждой части состояния нашей системы.
Абстракции привязки данных в пользовательских интерфейсах для модели и представления уже давно являются основой инфраструктур пользовательского интерфейса на нескольких языках программирования, несмотря на отсутствие какого-либо такого механизма, встроенного в JS или веб-платформу. В средах и библиотеках JS было проведено большое количество экспериментов с различными способами представления этой привязки, и опыт показал мощь одностороннего потока данных в сочетании с первоклассным типом данных, представляющим ячейку состояния или вычисления. полученные из других данных, которые теперь часто называют «Сигналами». Этот первоклассный подход с использованием реактивных значений, похоже, впервые стал популярным в веб-фреймворках JavaScript с открытым исходным кодом вместе с Knockout в 2010 году. С тех пор было создано множество вариаций и реализаций. За последние 3-4 года примитив Signal и связанные с ним подходы получили дальнейшее распространение: почти каждая современная библиотека или фреймворк JavaScript имеет что-то подобное под тем или иным именем.
Чтобы понять сигналы, давайте взглянем на приведенный выше пример, переосмысленный с помощью Signal API, который будет подробно описан ниже.
const counter = new Signal.State(0);const isEven = new Signal.Computed(() => (counter.get() & 1) == 0);const parity = new Signal.Computed(() => isEven .get() ? "even" : "odd"); // Библиотека или фреймворк определяет эффекты на основе других примитивов Signal. Объявляем функцию effect(cb: () => void): (() => void);effect(() => element.innerText = parity.get());// Имитируем внешние обновления счетчика...setInterval(() => counter.set(counter.get() + 1), 1000 );
Есть несколько вещей, которые мы видим сразу:
Мы устранили шумный шаблон вокруг переменной counter
из нашего предыдущего примера.
Существует унифицированный API для обработки значений, вычислений и побочных эффектов.
Между counter
и render
нет проблем с циклическими ссылками или перевернутыми зависимостями.
Нет никаких ручных подписок и нет необходимости вести бухгалтерию.
Существуют средства контроля времени/планирования побочных эффектов.
Однако сигналы дают нам гораздо больше, чем то, что можно увидеть на поверхности API:
Автоматическое отслеживание зависимостей . Вычисленный сигнал автоматически обнаруживает любые другие сигналы, от которых он зависит, независимо от того, являются ли эти сигналы простыми значениями или другими вычислениями.
Ленивая оценка . Вычисления не оцениваются немедленно при их объявлении и не оцениваются немедленно при изменении их зависимостей. Они оцениваются только тогда, когда их значение явно запрошено.
Мемоизация — вычисляемые сигналы кэшируют свое последнее значение, поэтому вычисления, в зависимостях которых нет изменений, не требуют повторной оценки, независимо от того, сколько раз к ним обращаются.
Каждая реализация сигнала имеет собственный механизм автоматического отслеживания, позволяющий отслеживать источники, встречающиеся при оценке вычисленного сигнала. Это затрудняет совместное использование моделей, компонентов и библиотек между различными платформами — они, как правило, имеют ложную связь с механизмом представления (учитывая, что сигналы обычно реализуются как часть JS-фреймворков).
Цель этого предложения — полностью отделить реактивную модель от представления рендеринга, что позволит разработчикам переходить на новые технологии рендеринга без переписывания своего кода, отличного от пользовательского интерфейса, или разрабатывать общие реактивные модели в JS для развертывания в различных контекстах. К сожалению, из-за управления версиями и дублирования оказалось непрактичным достичь высокого уровня совместного использования с помощью библиотек уровня JS — встроенные модули предлагают более надежную гарантию совместного использования.
Поставка меньшего количества кода всегда дает небольшой потенциальный прирост производительности из-за встроенных часто используемых библиотек, но реализации Signals, как правило, довольно малы, поэтому мы не ожидаем, что этот эффект будет очень большим.
Мы подозреваем, что собственные реализации структур данных и алгоритмов, связанных с Signal, на C++ могут быть немного более эффективными, чем то, что достижимо в JS, с постоянным коэффициентом. Однако никаких алгоритмических изменений по сравнению с тем, что будет присутствовать в полифиле, не ожидается; от двигателей здесь не ожидается волшебства, а сами алгоритмы реактивности будут четко определены и однозначны.
Группа чемпионов планирует разработать различные реализации сигналов и использовать их для исследования возможностей производительности.
При использовании существующих библиотек Signal на языке JS может быть сложно отследить такие вещи, как:
Стек вызовов в цепочке вычисленных сигналов, показывающий причинно-следственную цепочку ошибки.
Справочный граф среди сигналов, когда один зависит от другого — важно при отладке использования памяти.
Встроенные сигналы позволяют средам выполнения JS и DevTools потенциально иметь улучшенную поддержку проверки сигналов, особенно для отладки или анализа производительности, независимо от того, встроены ли они в браузеры или через общее расширение. Существующие инструменты, такие как инспектор элементов, снимок производительности и профилировщики памяти, можно обновить, чтобы специально выделить сигналы в их представлении информации.
В целом, у JavaScript довольно минимальная стандартная библиотека, но тенденция в TC39 заключалась в том, чтобы сделать JS скорее языком «с батарейками», с высококачественным встроенным набором доступных функций. Например, Temporal заменяет moment.js, а ряд небольших функций, например Array.prototype.flat
и Object.groupBy
заменяют многие варианты использования lodash. Преимущества включают меньшие размеры пакетов, улучшенную стабильность и качество, меньше необходимости учиться при присоединении к новому проекту и общий словарный запас среди разработчиков JS.
Текущая работа W3C и разработчиков браузеров направлена на внедрение собственных шаблонов в HTML (части DOM и создание экземпляров шаблонов). Кроме того, группа W3C по веб-компонентам изучает возможность расширения веб-компонентов, чтобы предложить полностью декларативный HTML API. Для достижения обеих этих целей HTML в конечном итоге понадобится реактивный примитив. Кроме того, можно представить множество эргономических улучшений DOM за счет интеграции Signals, о которых просило сообщество.
Обратите внимание, что эта интеграция будет отдельной попыткой, а не частью самого предложения.
Усилия по стандартизации иногда могут быть полезны только на уровне «сообщества», даже без изменений в браузерах. Проект Signals объединяет множество авторов различных фреймворков для глубокого обсуждения природы реактивности, алгоритмов и совместимости. Это уже было полезно и не оправдывает включение в JS-движки и браузеры; Сигналы следует добавлять в стандарт JavaScript только в том случае, если есть значительные преимущества, выходящие за рамки обмена информацией об экосистеме.
Оказывается, существующие библиотеки Signal по своей сути не так уж и отличаются друг от друга. Это предложение направлено на развитие их успеха путем реализации важных качеств многих из этих библиотек.
Тип сигнала, который представляет состояние, т. е. сигнал, доступный для записи. Это значение, которое могут прочитать другие.
Вычисляемый/заметный/производный тип сигнала, который зависит от других, лениво вычисляется и кэшируется.
Вычисления являются ленивыми, то есть вычисленные сигналы не вычисляются снова по умолчанию при изменении одной из их зависимостей, а запускаются только в том случае, если кто-то действительно их читает.
Вычисления выполняются без сбоев, что означает, что ненужные вычисления никогда не выполняются. Это означает, что когда приложение считывает вычисленный сигнал, происходит топологическая сортировка потенциально «грязных» частей графа для запуска, чтобы исключить любые дубликаты.
Вычисления кэшируются, что означает, что если после последнего изменения зависимости никакие зависимости не изменились, то вычисленный Сигнал не пересчитывается при доступе.
Пользовательские сравнения возможны как для вычисленных сигналов, так и для сигналов состояния, чтобы отметить, когда дальнейшие вычисленные сигналы, которые зависят от них, должны быть обновлены.
Реакции на условие, когда вычисленный Сигнал имеет одну из своих зависимостей (или вложенных зависимостей), становятся «грязными» и изменяются, а это означает, что значение Сигнала может устареть.
Эта реакция предназначена для планирования более важной работы, которая будет выполнена позже.
Эффекты реализуются с точки зрения этих реакций, а также планирования на уровне платформы.
Вычисляемым сигналам необходима способность реагировать на то, зарегистрированы ли они как (вложенная) зависимость одной из этих реакций.
Разрешите JS-фреймворкам выполнять собственное планирование. Нет встроенного принудительного планирования в стиле Promise.
Синхронные реакции необходимы для того, чтобы можно было планировать дальнейшую работу на основе логики инфраструктуры.
Записи синхронны и немедленно вступают в силу (фреймворк, который выполняет пакетную запись, может делать это поверх).
Можно отделить проверку того, может ли эффект быть «грязным», от фактического запуска эффекта (включив двухэтапный планировщик эффектов).
Возможность читать сигналы, не вызывая зависимостей для записи ( untrack
).
Включите композицию различных кодовых баз, использующих сигналы/реактивность, например:
Совместное использование нескольких платформ для отслеживания/реактивности (упущения по модулю, см. ниже)
Независимые от платформы реактивные структуры данных (например, рекурсивно-реактивный прокси-сервер хранилища, реактивная карта, набор и массив и т. д.)
Препятствуйте/запрещайте наивное неправильное использование синхронных реакций.
Риск работоспособности: при неправильном использовании могут возникнуть «сбои»: если рендеринг выполняется сразу после установки сигнала, это может привести к тому, что конечному пользователю будет показано неполное состояние приложения. Поэтому эту функцию следует использовать только для интеллектуального планирования работы на потом, после завершения логики приложения.
Решение. Запретите чтение и запись любого сигнала из обратного вызова синхронной реакции.
Не поощряйте untrack
и отмечать его необоснованный характер.
Риск надежности: позволяет создавать вычисляемые Сигналы, значение которых зависит от других Сигналов, но которые не обновляются при изменении этих Сигналов. Его следует использовать, когда неотслеживаемые доступы не изменят результат вычисления.
Решение: API помечен в названии как «небезопасный».
Примечание. Это предложение позволяет как читать, так и записывать сигналы из вычисленных и воздействующих сигналов, не ограничивая записи, которые происходят после чтения, несмотря на риск надежности. Такое решение было принято для сохранения гибкости и совместимости при интеграции с фреймворками.
Должна быть прочной основой для нескольких фреймворков для реализации их механизмов сигналов/реактивности.
Должно быть хорошей основой для рекурсивных прокси-серверов хранилища, реактивности полей классов на основе декораторов, а также API-интерфейсов в стиле .value
и [state, setState]
.
Семантика способна выражать допустимые шаблоны, поддерживаемые различными платформами. Например, должна быть возможность, чтобы эти Сигналы были основой либо для немедленно отраженных записей, либо для записей, которые группируются и применяются позже.
Было бы неплохо, если бы этот API могли использовать непосредственно разработчики JavaScript.
Идея: предоставить все хуки, но по возможности включать ошибки при неправильном использовании.
Идея: поместить тонкие API в subtle
пространство имен, аналогичное crypto.subtle
, чтобы обозначить границу между API, которые необходимы для более продвинутого использования, такого как реализация инфраструктуры или создание инструментов разработки, и более повседневного использования разработки приложений, такого как создание экземпляров сигналов для использования с рамки.
Однако важно не дублировать в буквальном смысле одни и те же имена!
Если функция соответствует концепции экосистемы, полезно использовать общий словарь.
Напряжение между «удобством использования JS-разработчиками» и «предоставлением всех возможностей для фреймворков»
Быть реализуемым и удобным в использовании с хорошей производительностью — поверхностный API не вызывает слишком больших накладных расходов.
Включите создание подклассов, чтобы платформы могли добавлять свои собственные методы и поля, включая частные поля. Это важно, чтобы избежать необходимости дополнительных ассигнований на уровне структуры. См. «Управление памятью» ниже.
Если возможно: вычисленный сигнал должен быть доступен для сбора мусора, если ничто живое не ссылается на него для возможных будущих операций чтения, даже если он связан с более широким графом, который остается активным (например, путем чтения состояния, которое остается активным).
Обратите внимание, что сегодня большинство фреймворков требуют явного удаления вычисленных сигналов, если они имеют какую-либо ссылку на другой граф сигналов или из него, который остается живым.
В конечном итоге это не так уж и плохо, если их время жизни привязано к времени жизни компонента пользовательского интерфейса, а от эффектов в любом случае нужно избавиться.
Если выполнение с такой семантикой обходится слишком дорого, то нам следует добавить явное удаление (или «отсоединение») вычисленных сигналов к приведенному ниже API, в котором в настоящее время его нет.
Отдельная связанная цель: минимизировать количество выделений, например,
чтобы сделать записываемый сигнал (избегайте двух отдельных замыканий + массива)
для реализации эффектов (избегайте замыкания для каждой отдельной реакции)
В API для наблюдения за изменениями Сигнала избегайте создания дополнительных временных структур данных.
Решение: API на основе классов, позволяющий повторно использовать методы и поля, определенные в подклассах.
Первоначальная идея Signal API приведена ниже. Обратите внимание, что это всего лишь ранний проект, и мы ожидаем изменений со временем. Давайте начнем с полного файла .d.ts
чтобы получить представление об общей форме, а затем обсудим детали того, что все это означает.
интерфейс Signal<T> {// Получить значение signalget(): T;}namespace Signal {// Класс Signal для чтения и записи State<T> реализует Signal<T> {// Создать состояние Signal, начиная со значения tconstructor(t: T, options?: SignalOptions<T>); // Получаем значение signalget(): T; // Устанавливаем значение Signal состояния на tset(t: T): void;}// A Signal который формула, основанная на другом классе Signals Computed<T =known>, реализует Signal<T> {// Создайте сигнал, который оценивается как значение, возвращаемое обратным вызовом.// Обратный вызов вызывается с этим сигналом как this value.constructor(cb: (this: Computed<T>) => T, options?: SignalOptions<T>);// Получаем значение signalget(): T;}// Это пространство имен включает в себя «расширенные» функции, которые лучше чтобы// оставить авторам фреймворка, а не разработчикам приложений. // Аналогично `crypto.subtle`namespace unique {// Запустить обратный вызов с отключенным отслеживанием. function untrack<T>(cb: () => T): T;/ / Получить текущий вычисленный сигнал, который отслеживает любые чтения сигналов, если таковые имеются. function currentComputed(): Computed | null;// Возвращает упорядоченный список всех сигналов, на которые ссылался этот // во время последней оценки. // Для наблюдателя перечисляет набор сигналов, которые он наблюдает. function introspectSources(s: Computed | Watcher): (State | Computed)[];// Возвращает наблюдатели, в которых содержится этот сигнал, плюс любые // вычисленные сигналы, которые считывали этот сигнал в последний раз, когда они оценивались, // если этот вычисленный сигнал (рекурсивно) watch.function introspectSinks(s: State | Computed): (Computed | Watcher)[];// True, если этот сигнал «живой», то есть за ним наблюдает наблюдатель, // или его читает Вычисляемый сигнал, который (рекурсивно) является live.function hasSinks(s: State | Computed): boolean;// True, если этот элемент является "реактивным", в том смысле, что он зависит// от какого-либо другого сигнала. Computed, где hasSources имеет значение false // всегда будет возвращать одну и ту же константу. function hasSources(s: Computed | Watcher): boolean;class Watcher {// Когда записывается (рекурсивный) источник Watcher, вызовите этот обратный вызов, // если он еще не был вызван с момента последнего вызова `watch`. // Никакие сигналы не могут быть прочитаны или записаны во время notify.constructor(notify: (this: Watcher) => void); // Добавьте эти сигналы в набор Watcher и настройте наблюдатель на запуск своего // обратного вызова в следующий раз, когда изменится какой-либо сигнал в наборе (или одной из его зависимостей). // Может быть вызван без аргументов только для того, чтобы сбросим состояние «уведомлено», чтобы // обратный вызов уведомления был вызван снова.watch(...s: Signal[]): void;// Удалить эти сигналы из наблюдаемого набора (например, для эффекта, который удален)отменить просмотр(...s: Signal[]): void;// Возвращает набор источников в наборе Watcher, которые все еще загрязнены, или представляет собой вычисленный сигнал // с источником, который загрязнен или находится в ожидании и еще не был повторно оценен getPending(): Signal[];}// Хуки для наблюдения за тем, что просматривается или больше не просматривается. var просматривается: Символ;var не просматривается: Символ;}interface SignalOptions<T> {// Пользовательская функция сравнения между старым и новым значением. По умолчанию: Object.is.// Сигнал передается как значение this для context.equals?: (this: Signal<T>, t: T, t2: T) => boolean;// Обратный вызов, вызываемый, когда isWatched становится true, если ранее оно было false[Signal.subtle.watched]?: (this: Signal<T>) => void;// Обратный вызов, вызываемый всякий раз, когда isWatched становится ложным, если это было ранее true[Signal.subtle.unwatched]?: (это: Signal<T>) => void;}}
Сигнал представляет собой ячейку данных, которая может меняться со временем. Сигналы могут быть либо «состоятельными» (просто значением, которое задается вручную), либо «вычисленным» (формула, основанная на других сигналах).
Вычисленные сигналы работают путем автоматического отслеживания того, какие другие сигналы считываются во время их оценки. Когда вычисление считывается, оно проверяет, не изменились ли какие-либо из ранее записанных зависимостей, и, если да, переоценивает себя. Когда несколько вычисленных сигналов вложены, вся атрибуция отслеживания переходит к самому внутреннему.
Вычисляемые сигналы являются ленивыми, то есть основанными на извлечении: они переоцениваются только при доступе к ним, даже если одна из их зависимостей изменилась ранее.
Обратный вызов, передаваемый в вычисляемые сигналы, обычно должен быть «чистым» в том смысле, что он является детерминированной функцией без побочных эффектов других сигналов, к которым он обращается. В то же время время вызова обратного вызова является детерминированным, что позволяет осторожно использовать побочные эффекты.
Сигналы отличаются заметным кэшированием/мемоизацией: как состояния, так и вычисленные сигналы запоминают свое текущее значение и запускают перерасчет вычисленных сигналов, которые ссылаются на них, только если они действительно изменяются. Повторное сравнение старых и новых значений даже не требуется — сравнение выполняется один раз, когда исходный сигнал сбрасывается/переоценивается, а механизм сигнала отслеживает, какие вещи, ссылающиеся на этот сигнал, не обновились на основе нового. ценность еще. Внутри это обычно представляется посредством «раскраски графа», как описано в (сообщении в блоге Майло).
Вычисляемые сигналы динамически отслеживают свои зависимости — каждый раз, когда они запускаются, они могут зависеть от разных вещей, и этот точный набор зависимостей сохраняется в графе Signal. Это означает, что если у вас есть зависимость, необходимая только для одной ветви, а предыдущее вычисление использовало другую ветвь, то изменение этого временно неиспользуемого значения не приведет к пересчету вычисленного Сигнала, даже при его извлечении.
В отличие от обещаний JavaScript, в Signals все работает синхронно:
Установка нового значения сигнала происходит синхронно, и это немедленно отражается при чтении любого вычисленного сигнала, который впоследствии зависит от него. Встроенной пакетной обработки этой мутации не существует.
Чтение вычисленных сигналов происходит синхронно — их значение всегда доступно.
Обратный вызов notify
в Watchers, как описано ниже, выполняется синхронно во время вызова .set()
который его инициировал (но после завершения раскраски графа).
Как и обещания, сигналы могут представлять состояние ошибки: если выдается обратный вызов вычисленного сигнала, эта ошибка кэшируется так же, как другое значение, и выдается повторно каждый раз, когда сигнал читается.
Экземпляр Signal
представляет собой возможность считывать динамически изменяющееся значение, обновления которого отслеживаются с течением времени. Он также неявно включает возможность подписки на Сигнал, неявно посредством отслеживаемого доступа из другого вычисленного Сигнала.
API здесь разработан так, чтобы соответствовать очень грубому консенсусу экосистемы среди значительной части библиотек Signal по использованию таких имен, как «сигнал», «вычисляемый» и «состояние». Однако доступ к вычисляемым сигналам и сигналам состояния осуществляется через метод .get()
, что не соответствует всем популярным API-интерфейсам Signal, которые используют либо метод доступа в стиле .value
, либо синтаксис вызова signal()
.
API предназначен для сокращения количества выделений, чтобы сделать сигналы пригодными для встраивания в платформы JavaScript, обеспечивая при этом такую же или лучшую производительность, чем существующие сигналы, настроенные для платформы. Это подразумевает:
Сигналы состояния — это единый записываемый объект, к которому можно получить доступ и установить его по одной и той же ссылке. (См. последствия ниже в разделе «Разделение возможностей».)
Как состояние, так и вычисляемые сигналы предназначены для создания подклассов, чтобы облегчить возможность фреймворков добавлять дополнительные свойства через поля открытого и закрытого класса (а также методы для использования этого состояния).
Различные обратные вызовы (например, equals
, вычисленный обратный вызов) вызываются с соответствующим сигналом в качестве значения this
для контекста, так что новое замыкание не требуется для каждого сигнала. Вместо этого контекст можно сохранить в дополнительных свойствах самого сигнала.
Некоторые условия возникновения ошибок, налагаемые этим API:
Рекурсивное чтение вычисленного значения является ошибкой.
Обратный вызов notify
наблюдателя не может читать или записывать какие-либо сигналы.
Если обратный вызов вычисленного Сигнала выдает, то последующие обращения к Сигналу повторно выдают эту кэшированную ошибку, пока одна из зависимостей не изменится и она не будет пересчитана.
Некоторые условия, которые не соблюдаются:
Вычисляемые сигналы могут писать в другие сигналы синхронно в рамках их обратного вызова.
Работа, поставленная в очередь обратным вызовом notify
Watcher, может считывать или записывать сигналы, что позволяет копировать классические антишаблоны React с точки зрения сигналов!
Определенный выше интерфейс Watcher
дает основу для реализации типичных JS API для эффектов: обратных вызовов, которые повторно запускаются при изменении других сигналов исключительно из-за их побочного эффекта. Функцию effect
, использованную выше в исходном примере, можно определить следующим образом:
// Эта функция обычно находится в библиотеке/фреймворке, а не в коде приложения.// ПРИМЕЧАНИЕ. Эта логика планирования слишком проста, чтобы быть полезной. Не копируйте/вставляйте.let pending = false;let w = new Signal.subtle.Watcher(() => {if (!pending) {pending = true;queueMicrotask(() => {pending = false;for (let s of w.getPending()) s.get();w.watch();});}});// Эффект эффекта Сигнал, который оценивается как cb, который планирует чтение // самого себя в очереди микрозадач всякий раз, когда одна из его зависимостей может измениться, функция экспорта effect(cb) {let destructor;let c = new Signal.Computed(() => { destructor?.(); destructor = cb(); });w.watch(c) ;c.get();return () => { деструктор?.(); w.unwatch(c) };}
Signal API не включает никаких встроенных функций, таких как effect
. Это связано с тем, что планирование эффектов является тонким и часто связано с циклами рендеринга платформы и другими высокоуровневыми состояниями или стратегиями, специфичными для платформы, к которым у JS нет доступа.
Прогулка по различным операциям, используемым здесь: обратный вызов notify
, передаваемый в конструктор Watcher
, — это функция, которая вызывается, когда Сигнал переходит из «чистого» состояния (когда мы знаем, что кеш инициализирован и действителен) в «проверенное» или «грязное» состояние. " состояние (где кеш может быть действительным, а может и не быть действительным, поскольку по крайней мере одно из состояний, от которых это рекурсивно зависит, было изменено).
Вызовы для notify
в конечном итоге инициируются вызовом .set()
для некоторого сигнала состояния. Этот вызов синхронный: он происходит до возврата .set
. Но нет необходимости беспокоиться об этом обратном вызове, наблюдая за графиком сигнала в полуобработанном состоянии, поскольку во время обратного вызова notify
ни один сигнал не может быть прочитан или записан, даже при untrack
вызове. Поскольку notify
вызывается во время .set()
, он прерывает другой логический поток, который может быть неполным. Чтобы прочитать или записать сигналы из notify
, запланируйте выполнение работы позже, например, записав сигнал в список, чтобы к нему можно было получить доступ позже, или с queueMicrotask
, как указано выше.
Обратите внимание, что вполне возможно эффективно использовать Signals без Symbol.subtle.Watcher
, планируя опрос вычисленных сигналов, как это делает Glimmer. Однако многие платформы обнаружили, что очень часто бывает полезно синхронное выполнение этой логики планирования, поэтому API сигналов включает ее.
Как вычисленные сигналы, так и сигналы состояния подлежат сборке мусора, как и любые значения JS. Но у наблюдателей есть особый способ поддержания активности: любые сигналы, которые просматривает наблюдатель, будут поддерживаться до тех пор, пока доступно любое из базовых состояний, поскольку они могут вызвать будущий вызов notify
(а затем будущий .get()
). По этой причине не забудьте вызвать Watcher.prototype.unwatch
для очистки эффектов.
Signal.subtle.untrack
— это аварийный выход, позволяющий читать сигналы без отслеживания этих чтений. Эта возможность небезопасна, поскольку позволяет создавать вычисляемые сигналы, значение которых зависит от других сигналов, но которые не обновляются при изменении этих сигналов. Его следует использовать, когда неотслеживаемые доступы не изменят результат вычисления.
Эти функции могут быть добавлены позже, но они не включены в текущий проект. Их отсутствие связано с отсутствием устоявшегося консенсуса в области проектирования среди фреймворков, а также с продемонстрированной способностью обойти их отсутствие с помощью механизмов поверх понятия сигналов, описанных в этом документе. Однако, к сожалению, это упущение ограничивает потенциал взаимодействия между платформами. По мере создания прототипов сигналов, описанных в этом документе, будут предприняты попытки перепроверить, были ли эти упущения правильным решением.
Async : в этой модели сигналы всегда доступны для оценки синхронно. Однако часто бывает полезно иметь определенные асинхронные процессы, которые приводят к установке сигнала, и понимать, когда сигнал все еще «загружается». Одним из простых способов моделирования состояния загрузки является использование исключений, и поведение кэширования исключений вычисленных сигналов в некоторой степени сочетается с этим методом. Улучшенные методы обсуждаются в выпуске № 30.
Транзакции : для переходов между представлениями часто бывает полезно поддерживать активное состояние как для состояний «от», так и для «до». Состояние «до» отображается в фоновом режиме до тех пор, пока оно не будет готово к переключению (фиксации транзакции), в то время как состояние «от» остается интерактивным. Поддержание обоих состояний одновременно требует «разветвления» состояния графа сигналов, и может быть даже полезно поддерживать несколько ожидающих переходов одновременно. Обсуждение в выпуске №73.
Некоторые возможные удобные методы также опущены.
Это предложение включено в повестку дня TC39 на апрель 2024 года для Этапа 1. В настоящее время его можно рассматривать как «Этап 0».
Для этого предложения доступен полифилл с некоторыми базовыми тестами. Некоторые авторы фреймворка начали экспериментировать с заменой этой реализации сигнала, но это использование находится на ранней стадии.
Участники проекта Signal хотят быть особенно консервативными в том, как мы продвигаем это предложение, чтобы не попасть в ловушку поставки чего-то, о чем мы в конечном итоге сожалеем и фактически не используем. Наш план состоит в том, чтобы выполнить следующие дополнительные задачи, не требуемые процессом TC39, чтобы убедиться, что это предложение выполняется:
Прежде чем подавать заявку на Этап 2, мы планируем:
Разработайте несколько реализаций полифилов промышленного уровня, которые будут надежными, хорошо протестированными (например, проходящим тесты из различных платформ, а также тесты в стиле test262) и конкурентоспособными с точки зрения производительности (что подтверждено тщательным набором тестов сигналов/фреймворков).
Интегрируйте предложенный Signal API в большое количество JS-фреймворков, которые мы считаем в некоторой степени репрезентативными, и на этой основе работают некоторые крупные приложения. Проверьте, работает ли он эффективно и правильно в этих контекстах.
Иметь четкое представление о возможных расширениях API и прийти к выводу, какие из них (если таковые имеются) следует добавить в это предложение.
В этом разделе описываются все API-интерфейсы, доступные для JavaScript, с точки зрения алгоритмов, которые они реализуют. Это можно рассматривать как протоспецификацию, и она включена на этом раннем этапе, чтобы закрепить один возможный набор семантики, но при этом остается очень открытой для изменений.
Некоторые аспекты алгоритма:
Порядок чтения сигналов внутри вычисляемого значения важен и наблюдаем в том порядке, в котором выполняются определенные обратные вызовы (которые вызываются Watcher
, equals
первому параметру new Signal.Computed
и watched
/ unwatched
обратным вызовам). Это означает, что источники вычисленного Сигнала должны храниться упорядоченно.
Все эти четыре обратных вызова могут вызывать исключения, и эти исключения предсказуемым образом распространяются на вызывающий код JS. Исключения не останавливают выполнение этого алгоритма и не оставляют граф в полуобработанном состоянии. В случае ошибок, возникающих в обратном вызове notify
наблюдателя, это исключение отправляется в вызов .set()
, который его инициировал, с использованием AggregateError, если было создано несколько исключений. Остальные (включая watched
/ unwatched
?) сохраняются в значении Сигнала, чтобы быть повторно выданными при чтении, и такой повторно выдаваемый Сигнал может быть помечен как ~clean~
как и любой другой с нормальным значением.
Принимаются меры предосторожности, чтобы избежать цикличности в случаях, когда вычисленные сигналы не «наблюдаются» (за ними наблюдает любой наблюдатель), чтобы их можно было собирать мусором независимо от других частей графа сигналов. Внутри это можно реализовать с помощью системы номеров поколений, которые всегда собираются; Обратите внимание, что оптимизированные реализации могут также включать локальные номера генерации для каждого узла или избегать отслеживания некоторых чисел в отслеживаемых сигналах.
Сигнальные алгоритмы должны ссылаться на определенное глобальное состояние. Это состояние является глобальным для всего потока или «агента».
computing
: самый внутренний вычисляемый или эффектный сигнал, который в данный момент переоценивается из-за вызова .get
или .run
, или null
. Изначально null
.
frozen
: логическое значение, обозначающее, выполняется ли в данный момент обратный вызов, который требует, чтобы граф не изменялся. Изначально false
.
generation
: увеличивающееся целое число, начиная с 0, используемое для отслеживания актуальности значения, избегая при этом цикличности.
Signal
Signal
— это обычный объект, который служит пространством имен для классов и функций, связанных с Signal.
Signal.subtle
— аналогичный объект внутреннего пространства имен.
Signal.State
Signal.State
value
: Текущее значение сигнала состояния.
equals
: функция сравнения, используемая при изменении значений.
watched
: Обратный вызов, который будет вызван, когда эффект наблюдает за сигналом.
unwatched
: обратный вызов, который будет вызван, когда эффект больше не наблюдает за сигналом.
sinks
: набор наблюдаемых сигналов, которые зависят от этого
Signal.State(initialValue, options)
Установите value
этого сигнала на initialValue
.
Установите для этого сигнала equals
options?.equals
Установите для этого сигнала параметры watched
?.[Signal.subtle.watched]
Установите для этого сигнала значение unwatched
в параметрах?.[Signal.subtle.unwatched]
Установите sinks
этого сигнала в пустой набор
Signal.State.prototype.get()
Если frozen
истинно, выдайте исключение.
Если computing
не является undefined
, добавьте этот Сигнал в набор sources
computing
.
ПРИМЕЧАНИЕ. Мы не добавляем computing
в набор sinks
этого сигнала до тех пор, пока за ним не наблюдает наблюдатель.
Верните value
этого сигнала.
Signal.State.prototype.set(newValue)
Если текущий контекст выполнения frozen
, выдайте исключение.
Запустите алгоритм «установки значения сигнала» с этим сигналом и первым параметром значения.
Если этот алгоритм вернул ~clean~
, верните неопределенное.
Установите state
всех sinks
этого сигнала (если это вычисляемый сигнал) ~dirty~
если они ранее были чистыми, или (если это наблюдатель) ~pending~
если он ранее ~watching~
.
Установите для всех зависимостей вычисляемых сигналов приемников (рекурсивно) state
~checked~
если они ранее были ~clean~
(то есть оставьте грязные метки на месте), или для наблюдателей ~pending~
если ранее ~watching~
.
Для каждого ранее ~watching~
Наблюдателя, встреченного в этом рекурсивном поиске, затем в порядке глубины:
Установите для параметра frozen
значение «истина».
Вызов их обратного вызова notify
(сохраняя все возникающие исключения, но игнорируя возвращаемое значение notify
).
Восстановить frozen
значение false.
Установите state
Наблюдателя на ~waiting~
.
Если в обратных вызовах notify
было создано какое-либо исключение, передайте его вызывающему объекту после выполнения всех обратных вызовов notify
. Если существует несколько исключений, упакуйте их вместе в AggregateError и выдайте его.
Возврат неопределенный.
Signal.Computed
Signal.Computed
State Machine state
вычисляемого сигнала может быть одним из следующих:
~clean~
: значение сигнала присутствует и известно, что оно не устарело.
~checked~
: изменился (косвенный) источник этого сигнала; этот сигнал имеет значение, но может быть устаревшим. Устарело оно или нет, станет известно только после того, как будут оценены все непосредственные источники.
~computing~
: Обратный вызов этого Signal в настоящее время выполняется как побочный эффект вызова .get()
.
~dirty~
: Либо этот Сигнал имеет заведомо устаревшее значение, либо он никогда не оценивался.
График перехода выглядит следующим образом:
stateDiagram-v2
[*] --> грязный
грязный --> вычисления: [4]
вычисления --> очистить: [5]
чистый --> грязный: [2]
очистить --> проверено: [3]
проверено --> очистить: [6]
проверено --> грязно: [1]
ЗагрузкаПереходы:
Число | От | К | Состояние | Алгоритм |
---|---|---|---|---|
1 | ~checked~ | ~dirty~ | Непосредственный источник этого сигнала, который является вычисленным сигналом, был оценен, и его значение изменилось. | Алгоритм: пересчитать грязный вычисленный сигнал. |
2 | ~clean~ | ~dirty~ | Был установлен непосредственный источник этого сигнала, которым является состояние, значение которого не равно его предыдущему значению. | Метод: Signal.State.prototype.set(newValue) |
3 | ~clean~ | ~checked~ | Был установлен рекурсивный, но не непосредственный источник этого сигнала, который является состоянием, со значением, не равным его предыдущему значению. | Метод: Signal.State.prototype.set(newValue) |
4 | ~dirty~ | ~computing~ | Мы собираемся выполнить callback . | Алгоритм: пересчитать грязный вычисленный сигнал. |
5 | ~computing~ | ~clean~ | callback завершил оценку и либо вернул значение, либо выдал исключение. | Алгоритм: пересчитать грязный вычисленный сигнал. |
6 | ~checked~ | ~clean~ | Все непосредственные источники этого сигнала были оценены, и все они были обнаружены неизмененными, так что теперь известно, что мы не устарели. | Алгоритм: пересчитать грязный вычисленный сигнал. |
Signal.Computed
Внутренние слоты value
: предыдущее кэшированное значение Сигнала или ~uninitialized~
для никогда не читаемого вычисленного Сигнала. Значение может быть исключением, которое генерируется повторно при чтении значения. Всегда undefined
для сигналов эффектов.
state
: может быть ~clean~
, ~checked~
, ~computing~
или ~dirty~
.
sources
: упорядоченный набор сигналов, от которых зависит этот сигнал.
sinks
: упорядоченный набор сигналов, которые зависят от этого сигнала.
equals
: метод равенства, указанный в параметрах.
callback
: обратный вызов, который вызывается для получения вычисленного значения сигнала. Установите первый параметр, переданный конструктору.
Signal.Computed
КонструкторКонструктор устанавливает
callback
к своему первому параметру
equals
в зависимости от параметров, по умолчанию — Object.is
, если он отсутствует.
state
~dirty~
value
~uninitialized~
При использовании AsyncContext обратный вызов, передаваемый в new Signal.Computed
, закрывается на снимке, сделанном при вызове конструктора, и восстанавливает этот снимок во время его выполнения.
Signal.Computed.prototype.get
Если текущий контекст выполнения frozen
, или если этот Сигнал имеет состояние ~computing~
, или если этот сигнал является Эффектом и computing
вычисленный Сигнал, выдайте исключение.
Если computing
не равно null
, добавьте этот сигнал в набор sources
computing
.
ПРИМЕЧАНИЕ. Мы не добавляем computing
в набор sinks
этого сигнала до тех пор, пока/если он не станет наблюдаемым Наблюдателем.
Если состояние этого сигнала ~dirty~
или ~checked~
: повторяйте следующие шаги, пока этот сигнал не станет ~clean~
:
Выполните рекурсию вверх по sources
чтобы найти самый глубокий, самый левый (т.е. самый ранний из наблюдаемых) рекурсивный источник, который представляет собой вычисленный сигнал, помеченный ~dirty~
(отключение поиска при попадании в ~clean~
вычисленный сигнал и включение этого вычисленного сигнала в последнюю очередь). искать).
Выполните алгоритм «пересчета грязного вычисленного сигнала» для этого сигнала.
На этом этапе состояние этого сигнала будет ~clean~
, и никакие рекурсивные источники не будут ~dirty~
или ~checked~
. Верните value
сигнала. Если значение является исключением, повторно создайте это исключение.
Signal.subtle.Watcher
Signal.subtle.Watcher
Конечный автомат state
Наблюдателя может быть одним из следующих:
~waiting~
: обратный вызов notify
был запущен, или наблюдатель новый, но не отслеживает активные сигналы.
~watching~
: Наблюдатель активно отслеживает сигналы, но пока не произошло никаких изменений, которые потребовали бы обратного вызова notify
.
~pending~
: зависимость наблюдателя изменилась, но обратный вызов notify
еще не был запущен.
График перехода выглядит следующим образом:
stateDiagram-v2
[*] --> ожидание
ожидаю --> смотрю: [1]
смотрю --> ожидаю: [2]
смотрю --> в ожидании: [3]
в ожидании --> ожидание: [4]
ЗагрузкаПереходы:
Число | От | К | Состояние | Алгоритм |
---|---|---|---|---|
1 | ~waiting~ | ~watching~ | Был вызван метод watch Watcher. | Метод: Signal.subtle.Watcher.prototype.watch(...signals) |
2 | ~watching~ | ~waiting~ | Был вызван метод unwatch наблюдателя, и последний просматриваемый сигнал был удален. | Метод: Signal.subtle.Watcher.prototype.unwatch(...signals) |
3 | ~watching~ | ~pending~ | Наблюдаемый сигнал мог изменить значение. | Метод: Signal.State.prototype.set(newValue) |
4 | ~pending~ | ~waiting~ | Обратный вызов notify выполнен. | Метод: Signal.State.prototype.set(newValue) |
Signal.subtle.Watcher
state
: может быть ~watching~
, ~pending~
или ~waiting~
signals
: упорядоченный набор сигналов, которые наблюдатель наблюдает.
notifyCallback
: обратный вызов, который вызывается, когда что-то меняется. Установите первый параметр, переданный конструктору.
new Signal.subtle.Watcher(callback)
state
установлено в ~waiting~
.
Инициализируйте signals
как пустой набор.
notifyCallback
установлен в параметр обратного вызова.
При использовании AsyncContext обратный вызов, передаваемый в new Signal.subtle.Watcher
, не закрывается на снимке с момента вызова конструктора, поэтому контекстная информация вокруг записи видна.
Signal.subtle.Watcher.prototype.watch(...signals)
Если frozen
истинно, выдайте исключение.
Если какой-либо из аргументов не является сигналом, выдайте исключение.
Добавьте все аргументы в конец signals
этого объекта.
Для каждого вновь просмотренного сигнала в порядке слева направо
Добавьте этого наблюдателя в качестве sink
этого сигнала.
Если это был первый приемник, вернитесь к источникам, чтобы добавить этот сигнал в качестве приемника.
Установите для параметра frozen
значение «истина».
Вызовите watched
обратный вызов, если он существует.
Восстановить frozen
значение false.
Если state
сигнала ~waiting~
, установите его в ~watching~
.
Signal.subtle.Watcher.prototype.unwatch(...signals)
Если frozen
истинно, выдайте исключение.
Если какой-либо из аргументов не является сигналом или не отслеживается наблюдателем, выдайте исключение.
Для каждого сигнала в аргументах в порядке слева направо:
Удалите этот сигнал из набора signals
этого Наблюдателя.
Удалите этого Наблюдателя из набора sink
этого Сигнала.
Если набор sink
этого Сигнала стал пустым, удалите этот Сигнал как приемник из каждого из его источников.
Установите для frozen
значение true.
Вызовите unwatched
обратный вызов, если он существует.
Восстановить frozen
значение false.
Если у наблюдателя теперь нет signals
и его state
— ~watching~
, то установите его в ~waiting~
.
Signal.subtle.Watcher.prototype.getPending()
Возвращает массив, содержащий подмножество signals
, которые являются вычисляемыми сигналами в состояниях ~dirty~
или ~pending~
.
Signal.subtle.untrack(cb)
Пусть c
будет текущим computing
состоянием контекста выполнения.
Установите для computing
null.
Позвоните cb
.
Восстановите computing
в c
(даже если cb
выдал исключение).
Вернуть возвращаемое значение cb
(повторно выдавая любое исключение).
Примечание: untrack не выводит вас из frozen
состояния, которое строго поддерживается.
Signal.subtle.currentComputed()
Вернуть текущее computing
значение.
Очистите набор sources
этого сигнала и удалите его из наборов sinks
этих источников.
Сохраните предыдущее computing
значение и установите computing
для этого сигнала.
Установите состояние этого сигнала на ~computing~
.
Запустите обратный вызов этого вычисленного Сигнала, используя этот Сигнал в качестве значения this. Сохраните возвращаемое значение, и если обратный вызов вызвал исключение, сохраните его для повторного создания.
Восстановите предыдущее computing
значение.
Примените алгоритм «установить значение сигнала» к возвращаемому значению обратного вызова.
Установите состояние этого сигнала на ~clean~
.
Если этот алгоритм вернул ~dirty~
: отметьте все приемники этого Сигнала как ~dirty~
(ранее приемники могли быть смесью проверенных и грязных). (Или, если за этим никто не следит, присвойте номер нового поколения, чтобы указать на загрязненность, или что-то в этом роде.)
В противном случае этот алгоритм вернул ~clean~
: в этом случае для каждого ~checked~
приемника этого Сигнала, если все источники этого Сигнала теперь чисты, то этот Сигнал также помечается как ~clean~
. Примените этот шаг очистки к дальнейшим приемникам рекурсивно, к любым новым очищенным сигналам, которые проверили приемники. (Или, если за этим никто не следит, каким-то образом укажите это, чтобы очистка могла продолжаться лениво.)
Если этому алгоритму было передано значение (в отличие от исключения для повторного создания из алгоритма пересчета грязного вычисленного сигнала):
Вызовите функцию equals
этого сигнала, передав в качестве параметров текущее value
, новое значение и этот сигнал. Если генерируется исключение, сохраните это исключение (для повторного создания при чтении) как значение Signal и продолжайте, как если бы обратный вызов вернул false.
Если эта функция вернула true, верните ~clean~
.
Установите value
этого сигнала для параметра.
Вернуться ~dirty~
Вопрос : Не слишком ли рано начинать стандартизировать что-то, связанное с сигналами, когда они только начали становиться популярной новинкой в 2022 году? Разве мы не должны дать им больше времени для развития и стабилизации?
О : Текущее состояние Signals в веб-фреймворках — результат более чем 10-летней непрерывной разработки. По мере роста инвестиций, как это происходит в последние годы, почти все веб-фреймворки приближаются к очень похожей базовой модели Signals. Это предложение является результатом совместного проектирования большого числа нынешних лидеров веб-фреймворков, и оно не будет продвигаться к стандартизации без проверки этой группой экспертов в различных контекстах.
Вопрос : Могут ли встроенные сигналы использоваться фреймворками, учитывая их тесную интеграцию с рендерингом и владением?
Ответ : Части, которые более специфичны для структуры, как правило, относятся к области эффектов, планирования и владения/распоряжения, которые это предложение не пытается решить. Нашей первоочередной задачей при создании прототипов сигналов, отслеживающих стандарты, является подтверждение того, что они могут располагаться «под» существующими платформами, совместимо и с хорошей производительностью.
Вопрос : Предназначен ли Signal API для непосредственного использования разработчиками приложений или он заключен в фреймворки?
О : Хотя этот API может использоваться непосредственно разработчиками приложений (по крайней мере, той частью, которая не входит в пространство имен Signal.subtle
), он не предназначен для обеспечения особой эргономичности. Вместо этого приоритетными являются потребности авторов библиотек/фреймворков. Ожидается, что большинство фреймворков будут обертывать даже базовые API Signal.State
и Signal.Computed
чем-то, выражающим их эргономический подход. На практике, как правило, лучше всего использовать Signals через структуру, которая управляет более сложными функциями (например, Watcher, untrack
), а также управляет владением и удалением (например, определяет, когда сигналы следует добавлять в наблюдатели и удалять из них), и планирование рендеринга в DOM — это предложение не пытается решить эти проблемы.
Вопрос : Должен ли я удалять сигналы, связанные с виджетом, когда этот виджет уничтожен? Какой API для этого?
О : Соответствующая операция по удалению здесь — Signal.subtle.Watcher.prototype.unwatch
. Очищать необходимо только просматриваемые сигналы (отменив их просмотр), а непросмотренные сигналы можно автоматически собирать мусором.
Вопрос : Работают ли сигналы с VDOM или напрямую с базовым HTML DOM?
А : Да! Сигналы не зависят от технологии рендеринга. Существующие фреймворки JavaScript, использующие конструкции, подобные Signal, интегрируются с VDOM (например, Preact), собственным DOM (например, Solid) и их комбинацией (например, Vue). То же самое будет возможно и со встроенными Сигналами.
Вопрос : Будет ли эргономично использовать Signals в контексте фреймворков на основе классов, таких как Angular и Lit? А как насчет фреймворков на основе компиляторов, таких как Svelte?
О : Поля классов можно сделать на основе Signal с помощью простого декоратора-аксессора, как показано в файле readme полифила Signal. Сигналы очень тесно связаны с рунами Svelte 5 — компилятору легко преобразовать руны в API сигналов, определенный здесь, и фактически это то, что Svelte 5 делает внутри (но с собственной библиотекой сигналов).
Вопрос : Работают ли сигналы с SSR? Гидратация? Возобновляемость?
А : Да. Qwik эффективно использует сигналы с обоими этими свойствами, а в других платформах есть другие хорошо разработанные подходы к гидратации с помощью сигналов с различными компромиссами. Мы считаем, что можно смоделировать возобновляемые сигналы Qwik, используя соединенные вместе сигнал состояния и вычисляемый сигнал, и планируем доказать это в коде.
Вопрос : Работают ли Signals с односторонним потоком данных, как React?
О : Да, сигналы — это механизм одностороннего потока данных. Платформы пользовательского интерфейса на основе сигналов позволяют выразить свое мнение как функцию модели (если модель включает сигналы). Граф состояний и вычисленных сигналов по своей конструкции ацикличен. Также возможно воссоздать антишаблоны React внутри Signals (!), например, Signal-эквивалент setState
внутри useEffect
— использовать Watcher для планирования записи в сигнал State.
Вопрос : Как сигналы связаны с системами управления состоянием, такими как Redux? Способствуют ли сигналы неструктурированному состоянию?
Ответ : Сигналы могут сформировать эффективную основу для абстракций управления состоянием, подобных хранилищам. Распространенным шаблоном, встречающимся во многих средах, является объект, основанный на прокси, который внутренне представляет свойства с помощью сигналов, например, Vue reactive()
или хранилищ Solid. Эти системы позволяют гибко группировать состояния на нужном уровне абстракции для конкретного приложения.
Вопрос : Что предлагают Сигналы, которые в настоящее время не обрабатывает Proxy
?
О : Прокси и сигналы дополняют друг друга и хорошо сочетаются друг с другом. Прокси позволяют перехватывать мелкие операции с объектами, а сигналы координируют граф зависимостей (ячейок). Поддержка прокси с помощью сигналов — отличный способ создать вложенную реактивную структуру с отличной эргономикой.
В этом примере мы можем использовать прокси, чтобы сигнал имел свойства getter и setter вместо использования методов get
и set
:
const a = новый Signal.State(0);const b = новый прокси(a, { get(цель, свойство, получатель) {if (свойство === 'значение') { return target.get():} } set(цель, свойство, значение, получатель) {if (свойство === 'значение') { target.set(значение)!} }});// использование в гипотетическом реактивном контексте:<шаблон> {б.значение} <button onclick={() => {b.value++; }}>изменить</button></template>
при использовании средства визуализации, оптимизированного для детальной реактивности, нажатие кнопки приведет к обновлению ячейки b.value
.
Видеть:
примеры вложенных реактивных структур, созданных как с помощью сигналов, так и с помощью прокси: signal-utils
пример предыдущих реализаций, показывающий взаимосвязь между реактивными данными и прокси-серверами: отслеживаемые встроенные модули
обсуждение.
Вопрос : Сигналы основаны на push или pull?
О : Оценка вычисленных сигналов основана на извлечении: вычисленные сигналы оцениваются только при вызове .get()
, даже если базовое состояние изменилось намного раньше. В то же время изменение сигнала состояния может немедленно вызвать обратный вызов наблюдателя, «отправив» уведомление. Таким образом, сигналы можно рассматривать как «тянущую» конструкцию.
Вопрос : Вносят ли сигналы недетерминированность в выполнение JavaScript?
О : Нет. Во-первых, все операции Signal имеют четко определенную семантику и порядок и не будут различаться в разных реализациях. На более высоком уровне Сигналы следуют определенному набору инвариантов, по отношению к которым они «правильны». Вычисленный Сигнал всегда наблюдает за графом Сигнала в согласованном состоянии, и его выполнение не прерывается другим кодом, изменяющим Сигнал (за исключением того, что он вызывает сам). Смотрите описание выше.
Вопрос : Когда я пишу в сигнал состояния, когда запланировано обновление вычисленного сигнала?
A : Это не запланировано! Вычисленный Сигнал пересчитывается в следующий раз, когда кто-то его прочитает. Синхронно может быть вызван обратный вызов notify
Watcher, что позволяет платформам планировать чтение в то время, которое они считают подходящим.
Вопрос : Когда запись в сигналы состояния вступает в силу? Сразу или партиями?
О : Запись в сигналы состояния отражается немедленно — в следующий раз, когда вычисленный сигнал, который зависит от сигнала состояния, будет прочитан, он при необходимости пересчитает себя, даже если это происходит в следующей строке кода. Однако ленивость, присущая этому механизму (вычисленные сигналы вычисляются только при чтении), означает, что на практике вычисления могут происходить пакетным образом.
Вопрос : Что означает для Signals возможность выполнения без сбоев?
О : Более ранние модели реактивности, основанные на push-уведомлениях, сталкивались с проблемой избыточных вычислений: если обновление сигнала состояния приводит к быстрому запуску вычисленного сигнала, в конечном итоге это может привести к обновлению пользовательского интерфейса. Но эта запись в пользовательский интерфейс может быть преждевременной, если перед следующим кадром должно было произойти еще одно изменение исходного сигнала состояния. Иногда из-за таких сбоев конечным пользователям даже показывались неточные промежуточные значения. Сигналы избегают этой динамики, поскольку основаны на извлечении, а не на принудительной отправке: в то время, когда платформа планирует рендеринг пользовательского интерфейса, она извлекает соответствующие обновления, избегая напрасной работы как при вычислениях, так и при записи в DOM.
Вопрос : Что означает, что сигналы «с потерями»?
О : Это обратная сторона выполнения без сбоев: сигналы представляют собой ячейку данных — просто мгновенное текущее значение (которое может измениться), а не поток данных с течением времени. Таким образом, если вы записываете в сигнал состояния дважды подряд, не делая ничего другого, первая запись «теряется» и никогда не видна никаким вычисленным сигналам или эффектам. Это считается особенностью, а не ошибкой — другие конструкции (например, асинхронные итерации, наблюдаемые) более подходят для потоков.
Вопрос : Будут ли встроенные сигналы быстрее существующих реализаций JS Signal?
О : Мы надеемся на это (с небольшим постоянным коэффициентом), но это еще предстоит доказать в коде. JS-движки не являются волшебством, и в конечном итоге им придется реализовывать те же алгоритмы, что и JS-реализации сигналов. См. раздел выше о производительности.
Вопрос : Почему это предложение не включает функцию effect()
, хотя эффекты необходимы для любого практического использования сигналов?
Ответ : Эффекты по своей сути связаны с планированием и удалением, которые управляются структурами и выходят за рамки данного предложения. Вместо этого это предложение включает основу для реализации эффектов через более низкоуровневый API Signal.subtle.Watcher
.
Вопрос : Почему подписки осуществляются автоматически, а не вручную?
О : Опыт показал, что интерфейсы подписки, выполняемые вручную, для обеспечения реактивности неэргономичны и подвержены ошибкам. Автоматическое отслеживание более компонуемо и является основной функцией Signals.
Вопрос : Почему обратный вызов Watcher
выполняется синхронно, а не запланировано в микрозадаче?
О : Поскольку обратный вызов не может читать или записывать сигналы, его синхронный вызов не вызывает никаких нарушений. Типичный обратный вызов добавляет сигнал в массив, который будет прочитан позже, или отмечает где-то немного. Делать для всех подобных действий отдельную микрозадачу ненужно и непрактично дорого.
Вопрос : В этом API отсутствуют некоторые приятные особенности, которые предоставляет моя любимая платформа, которые упрощают программирование с помощью Signals. Можно ли это тоже добавить в стандарт?
А : Возможно. Различные расширения все еще находятся на стадии рассмотрения. Пожалуйста, сообщите о проблеме, чтобы обсудить любую недостающую функцию, которую вы считаете важной.
Вопрос : Можно ли уменьшить размер или сложность этого API?
О : Целью определенно является минимизация API, и мы постарались сделать это с помощью того, что представлено выше. Если у вас есть идеи по поводу дополнительных вещей, которые можно удалить, сообщите о проблеме для обсуждения.
Вопрос : Не следует ли нам начать работу по стандартизации в этой области с более примитивной концепции, такой как наблюдаемые?
Ответ : Observables могут быть хорошей идеей для некоторых вещей, но они не решают проблем, которые призваны решить Signals. Как описано выше, наблюдаемые объекты или другие механизмы публикации/подписки не являются полным решением для многих типов программирования пользовательского интерфейса из-за слишком большого количества работы по настройке, подверженной ошибкам, для разработчиков, а также из-за напрасной работы из-за отсутствия лени, среди других проблем.
Вопрос : Почему в TC39 предлагаются сигналы, а не DOM, учитывая, что большинство его приложений основаны на веб-технологиях?
Ответ : Некоторые соавторы этого предложения заинтересованы в средах пользовательского интерфейса, не связанных с веб-интерфейсом, в качестве цели, но в наши дни для этого может подойти любое место, поскольку веб-API все чаще реализуются за пределами Интернета. В конечном счете, Signals не должны зависеть от каких-либо API-интерфейсов DOM, поэтому работает любой вариант. Если у кого-то есть веская причина для переключения этой группы, сообщите нам об этом в проблеме. На данный момент все участники подписали соглашения об интеллектуальной собственности TC39, и планируется представить их TC39.
Вопрос : Сколько времени пройдет, прежде чем я смогу использовать стандартные сигналы?
О : Полифил уже доступен, но лучше не полагаться на его стабильность, поскольку этот API развивается в процессе проверки. Через несколько месяцев или год можно будет использовать высококачественный, высокопроизводительный стабильный полифил, но он все еще будет подлежать пересмотру комитетом и еще не станет стандартом. Следуя типичной траектории предложения TC39, ожидается, что потребуется как минимум 2-3 года, чтобы сигналы стали изначально доступны во всех браузерах, начиная с нескольких версий, так что полифиллы не нужны.
Вопрос : Как мы можем предотвратить слишком раннюю стандартизацию неправильных типов сигналов, например, {{функция JS/web, которая вам не нравится}}?
О : Авторы этого предложения планируют приложить дополнительные усилия, создавая прототипы и проверяя все, прежде чем запрашивать повышение уровня на TC39. См. «Состояние и план развития» выше. Если вы видите пробелы в этом плане или возможности для улучшения, сообщите о проблеме с объяснением.