Галоўная / Артыкулы

Заводы з абсалютным кантролем: прыватна дзяржава, абсарбаванне, ліміты швайнаў і ануляванне

Пазіце, як клозуры падтримваюць прыватны стан жывым у фабрыкі JavaScript, чаму яны спрычыняюць застарэлія значэнняў у таймерах React, і як debounce, rate limiting і undo залежнаць ад ўсіх іх.

Выдавец: REACTAPP.TOP7 хв чытання1631 слоў

Крыніцы: Closures in JavaScript and React, Explained Simply

Закрыція стояць за вельмі большым чыслаю ўжываных ў павсядневным JavaScript элементаў: лічыльнікаў, функцый з адкладанням выконання, механізмаў контролю частоты запытоў, стакаў для відкату змян і практычна кожнага хука React. Яны таксама стояць за цэлым родам багоў, калі функцыя-вызывач аддае значэнне, якое больш не ўсунуто. У гэтым кяліку даўаецца рабочая адзначэнне, паказваецца, што гэта значыць для стану React, а пасля — расказваецца пра чатыры практычныя шаблону, укладзеныя ў фабрыку, включаючы меры абережнасці, якія трэба ведаць пры ўжыванні іх у продакшэне.

Рабочая адзначэнне

Закрыцьце — это функцыя, якая ўжоўваеся разам з мэнберамі, якія былі ў дыапазоне значэнняў там, дзе яна была з’явлена. Гэтая функцыя застаецца з даступам да тых мэнбероў нават пасля таго, як зовнішняя функцыя, якая іх выклекла, вярнулася. Прыемным аправаджэнням можа быць прыклад функцыі, якая «выходзіць з дома», носячы сумку з мэнберамі, да якіх яна апыляецца; незалежна ад таго, дзе яна пазней будзе вызвана, яна апыляецца самэй той сумкай.

Фабрыка лічыльнікаў

Класычным прыемам ёсць фабрыка, якая вяртае функцыю, якая адкорэгуе зменную, якая ёй не належыць:

function makeCounter() {
  let count = 0; // lives inside makeCounter

  function increment() {
    count = count + 1;
    console.log(count);
  }

  return increment;
}

const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3

Шаг за шагам так выглядае процес:

  • makeCounter() выканана адна раз і прызначае count.
  • Яна вяртае increment, якай апыляецца на count, пасля чаго makeCounter завершаецца.
  • Кожны вызов counter() чытае і адкорэгуе тую ж самую count, і самэй гэтам выхідныя значэння зростаюць з 1 да 3.

Зазвычай локальныя зменнікі функцыі стаюць недоступнымі, калі яна вяртаеся, і двайчык можа ўвольняць іх. У даным случае increment яшчэ павярнуецца да count, таму зменнік застаецца жывым так далека, як існуе increment. Якщо вы хочаце знать асновныя правілы таго, як двайчык спачатку знаходзіць count, стаття па адресе як ланцуг дыяпазону разв’язвае зменнікі распаводзіцца пра гэты механізм.

Чаму стан React стае застарэлым у таймерах

React не дадзеў нічога спецыяльнага паўерх закрыццяў; ён проста выкарыстоўвае іх у всім, асобліва ў хукі. Гэта выкарыстоўванне пояснюе адна з найбольш распашчараваючых прыгод у коде компонентаў:

function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setTimeout(() => {
      console.log("Count is: " + count);
    }, 3000);
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Add</button>
      <button onClick={handleClick}>Log count in 3 seconds</button>
    </div>
  );
}

Пачніце з count равным 0. Клікніце на „Запісаць лічбу за 3 секунды“, а пасля трохце разоў нажміце „Дадаць“ пры тым, калі ўжо не пачнуўся таймер. У консолі будзе выведацца 0, а не 3.

Прычына ў тым, што кожная перрадарба элемента Counter ўособлівае вызванне функцыі. Кожна такая вызванне атрымвае свой скалярны значэння count і стварае свой функцыю handleClick. Функцыя-колбэк setTimeout была створана пад час той перрадарбы, калі count равнаўся 0, і гэтае єдыне значэння count, якое яна можа бачыць.

Эта модэль адказвае на большасць запытанняў типу «Чаму мой статус застарэлы?», і самэй гэтым ёсць мета массива залежнасцяў у useEffect: ён паведамляе React, на якія значэння спылвае эфект, тады React можа зноў запрацаваць яго з новымі значэннямі, калі яны зменяюцца. Калі вам справаўна патрэбна найновейшая значэнне ў адкладзенам калебэку, звычныя варыянты — гэта функцыйнае апдэйтаванне, такое як setCount(c => c + 1) для збіркі дадзеных, альбо ref, які трэба падтрымляць у актуальным стане для чытання. Альтэратывы між гэтымі варыянтамі расследуецца ў супэрнацыя snapshot і жывых значэнняў у адкладзеных калебэках.

Чатыры шаблону фабрыкі, якія вы зустрэнете ў рэальным кодзе

Інкапсульаваны статус без класаў

Даўжыню класу, пры якой іменнікі маюць прыметнік #, яшчэ не стала частью языка, закрыцці былі стандартным спосабам адгораджвання данных, і яны застаюцца прыемным выклікам і сёньдзе. Всё, што заявляецца ў фабрыке, доступна толькі через функцыі, якія ў яе вяртаюць:

function createBankAccount(balance) {
  return {
    deposit(amount) {
      balance += amount;
      return balance;
    },
    withdraw(amount) {
      balance -= amount;
      return balance;
    },
    getBalance() {
      return balance;
    }
  };
}

const account = createBankAccount(100);
account.deposit(50);
console.log(account.getBalance()); // 150
console.log(account.balance); // undefined — can't touch it directly

balance не ўтварае атрыбута вярнутага об’екта, таму account.balance ў значэнні undefined. Ядыны спосаб змяніць яго — через deposit і withdraw.

Задзейсненне пошуку пасля паузы

Механізм задзейснення пасля паузы затрымлівае дзеянне, пакуль введання не зупініцца на назначаны час. Самэ гэта не дазволяе полі пошуку выканаць запит пасля кожнага натыкання на клавішу:

function debounce(func, delay) {
  let timer; // remembered between calls
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => func(...args), delay);
  };
}

const searchInput = document.getElementById("search");
const handleSearch = debounce((e) => {
  fetch(`/api/search?q=${e.target.value}`);
}, 300);

searchInput.addEventListener("input", handleSearch);

Ключовым моментам ёсць тое, дзе адгласуецца timer: раз, унутрь debounce, за межамі вярнутага функцыі. Кожны натиск клавішы вызывае вярнутую функцыю, якая абрывае чакаючы таймаут і плануе новы, такім чынам запит адправляецца толькі пасля 300 мс без натыскання клавіш. Якбы timer адгласуецца ўнутрь вярнутай функцыі, кожны вызыв отрымаў бы новую зменную, пярэйшы таймаут ніколі не мог бы быть абрыты, і кожны натиск клавішы все равно стварыў бы запит.

У компоненте React тая ж логіка прыменяецца, але з дадатковым крокам: функцыя адгласавання павінна быць створана раз і застаць стабільной (напрыклад, з useMemo або ref), інакш кожны рендер стварае новы клоузур з новым timer, і адгласаванне таямніча перестае працаваць.

Просты лімітар запитоў

Тая ж самая тэхніка можа абмежаваць частоту выдання дадзеных кан канцэнтры:

function createRateLimiter(maxRequests, windowMs) {
  let requestCount = 0;
  setInterval(() => (requestCount = 0), windowMs);

  return function () {
    requestCount++;
    return requestCount <= maxRequests;
  };
}

const rateLimiter = createRateLimiter(5, 60000); // 5 requests per minute

app.get("/api/data", (req, res) => {
  if (!rateLimiter()) {
    return res.status(429).send("Too many requests");
  }
  res.send("Here's your data");
});

requestCount аднаўляецца чыста прыватна праз кожны вызов rateLimiter, і ў кожнам періаде часу ён сяражаецца. Што стосуецца гэтай версіі, то ў яй выкарыстоўваецца лічыльнік у памяці з фіксаваным періадам, таму трэба памятаць пра яе меры абмежэння:

  • ён лічыць усіх вызываючых разам, таму адны кліент можа выкарыстаць усю квоту для всіх; справжнія лімітары лічаць вызывы па корыстніку, токену чыя IP-адрэсе
  • стан зберагаецца ў аднам процэсе, таму калькі серверных інстанцый можа выкарыстоўваць усю максымальную квоту
  • фіксаваныя періяды дазваляюць раптовыя сплескі вызывоў на межах періаду, да два разы больш за ліміт за короткі час
  • функцыя setInterval ніколі не абрываецца, што мае значэнне пад час тэставання чыра дынамічнага стварэння лімітароў

Історыя анулювання і паўторэння

Менеджер історыі зберагае свой стак і курсар прыватна для кожнай інстанцыі:

function createHistoryManager() {
  let history = [];
  let currentIndex = -1;

  return {
    save(state) {
      history = history.slice(0, currentIndex + 1);
      history.push(state);
      currentIndex++;
    },
    undo() {
      if (currentIndex > 0) currentIndex--;
      return history[currentIndex];
    },
    redo() {
      if (currentIndex < history.length - 1) currentIndex++;
      return history[currentIndex];
    }
  };
}

save спачатку адзырае ўсё, што знаходзіцца пасля тэчныя пазыцыі, таму запіс новых змян пасля выканання undo усувае варыянт redo, што адпавядае спосабу працы большасці рэдагароў. undo і redo проста перамешчаюць currentIndex у межах дапустамых значэнняў і вяртаюць стан у тым моменте. Заўважкай, што збераганыя станы павінны быць незменнымі кадрамі; якщо вы запішаеце об’ект, а пасля таго яго змяніце, кожны элемент історыі будзе вядомаць пра тыя ж змененыя данні.

Кораткія методы для выяўлення клозураў

  • Внутрэшняя функцыя выкарыстоўвае зовнішнюю. Функцыя, якая визначаецца ўнутрь іншай функцыі і якая аб’язана да зменных зовнішней функцыі, ёсць клозурам.
  • Ён фіксуеяе звязку, а не жывыя даны новых рэндараў. У React кожны рэндар стварае новыя звязку, таму стары калбэк захоўвае значэнні з свайго рэндара. Акруатна кажучы, клозура захоўвае саму зменную, таму якщо гэтая ж зменная пазней будзе перазначана, як count у фабрыцы лічыльніка, клозура бачыць новае значэнне.
  • Функцыя, якая вяртаецца, — гэта яскравы сігнал. Калі функцыя вяртае іншую функцыю, практычна завжды ўсёлякія клозуры.
  • Пастка var-а ў цыкле

    Зменныя цыклаў найясней паказваюць правіло звязку:

    for (var i = 0; i < 3; i++) {
      setTimeout(() => console.log(i), 1000);
    }
    // Logs: 3, 3, 3 — var is shared across the whole loop
    
    for (let i = 0; i < 3; i++) {
      setTimeout(() => console.log(i), 1000);
    }
    // Logs: 0, 1, 2 — let creates a new i on every iteration
    

    За дапамою var усія функцыя выкорыстоўвае аднаго толькі зменнага i; калі наступае таймаут, цикл уже зменіў яго на 3, таму всі трое калбэкаў выведаюць 3. За дапамою let кожная ітерацыя мае свой окремы экземпляр зменнага i, і кожны калбэк працюе з адним конкрэтным значэнням, таму выведаюцься 0, 1 і 2.

    Узагадкі

    Механізмы замыкань ёсць адказальнікам прыватных статаў, вводу з адзінчынам, лімітараў запытаў, стакоў для відкладання дзеянь і спецыяльных хуків, а таксама багоў, выніклых у зв’язку з застарэлым статам. Два пытанні раз’ясняюць большасць незрозумеласцей: якую зменную захопіла гэта функцыя і чы была тая зменная пасля таго заменена на новую? У простых фабрыках JavaScript захопленая зменная дзеліцца і апдэйтуецца на месцы. У React кожны рендер прыносіць новыя зменныя, таму будзь-які калбэк, які працюе дыяўольш чым свой рендер, бачыць старэе становішча, як толькі вы нямерна не даеце ў яго можласці чытаць тэкучае.