Эпическая Сага О Небольшом Кастомном Хуке Для React (Генераторы, Саги, Rxjs), Часть 2

Часть 1. Нестандартный крючок Часть 3. Redux-сага



О генераторах

Генераторы — это новый тип функций, представленный в ES6. О них написано много статей и приведено много теоретических примеров.

Как по мне, книга помогла прояснить суть генераторов и способы их использования.

Ты не знаешь JS , часть асинхронности и производительности.

Из всех книг по JS, которые я изучал, эта содержит больше всего полезной информации и ничего лишнего.

Давайте представим, что генератор (функция в объявлении *) — это некое электрическое устройство с дистанционным управлением.

После создания и монтирования генератора (объявления функции) его нужно «раскрутить» (выполнить эту функцию), чтобы он вращался на холостом ходу и «запитывал» панель управления (при выполнении функции генератора она возвращает итератор ).

На этой панели управления есть две кнопки: Start (первый вызов следующего метода итератора) и Next (последующие вызовы следующего метода итератора).

Тогда с помощью этого пульта управления можно носиться по всей электростанции (согласно нашему приложению) и когда вам понадобится электроэнергия (определённые значения из функции генератора), нажать на пульте кнопку «Далее» (выполнить следующую () метод генератора).

Генератор вырабатывает необходимое количество электроэнергии (возвращает определенное значение через доходность) и снова переходит в режим ожидания (функция генератора ждет следующего вызова next от итератора).

Цикл продолжается до тех пор, пока генератор не сможет производить электричество (есть операторы доходности) или не остановится (в функции генератора не встретится возврат).

И во всей этой аналогии ключевым моментом является панель управления (итератор).

Его можно передавать в разные части приложения и в нужный момент «забирать» значения из генератора.

Для полноты картины можно добавить на панель управления неограниченное количество кнопок для запуска генератора в определенных режимах (передача параметров следующему методу (любые параметры) итератора), но для реализации хука достаточно двух кнопок.



Вариант 4. Генератор без обещаний

Эта опция предусмотрена для наглядности, т.к.

генераторы работают в полную силу вместе с промисами (механизм async/await).

Но этот вариант рабочий и имеет право на существование в некоторых простых ситуациях.

В хуке создаю переменную для хранения ссылки на итератор (ячейку для панели управления генератором)

  
  
  
  
  
  
   

const iteratorRef = useRef(null);

Вам нужно изменить логику хука.

Когда обработчик событий будет выполнен, запустится генератор.

В этом случае между генератором и кодом, выполняющим метод next() итератора, нет связи (генератор просто делает один оборот при нажатии следующей кнопки).

Это выглядит так:

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 }); } }

Разумеется, при монтировании хука нужно «раскрутить» генератор, чтобы он запитал консоль (вернул итератор в iteratorRef. И после этого нажать кнопку «Старт» (первый раз выполнить следующий метод итератора) .

Ниже приведен полный исходный код хука с использованием генератора без промисов.

Генератор исходного кода без обещаний

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. Генератор с обещаниями

Как вы уже догадались, генератор вернет обещание.

Таким образом, обработчикам событий не нужно будет вызывать следующий метод итератора (вам не нужно будет передавать им пульт дистанционного управления и инструкции по его использованию).

Мы просто искажаем обратный вызов (мы сами будем знать, когда обработчик выполнился без его ведома).

Код генератора изменится следующим образом:

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. У него есть панель управления генератором, и он автоматически нажимает кнопки «Пуск» и «Далее».

У этого варианта есть один небольшой недостаток — последовательная обработка промисов.

Первое изображение может оказаться большим по размеру, а затем изображения поменьше будут ждать разрешения первого промиса, несмотря на то, что они загружены уже давно и промисы их обработчиков уже давно разрешены.

Исходный код Генератора хуков с промисами

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.
Ссылка к песочница Ссылка к репозиторий Продолжение следует. редукс-сага.

Теги: #JavaScript #react.js #react #generators #promises #iterators #generators #hooks #promises #await/async #preloader

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.