Простая шина событий на JavaScript
2 года назад·3 мин. на чтение
Шины событий — это чрезвычайно полезный инструмент для разделения компонентов приложений.
Использование шины событий имеет свои плюсы и минусы. Поэтому она должна добавляться осторожно, иначе вы можете получить код, который будет трудно поддерживать и понимать.
Но нет никаких сомнений в том, что шина событий может значительно ускорить процесс прототипирования или улучшить архитектуру малого и среднего приложения. Большому приложению могут потребоваться некоторые дополнительные инструменты и подходы.
В этой статье рассмотрим как реализовать простую шину событий в JavaScript.
Идентификаторы используются для быстрой идентификации (за время O(1)) подписчика при вызове функции
Что такое шина событий (event bus)?
Шина событий реализует шаблон издатель/подписчик. Его можно использовать для разъединения компонентов приложения, так что компонент может реагировать на события, инициируемые другим компонентом, без их прямых зависимостей друг от друга. Им нужно только знать шину событий. Каждый подписчик может подписаться на определенное событие. Подписчик будет уведомлен, когда событие, на которое он подписан, будет опубликовано в шине событий. Издатель может публиковать события в шине событий, когда что-то происходит.Реализация шины событий
В этой реализации подписчик (subscriber) является простой функцией. Функция вызывается, когда интересующее событие публикуется в шине событий. Для сопоставления события с подписчиками можно использовать простой объект. Формат данных в этом объекте будет следующим:{ eventType: { id: callback } }
. Например:
События{ event1: { 1: func1, 2: func2 }, event2: { 3: func3 } }
event1
и event2
могут быть любого типа. В большинстве случаев имеет смысл использовать простые строки.
Подписчики func1
, func2
и func3
являются простыми JavaScript функциями.func1
и func2
являются подписчиками, подписанными на события типа event1
. func3
является подписчиком, подписанным на события типа event2
.
Идентификаторы 1
, 2
и 3
будут использоваться позже для отмены подписки на подписчиков.
Как подписаться на событие?
Функцияsubscribe
принимает в качестве аргументов интересующее событие и подписчика.
Функция возвращает объект, предоставляющий функцию отмены подписки - unsubscribe
. Функция unsubscribe
может быть вызвана для удаления зарегистрированного подписчика.
В этом примере используем генератор id для получения уникальных идентификаторов.Идентификаторы используются для быстрой идентификации (за время O(1)) подписчика при вызове функции
unsubscribe
.
Вместо использования генератора id можно рассмотреть реализацию на основе ES6 символов.
const subscriptions = {} const getNextUniqueId = getIdGenerator() function subscribe(eventType, callback) { const id = getNextUniqueId() // создаем новый элемент для eventType if(!subscriptions[eventType]) { subscriptions[eventType] = {} } // регистрируем функцию обратного вызова subscriptions[eventType][id] = callback return { unsubscribe: () => { delete subscriptions[eventType][id] if(Object.keys(subscriptions[eventType]).length === 0) { delete subscriptions[eventType] } } } }
Как опубликовать событие?
Функция публикации принимает в качестве аргументов событие и аргументы для подписчиков. Если подписчиков наeventType
нет, то просто завершаем функцию.
В противном случае перебираются идентификаторы подписчиков, зарегистрированных для eventType
, и вызывается каждая функция с предоставленными аргументами.
function publish(eventType, arg) { if(!subscriptions[eventType]) { return } Object.keys(subscriptions[eventType]) .forEach(id => subscriptions[eventType][id](arg)) }
Пример использования
В качестве простого примера предположим, что мы хотим печатать что-то в консоли каждый раз, когда появляется событие типаprint
.
Мы можем подписаться на событие следующим образом:
Затем мы можем выпустить событие, подобное этому:const subscription = EventBus.subscribe( "print", message => console.log(`printing: ${message}`) )
Если в какой-то момент мы захотим прекратить прослушивание события с типомEventBus.publish("print", "some text")
print
, мы можем отписаться следующим образом:
subscription.unsubscribe()
Полная реализация
Вот полная реализация этой шины событий. Он может быть легко преобразован в простую функцию Javascript.// eventBus.js const subscriptions = {} const getNextUniqueId = getIdGenerator() function subscribe(eventType, callback) { const id = getNextUniqueId() if(!subscriptions[eventType]) { subscriptions[eventType] = {} } subscriptions[eventType][id] = callback return { unsubscribe: () => { delete subscriptions[eventType][id] if(Object.keys(subscriptions[eventType]).length === 0) { delete subscriptions[eventType] } } } } function publish(eventType, arg) { if(!subscriptions[eventType]) { return } Object.keys(subscriptions[eventType]).forEach(key => subscriptions[eventType][key](arg)) } function getIdGenerator() { let lastId = 0 return function getNextUniqueId() { lastId += 1 return lastId } } module.exports = { publish, subscribe }
От JavaScript к Rust: руководство для начинающих
год назад·9 мин. на чтение
Узнаем о сходствах и различиях между Rust и JavaScript и расширим свой инструментарий программирования.
Rust, язык системного программирования, известный своей производительностью, надежностью и безопасностью памяти, набирает популярность среди разработчиков. Если вы JavaScript разработчик и хотите исследовать новые горизонты и добавить Rust в свой набор навыков, это руководство для вас. В этом подробном руководстве мы познакомимся с основами Rust и поймем, как он может помочь в процессе разработки.
Выражения
В Rust операторы Цикл
Ключевое слово
В этом примере мы инициализируем Цикл
Ключевое слово Выражения
Выражение
Зачем переходить с JavaScript на Rust?
JavaScript, де-факто язык Интернета, является динамичным, гибким и вездесущим. Это язык, который понимают браузеры, что делает его важным инструментом в арсенале веб-разработчика. Несмотря на это, JavaScript имеет свои ограничения, особенно когда речь идет о производительности и безопасности типов. Rust — системный язык программирования, который работает молниеносно, предотвращает ошибки сегментации и гарантирует безопасность потоков. Разработанный Mozilla, Rust неуклонно набирает обороты благодаря своей скорости, надежности, предлагая JavaScript разработчикам прочный мост к системному программированию.Установка Rust
Чтобы начать работу с Rust, вам необходимо настроить среду разработки. Первым делом необходимо установить необходимые инструменты. Rust предоставляет инструментrustup
, который помогает вам управлять различными версиями компилятора Rust. Выполните следующие действия, чтобы установить Rust:
- Посетите официальный сайт Rust: rustup.rs.
- Следуйте инструкциям на веб-сайте, чтобы загрузить и установить
rustup
для вашей операционной системы. - После установки
rustup
откройте терминал или командную строку и выполните командуrustup update
, чтобы убедиться, что у вас установлена последняя версия Rust. После установкиrustup
у вас будет доступ к трем важным командам:rustup
,rustc
иcargo
.rustup
позволяет вам управлять цепочкой инструментов компилятора Rust,rustc
— это сам компилятор Rust (хотя вам редко придется использовать его напрямую), аcargo
— это менеджер пакетов и система сборки Rust, похожая на npm в JavaScript.
Настройка проекта Rust
Теперь, когда у вас установлен Rust, давайте создадим новый проект Rust. Rust предоставляет мощный инструмент под названиемcargo
, который упрощает управление проектами и выстраивает процессы. Выполните следующие действия, чтобы создать новый проект Rust:
- Откройте терминал или командную строку и перейдите в каталог, в котором вы хотите создать проект.
- Выполните команду
cargo new my_project_name
, заменивmy_project_name
нужным именем для вашего проекта. cargo
создаст новый каталог с указанным именем проекта, содержащий необходимые файлы и папки для базовой структуры проекта Rust. Внутри каталога проекта вы найдете файлCargo.toml
, который служит конфигурационным файлом проекта, аналогичноpackage.json
в JavaScript. Он определяет метаданные проекта, зависимости и другие параметры. Кроме того, вы найдете каталогsrc
, который содержит файлы исходного кода для вашего проекта.
Написание первой программы на Rust
Давайте напишем простую программу «Hello, World!» на Rust, чтобы почувствовать синтаксис языка. Откройте файлsrc/main.rs
в каталоге проекта и замените его содержимое следующим кодом:
В Rust функцияfn main() { println!("Hello, World!"); }
main
является точкой входа для выполнения программы, аналогичная функции main
в JavaScript. Макрос println!
используется для печати сообщения "Hello, World!"
на консоли. Rust использует макросы для генерации кода, что позволяет создавать мощные и гибкие синтаксические расширения.
Чтобы запустить программу, откройте терминал или командную строку, перейдите в каталог проекта и выполните команду cargo run. Cargo скомпилирует ваш код и выполнит полученный двоичный файл. Вы должны увидеть сообщение «Hello, World!», напечатанное в консоли.
Поздравляю! Вы написали и запустили свою первую программу на Rust. Теперь давайте углубимся в некоторые ключевые понятия и синтаксис в Rust.
Переменные и типы данных в Rust
В Rust переменные по умолчанию неизменяемы (иммутабельны), что означает, что их значения не могут быть изменены после назначения. Однако вы можете использовать ключевое словоmut
(mutable, мутабельные) для создания изменяемых переменных. Давайте посмотрим на несколько примеров:
В этом примереfn main() { let name = "Alice"; // Иммутабельная переменная let mut age = 30; // Мутабельная переменная age += 1; // Инкремент переменной на 1 println!("Name: {}", name); println!("Age: {}", age); }
name
— это неизменяемая переменная, содержащая строку, а age
— это изменяемая переменная, содержащая целое число. Мы можем изменить значение age
с помощью оператора +=
. Макрос println!
снова используется для отображения значений переменных.
Rust предоставляет несколько встроенных примитивных типов данных, включая целые числа, числа с плавающей запятой, логические значения, символы и строки. Ниже приведен обзор некоторых распространенных типов данных:
- Целочисленные типы:
i8
,i16
,i32
,i64
,u8
,u16
,u32
,u64
- Типы чисел с плавающей запятой:
f32
,f64
- Логический тип:
bool
- Символьный тип:
char
- Строковый тип:
String
(изменяемая строка с динамическим размером) и фрагменты строки (&str
)
let variable: Type = value;
. Например:
fn main() { let x: i32 = 42; let pi: f64 = 3.14; let is_true: bool = true; let message: &str = "Hello, Rust!"; }
Поток управления
Как и JavaScript, Rust предоставляет различные операторы потока управления, включая выраженияif
, циклы for
и while
и выражения match
. Давайте рассмотрим каждый из них.
Выражения if
В Rust операторы if
— это выражения, которые вычисляют условие и выполняют блок кода на основе результата. Вот пример:
В этом примере мы проверяем, делится ли переменнаяfn main() { let number = 5; if number % 2 == 0 { println!("The number is even"); } else { println!("The number is odd"); } }
number
на 2
. Если это так, мы печатаем, что число четное; В противном случае мы печатаем, что это нечетное.
Цикл loop
Ключевое слово loop
создает бесконечный цикл, который продолжается до явного завершения. Вы можете использовать ключевое слово break
, чтобы выйти из цикла. Вот пример:
fn main() { let mut count = 0; loop { println!("Count: {}", count); count += 1; if count >= 5 { break; } } }
count
равным 0
и используем loop
для многократного вывода ее значения. Цикл продолжается до тех пор, пока count
не достигнет 5
, после чего мы выходим из цикла с помощью оператора break
.
Цикл while
Ключевое слово while
позволяет создать цикл, который выполняется до тех пор, пока выполняется определенное условие. Вот пример:
В этом примере цикл продолжается до тех пор, покаfn main() { let mut count = 0; while count < 5 { println!("Count: {}", count); count += 1; } }
count
меньше 5
. Мы увеличиваем count
на 1
в каждой итерации и выводим его значение.
Выражения match
Выражение match
Rust похоже на оператор switch
JavaScript, но с более мощными возможностями сопоставления шаблонов (pattern matching). Он позволяет сопоставлять значение с набором шаблонов и выполнять код на основе совпадающего шаблона. Давайте посмотрим на пример:
В этом примере мы сопоставляем значение переменнойfn main() { let number = 2; match number { 1 => println!("One"), 2 => println!("Two"), 3 => println!("Three"), _ => println!("Other"), } }
number
с различными шаблонами. Если значение равно 1
, печатаем "One"
. Если это 2
, мы печатаем "Two"
. Если это 3
, мы печатаем "Three"
. Если ни один из шаблонов не совпадает, представленный символом подчеркивания _
, мы печатаем "Other"
.
Выражения match
могут быть более сложными, что позволяет сопоставлять шаблоны с перечислениями, диапазонами и даже захватывать переменные. Это мощная конструкция, которая может обрабатывать широкий спектр сценариев.
Выражения
Еще одним важным аспектом Rust является его природа, основанная на выражениях (expression-based), которая отличается от подхода JavaScript, основанного на утверждениях (statement-based). В Rust почти все является выражением, включая блоки кода. Это позволяет создавать мощные и лаконичные шаблоны. Для примера сравним простой пример потока управления в JavaScript и Rust:В этом примере Rust мы определяем блок кода с помощью фигурных скобокlet x = { let y = 1 + 1; y * y }; if x == 4 { println!("Hello, world!"); }
{}
. Последнее выражение внутри блока (y * y
) становится значением всего блока, которое затем присваивается переменной x
.
Функции и модули
Функции в Rust определяются с помощью ключевого словаfn
, аналогично JavaScript. Давайте посмотрим на пример:
fn greet(name: &str) { println!("Hello, {}!", name); }
В этом примере мы определяем функциюfn main() { greet("Alice"); greet("Bob"); }
greet
, которая принимает строковый фрагмент (&str
) в качестве параметра и выводит приветственное сообщение. Затем мы дважды вызываем функцию greet
в main
функции с разными именами.
Rust также поддерживает определение модулей, которые помогают организовать и структурировать ваш код. Модули позволяют группировать связанные функции, типы и другие элементы вместе. Ниже приведен пример определения модуля:
mod math { pub fn add(a: i32, b: i32) -> i32 { a + b } pub fn subtract(a: i32, b: i32) -> i32 { a - b } }
В этом примере мы определяемfn main() { let result = math::add(5, 3); println!("Result: {}", result); }
math
модуль, который содержит две функции: add
и subtract
. Эти функции выполняют основные арифметические действия. Ключевое слово pub
указывает, что функции являются общедоступными и к ним можно получить доступ из-за пределов модуля.
В main
функции вызываем функцию add
из модуля math
, передавая аргументы 5
и 3
. Результат сохраняется в переменной result
, которую затем выводим на консоль.
Модули позволяют упорядочить код в логические блоки, упрощая управление и обслуживание более крупных проектов. Вы можете вкладывать модули в другие модули, чтобы создать иерархическую структуру.
Обработка ошибок
Обработка ошибок в Rust является значительным улучшением по сравнению с JavaScript. В то время как JavaScript выбрасывает и ловит ошибки, Rust использует механизм под названием «Result» для обработки ошибок, который представляет собой либо успешное значение (Ok
), либо ошибку (Err
). Это похоже на использование блоков try
/catch
в JavaScript.
В этом примере мы определяем функциюuse std::fs::File; use std::io::Read; fn read_file_contents(file_path: &str) -> Result<String, std::io::Error> { let mut file = File::open(file_path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; Ok(contents) } fn main() { match read_file_contents("data.txt") { Ok(contents) => println!("File contents: {}", contents), Err(error) => eprintln!("Error: {}", error), } }
read_file_contents
, которая считывает содержимое файла с указанием пути к нему. Функция возвращает тип Result
, где успешным значением является String
, содержащая содержимое файла, а тип ошибки — std::io::Error
.
Внутри функции мы используем оператор ?
для распространения любых ошибок, возникающих во время операций с файлами. Если произойдет ошибка, функция немедленно вернет значение ошибки.
В main
функции мы обрабатываем Result
с помощью выражения match
. Result
в Ok
, мы распечатываем содержимое файла. Err
, мы печатаем сообщение об ошибке.
Используя тип Result
и распространение ошибок, Rust обеспечивает обработку ошибок и делает их явными в коде. Это приводит к более надежным программам.
Управление памятью
Одной из выдающихся особенностей Rust является его система, которая обеспечивает безопасность памяти и устраняет распространенные проблемы, такие как исключения null указателей и гонки данных. У каждого значения в Rust есть один владелец. Когда владелец выходит за пределы области, значение очищается. Это избавляет от необходимости сборки мусора. Заимствование позволяет временно заимствовать ссылку на значение, не вступая в права собственности. Rust применяет строгие правила заимствования во время компиляции, чтобы предотвратить гонку данных и недопустимый доступ к памяти. Время жизни гарантирует, что заимствованные ссылки остаются действительными до тех пор, пока они используются. Они позволяют компилятору определять время жизни ссылок и предотвращают висячие ссылки. Вот простой пример, демонстрирующий владение и заимствование в Rust:В этом примере функцияfn process_string(s: String) { println!("Processing: {}", s); } // s остается за пределами и уничтожается fn main() { let message = String::from("Hello, Rust!"); process_string(message); // Владение `message` передается функции // println!("Message: {}", message); // Эта строка приводит к ошибке компиляции }
process_string
становится владельцем String
параметра s
. Как только вызов функции завершен, s
выходит за пределы области и удаляется. Если мы попытаемся использовать message
после его передачи функции, это приведет к ошибке компиляции, потому что право собственности было передано.
Чтобы позаимствовать ссылку на значение без передачи права собственности, мы можем использовать ссылки (&
). Вот пример:
В этом примере мы определяем функциюfn process_string(s: &str) { println!("Processing: {}", s); } fn main() { let message = String::from("Hello, Rust!"); process_string(&message); // Позаимствовать ссылку на `message` println!("Message: {}", message); // `message` все еще доступен }
process_string
для получения ссылки на строковый фрагмент (&str
). Вместо того, чтобы передавать String
напрямую, мы передаем ссылку на String
с помощью оператора &
. Это позволяет функции заимствовать значение, не становясь владельцем.
Используя ссылки и систему владения, Rust обеспечивает безопасность памяти и устраняет многие распространенные ошибки программирования, связанные с управлением памятью.
Асинхронное программирование
Rust также поддерживает синтаксисasync
/await
, но его работа по стандартизации все еще продолжается, и многие вещи еще не работают или могут привести к ошибкам. Но в настоящее время сообщество предоставляет мощное асинхронное решение, такое как tokio
.
Например, асинхронные привязки ввода-вывода встроены в Node.js, а в Rust — нет. Но мы можем использовать различные мощные асинхронные среды выполнения, например, использовать async-std
для написания простого примера ниже:
Rust «Futures» (аналог Promises) начинают выполняться только тогда, когда они вызываются сuse async_std::fs; #[async_std::main] async fn main() { match fs::read_to_string("./hello.txt").await { Ok(file) => println!("read {} chars", file.len()), Err(e) => eprintln!("{}", e), }; }
.await
.
Параллелизм и многопоточность
Rust отлично поддерживает параллельное программирование и предоставляет безопасные абстракции для работы с потоками и общими данными. Модульstd::thread
позволяет создавать потоки в Rust и управлять ими.
Ниже приведен пример использования потоков для параллельного выполнения:
В этом примере мы создаем новый поток с помощьюuse std::thread; fn main() { let handle = thread::spawn(|| { for i in 1..=5 { println!("Thread: {}", i); } }); for i in 1..=5 { println!("Main: {}", i); } handle.join().unwrap(); // Ожидаем пока thread закончит выполнение }
thread::spawn
и одновременно выполняем отдельный блок кода. Основной поток и порожденный поток печатают свои соответствующие номера, демонстрируя параллельное выполнение.