Новая книга наконец-то готова.Сегодня у меня есть немного свободного времени.Я посвящу вам статью о стиле языка JavaScript.Главным героем является декларативная разработка функций. Мы берем простую объектно-ориентированную систему EventEmitter шаг за шагом, чтобы преобразовать ее в функциональный стиль. И в сочетании с примерами, чтобы проиллюстрировать отличные возможности функционала.
Гибкий JavaScript и его мультипарадигма
Я считаю, что понятие "функциональный" уже знакомо многим front-end разработчикам: мы знаем, что JavaScript - очень гибкий и мультипарадигмальный язык. В этой статье будут показаны императивный языковой стиль и декларативный язык в JavaScript. Цель перехода Стили должны помочь читателям понять соответствующие характеристики этих двух разных языковых режимов, чтобы сделать разумный выбор в повседневной разработке и использовать максимальную мощь JavaScript.
Для удобства объяснения мы начнем с типичной системы публикации и подписки на события и шаг за шагом завершим преобразование функционального стиля. Система публикации и подписки на события, так называемый режим наблюдателя (режим Pub/Sub), придерживается идеи, управляемой событиями, и реализует концепцию «высокой связанности и низкой связанности».
Если читатель не знаком с этим режимом, рекомендуется сначала прочитать мою оригинальную статью:Исследуйте исходный код механизма событий Node.js Создайте собственную систему публикации событий и подписки. Эта статья начинается с исходного кода модуля событий Node.js, анализирует реализацию системы публикации событий и подписки, а также реализует императивную объектно-ориентированную публикацию событий и ответчик на основе синтаксиса ES Next. Для этого основного содержания эта статья не будет слишком расширяться.
Типичные проблемы EventEmitter и модернизации
Основываясь на понимании идеи реализации системы публикации событий и подписки, давайте рассмотрим простую и типичную базовую реализацию:
class EventManager {
construct (eventMap = new Map()) {
this.eventMap = eventMap;
}
addEventListener (event, handler) {
if (this.eventMap.has(event)) {
this.eventMap.set(event, this.eventMap.get(event).concat([handler]));
} else {
this.eventMap.set(event, [handler]);
}
}
dispatchEvent (event) {
if (this.eventMap.has(event)) {
const handlers = this.eventMap.get(event);
for (const i in handlers) {
handlers[i]();
}
}
}
}
Приведенный выше код реализует класс EventManager: мы поддерживаем карту событий типа Map для поддержки всех функций обратного вызова (обработчиков) различных событий.
- Метод addEventListener сохраняет функцию обратного вызова указанного события;
- Метод dispatchEvent выполняет свои функции обратного вызова одну за другой для указанных триггерных событий.
На уровне потребления:
const em = new EventManager();
em.addEventListner('hello', function() {
console.log('hi');
});
em.dispatchEvent('hello'); // hi
Их легче понять. Ниже наши задачи:
- Преобразуйте приведенные выше 20 строк императивного кода в 7 строк декларативного кода с 2 выражениями;
- Больше не используйте {...} и условия if;
- Реализовано с помощью чистых функций, чтобы избежать побочных эффектов;
- Используйте унарные функции, то есть в уравнении функции требуется только один параметр;
- Сделайте функции составными;
- Реализация кода должна быть чистой, элегантной и мало связанной.
Давайте посмотрим на окончательный карта сравнения результатов:
После того, как мы шаг за шагом предпримем введение этого процесса преобразования.
Шаг 1: Используйте функции вместо классов
Из-за вышеуказанных проблем addEventListener и dispatchEvent больше не отображаются как методы класса EventManager, а становятся двумя независимыми функциями с eventMap в качестве переменной:
const eventMap = new Map();
function addEventListener (event, handler) {
if (eventMap.has(event)) {
eventMap.set(event, eventMap.get(event).concat([handler]));
} else {
eventMap.set(event, [handler]);
}
}
function dispatchEvent (event) {
if (eventMap.has(event)) {
const handlers = this.eventMap.get(event);
for (const i in handlers) {
handlers[i]();
}
}
}
В соответствии с требованием модульности мы можем экспортировать эти две функции:
export default {addEventListener, dispatchEvent};
В то же время используйте импорт для введения зависимостей.Обратите внимание, что реализация импорта представляет собой шаблон синглтона (singleton):
import * as EM from './event-manager.js';
EM.dispatchEvent('event');
Поскольку модуль является одноэлементным, внутренняя переменная eventMap совместно используется при импорте разных файлов, что в точности соответствует ожиданиям.
Шаг 2: Используйте стрелочные функции
Стрелочные функции отличаются от традиционных функциональных выражений и больше соответствуют функциональному «вкусу»:
const eventMap = new Map();
const addEventListener = (event, handler) => {
if (eventMap.has(event)) {
eventMap.set(event, eventMap.get(event).concat([handler]));
} else {
eventMap.set(event, [handler]);
}
}
const dispatchEvent = event => {
if (eventMap.has(event)) {
const handlers = eventMap.get(event);
for (const i in handlers) {
handlers[i]();
}
}
}
Обратите особое внимание на привязку стрелочных функций к этому здесь. Конечно, сама стрелочная функция также называется лямбда-функцией, что очень «функционально» из названия.
Шаг 3: Удалите побочные эффекты и увеличьте возвращаемое значение
Чтобы обеспечить чистые характеристики функции, отличные от вышеописанной обработки, мы больше не можем изменять eventMap, а должны возвращать новую переменную типа Map, и в то же время изменять параметры методов addEventListener и dispatchEvent, а также добавлять «предыдущее состояние» eventMap, чтобы вывести новую карту событий:
const addEventListener = (event, handler, eventMap) => {
if (eventMap.has(event)) {
return new Map(eventMap).set(event, eventMap.get(event).concat([handler]));
} else {
return new Map(eventMap).set(event, [handler]);
}
}
const dispatchEvent = (event, eventMap) => {
if (eventMap.has(event)) {
const handlers = eventMap.get(event);
for (const i in handlers) {
handlers[i]();
}
}
return eventMap;
}
Да, этот процесс очень похож на функцию редьюсера в Redux. Сохранение чистоты функции — чрезвычайно важный момент в функциональной философии.
Шаг 4: Удалите декларативный цикл for
Далее мы используем forEach вместо цикла for:
const addEventListener = (event, handler, eventMap) => {
if (eventMap.has(event)) {
return new Map(eventMap).set(event, eventMap.get(event).concat([handler]));
} else {
return new Map(eventMap).set(event, [handler]);
}
}
const dispatchEvent = (event, eventMap) => {
if (eventMap.has(event)) {
eventMap.get(event).forEach(a => a());
}
return eventMap;
}
Шаг 5: Примените бинарные операторы
Мы используем || и &&, чтобы сделать код более кратким и интуитивно понятным:
const addEventListener = (event, handler, eventMap) => {
if (eventMap.has(event)) {
return new Map(eventMap).set(event, eventMap.get(event).concat([handler]));
} else {
return new Map(eventMap).set(event, [handler]);
}
}
const dispatchEvent = (event, eventMap) => {
return (
eventMap.has(event) &&
eventMap.get(event).forEach(a => a())
) || event;
}
Особое внимание следует уделить выражению оператора return, которое обычно обрабатывается:
return (
eventMap.has(event) &&
eventMap.get(event).forEach(a => a())
) || event;
Шаг 6: используйте тернарный оператор вместо if
Как может существовать такое императивное «уродливое»?Мы используем тернарный оператор, чтобы быть более интуитивным и кратким:
const addEventListener = (event, handler, eventMap) => {
return eventMap.has(event) ?
new Map(eventMap).set(event, eventMap.get(event).concat([handler])) :
new Map(eventMap).set(event, [handler]);
}
const dispatchEvent = (event, eventMap) => {
return (
eventMap.has(event) &&
eventMap.get(event).forEach(a => a())
) || event;
}
Шаг 7. Удалите фигурные скобки {...}
Поскольку стрелочные функции всегда возвращают значение выражения, нам больше не нужны {...} :
const addEventListener = (event, handler, eventMap) =>
eventMap.has(event) ?
new Map(eventMap).set(event, eventMap.get(event).concat([handler])) :
new Map(eventMap).set(event, [handler]);
const dispatchEvent = (event, eventMap) =>
(eventMap.has(event) && eventMap.get(event).forEach(a => a())) || event;
Шаг 8: Завершите каррирование
Последний шаг — реализовать операцию каррирования Конкретная идея состоит в том, чтобы превратить нашу функцию в унарную (принимать только один параметр), а метод реализации — использовать функцию более высокого порядка. Для упрощения понимания читатель может подумать, что параметр (a, b, c) просто меняется на a => b => c:
const addEventListener = handler => event => eventMap =>
eventMap.has(event) ?
new Map(eventMap).set(event, eventMap.get(event).concat([handler])) :
new Map(eventMap).set(event, [handler]);
const dispatchEvent = event => eventMap =>
(eventMap.has(event) && eventMap.get(event).forEach (a => a())) || event;
Если читателю трудно это понять, рекомендуется сначала дополнить знание о каррировании, которое здесь не будет расширяться.
Конечно, при таком виде обработки необходимо учитывать порядок параметров. Мы используем примеры для усвоения.
использование карри:
const log = x => console.log (x) || x;
const myEventMap1 = addEventListener(() => log('hi'))('hello')(new Map());
dispatchEvent('hello')(myEventMap1); // hi
частичное использование:
const log = x => console.log (x) || x;
let myEventMap2 = new Map();
const onHello = handler => myEventMap2 = addEventListener(handler)('hello')(myEventMap2);
const hello = () => dispatchEvent('hello')(myEventMap2);
onHello(() => log('hi'));
hello(); // hi
Читатели, знакомые с python, могут лучше понять концепцию партиала. Проще говоря, частичное применение функции можно понимать как:
Когда функция выполняется, она вызывается со всеми необходимыми параметрами. Однако иногда параметры могут быть известны заранее до вызова функции. В этом случае у функции есть один или несколько параметров, доступных заранее, так что функция может быть вызвана с меньшим количеством параметров.
Например:
const sum = a => b => a + b;
const sumTen = sum(10)
sumTen(20)
// 30
является проявлением.
Вернёмся к нашему сценарию для функции onHello, её параметром является обратный вызов при запуске события hello. Здесь предустановлены события myEventMap2 и hello. То же самое верно и для функции приветствия, ей нужно только вызвать событие приветствия.
Используйте в сочетании:
const log = x => console.log (x) || x;
const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)));
const addEventListeners = compose(
log,
addEventListener(() => log('hey'))('hello'),
addEventListener(() => log('hi'))('hello')
);
const myEventMap3 = addEventListeners(new Map()); // myEventMap3
dispatchEvent('hello')(myEventMap3); // hi hey
Здесь особое внимание следует уделить методу компоновки. Читатели, знакомые с Redux, должны быть знакомы с compose, если они читали исходный код Redux. С помощью compose мы реализовали комбинацию двух функций обратного вызова для события приветствия и комбинацию функций ведения журнала.
compose(f, g, h) 等同于 (...args) => f(g(h(...args))).
За загадочностью метода compose и разных способов реализации прошу обратить внимание на автора:Lucas HC, я напишу специальную статью, чтобы представить, иПроанализируйте, почему реализация compose в Redux немного неясна, и проанализируйте более интуитивную реализацию.
Суммировать
Функциональные концепции могут быть не очень удобны для новичков. Читатели могут прекратить чтение в любой момент, выполнив 8 шагов, описанных выше, в зависимости от их знакомства и предпочтений. Обсуждение также приветствуется.
Эта статья перефразирует Мартина Новака.новая статья, Добро пожаловать в великий топор.
подобно@yanhaijingБольшой парень сказал:
Результат функционального стиля таков, что в конце концов вы не можете понять его сами. . .
коммерческое время:Если вы интересуетесь фронтенд-разработкой, особенно стеком React: возможно, в моей новой книге есть что-то, что вы хотели бы увидеть. Следите за авторомLucas HC, после выхода новой книги будет розыгрыш.
Happy Coding!
ПС: авторРепозиторий на гитхабе а также Знай ссылку на вопрос и ответПриветствуются все формы общения.