EventEmitter: от императивного класса JavaScript до объявления функционального красивого поворота

внешний интерфейс JavaScript React.js Redux
EventEmitter: от императивного класса JavaScript до объявления функционального красивого поворота

从命令式到函数式

Новая книга наконец-то готова.Сегодня у меня есть немного свободного времени.Я посвящу вам статью о стиле языка 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!

ПС: авторРепозиторий на гитхабе а также Знай ссылку на вопрос и ответПриветствуются все формы общения.