[JavaScript, ReactJS] Эпическая сага про маленький custom hook для React (генераторы, sagas, rxjs) часть 2
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Часть перваяО генераторахГенераторы - это новый вид функций, который появился в 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...
===========
Источник:
habr.com
===========
Похожие новости:
- [JavaScript, Программирование, TypeScript] Кастомизация компонентов Ant Design и оптимизация бандла
- [Python, JavaScript, Браузеры] Brython: заменяем JavaScript на Python на фронтенде (перевод)
- [JavaScript, ReactJS] Эпическая сага про маленький custom hook для React (генераторы, sagas, rxjs)
- [JavaScript, Node.JS, TypeScript] Оптимизация трафика при синхронизация состояний через Jsonpatch
- [Разработка веб-сайтов, JavaScript, Программирование] Сниппет, расширение для VSCode и CLI. Часть 2
- [JavaScript, Интерфейсы, ReactJS, TypeScript] Использование Effector в стеке React + TypeScript
- [JavaScript, Реверс-инжиниринг, Софт] Frida изучаем эксплуатацию алгоритмов Heap
- [JavaScript, Node.JS] Разработка сервера для многопользовательской игры с помощью nodejs и magx
- [JavaScript, Программирование, Node.JS] Декораторы в JavaScript с нуля (перевод)
- [JavaScript, Программирование, ReactJS] Начинающим React-разработчикам: приложение со списком дел (покупок) (перевод)
Теги для поиска: #_javascript, #_reactjs, #_huki (хуки), #_react, #_generatory (генераторы), #_promisy (промисы), #_promises, #_generators, #_iteratory (итераторы), #_await/async, #_preloader, #_preloader (прелоадер), #_javascript, #_reactjs
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 03:14
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Часть перваяО генераторахГенераторы - это новый вид функций, который появился в 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);
const updateCounter = () => {
iteratorRef.current.next(); }; const checkImageLoading = (url) => { const imageChecker = new Image(); imageChecker.addEventListener("load", updateCounter); imageChecker.addEventListener("error", updateCounter); imageChecker.src = url; }; 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 }); } } 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; 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 }); } 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;
=========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 03:14
Часовой пояс: UTC + 5