О генераторах
Генераторы - это новый вид функций, который появился в ES6. О них написано немало статей и приведено множество теоретических примеров. Что касается меня, то прояснить суть генераторов и способ их использования помогла книга You don't know JS, часть async & performance. Из всех книг по JS, которые я изучал, эта наиболее упакована полезной информацией без воды.
Представим, что генератор (функция в объявлении, которой есть *) - это некое электрическое устройство с дистанционным пультом управления. После создания и монтирования генератора (объявления функции) нужно его "крутануть" (выполнить эту функцию), чтобы он вращался на холостых оборотах и "запитал" пульт управления собой (при выполнении функции-генератора возвращает итератор). На этом пульте управления две кнопки: Пуск (вызвать первый раз метод next итератора) и Next (последующие вызовы метода next итератора). Далее с этим пультом управления можно носиться по всей электростанции (по нашему приложению) и когда понадобиться электрическая энергия (некие значения из функции-генератора) нажимать на пульте кнопку next (выполнять метод next() генератора). Генератор производит нужное количество электроэнергии (возвращает некое значение через yield) и опять переходит в холостой режим (функция-генератор ждёт следующего вызова next от итератора). Цикл продолжается, пока генератор может производить электричество (имеются операторы yield) или он не остановится (в функции-генераторе встретится return).
И во всей этой аналогии ключевой момент - это пульт управления (итератор). Его можно передавать в разные части приложения и в нужный момент "забирать" значения из генератора. Для полноты картины, на пульте управления можно добавить неограниченное количество кнопок для запуска генератора в определенных режимах (передача параметров в метод next(любые параметры) итератора), но для реализации хука достаточно и двух кнопок.
Вариант 4. Генератор без промисов
Этот вариант приводится для наглядности, т.к. в полную силу генераторы работают совместно с промисами (механизм async/await). Но этот вариант рабочий и имеет право на существование в определенных простых ситуациях.
Создаю в хуке переменную для хранения ссылки на итератор (ячейка для пульта управления генератором)
const iteratorRef = useRef(null);
Нужно изменить логику хука. При выполнении обработчика события будет запускаться генератор. В данном случае нет никакого обмена данными между генератором и кодом, выполняющим метод next() итератора (генератор просто делает один оборот при нажатии кнопки next). Выглядит это так:
const updateCounter = () => {
iteratorRef.current.next();
};
const checkImageLoading = (url) => {
const imageChecker = new Image();
imageChecker.addEventListener("load", updateCounter);
imageChecker.addEventListener("error", updateCounter);
imageChecker.src = url;
};
При запуске генератор расставляет обработчики событий и затем запускает цикл по сборке ответов из этих обработчиков. Каждому обработчику был вручён пульт и было объяснено, что, как только придёт событие, нужно нажать кнопку next на пульте. Когда приходит событие, обработчик добросовестно жмёт на кнопку и тем самым "прокручивает генератор". В процессе оборота происходит dispatch нужного действия и генератор опять переходит в холостой режим, ожидая следующего сигнала от пульта. Ниже приведён код самого генератора:
function* main() {
for (let i = 0; i < imgArray.length; i++) {
checkImageLoading(imgArray[i].src);
}
for (let i = 0; i < imgArray.length; i++) {
yield true;
dispatch({
type: ACTIONS.SET_COUNTER,
data: stateRef.current.counter + stateRef.current.counterStep
});
}
}
Конечно при монтировании хука нужно "крутануть" генератор, чтобы он запитал пульт (вернул итератор в iteratorRef. И после этого нажать кнопку Пуск (выполнить метод next итератора первый раз).
Ниже привожу полный исходный код хука с использованием генератора без промисов.
Исходный код хука Генератор без промисов
import { useReducer, useEffect, useLayoutEffect, useRef } from "react";
import { reducer, initialState, ACTIONS } from "./state";
const PRELOADER_SELECTOR = ".preloader__wrapper";
const PRELOADER_COUNTER_SELECTOR = ".preloader__counter";
const usePreloader = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const stateRef = useRef(state);
const iteratorRef = useRef(null);
const preloaderEl = document.querySelector(PRELOADER_SELECTOR);
const counterEl = document.querySelector(PRELOADER_COUNTER_SELECTOR);
const updateCounter = () => {
iteratorRef.current.next();
};
const checkImageLoading = (url) => {
const imageChecker = new Image();
imageChecker.addEventListener("load", updateCounter);
imageChecker.addEventListener("error", updateCounter);
imageChecker.src = url;
};
useEffect(() => {
const imgArray = document.querySelectorAll("img");
if (imgArray.length > 0) {
dispatch({
type: ACTIONS.SET_COUNTER_STEP,
data: Math.floor(100 / imgArray.length) + 1
});
}
function* main() {
for (let i = 0; i < imgArray.length; i++) {
checkImageLoading(imgArray[i].src);
}
for (let i = 0; i < imgArray.length; i++) {
yield true;
dispatch({
type: ACTIONS.SET_COUNTER,
data: stateRef.current.counter + stateRef.current.counterStep
});
}
}
iteratorRef.current = main();
iteratorRef.current.next();
}, []);
useLayoutEffect(() => {
stateRef.current = state;
if (counterEl) {
stateRef.current.counter < 100
? (counterEl.innerHTML = `${stateRef.current.counter}%`)
: hidePreloader(preloaderEl);
}
}, [state]);
return;
};
const hidePreloader = (preloaderEl) => {
preloaderEl.remove();
};
export default usePreloader;
Но в полной мере мощь генераторов по управлению асинхронным кодом проявляется совместно с использованием промисов.
Вариант 5. Генератор с промисами
Как вы уже догадались генератор будет возвращать промис. Таким образом обработчикам событий не нужно будет вызывать метод next итератора (не нужно будет отдавать им пульт управления и давать инструкции по его использованию). Просто промисифицируем колбэк (мы сами будем знать когда отработал обработчик и без его ведома).
Код генератор изменится следующим образом:
const getImageLoading = async function* (imagesArray) {
for (const img of imagesArray) {
yield new Promise((resolve, reject) => {
const imageChecker = new Image();
imageChecker.addEventListener("load", () => resolve(true));
imageChecker.addEventListener("error", () => resolve(true));
imageChecker.src = img.url;
});
}
};
А вызывающий код будет выглядеть так:
for await (const response of getImageLoading(imgArray)) {
dispatch({
type: ACTIONS.SET_COUNTER,
data: stateRef.current.counter + stateRef.current.counterStep
});
}
Основную работу по сравнению с предыдущим вариантом выполняет цикл for await ... of. Пульт управления генератором находится у него и он автоматически нажимает кнопку Пуск и Next.
У этого варианта есть один небольшой недостаток - последовательная обработка промисов. Первое изображение может оказаться большим по размеру и тогда менее тяжёлые картинки будут ожидать разрешения первого промиса, несмотря на то, что они уже давно загрузились и промисы их обработчиков давно разрешены.
Исходный код хука Генератор с промисами
import { useReducer, useEffect, useRef } from "react";
import { reducer, initialState, ACTIONS } from "./state";
const PRELOADER_SELECTOR = ".preloader__wrapper";
const PRELOADER_COUNTER_SELECTOR = ".preloader__counter";
const usePreloader = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const stateRef = useRef(state);
const preloaderEl = document.querySelector(PRELOADER_SELECTOR);
const counterEl = document.querySelector(PRELOADER_COUNTER_SELECTOR);
useEffect(() => {
async function imageLoading() {
const imgArray = document.querySelectorAll("img");
if (imgArray.length > 0) {
dispatch({
type: ACTIONS.SET_COUNTER_STEP,
data: Math.floor(100 / imgArray.length) + 1
});
for await (const response of getImageLoading(imgArray)) {
dispatch({
type: ACTIONS.SET_COUNTER,
data: stateRef.current.counter + stateRef.current.counterStep
});
}
}
}
imageLoading();
}, []);
useEffect(() => {
stateRef.current = state;
if (counterEl) {
stateRef.current.counter < 100
? (counterEl.innerHTML = `${stateRef.current.counter}%`)
: hidePreloader(preloaderEl);
}
}, [state]);
return;
};
const getImageLoading = async function* (imagesArray) {
for (const img of imagesArray) {
yield new Promise((resolve, reject) => {
const imageChecker = new Image();
imageChecker.addEventListener("load", () => resolve(true));
imageChecker.addEventListener("error", () => resolve(true));
imageChecker.src = img.url;
});
}
};
const hidePreloader = (preloaderEl) => {
preloaderEl.remove();
};
export default usePreloader;
Итого:
В этой части статьи показано:
как использовать useRef для хранения и использования нужных значений на протяжении всего времени жизни компонента (некий аналог глобальных переменных для компонента)
как управлять потоком событий с помощью генераторов, но без использования промисов (с использованием колбэков)
как управлять потоком событий, обработчики которых промисифицированы, с помощью генераторов и цикла for await ... of
Ссылка на песочницу
Ссылка на репозиторий
Продолжение следует... redux-saga...
DmitryKazakov8
Кажется, некоторые молодые домашние эксперименты должны оставаться в домашних репозиториях
Akinyaev Автор
К сожалению, стать профессионалом сразу не получится: этого можно достичь продвигаясь пошагово. Почему вы считаете, что первые шаги не имеют права на то, чтобы увидеть свет, дабы шагающий следом прошёл эту ступеньку быстрее.
MaZaAa
Так назовите статью, «Такой код писать нельзя! Я просто делаюсь своим неудачным опытом и шагами обучения и экспериментов».
А то ещё кто-то расценит это как норму и будет писать в таком стиле код, это будет катастрофа. Иначе каждый первый проект каждый пол года будет подлежать полному переписыванию с нуля.
Более того, вы используете в 2020 году redux, redux-saga это же просто смешно, с таким стэком эти доисторические первые шаги нужно было проходить в 2015 году, и забыть их как страшный сон, в 2020 уже всё совсем совсем по другому.
Если уж вам так нравится капаться в далеком прошлом, то поэкспериментируйте с первой версией PHP из 90-ых и напишите статью об этом =)
Akinyaev Автор
Почти так и написал в первой части статьи: «Задача была решена, но продолжил исследования».
По инструментам — не согласен. Redux, redux-saga, да и другие «доисторические» (посмотрите на jQuery) библиотеки и фреймворки повсеместно встречаются в существующих legacy проектах, которые необходимо сопровождать.
MaZaAa
И что теперь? Пусть динозавры их и поддерживают, пока не надоест, вы то тут причем, тем более я так понимаю вы только начинаете свой путь в разработке. Все равно с нуля все будет переписано как ни крути. Но накой спрашивается… эти недоразумения и мертворожденные библиотеки применять сейчас???? Это же полный абсурд.
Я вам категорически советую познакомится с MobX и реализовать вашу задачу с помощью MobX. Без всяких редаксов, саг и санок.
Akinyaev Автор
Спасибо за совет. В моих планах MobX это следующий state manager. В данный момент изучаю rxjs и его использование в реакте. Тут, конечно я попал в засаду к реактивному программированию и функциональному программированию. Копаю в первой итерации, пока мозги не поплывут. Поплывут, переключусь на MobX.
MaZaAa
Какой смысл на эту дичь тратить время? Разе с первого взгляда не видно какой говнокод с ней придется писать? Разве только что вы не мазохист =)
DmitryKazakov8
Тоже так думал на каком-то этапе, но каждый разработчик должен создать тысячу разного рода решений, чтобы понять "как лучше", а если выкладывать каждый из экспериментов, то Хабр действительно превратится в треш, и найти действительно что-то полезное станет еще сложней (так, плодятся как грибы первые эксперименты по созданию todo list, модалок, базовой настройки Webpack). Второй аргумент против — многие разработчики хотят попробовать на практике то, что читают, причем в коммерческих проектах, что ведет к распространению неэффективных практик по сотням репозиториев. Поэтому все же лучше самостоятельно проработать множество вариантов и написать статью по их сравнению, выбрав лучшее и получив фидбек по возможным улучшениям, чем выложить один из промежуточных.
Akinyaev Автор
Абсолютно согласен. Это и есть решение очень простой задачи десятком вариантов. Просто статью с сагами и rxjs ещё не опубликовал. Просто посчитал, что на простом примере с прелоадером можно наглядно осветить проблематику работы с потоком событий и его обработкой различными способами. По поводу разработчиков — да так и есть: основная часть непродуктовых проектов являются «франкинштейнами», созданными с помощью copy-past driven development, т.к. агенству нужно на вчера и за рубль. Понимаю, что с продуктом по-другому, но там копипаст не прокатит, т.к. его ещё нужно тестами покрыть и код-ревью пройти. Критика аргументирована, поэтому принята. Эту серию статей закончу, как планировал. А в будущем попробую, предложенный Вами формат статьи, хотя считаю, что нужно осветить множество вариантов, а делать выводы лучше предоставить читателю.