stream()
Описание
Поток (Stream) — это реактивная структура данных, аналогичная ячейкам в табличных редакторах.
Например, в электронной таблице, если A1 = B1 + C1
, то изменение значения B1
или C1
автоматически изменяет значение A1
.
Аналогично, можно создать поток, зависящий от других потоков, чтобы изменение значения одного из них автоматически обновляло другой. Это полезно, когда у вас есть ресурсоемкие вычисления, и вы хотите запускать их только при необходимости, а не при каждой перерисовке.
Потоки не входят в основной дистрибутив Mithril.js. Чтобы использовать модуль Streams, подключите его:
var Stream = require('mithril/stream');
Вы также можете загрузить модуль напрямую, если ваша среда не поддерживает инструменты сборки:
<script src="https://unpkg.com/mithril/stream/stream.js"></script>
При прямой загрузке с помощью тега <script>
(а не через require
), библиотека потоков будет доступна как window.m.stream
. Если window.m
уже определен (например, потому что вы также используете основной скрипт Mithril.js), он будет присоединен к существующему объекту. В противном случае будет создан новый window.m
. Если вы хотите использовать потоки вместе с Mithril.js через теги <script>
, вам следует подключить Mithril.js перед mithril/stream
, иначе mithril
перезапишет объект window.m
, определенный mithril/stream
. Это не является проблемой, когда библиотеки используются как модули CommonJS (с использованием require(...)
).
Сигнатура
Создает поток
stream = Stream(value)
Аргумент | Тип | Обязательный | Описание |
---|---|---|---|
value | any | Нет | Если этот аргумент указан, значение потока устанавливается равным ему |
возвращает | Stream | Возвращает поток |
Статические члены
Stream.combine
Создает вычисляемый поток, который реактивно обновляется при изменении любого из его родительских потоков. См. Объединение потоков
stream = Stream.combine(combiner, streams)
Аргумент | Тип | Обязательный | Описание |
---|---|---|---|
combiner | (Stream..., Array) -> any | Да | См. аргумент combiner |
streams | Array<Stream> | Да | Список потоков для объединения |
возвращает | Stream | Возвращает поток |
combiner
Определяет, как генерируется значение вычисляемого потока. См. Объединение потоков
any = combiner(streams..., changed)
Аргумент | Тип | Обязательный | Описание |
---|---|---|---|
streams... | splat of Streams | Нет | Набор из нуля или более потоков, соответствующих потокам, переданным в качестве второго аргумента в stream.combine |
changed | Array<Stream> | Да | Список потоков, которые были изменены |
возвращает | any | Возвращает вычисленное значение |
Stream.merge
Создает поток, значение которого является массивом значений из массива потоков
stream = Stream.merge(streams)
Аргумент | Тип | Обязательный | Описание |
---|---|---|---|
streams | Array<Stream> | Да | Список потоков |
возвращает | Stream | Возвращает поток, значение которого является массивом значений входных потоков |
Stream.scan
Создает новый поток с результатами вызова функции для каждого значения в потоке, используя аккумулятор и входящее значение.
Обратите внимание, что вы можете предотвратить обновление зависимых потоков, вернув специальное значение stream.SKIP
внутри функции аккумулятора.
stream = Stream.scan(fn, accumulator, stream)
Аргумент | Тип | Обязательный | Описание |
---|---|---|---|
fn | (accumulator, value) -> result | SKIP | Да | Функция, которая принимает параметр аккумулятора и значения и возвращает новое значение аккумулятора того же типа |
accumulator | any | Да | Начальное значение для аккумулятора |
stream | Stream | Да | Поток, содержащий значения |
возвращает | Stream | Возвращает новый поток, содержащий результат |
Stream.scanMerge
Принимает массив пар потоков и функций сканирования и объединяет все эти потоки с использованием заданных функций в один поток.
stream = Stream.scanMerge(pairs, accumulator)
Аргумент | Тип | Обязательный | Описание |
---|---|---|---|
pairs | Array<[Stream, (accumulator, value) -> value]> | Да | Массив кортежей потока и функций сканирования |
accumulator | any | Да | Начальное значение для аккумулятора |
возвращает | Stream | Возвращает новый поток, содержащий результат |
Stream.lift
Создает вычисляемый поток, который реактивно обновляется при изменении любого из его родительских потоков. См. Объединение потоков. В отличие от combine
, входные потоки передаются как переменное число аргументов (а не массив), а функция обратного вызова получает значения потоков, а не сами потоки. Параметр changed
отсутствует. Как правило, это более удобная функция для приложений, чем combine
.
stream = Stream.lift(lifter, stream1, stream2, ...)
Аргумент | Тип | Обязательный | Описание |
---|---|---|---|
lifter | (any...) -> any | Да | См. аргумент lifter |
streams... | list of Streams | Да | Потоки для lift |
возвращает | Stream | Возвращает поток |
lifter
Определяет, как генерируется значение вычисляемого потока. См. Объединение потоков
any = lifter(streams...)
Аргумент | Тип | Обязательный | Описание |
---|---|---|---|
streams... | splat of Streams | Нет | Набор из нуля или более значений, соответствующих значениям потоков, переданных в stream.lift |
возвращает | any | Возвращает вычисленное значение |
Stream.SKIP
Специальное значение, которое можно вернуть в обратных вызовах потока, чтобы пропустить выполнение зависимых потоков
Stream["fantasy-land/of"]
Этот метод функционально идентичен stream
. Он существует для соответствия спецификации Applicative Fantasy Land. Для получения дополнительной информации см. раздел Что такое Fantasy Land.
stream = Stream["fantasy-land/of"](value)
Аргумент | Тип | Обязательный | Описание |
---|---|---|---|
value | any | Нет | Если этот аргумент указан, значение потока устанавливается равным ему |
возвращает | Stream | Возвращает поток |
Члены экземпляра
stream.map
Создает зависимый поток, значение которого устанавливается равным результату функции обратного вызова. Этот метод является псевдонимом для stream["fantasy-land/map"].
dependentStream = stream().map(callback)
Аргумент | Тип | Обязательный | Описание |
---|---|---|---|
callback | any -> any | Да | Обратный вызов, возвращаемое значение которого становится значением потока |
возвращает | Stream | Возвращает поток |
stream.end
Связанный поток, который дерегистрирует зависимые потоки при установке значения true. См. Завершенное состояние.
endStream = stream().end
stream["fantasy-land/of"]
Этот метод функционально идентичен stream
. Он существует для соответствия спецификации Applicative Fantasy Land. Для получения дополнительной информации см. раздел Что такое Fantasy Land.
stream = stream()["fantasy-land/of"](value)
Аргумент | Тип | Обязательный | Описание |
---|---|---|---|
value | any | Нет | Если этот аргумент указан, значение потока устанавливается равным ему |
возвращает | Stream | Возвращает поток |
stream["fantasy-land/map"]
Создает зависимый поток, значение которого устанавливается равным результату функции обратного вызова. См. Цепочка потоков
Этот метод существует для соответствия спецификации Applicative Fantasy Land. Для получения дополнительной информации см. раздел Что такое Fantasy Land.
dependentStream = stream()["fantasy-land/map"](callback)
Аргумент | Тип | Обязательный | Описание |
---|---|---|---|
callback | any -> any | Да | Обратный вызов, возвращаемое значение которого становится значением потока |
возвращает | Stream | Возвращает поток |
stream["fantasy-land/ap"]
Название этого метода означает apply
(применить). Если поток a
имеет функцию в качестве своего значения, другой поток b
может использовать его в качестве аргумента для b.ap(a)
. Вызов ap
вызовет функцию со значением потока b
в качестве аргумента и вернет другой поток, значение которого является результатом вызова функции. Этот метод существует для соответствия спецификации Applicative Fantasy Land. Для получения дополнительной информации см. раздел Что такое Fantasy Land.
stream = stream()["fantasy-land/ap"](apply)
Аргумент | Тип | Обязательный | Описание |
---|---|---|---|
apply | Stream | Да | Поток, значение которого является функцией |
возвращает | Stream | Возвращает поток |
Основное использование
Потоки не являются частью основного дистрибутива Mithril.js. Чтобы включить их в проект, подключите модуль:
var stream = require('mithril/stream');
Потоки как переменные
stream()
возвращает поток. На базовом уровне поток работает аналогично переменной или свойству с геттером и сеттером: он может хранить состояние, которое можно изменять.
var username = stream('John');
console.log(username()); // выводит "John"
username('John Doe');
console.log(username()); // выводит "John Doe"
Основное отличие состоит в том, что поток является функцией и, следовательно, может быть скомпонован в функции более высокого порядка.
var users = stream();
// запросить пользователей с сервера с помощью fetch API
fetch('/api/users')
.then(function (response) {
return response.json();
})
.then(users);
В приведенном выше примере поток users
заполняется данными ответа, когда запрос завершается успешно.
Двунаправленная привязка
Потоки также могут быть заполнены данными из функций обратного вызова событий и т.п.
// поток
var user = stream('');
// двунаправленная привязка к потоку
m('input', {
oninput: function (e) {
user(e.target.value);
},
value: user(),
});
В приведенном выше примере, когда пользователь вводит данные в поле ввода, значение потока user
обновляется значением поля ввода.
Вычисляемые свойства
Потоки полезны для реализации вычисляемых свойств:
var title = stream('');
var slug = title.map(function (value) {
return value.toLowerCase().replace(/\W/g, '-');
});
title('Hello world');
console.log(slug()); // выводит "hello-world"
В приведенном выше примере значение slug
вычисляется при обновлении title
, а не при чтении slug
.
Конечно, также возможно вычислять свойства на основе нескольких потоков:
var firstName = stream('John');
var lastName = stream('Doe');
var fullName = stream.merge([firstName, lastName]).map(function (values) {
return values.join(' ');
});
console.log(fullName()); // выводит "John Doe"
firstName('Mary');
console.log(fullName()); // выводит "Mary Doe"
Вычисляемые свойства в Mithril.js обновляются атомарно: потоки, зависящие от нескольких потоков, никогда не будут вызываться более одного раза за обновление значения, независимо от сложности графа зависимостей вычисляемого свойства.
Цепочка потоков
Потоки можно связывать в цепочки с помощью метода map
. Связанный поток также известен как зависимый поток.
// родительский поток
var value = stream(1);
// зависимый поток
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // выводит 2
Зависимые потоки реактивны: их значения обновляются каждый раз, когда обновляется значение их родительского потока. Это происходит независимо от того, был ли зависимый поток создан до или после установки значения родительского потока.
Вы можете предотвратить обновление зависимых потоков, вернув специальное значение stream.SKIP
var skipped = stream(1).map(function (value) {
return stream.SKIP;
});
skipped.map(function () {
// никогда не будет запущено
});
Объединение потоков
Потоки могут зависеть более чем от одного родительского потока. Такие потоки можно создать с помощью stream.merge()
var a = stream('hello');
var b = stream('world');
var greeting = stream.merge([a, b]).map(function (values) {
return values.join(' ');
});
console.log(greeting()); // выводит "hello world"
Или вы можете использовать вспомогательную функцию stream.lift()
var a = stream('hello');
var b = stream('world');
var greeting = stream.lift(
function (_a, _b) {
return _a + ' ' + _b;
},
a,
b
);
console.log(greeting()); // выводит "hello world"
Существует также метод более низкого уровня под названием stream.combine()
, который предоставляет сами потоки в реактивных вычислениях для более продвинутых случаев использования.
var a = stream(5);
var b = stream(7);
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // выводит 12
Поток может зависеть от любого количества потоков, и гарантируется, что он будет обновляться атомарно. Например, если поток A имеет два зависимых потока B и C, а четвертый поток D зависит как от B, так и от C, поток D обновится только один раз при изменении значения A. Это гарантирует, что обратный вызов для потока D никогда не будет вызван с нестабильными значениями, например, когда B имеет новое значение, а C имеет старое значение. Атомарность также обеспечивает преимущества в производительности, заключающиеся в том, что зависимые потоки не пересчитываются без необходимости.
Вы можете предотвратить обновление зависимых потоков, вернув специальное значение stream.SKIP
var skipped = stream.combine(
function (stream) {
return stream.SKIP;
},
[stream(1)]
);
skipped.map(function () {
// никогда не будет запущено
});
Состояния потока
В любой момент времени поток может находиться в одном из трех состояний: в ожидании (pending), активен (active) и завершен (ended).
Состояние ожидания
Ожидающие потоки можно создать, вызвав stream()
без параметров.
var pending = stream();
Если поток зависит более чем от одного потока, и какой-либо из его родительских потоков находится в состоянии ожидания, зависимый поток также находится в состоянии ожидания и не обновляет свое значение.
var a = stream(5);
var b = stream(); // ожидающий поток
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // выводит undefined
В приведенном выше примере added
является ожидающим потоком, потому что его родительский поток b
также находится в состоянии ожидания.
Это также относится к зависимым потокам, созданным с помощью stream.map
:
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // выводит undefined, потому что `doubled` находится в состоянии ожидания
Активное состояние
Когда поток получает значение, он становится активным (если поток не завершен).
var stream1 = stream('hello'); // stream1 активен
var stream2 = stream(); // stream2 начинается в состоянии ожидания
stream2('world'); // затем становится активным
Зависимый поток с несколькими родителями становится активным, если все его родители активны.
var a = stream('hello');
var b = stream();
var greeting = stream.merge([a, b]).map(function (values) {
return values.join(' ');
});
В приведенном выше примере поток a
активен, но b
находится в состоянии ожидания. Установка b("world")
приведет к тому, что b
станет активным, и, следовательно, greeting
также станет активным и будет обновлен до значения "hello world"
Завершенное состояние
Поток может перестать влиять на свои зависимые потоки, вызвав stream.end(true)
. Это эффективно удаляет связь между потоком и его зависимыми потоками.
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
value.end(true); // установить в завершенное состояние
value(5);
console.log(doubled());
// выводит undefined, потому что `doubled` больше не зависит от `value`
Завершенные потоки по-прежнему имеют семантику контейнера состояния, т.е. вы все еще можете использовать их в качестве геттера и сеттера, даже после их завершения.
var value = stream(1);
value.end(true); // установить в завершенное состояние
console.log(value(1)); // выводит 1
value(2);
console.log(value()); // выводит 2
Завершение потока может быть полезно в случаях, когда поток имеет ограниченный срок службы (например, реагирование на события mousemove
только во время перетаскивания элемента DOM, но не после его отпускания).
Сериализация потоков
Потоки реализуют метод .toJSON()
. Когда поток передается в качестве аргумента в JSON.stringify()
, значение потока сериализуется.
var value = stream(123);
var serialized = JSON.stringify(value);
console.log(serialized); // выводит 123
Потоки не запускают рендеринг
В отличие от таких библиотек, как Knockout, потоки Mithril.js не запускают повторный рендеринг шаблонов. Перерисовка происходит в ответ на обработчики событий, определенные в представлениях компонентов Mithril.js, изменения маршрута или после разрешения вызовов m.request
.
Если требуется перерисовка в ответ на другие асинхронные события (например, setTimeout
/setInterval
, подписка на websocket, обработчик событий сторонней библиотеки и т. д.), вам следует вручную вызвать m.redraw()
.
Что такое Fantasy Land
Fantasy Land определяет совместимость общих алгебраических структур. Проще говоря, это означает, что библиотеки, соответствующие спецификациям Fantasy Land, можно использовать для написания универсального кода в функциональном стиле, независимо от того, как эти библиотеки реализуют конструкции.
Например, скажем, мы хотим создать универсальную функцию под названием plusOne
. Наивная реализация будет выглядеть так:
function plusOne(a) {
return a + 1;
}
Проблема с этой реализацией заключается в том, что ее можно использовать только с числом. Однако возможно, что любая логика, которая создает значение для a
, может также создавать состояние ошибки (завернутое в Maybe или Either из такой библиотеки, как Sanctuary или Ramda-Fantasy), или это может быть поток Mithril.js, поток Flyd и т. д. В идеале, мы не хотели бы писать аналогичную версию одной и той же функции для каждого возможного типа, который может иметь a
, и мы не хотели бы писать код для обертывания/развертывания/обработки ошибок повторно.
Здесь может помочь Fantasy Land. Давайте перепишем эту функцию с точки зрения алгебры Fantasy Land:
var fl = require('fantasy-land');
function plusOne(a) {
return a[fl.map](function (value) {
return value + 1;
});
}
Теперь этот метод работает с любым Functor, совместимым с Fantasy Land, таким как R.Maybe
, S.Either
, stream
и т. д.
Этот пример может показаться запутанным, но это компромисс в сложности: наивная реализация plusOne
имеет смысл, если у вас простая система и вы только увеличиваете числа, но реализация Fantasy Land становится более мощной, если у вас большая система со многими абстракциями обертки и повторно используемыми алгоритмами.
При принятии решения о том, следует ли использовать Fantasy Land, следует учитывать знакомство команды с функциональным программированием и реалистично оценивать уровень дисциплины, который команда готова поддерживать для обеспечения качества кода (в сравнении с необходимостью быстрой разработки новых функций и соблюдения сроков). Функциональный стиль программирования в значительной степени зависит от компиляции, организации и освоения большого набора небольших, точно определенных функций и, следовательно, не подходит для команд, у которых нет надежной практики документирования и/или отсутствует опыт работы с функционально ориентированными языками.