В оригинале эта заметка называется Hooks and Streams. Этот текст — не дословный перевод оригинала, часть оригинального текста изменена, часть пропущена. Иногда вместо термина Hooks используется слово ловушки или хуки, а вместо Streams — потоки. Тем не менее, я старался придерживаться стиля автора.
Hooks and Streams
Dan Abramov написал замечательный пост, о там как можно написать setInterval
в декларативном стиле при помощи React Hooks.
Я полагаю, что Hooks хотя и не лишены недостатков, действительно интересное и технически перспективное изобретение. Если вы знаете the rules of hooks и понимаете, зачем хуки нужны, значит все в порядке. Но я надеюсь убедить вас, что существует более простое и лучшее решение.
Чтобы использовать Hooks, необходимо придерживаться четкого набора правил, так как, в конечном счете ловушки — это просто очень ловкий трюк. Хуки позволяют думать, что некоторое внешнее состояние доступно в области видимости вашей функции в момент ее вызова. Но это невозможно без дополнительной механики. В случае React эти механизмы вычисляют, какое состояние будет доступно некоторой функции в момент ее вызова, подсчитывая количество вызовов. Хотя реальный механизм сложнее, но в конечном итоге, это подсчет.
Немного истории
The V in MVC
Изначально компонеты React строились как классы. Вы создаете класс, вызывая React.createClass
, и поскольку каждый компонент это экземпляр класса, он должен иметь состояние. Локальное состояние компонента можно контролировать через this.setState(newState)
. Это позволяет следить за состояниями полей форм, проверкой ввода и т.п. Такой подход, хотя и не был уникальным на момент создания React, но и не был широко распространен.
Однако, преимущество React было не столько в компонентном подходе к дизайну, сколько в декларации, что React это "View in Model View Controller" ("V in MVC"). MVC подход преобладал в проектировании GUI на тот момент, и наиболее популярными альтернативами React были MVC фреймворки Backbone.js и Angular.js. Они были совершенно разными, но оба использовали паттерн MVC, поскольку тогда такой подход считался необходимым при проектировании любых не тривиальных приложений. React удалось продать в основном за счет декларативных представлений (View), что было заметным преимуществом в мире, где нормальным считалось модифицировать станицы с помощью jQuery выражений, в ответ на события DOM.
Даже являясь частью паттерна MVС, декларативные представления React оказали значительное влияние на разработку UI. Клиентские приложения на JavaScript были тогда достаточно новым делом, и казалось естественным использовать устоявшиеся приемы серверного программирование и в этой области. И хотя React опирался на ООП в своем дизайне, со временем многие разработчики осознали, что фреймворк имеет гораздо больше общих черт с функциональной парадигмой. Сообщество ФП начало использовать React в своих разработках, благодаря именно декларативному стилю представлений React.
Вслед за React стали появляться и другие библиотеки с компонентным дизайном, которые часто заменяли MVC просто уровнем компонентов, и казалось, что компоненты это гораздо проще чем паттерн MVC. Компонентный дизайн виделся очень перспективным, однако, скоро выяснилось, что имеется ряд существенных ограничений дизайна на реальных проектах. Каждый компонент имел собственное внутренне состояние, и считалось плохой практикой прямо модифицировать его в другом компоненте. В качестве альтернативы предлагалось передавать в дочерний компонент колбэк, который сообщал родительскому, о том, что состояние потомка изменилось.
Events and Callbacks
Взаимодействие компонентов обычное дело для любого сложного приложения. Паттерн MVC позволяет компоненту запускать события, и любой другой компонент может прослушивать события и реагировать на них. Но, в конечном счете, событийная архитектура упирается в те же проблемы, что и межкомпонентное изменение внутренних состояний. При отладке сложного, большого кода трудно понять, почему происходят изменения, и что их вызывает.
Колбэки решают проблему, позволяя выстраивать иерархии и структуры управления, за счет написания вручную рутинного и многословного кода, даже для элементарной композиции компонентов.
Архитектура и с колбэками, и с управлением событиями плохо масштабировалась. На этом фоне ФП сообщество разрабатывало свои проекты для GUI. Вероятно, наиболее влиятельным был проект Elm, с представлениями без состояний, и модель состояния в виде свертки потоков изменений в новое состояние.
Эта модель впоследствии была включена в React в виде Redux Дэна Абрамова.
Redux — Не совсем то, чего хотелось
С помощью Redux можно писать большие масштабируемые React приложения, большими командами не прибегая к колбэкам и не опираясь на архитектуру событий.
Благодаря однонаправленному потоку изменений, потребность во внутреннем состоянии компонентов в значительной степени потеряла актуальность. Простой контроль управления, казалось, решал все проблемы с событиями и обеспечивал необходимые условия для разработки приложений в больших командах за счет повторяемости шаблонов и масштабируемости.
Но с Redux возникли две новые проблемы.
Во-первых, он очень многословен. Redux собственно — это интерпретация на JavaScript функционального, алгебраического типа данных union (сумма) и специальной техники называемой pattern matching (в виде JavaScript оператора switch
).
Несколько библиотек пытались решить проблему многословности, генерируя Redux выражения с помощью небольшого DSL, но, без особого успеха.
Другая проблема Redux — производительность. Пересчитанное с помощью actions состояние необходимо передать в каждый компонент. Это привело к появлению значительного количества новых способов проектирования типа кэширования свойств (prop memoization), при этом нужно вычислять не только какие узлы virtual dom изменились, но и изменившиеся свойства, что в свою очередь приводило к неоднозначности.
И конечно, на этом история не заканчивается. Мы видим решения типа Immutable.JS, как JS модель структур данных языка Clojure. Здесь вы вроде не мутируете все состояние, но за счет ссылочного равенства изменяете только те фрагменты данных, которые реально нужно изменить.
Обобщая, мы понимаем, что нам нужно как-то реагировать на изменение состояния (и обновлять UI), но реагировать только на события, которые реально существенны (что бы наш интерфейс не тормозил). И мы видим, что и управления событиями, и колбэки, и вычисление собственных свойств, и действия redux, нужны только для того, чтобы решить ту же самую задачу. И все эти подходы опираются на попытку определить что изменилось, сравнивая предыдущее состояние с актуальным, и каждый подход имеет вполне определенные ограничения.
Однако, есть структура данных, которая делает именно то, что нам нужно, без необходимости постоянно определять что поменялось. Такая структура называется stream (поток, или каскад если хотите).
Чисто функциональные компонеты должны быть замыканиями
К сожалению, даже при наличии потоков React API не позволяет строить компоненты без классов или хуков.
Смысл хуков сводится в итоге к ситуации, что у нас как бы есть замыкание, так почему бы просто не использовать замыкания.
Замыкание — это функция возвращиющая функцию, а в React функции это компоненты:
function Hello({ name }){
return <p>Hello {name}</p>
}
Если мы сделаем так:
// What if components were functions
// that returned a view function?
function Hello({ name }){
return () => <p>Hello {name}</p>
}
У нас возникает промежуточное состояние в компоненте, без хуков, классов и редуксов. Анонимная функция ()=> React.createElement
это все, что нужно React чтобы хранить состояние компонента внутри функции без хуков или классов. Вот как упростилось бы управление состоянием, если бы компоненты React можно было писать в виде замыканий:
function Counter({ name }){
let count = 0
return () => <>
<p>Count {count}</p>
<button onClick={ () => count++ }>Increment</button>
<button onClick={ () => count-- }>Decrement</button>
</>
}
Но мы не можем вернуть такую функцию, мы должны вернуть React.createElement
или литерал JSX, который транспилируется в React.createElement
. Возвращая анонимную функцию, мы разваливаем рекурсию React.createElement
, и такой код работать не будет.
Нужен ли setState
?
У нас есть еще одна проблема. React не перерисовывает страницу, если возникает событие DOM. В примере выше onClick
. Отрисовка запускается только при изменении состояния компонента. Но наш count
— это не часть состояния, так что, его изменения не влияют на рендеринг. Не все фреймворки так устроены.
Предположим, что React в теле обработчика события указанного в JSX коде вызывает setState({})
, всякий раз, когда возникает событие DOM. Иначе зачем привязывать обработчик к элементу DOM если событие никак не обновляет локальное состояние и не влияет на рендеринг?
Добавление этой возможности в значительной степени сделало бы бесполезными хуки, и по моему мнению, такие изменения следует сделать в будущих версиях React.
Не считая useState
, хуки декларировались и как способ композиции эффектов.
Но то же что и с хуками и гораздо больше мы можем решить с помощью простого механизма — потоков, streams. Потоки — это комбинируемые, управляемые и постоянные структуры данных.
Декларативный useInterval
Давайте напишем декларативный setInterval
с такой же функциональностью, что и пример с хуками Дэна Абрамова. И мы будем использовать потоки для композиции эффектов и управления общими данными. Мы сделаем компонент в виде замыкания, которых нет в React. Я считаю, что вы уже прочитали блог Дэна, поскольку мы возьмем за основу его прекрасную работу.
Учитывая все сказанное выше мы будем использовать Mithril, который очень похож на React, но позволяет использовать замыкания в качестве компонентов, и автоматически перерисовывает страницу в ответ на событие DOM.
Для начала кратко о Mithril и Mithril Streams.
Mithril опирается на концепцию vnode (виртуальный узел). Vnode эта структура данных представляющая компонент, или выражение на гиперскрипте. И выглядит примерно так:
// vnode = virtual dom node
{ tag: 'input'
, oninput: [Function]
, value: 'hello'
, attrs: { style: { color: 'red'}, disabled: true }
, oncreate: [Function]
, onupdate: [Function]
, onremove: [Function]
, dom: [HTMLInputElement] // appears after first render
}
Такой объект является представлением выражения на гиперскрипте:
m('input[disabled]'
, { style: { color: 'red' }
, oncreate
, oupdate
, onremove
, oninput
})
или если хотите JSX:
<input
disabled
style={{ color: 'red' }}
oncreate={oncreate}
onupdate={onupdate}
onremove={onremove}
oninput={oninput}
/>
Обратите внимание, Vnode имеет методы жизненного цикла, указанные прямо в объекте (мы определили их при вызове m()). И эти методы привязаны прямо к узлу DOM, без необходимости писать компонент.
Компонент — это такой же узел Vnode. Определим компонент в виде функции:
function MyComponent({ attrs }){
return {
view: () => m('p', 'hello '+attrs.name)
}
}
// mounted like so:
m(MyComponent, { name: 'Mithril' })
// or like so
<MyComponent name="mithril" />
Здесь мы определили компонент в виде замыкания, что очень удобно, если мы хотим использовать состояние компонента, не используя классы. Хотя можно использовать и обычный javascript объект и класс.
Mithril отличается одним замечательным свойством — в каждый виртуальный узел передаются все методы жизненного цикла, так что если вам нужен доступ к представлению узла, он у вас есть:
function MyComponent(vnode){
console.log(vnode) // { attrs, children, tag, view, ...etc }
return {
view: vnode => m('p', 'hello ' + vnode.attrs.name),
oncreate: vnode => {},
onupdate: vnode => {},
onbeforeremove: vnode => {},
onremove: vnode => {}
}
}
Вам не нужны refs
для доступа к DOM элементу, вы обращаетесь к нему как vnode.dom
.
Такой дизайн API позволяет легко представлять не декларативные побочные эффекты в виде декларативного интерфейса компонента. Mithril позволяет делать это явным и разнообразным образом, он легко расширяем.
Потоки — Streams
В Mithril есть отдельный, очень маленький, импортируемый модуль 'mithril/stream':
import stream from 'mithril/stream'
// 0 is our initial value
const count = stream(0)
// respond to changes
const double = count.map( x => x * 2 )
count() // read last value
// => 0
count(2) // write new value
count() //read new value
// => 2
double() // read inferred value
// => 4
Здесь мы имеем функцию (в виде именованного выражения count
), с помощью которой добавляем данные в поток, или считываем последнее значение.
Мы так же создали зависимый double
поток, который отслеживает изменения в родительском потоке и всякий раз пересчитывает свое значение при изменении значения в родительском.
Поток можно закрыть (финализировать), этим мы одновременно закрываем все зависимые потоки, и выполняем какие-то финальные операции, если они необходимы.
// Every stream has a .end stream.
// You can be notified when a stream ends
// by mapping over it (just like any other
// stream)
count.end.map(
() => console.log('Stop counting')
)
// To end a stream, pass `true` to the end stream.
count.end(true)
// logs: Stop Counting
// Now that our stream is ended
// `double` will not update
double()
// => 4
count(100)
double()
// => 4
Потоки замечательным образом подходят для передачи сообщений, и разделения данных между компонентами, решая ту же проблему, что и React Hooks, React Context, prop callbacks, Redux и так далее.
useInterval
Вслед за Дэном, мы напишем декларативный useInterval
, используя замыкания, потоки, и методы жизненного цикла компонента.
Мы будем использовать только примитивные потоки, вот определение:
const stream = m.stream
function useInterval({
// delay - поток который хранит значение задержки
// используемое при вызове `setInterval`
delay
}){
// `setInterval`возвращает идентификатор интервала
// который мы будем использовать при вызове `clearInterval`
// и хранить в потоке, так чтобы использовать последний открытый интервал
// id - идентификатор интервала
const id = stream()
// Этот поток возвращается вызывающей функции
// и может использоваться для действий или композиции потоков
// tick - состояние замыкания в него записывается значение потока
// delay при запуске нового интервала
const tick = stream()
// этот поток считывается в поток tick и нужен для действия
// (побочного эффекта)
// Всякий раз когда он изменяется,
// вызывается функция определенная в delay.map
// где мы можем закрыть старый интервал
// и определить новый
delay.map(
delay => {
// получим id текущиго интервала id()
// и передадим его в `clearInterval`, чтобы закрыть
clearInterval(id())
// определим новый интервал
// кототый будет изменять поток tick каждые [delay]ms
// Третий аргумент передается в поток tick
id(setInterval(tick, delay, delay))
}
)
// дейстие при завкрытии потока
// когда поток delay закрывается
// мы также закрываем последний интервал
// это можно использовать например когда родительский компонент
// будет удален из дерева DOM
delay.end.map(
() => clearInterval(id())
)
// вернем поток tick вызывающей функции
// таким образом мы будет следить за тем
// когда запускается очередной интервал
return tick
}
function App(){
// delay хранит задержку перед тем как в очередной
// обновить поток tick.
// текущее значение можно получить так
//
// delay()
// 250
//
const delay = stream(250)
// этот поток отслеживает изменения потока tick
// каждый раз когда задержка в интервале кончается
// значение в потоке увеличивается на 1
// конечно он достаточно бесполезный
// но мы можем использовать его для отладки или
// как здесь, видим сколько интервалов кончилось
const count = stream(0)
// ссылка на поток tick
const tick = useInterval({ delay })
// здесь мы создаем дочерний поток для побочных действий
// каждый раз когда запускается новый интевал мы увеличиваем счетчик
tick.map(() => count( count() + 1 ))
// после каждого увеличения счетчика вызываем отрисовку
count.map(m.redraw)
return {
// при удалении компонента из vdom вызывается этот метод
onremove: () => delay.end(true),
view: () =>
// тэг p отображает текущее значение потока count.
// когда отрисовывается view мы считываем количество вызовов tick()
// при очередном изменеии mithril модифицирует DOM.
// Когда мы меняем значение задержки, мы модифицируем
// поток `delay`, который при изменеии закрывает старый интервал
// и открывает новый с новым значением задержки
// таким образм наш счетчик будет модифицироваться быстрее или медленнее
// в зависимости от величины интервала задержки
<div>
<p>Count: {count()}</p>
<label>
Delay:
<input
type="number"
value={delay()}
oninput={e => delay(e.target.value)}
/>
</label>
</div>
}
}
m.mount(document.body, App)
Обратите внимание, useInterval
это функция принимающая поток как параметр и возвращающая поток.
Функция, принимающая поток и возвращающая поток называется комбинатор потоков, мы комбинируем потоки в новый поток, почти так же как комбинируются хуки. Но в отличии от хуков, мы не полагаемся на отслеживание глобального состояния связанного с фреймворком. Мы используем простую структуру данных, и можем использовать ее в любом месте, и в любом фреймворке не зависимо от контекста.
Обратите внимание, все, что связано с моделью находится в замыкании и логически сгруппировано. В представлении view
у нас есть доступ к потокам, мы можем считывать их и модифицировать. Они легко могут быть доступны в других компонентах, что решает проблемы указанные в историческом обзоре выше достаточно элегантно.
Слой представления никак не связан с useInterval
, мы просто пишем в потоки и читаем из них. В этом преимущество потоков: вы определяете отношения в потоках вне текущего контекста и связываете данные в потоках с текущим контекстом.
Модифицируем код приведенный выше следующим образом:
const stream = m.stream
function useInterval({delay}){
// не изменилась
const id = stream()
const tick = stream()
delay.map(
delay => {
clearInterval(id())
id(setInterval(tick, delay, delay))
}
)
delay.end.map(
() => clearInterval(id())
)
return tick
}
// Теперь это просто функция принимающая
// поток как параметр и возвращающая виртуальный узел
const input = ({ delay }) =>
<input
type="number"
value={delay()}
oninput={e => delay(e.target.value)}
/>
// Тоже функция с потоком в качестве парамета
// и возвращающая виртуальный узел
const paragraph = ({ count }) =>
<p>Count: {count()}</p>
// здесь функция возвращает объект из потоков
const model = () => {
const delay = m.stream(250)
const count = m.stream(0)
const tick = useInterval({ delay })
tick.map(() => count( count() + 1 ))
count.map(m.redraw)
return { delay, count }
}
const App = () => {
const { delay, count } = model()
return {
onremove: () => delay.end(true)
, view: () =>
<div>
{paragraph({ count })}
<label>Delay: {input({ delay })}</label>
</div>
}
}
m.mount(document.body, App)
Функциональная композиция
Мы помним, что useInterval
это комбинатор потоков: входной параметр — поток, выходной — поток. Модель в коде выше также как бы принимает поток, просто игнорируя входной параметр, и возвращает объект из 2-х потоков. Так что, мы с помощью одной функции определяем логику для другой. Такой прием можно использовать бесконечно, можно изменять поведение потоков просто определяя функцию принимающую поток и возвращающую новый поток, и это не требует значительных усилий.
refs & useEffect
Вот цитата из блога Дена относительно разницы между декларативным React кодом setInterval
по отношению к императивному (Прим переводчика: что было названо довольно цветисто The Impedance Mismatch):
Состояние примонтириванного React компонента может меняется сколько угодно раз, но в результате отрисовано будет состояние когда все эти изменения будут применены все сразу и за один раз. (A React component may be mounted for a while and go through many different states, but its render result describes all of them at once.)
// Describes every render return <h1>{count}</h1>
Хуки позволяют нам использовать декларативный подход к эффектам:
// Describes every interval state useInterval(() => { setCount(count + 1); }, isRunning ? delay : null);
Мы не устанавливаем интервал, но определяем будет ли он установлен и с какой задержкой, благодаря нашему хуку, непрерывный процесс описан в дискретным образом.
НапротивsetInterval
не описывает процесс во времени, будучи установленным его нельзя изменить, только сбросить.
В этом заключается отличие между подходом React иsetInterval
API.
То, о чем Дэн здесь говорит в ФП называется функтор.
Это тип данных, который должен подчиняться 2 правилам. Функтор интуитивно можно описать так: это интерфейс к некоторому состоянию, непосредственно недоступному, который можно преобразовать (map
) в новый функтор того же типа применяя некоторую функции к состоянию.
Array.map
, Stream.map
являются примерами таких преобразований. Но у нас нет возможности написать нечто вроде React.Component.map
.
Но, цитируем Дэна:
Состояние примонтириванного React компонента может меняться сколько угодно раз, но в результате отрисовано будет состояние когда все эти изменения будут применены все сразу и за один раз.
То же самое можно сказать и о потоках:
const count = m.stream()
count.map(
// This function describes all future states
x => <p>Count {x} </p>
)
Можно ли говорить об эквивалентности потоков и компонентов? Не совсем, компоненты предназначены для вполне определенных целей. У них есть колбэки и интерфейсы, и они разработаны специально для построения UI и взаимодействия с DOM браузера. Но в принципе есть много общего.
- Компоненты можно инициализировать (stream(initialState))
- Компоненты могут хранить локальное состояние (stream())
- Состояние компонентов можно преобразовать с помощью методов жизненного цикла в новое представление (stream.map(...))
- Компоненты имеют логику исполняемую при удалении (stream.end.map(...))
Рассматривая компоненты как объекты со скрытым состоянием, мы можем извлекать и изменять это состояние методами жизненного цикла (render
например). Концептуально, мы можем преобразовать (map
) один компонент в другой, при этом изменяя состояние компонента.
Даже при изменении состояние компонента методами жизненного цикла, побочные эффекты инкапсулируются в функциях преобразования. Это не совсем, но очень похоже на то, как работает функтор.
Это полезная аналогия, она помогает понять, что мы зачастую можем использовать потоки вместо компонентов, и наоборот.
Преимущества использования поток очевидны, они просты и предсказуемы, и точно подходят для работы с UI.
Понимание того, как и где можно можно взаимозаменяемо использовать два этих инструмента, сродни интуитивному пониманию, где и когда взаимозаменяемо можно использовать хуки и компоненты.
Мы можем выбирать тот или иной инструмент в зависимости от контекста и наших нужд, только зная, что такая альтернатива существует.
Финал примера
В примере Дэна счетчик останавливается если delay принимает значение null.
Изменим нашу функцию таким же образом.
function useInterval({ delay }){
const id = stream()
const tick = stream()
delay.map(
delay => {
clearInterval(id())
// Only bind setInterval if delay isn't null
if( delay !== null ) {
id(setInterval(tick, delay, delay))
}
}
)
delay.end.map(
() => clearInterval(id())
)
return tick
}
Теперь мы хотим, чтобы параметр delay
, передаваемый useInterval
принимал значение null, если в каком-нибудь другом потоке состояние имеет значение, эквивалентное логическому false
.
const theirDelay =
m.stream.merge([delay, running]).map(
([delay, running]) => running ? delay : null
)
const tick = useInterval({ delay: theirDelay })
stream.merge
комбинирует массив потоков, в новый поток значениями которого будет массив значений в каждом из исходных потоков. stream.merge.map
новый поток, который будет получать значения каждый раз когда любой из входных потоков изменит свое значение.
Заключение, мои предпочтения
Разрабатывая UI в последние 5, 6 лет, я обнаружил, что стал реже писать компоненты, поскольку потоки и их композиции использовать проще. И в основном, при разработке я опираюсь на представления (view) в виде функций и потоки. Обычно, я делаю один большой компонент верхнего уровня для всех маршрутов, в котором определяю ряд потоков, а дальше, все остальное — это просто функции. Случается, что я пользуюсь компонентами — если их интерфейс предпочтителен — в особенности, если нужно работать с DOM непосредственно, и как-то чистить за собой потом. Ориентировочно, я пишу компоненты 5-10% времени разработки, и для меня это работает.
Возможно, это кажется странным, учитывая то, как часто нам говорят, что нужно использовать компоненты.Но, по моему мнению, компоненты — это сложный интерфейс, приводящий к сложному коду. Используйте их когда сложность оправдана и имеет смысл, если нет, выбирайте самый простой инструмент для работы.
Когда появились хуки, казалось, что они решают реальные проблемы, но для меня: хуки — это проблемы которые мне не нужны. Я использую замыкания для локальных состояний, и потоки для композиции преобразований и эффектов. По моему мнению, потоки более мощная и точная абстракция. И я предпочитаю работать с потоками, хотя хуки и остаются интересным решением для ряда специфичных проблем. Поэтому стоит использовать оба решения, и выбрать то которое вам более подходит.
Я думаю, что хуки не стали бы изобретать, если бы в React были замыкания, просто нужда заставила, с замыканиями такой нужды нет.
Для иллюстрации, рекомендую прочесть выдержку из поста Дэна:
Refs to the rescue и вы увидите, что единственной причиной изобретения такого приема является тот факт, что контекст представлений (view) не имеет состояния. Если у нас есть замыкания, мы просто определяем хук в контексте и используем его в представлении. И нет нужды в ссылках или подсчете количества обращений к функции.
Есть такой подход, сравнивать значения узлов DOM, ссылок, свойств, что бы определить следующее состояние. Это работает, но не до конца, и не однозначно, и поэтому, это не элегантное решение. Замыкания и потоки позволяют нам точно знать, а не высчитывать, что изменилось. Возможно, потоки вначале слегка трудны для понимания, но, поняв, как они работают, вы сможете использовать их без сомнений и ограничений. Это прекрасная структура данных: для реакции на изменения состояния, взаимодействия между компонентами, и управления побочными эффектами. И, вероятно, самое главное — они простые.
Hooks очень занимательный инструмент, и они внесли значительный вклад в решение проблем, но лично я думаю, они того не стоят. Рекомендую поиграть с потоками в вашей разработке. И если это вам не подходит, используйте хуки, в конечном итоге сегодня это лучшее что есть.
Вы можете использовать mithril-stream, это независимый модуль, или более продвинутую библиотеку flyd.
Спасибо за то, что прочли этот текст. Надеюсь, он был интересным и полезным для вас.
mayorovp
Что-то потоки какие-то переусложненные и при этом довольно слабые.
Если сравнивать с rx, то, судя по приведенному коду, видно следующее:
Любой поток работает как BehaviorSubject, т.е. хранит последнее значение. Это вроде бы удобно — но совершенно бесполезно для потоков, которые не представляют какое-то значение (и пример такого потока — как раз useInterval).
Нет оператора switchMap, а ведь это один из наиболее важных операторов rx.js
Смотрите как могла выглядеть функция useInterval с оператором switchMap:
Не различаются окончание потока и его отмена.
aughing Автор
1. useInterval — это контекст для вычисления потока count()
// ссылка на поток tick
const tick = useInterval({ delay })
// здесь мы создаем дочерний поток для побочных действий
// каждый раз когда запускается новый интервал мы увеличиваем счетчик
tick.map(() => count( count() + 1 ))
// после каждого увеличения счетчика вызываем отрисовку
count.map(m.redraw)
2. switchMap реализован идеоматичным для потоков образом в контексте
theirDelay()
3. parent.end касается только потомков, сам поток родитель parent() остается активным
собственно end и это и есть отмена дочернего потока.
Что бы закрыть контекст дочернего потока, можно использовать stream.SKIP
parent.map( ()=> stream.SKIP )
mayorovp
И что из этого следует? Кстати, count тоже реализован неоптимально, специально же для таких случаев придуман scan:
Не вижу ничего похожего на switchMap.
Во-первых, нет, не остаётся. Если сделать
parent.end(true)
— то потокparent
перейдёт в состояние"ended"
, в котором он игнорирует любые попытки обновления.Во-вторых, как эти рассуждения отменяют невозможность различить что произошло с потоком, был ли он отменён снаружи или "иссяк" изнутри?
Нет,
stream.SKIP
не закрывает никакого контекста, это просто пропуск обновления.aughing Автор
1. контекст нужен, для того, что бы вычислять в контексте.
2. автор оригинала не хотел использовать scan, я так думаю.
3. theirDelay это не switchMap, это theirDelay, который можно реализовать даже не трогая контекст useInterval.
4. A stream can stop affecting its dependent streams by calling stream.end(true). This effectively removes the connection between a stream and its dependent streams.
mithril.js.org/stream.html#ended-state
5. нам не нужно вычислять, мы точно знаем, что хотим закрыть поток
6. A special value that can be returned to stream callbacks to skip execution of downstreams
You can prevent dependent streams from being updated by returning the special value stream.SKIP
var skipped = stream.combine(function(stream) {
return stream.SKIP
}, [stream(1)])
skipped.map(function() {
// never runs
})
mayorovp
Я всё ещё не понимаю что вы пытаетесь сказать.
Ну так где реализация switchMap-то? Вы писали, что он где-то там идеоматичным образом реализован.
И что из этого следует?
А если нужно?
И?
aughing Автор
Как нибудь выберу время, напишу заметку, как я использую потоки, ну и вообще как это можно использовать