IlmHamroh
JavaScript Full-stack/11-qism. Asinxron JavaScript va tarmoq12/32-dars22 daqiqa
Mundarija (33)

AbortController: asinxron ishni bekor qilish — AbortSignal, timeout va any

Qisqacha: Promise'ning o'zida "to'xtat" tugmasi yo'q — bekor qilish alohida obyekt orqali bo'ladi. const pult = new AbortController() yaratasiz, pult.signal ni ishga berasiz va keraksiz bo'lganda pult.abort() chaqirasiz. Signalni qabul qilgan ish to'xtaydi va Promise AbortError bilan rad etiladi. Vaqt chegarasi uchun tayyor signal — AbortSignal.timeout(ms), bir nechta sababni birlashtirish uchun — AbortSignal.any([...]).

Bu darsda

  • AbortController va AbortSignal ning vazifalarini ajratasiz: kim bekor qiladi, kim kuzatadi.
  • O'z asinxron funksiyangizni { signal } opsiyasi bilan bekor qilinadigan qilib yozasiz — to'g'ri tozalash bilan.
  • AbortSignal.timeout bilan vaqt chegarasini va AbortSignal.any bilan "foydalanuvchi bekor qildi yoki vaqt tugadi" holatini yozasiz.
  • AbortError va TimeoutError ni haqiqiy xatolardan ajratasiz.
  • Qidiruvdagi "oxirgi so'rov yutadi" muammosini bekor qilish bilan hal qilasiz.

Oldin bilishingiz kerak: Listener opsiyalari: once, passive, signal, Promise kombinatorlari, Promise yordamchilari va promisify, Microtask va macrotask chuqur.

1. Nega bu kerak?

«Bahor» saytida menyudan qidiruv bor. Aziz "osh" deb yozmoqchi. Har harf bosilganda sayt serverdan so'raydi:

  1. "o" — 600 ms da javob keladi (ko'p taom mos, server sekin ishlaydi);
  2. "os" — 400 ms da;
  3. "osh" — 200 ms da.

Uchala so'rov deyarli bir vaqtda ketdi. Eng oxirgisi — "osh" — eng birinchi qaytdi va ekranda "osh" natijalari chiqdi. Keyin "os" ning javobi keldi va ularni bosib ketdi. Oxirida esa "o" — eng eski so'rov. Aziz "osh" deb yozgan, ekranda esa "o" bo'yicha natijalar.

Bu xato race condition ("poyga holati") deyiladi: natija ishlarning qaysi tartibda tugashiga bog'liq bo'lib qoladi. Eski so'rovlar endi keraksiz — ularni to'xtatish kerak edi.

Yana ikki holat:

  • Mehmon katta menyu yuklanayotgan sahifani yopib, boshqasiga o'tdi. Yuklash davom etyapti — internet va xotira behuda ketyapti.
  • Server javob bermayapti. Ilova abadiy "Yuklanmoqda..." holatida turibdi.

Promise kombinatorlari darsida ko'rdik: Promise.race ham, Promise.all ham yutqazgan ishlarni to'xtatmaydi — faqat kutishni to'xtatadi. Promise — natija uchun chipta, ish ustidan boshqaruv emas. Bugun boshqaruv vositasini o'rganamiz.

2. AbortController va AbortSignal

2.1 Pult va chiroq

Listener opsiyalari darsida AbortController bilan tanishgansiz. U yerda u tinglovchilarni guruh bilan olib tashlagan edi. Eslatma:

  • AbortController — "bekor qilish pulti". Uning abort() tugmasi bor.
  • AbortSignal — pultga ulangan "chiroq", controller.signal. U faqat holatni ko'rsatadi: bekor qilinganmi, yo'qmi.

Muhim tafsilot: ikkalasi alohida obyekt. Ishga faqat signal beriladi. Ish signalni kuzatadi, lekin uni bekor qila olmaydi — pult sizda qoladi. Ofitsiant oshxonaga buyurtma bilan birga "chiroq" beradi. Mehmon ketsa, ofitsiant pultni bosadi va chiroq yonadi. Oshpaz chiroqni ko'radi va pishirishni to'xtatadi. Oshpaz boshqa ofitsiantning buyurtmasini bekor qila olmaydi — uning qo'lida pult yo'q.

flowchart LR
  P["AbortController<br/>(pult)"] -- "abort()" --> S["signal"]
  S --> A["tayyorla(...)"]
  S --> B["addEventListener"]
  S --> C["fetch(...)"]

Bitta signalni bir nechta ishga berish mumkin — bitta abort() hammasini to'xtatadi. fetch — serverdan ma'lumot so'raydigan funksiya, uni fetch asoslari darsida o'rganamiz; u ham aynan shu signalni tushunadi.

2.2 Signalning xususiyatlari

Signal ichida nima borligini ko'ramiz:

js
const pult = new AbortController();
const signal = pult.signal;

console.log(signal.aborted, signal.reason);

signal.addEventListener("abort", () => {
  console.log("abort hodisasi keldi");
});

pult.abort();
console.log(signal.aborted);
console.log(signal.reason.name);
console.log(signal.reason.message);

Konsolda:

text
false undefined
abort hodisasi keldi
true
AbortError
This operation was aborted
  • signal.aborted — bekor qilinganmi (true/false).
  • "abort" hodisasi — abort() chaqirilganda, sinxron, shu zahoti keladi. Signal — EventTarget, shuning uchun unga addEventListener ishlaydi.
  • signal.reason — sabab: nega bekor qilindi. Sukut bo'yicha — AbortError nomli xato, matni "This operation was aborted" ("bu amal bekor qilindi").

Bu xato DOMException turida — brauzer API'larining xato klassi. U ham Error dan meros oladi: name va message odatdagidek ishlaydi.

2.3 O'z sababingiz

abort() ga sabab bersangiz, reason shu bo'ladi:

js
const pult = new AbortController();
pult.abort(new Error("Mehmon ketdi"));
console.log(pult.signal.reason.message); // Mehmon ketdi

pult.abort(new Error("Yana bir marta"));
console.log(pult.signal.reason.message); // Mehmon ketdi

const tayyor = AbortSignal.abort();
console.log(tayyor.aborted); // true

Ikki qoida ko'rindi. Birinchisi — signal bir marta bekor bo'ladi: ikkinchi abort() hech narsani o'zgartirmaydi, sabab ham birinchisicha qoladi. Ikkinchisi — AbortSignal.abort() darhol bekor qilingan signal yasaydi. U testlarda va "boshidanoq bekor" holatlarini sinashda qulay.

Tekshirib ko'ring: Nega ishga controller emas, faqat controller.signal beriladi?

Javob

Signal faqat kuzatish uchun — undan abort() qilib bo'lmaydi. Ishni chaqirgan kod pultni o'zida saqlaydi va faqat u bekor qila oladi. Ish (yoki uning ichidagi begona kutubxona) boshqa ishlarni tasodifan bekor qila olmaydi.

3. O'z funksiyangizni bekor qilinadigan qilish

3.1 Uch qoida

abort() hech narsani majburan to'xtatmaydi. U faqat chiroqni yoqadi. Ishni to'xtatish — signalni olgan funksiyaning vazifasi. Brauzer va Node'ning tayyor API'lari (fetch, addEventListener) buni o'zi qiladi. O'z funksiyangizda esa uchta qoidani bajarasiz:

  1. Boshida tekshiring: signal allaqachon bekor bo'lsa — ishni boshlamang, darhol signal.reason bilan rad eting.
  2. Abort'ni tinglang: ish davomida "abort" kelsa — ishni to'xtating (taymerni tozalang) va signal.reason bilan rad eting.
  3. Tozalang: ish o'zi tugasa — abort tinglovchisini olib tashlang.

Signal — opsiya sifatida, oxirgi parametrda: tayyorla(taom, ms, { signal }). Bu — options obyekti naqshi, fetch ham, Node API'lari ham aynan shunday qabul qiladi:

js
function tayyorla(taom, ms, { signal } = {}) {
  return new Promise((resolve, reject) => {
    if (signal?.aborted) {
      reject(signal.reason);
      return;
    }

    function bekorQil() {
      clearTimeout(taymer);
      console.log(`  ${taom}: pishirish to'xtatildi`);
      reject(signal.reason);
    }

    const taymer = setTimeout(() => {
      signal?.removeEventListener("abort", bekorQil);
      resolve(`${taom} tayyor`);
    }, ms);

    signal?.addEventListener("abort", bekorQil, { once: true });
  });
}

async function buyurtma(taom, ms, bekorMs) {
  const pult = new AbortController();
  setTimeout(() => pult.abort(), bekorMs);
  try {
    console.log(await tayyorla(taom, ms, { signal: pult.signal }));
  } catch (xato) {
    console.log(`${taom}: ${xato.name}`);
  }
}

async function sinab() {
  await buyurtma("somsa", 100, 300);
  await buyurtma("osh", 400, 150);
}

sinab();

Konsolda:

text
somsa tayyor
  osh: pishirish to'xtatildi
osh: AbortError

Somsa 100 ms da tayyor bo'ldi — pult 300 ms da bosildi, lekin ish allaqachon tugagan edi. abort() tugagan ishga ta'sir qilmaydi: Promise bir marta hal bo'ladi. Osh esa 400 ms kerak edi — 150 ms da pult bosildi, taymer tozalandi va Promise AbortError bilan rad etildi.

Qatorma-qator:

  • { signal } = {} — parametrda destructuring va standart qiymat: opsiya berilmasa ham funksiya ishlaydi.
  • signal?.aborted — optional chaining: signal berilmagan bo'lsa (undefined), xato bermaydi.
  • { once: true } — tinglovchi bir marta ishlab, o'zi olib tashlanadi.
  • removeEventListener — ish o'zi tugaganda tinglovchini tozalaymiz (3-qoida).
  • reject(signal.reason) — o'zimiz yangi xato yasamaymiz, signal sababini uzatamiz.
  • bekorQil ichida taymer ishlatilgan, u esa pastroqda e'lon qilingan. Bu xato emas: bekorQil faqat "abort" kelganda chaqiriladi, u paytda taymer allaqachon tayyor. "Avval e'lon, keyin ishlat" qoidasi chaqiruv paytiga tegishli (TDZ).

3.2 Qadamma-qadam

Oshning holatini kuzating — ayniqsa taymer qachon host'dan olib tashlanishiga:

Solishtiring: Promise kombinatorlari darsidagi Promise.race da yutqazgan oshning taymeri oxirigacha host'da qolgan edi. Bu yerda esa clearTimeout uni olib tashladi — oshxona pishirishni haqiqatan to'xtatdi.

3.3 Ko'p qadamli ish: throwIfAborted

Ish bir nechta qadamdan iborat bo'lsa, har qadam orasida signalni tekshirish kerak. Buning qisqa yo'li — signal.throwIfAborted(): signal bekor bo'lsa, signal.reason ni throw qiladi, bo'lmasa — hech narsa qilmaydi.

js
function kut(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function oshDamla(signal) {
  const qadamlar = ["guruch yuvildi", "zirvak tayyor", "damlandi"];
  for (const qadam of qadamlar) {
    signal.throwIfAborted();
    await kut(100);
    console.log(" ", qadam);
  }
  return "osh tayyor";
}

async function sinab() {
  const pult = new AbortController();
  setTimeout(() => pult.abort(new Error("Mehmon ketdi")), 150);
  try {
    console.log(await oshDamla(pult.signal));
  } catch (xato) {
    console.log("To'xtadi:", xato.message);
  }
}

sinab();

Konsolda:

text
  guruch yuvildi
  zirvak tayyor
To'xtadi: Mehmon ketdi

Pult 150 ms da bosildi — ikkinchi qadam (kut(100)) o'rtasida. Qadam o'z ishini tugatdi ("zirvak tayyor"), keyingi aylanishda esa throwIfAborted sababni otdi. async funksiya ichidagi throw — rad etilgan Promise. Oshni damlashga kirishilmadi.

Bu usul yumshoqroq: joriy qadam oxirigacha bajariladi. Har qadamni ham darhol to'xtatish kerak bo'lsa — signalni qadamlarga ham uzating (kut(100, { signal }), 1-mashqda yozasiz).

Tekshirib ko'ring: tayyorla da reject(signal.reason) o'rniga reject(new Error("Bekor qilindi")) yozilsa, nima yo'qoladi?

Javob

Sabab yo'qoladi. Chaqiruvchi endi nega bekor qilinganini bilmaydi: foydalanuvchi bosganmi (AbortError), vaqt tugaganmi (TimeoutError) yoki abort(new Error("Mehmon ketdi")) dagi o'z sababimizmi. signal.reason ni uzatsangiz, bu ma'lumot zanjir bo'ylab to'liq yetib boradi.

4. AbortSignal.timeout: vaqt chegarasi

4.1 Tayyor taymerli signal

Vaqt chegarasi uchun pult va setTimeout ni qo'lda yozish shart emas. AbortSignal.timeout(ms) — ms millisekunddan keyin o'zi bekor bo'ladigan signal:

js
function tayyorla(taom, ms, { signal } = {}) {
  return new Promise((resolve, reject) => {
    signal?.throwIfAborted();
    const taymer = setTimeout(resolve, ms, `${taom} tayyor`);
    signal?.addEventListener("abort", () => {
      clearTimeout(taymer);
      reject(signal.reason);
    }, { once: true });
  });
}

async function buyurtma(taom, ms) {
  try {
    const signal = AbortSignal.timeout(250);
    console.log(await tayyorla(taom, ms, { signal }));
  } catch (xato) {
    console.log(`${taom}: ${xato.name} — ${xato.message}`);
  }
}

async function sinab() {
  await buyurtma("somsa", 100);
  await buyurtma("osh", 400);
}

sinab();

Konsolda:

text
somsa tayyor
osh: TimeoutError — The operation was aborted due to timeout

Bu safar xato nomi — TimeoutError, AbortError emas: "The operation was aborted due to timeout" — "amal vaqt tugagani sababli bekor qilindi". Ikki xil nom ikki xil holatni ajratishga yordam beradi: foydalanuvchiga "Bekor qilindi" emas, "Server javob bermadi, qayta urinib ko'ring" deyish kerak.

tayyorla bu safar qisqaroq. setTimeout(resolve, ms, `${taom} tayyor`) — uchinchi argument vaqt kelganda resolve ga uzatiladi (Taymerlar darsidagi qo'shimcha argumentlar). Executor ichidagi throwIfAborted ham ishlaydi — executor'dagi throw Promise'ni rad etadi (Promise asoslari). Somsa uchun tozalash qadami (3-qoida) bu yerda ataylab tushirib qoldirildi — misol qisqa bo'lsin deb. Tozalash qachon muhimligini «Ko'p uchraydigan xatolar» da ko'ramiz.

Promise kombinatorlari darsidagi muddatBilan ni eslang: Promise.race, qo'lda taymer, finally da clearTimeout. AbortSignal.timeout bitta qator bilan undan ham ko'proq ishni qiladi — ishni kutishni emas, ishning o'zini to'xtatadi.

4.2 Bilib qo'ying

  • AbortSignal.timeout taymerini tozalash shart emas va tozalab ham bo'lmaydi. U dasturning tugashini ham ushlab turmaydi: Node faqat shu signal qolgan bo'lsa, uni kutmasdan jarayonni yopadi.
  • Brauzerda bu taymer faol vaqtni sanaydi: sahifa "orqaga-oldinga" keshida (bfcache — boshqa sahifaga o'tib, "Orqaga" bosganda tez qaytish uchun saqlangan nusxa) turganda u to'xtab turadi.
  • Node'ning ba'zi API'lari vaqt tugaganda TimeoutError ni o'raydi: node:timers/promises dagi setTimeout AbortError beradi, asl TimeoutError esa uning cause xususiyatida. Xatoni tekshirganda ikkalasiga ham qarang.

AbortSignal.timeout Baseline ma'lumotiga ko'ra 2024-yil apreldan hamma asosiy brauzerda bor. Node'da — 17.3 dan.

Tekshirib ko'ring: AbortSignal.timeout(250) ni buyurtma dan tashqarida bir marta yaratib, ikkala buyurtmaga ham bersak, nima bo'ladi?

Javob

Signal yaratilgandan 250 ms o'tgach bekor bo'ladi — buyurtma boshlangandan emas. Somsa (100 ms) ulguradi, osh esa 150 ms da — somsadan qolgan vaqtda — TimeoutError oladi. Har ish uchun yangi AbortSignal.timeout yarating: vaqt shu ish boshlangan paytdan sanalsin.

5. AbortSignal.any: bir nechta sabab

5.1 "Bekor qildi yoki vaqt tugadi"

Real ilovada odatda ikkala sabab birga kerak: foydalanuvchi "Bekor qilish" tugmasini bosishi mumkin va server javobi 5 soniyadan oshmasligi kerak. Lekin ishga faqat bitta signal berish mumkin.

AbortSignal.any(signallar) — berilgan signallardan birontasi bekor bo'lganda o'zi ham bekor bo'ladigan yangi signal. Uning reason i — birinchi bekor bo'lgan signalning sababi:

js
function tayyorla(taom, ms, { signal } = {}) {
  return new Promise((resolve, reject) => {
    signal?.throwIfAborted();
    const taymer = setTimeout(resolve, ms, `${taom} tayyor`);
    signal?.addEventListener("abort", () => {
      clearTimeout(taymer);
      reject(signal.reason);
    }, { once: true });
  });
}

async function buyurtma(taom, ms, mehmonKetadi) {
  const pult = new AbortController();
  setTimeout(() => pult.abort(), mehmonKetadi);
  const signal = AbortSignal.any([
    pult.signal,
    AbortSignal.timeout(300),
  ]);
  try {
    console.log(await tayyorla(taom, ms, { signal }));
  } catch (xato) {
    console.log(`${taom}: ${xato.name}`);
  }
}

async function sinab() {
  await buyurtma("somsa", 100, 500);
  await buyurtma("manti", 400, 150);
  await buyurtma("osh", 400, 500);
}

sinab();

Konsolda:

text
somsa tayyor
manti: AbortError
osh: TimeoutError

Uch holat — uch natija. Somsa ikkala chegaradan oldin tayyor. Manti — mehmon 150 ms da ketdi (pult, AbortError). Osh — mehmon kutdi, lekin 300 ms lik chegara tugadi (TimeoutError). tayyorla ning o'zi o'zgarmadi: u hamon bitta signal oladi va qayerdan kelganini bilmaydi.

Diqqat qiling: misolda pultning setTimeout i ish tugagach ham host'da qoladi va keyinroq abort() ni bosadi. Ish allaqachon tugagani uchun zarari yo'q — abort() tugagan ishga ta'sir qilmaydi.

AbortSignal.any Baseline ma'lumotiga ko'ra 2024-yil martidan hamma asosiy brauzerda (Chrome 116, Firefox 124, Safari 17.4), 2026-yil sentyabridan esa "keng tarqalgan". Node'da — 20.3 dan.

5.2 Signal zanjiri

AbortSignal.any bilan ierarxiya qurish mumkin. Masalan, sahifaning umumiy pulti (foydalanuvchi sahifadan ketdi — hammasi to'xtasin) va har so'rovning o'z pulti:

flowchart TD
  S["sahifa pulti<br/>(sahifadan chiqildi)"] --> A1["any: 1-so'rov"]
  S --> A2["any: 2-so'rov"]
  Q1["1-so'rov pulti"] --> A1
  T["timeout(5000)"] --> A1
  Q2["2-so'rov pulti"] --> A2

Sahifa pulti bosilsa — ikkala so'rov ham to'xtaydi. 1-so'rov pulti bosilsa — faqat birinchisi. React kabi freymvorklarda komponent yopilganda uning hamma so'rovlarini to'xtatish aynan shunday quriladi. React'ni 17-qismda o'rganamiz, hozir bilish shart emas.

Tekshirib ko'ring: AbortSignal.any([a, b]) yaratilganda a allaqachon bekor bo'lgan bo'lsa, yangi signal qanday holatda bo'ladi?

Javob

Darhol bekor bo'lgan holatda, reason i esa a ning sababi. any "birontasi bekor bo'lsa" qoidasini yaratilish paytida ham tekshiradi. Shuning uchun uni olgan ish «Uch qoida» dagi birinchisi ("boshida tekshiring") bo'yicha darhol rad etiladi.

6. Amaliyot: oxirgi so'rov yutadi

6.1 Muammo — haqiqiy chiqishda

«Nega bu kerak?» dagi qidiruvni yozamiz. Server o'rnida — menyudanQidir: qisqa so'rovga ko'proq taom mos keladi, javob ham sekinroq keladi:

js
const menyu = ["osh", "manti", "somsa", "shashlik", "sho'rva"];

function menyudanQidir(soz) {
  const ms = 800 - soz.length * 200;
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(menyu.filter((taom) => taom.startsWith(soz)));
    }, ms);
  });
}

async function qidir(soz) {
  const natija = await menyudanQidir(soz);
  console.log(`«${soz}» → ${natija.join(", ")}`);
}

qidir("o");
qidir("os");
qidir("osh");

Konsolda:

text
«osh» → osh
«os» → osh
«o» → osh

Bu misolda natijalar bir xil ko'rinadi — "osh" bilan boshlanadigan taom bitta. Lekin tartibga qarang: oxirgi chiqqani — eng eski so'rov "o". Haqiqiy saytda u ekranga oxirgi bo'lib yoziladi va "osh" natijasini bosib ketadi. "s" bilan sinasangiz ("s", "sh", "sho"), ekranda oxirida "somsa, shashlik, sho'rva" qolardi — garchi Aziz "sho" deb yozgan bo'lsa ham.

6.2 Yechim: oldingisini bekor qilish

Har yangi qidiruvda oldingi so'rovning pultini bosamiz. Natijani sinab ko'ring: maydonga yozing — har harf oldingi so'rovni bekor qiladi. Sahifa ochilganda "s", "sh", "sho" tez-tez yozilgandek uchta qidiruv o'zi ishga tushadi:

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  input { font: inherit; padding: 0.3rem 0.5rem; width: 12rem; }
</style>
<label for="qidiruv">Menyudan qidirish</label><br>
<input id="qidiruv" type="search" autocomplete="off">
<p id="natija" aria-live="polite">—</p>
<script>
  const menyu = ["osh", "manti", "somsa", "shashlik", "sho'rva"];
  const natijaP = document.getElementById("natija");
  let pult = null;

  function menyudanQidir(soz, { signal }) {
    const ms = 800 - Math.min(soz.length, 3) * 200;
    return new Promise((resolve, reject) => {
      signal.throwIfAborted();
      const taymer = setTimeout(() => {
        resolve(menyu.filter((taom) => taom.startsWith(soz)));
      }, ms);
      signal.addEventListener("abort", () => {
        clearTimeout(taymer);
        reject(signal.reason);
      }, { once: true });
    });
  }

  async function qidir(soz) {
    pult?.abort();
    pult = new AbortController();
    try {
      const { signal } = pult;
      const natija = await menyudanQidir(soz, { signal });
      natijaP.textContent = natija.join(", ") || "topilmadi";
      console.log(`«${soz}» → ${natijaP.textContent}`);
    } catch (xato) {
      if (xato.name === "AbortError") {
        console.log(`«${soz}» bekor qilindi`);
        return;
      }
      throw xato;
    }
  }

  const maydon = document.getElementById("qidiruv");
  maydon.addEventListener("input", () => {
    qidir(maydon.value.trim().toLowerCase());
  });

  qidir("s");
  qidir("sh");
  qidir("sho");
</script>

Konsolda:

text
«s» bekor qilindi
«sh» bekor qilindi
«sho» → sho'rva

Ekranda faqat "sho'rva" — oxirgi so'rov natijasi. Eski ikki so'rov javobini kutmasdan to'xtatildi: ularning taymerlari tozalandi va catch ga AbortError keldi.

Uch muhim joy:

  • pult?.abort() — birinchi qidiruvda pult hali null, ?. xatoni oldini oladi.
  • Har qidiruv uchun yangi AbortController — bekor qilingan pultni qayta ishlatib bo'lmaydi (Listener opsiyalari dagi tuzoq).
  • catch da AbortError — xato emas, kutilgan holat: foydalanuvchiga hech narsa ko'rsatilmaydi. Boshqa xatolar esa yuqoriga throw bilan uzatiladi.

Haqiqiy loyihada menyudanQidir o'rnida fetch(url, { signal }) turadi: fetch ning signal opsiyasini Request, Response, Headers va fetch opsiyalari darsida, shu naqshni haqiqiy server bilan esa API klient qatlami darsida ko'ramiz. Odatda bu naqsh debounce bilan birga ishlatiladi — u so'rovlar sonini kamaytiradi: foydalanuvchi yozishdan to'xtagandagina so'raydi. Uni 13-qismda, Debounce va throttle darsida o'rganamiz, hozir bilish shart emas. Debounce so'rovlarni kamaytiradi, abort esa baribir ketgan eskilarini to'xtatadi.

7. Signalni qabul qiladigan API'lar

Bitta signal — bitta kelishuv. Uni tushunadigan tayyor API'lar ko'p:

API Qayerda Bekor bo'lganda
addEventListener(..., { signal }) brauzer, Node tinglovchi olib tashlanadi
fetch(url, { signal }) brauzer, Node so'rov to'xtaydi, AbortError
setTimeout (node:timers/promises) Node taymer tozalanadi, AbortError
readFile(yol, { signal }) (node:fs/promises) Node o'qish to'xtaydi, AbortError

Node misoli:

js
import { setTimeout as kut } from "node:timers/promises";

const pult = new AbortController();
setTimeout(() => pult.abort(), 100);

try {
  await kut(1000, "choy tayyor", { signal: pult.signal });
} catch (xato) {
  console.log(xato.name, xato.code); // AbortError ABORT_ERR
}

xato.code — Node xatolarining qisqa kodi (Callbacklar darsidagi ENOENT kabi). Bu yerda await modulning eng yuqorisida — Promise yordamchilari darsidagi node:timers/promises misolida ko'rgansiz.

Qoida: o'z kutubxonangiz yoki API klientingizni yozsangiz, ular ham { signal } opsiyasini qabul qilsin va uni ichki fetch/taymerlarga uzatsin. Shunda chaqiruvchi hammasini bitta pult bilan boshqaradi.

8. Ko'p uchraydigan xatolar

8.1 "abort() ishni to'xtatadi" deb o'ylash

abort() faqat signalni yoqadi. Signalni hech kim tinglamasa — hech narsa to'xtamaydi: taymer ishlayveradi, Promise o'z vaqtida bajariladi. O'z funksiyangiz signalni qabul qilib, uch qoidani bajarsagina bekor qilish ishlaydi.

8.2 Abort tinglovchisini tozalamaslik

Sahifa uchun bitta uzoq yashovchi signal bo'lsa va har ish unga tinglovchi qo'shsa-yu, tugaganda olib tashlamasa — tinglovchilar to'planaveradi. Har biri closure orqali o'z ishining ma'lumotlarini ushlab turadi — bu xotira sizib chiqishi. Tuzatish: ish tugaganda removeEventListener (3-qoida) yoki har ishga AbortSignal.any bilan alohida qisqa umrli signal.

8.3 AbortError ni xato sifatida ko'rsatish

Foydalanuvchi o'zi bekor qildi — unga qizil "Xato: This operation was aborted" ko'rsatish g'alati. catch da avval xato.name === "AbortError" ni tekshiring va jim qayting. TimeoutError esa — haqiqiy muammo, uni tushunarli matn bilan ko'rsating.

8.4 Bekor qilingan pultni qayta ishlatish

Bir marta abort() qilingan signal abadiy bekor. Uni yangi ishga bersangiz, ish darhol rad etiladi (1-qoida). Har yangi ish — yangi AbortController.

8.5 Signalni ichki chaqiruvlarga uzatmaslik

oshDamla(signal) ichida kut(100) ga signal berilmasa, bekor qilish faqat qadamlar orasida seziladi. Funksiya signalni olgan bo'lsa — uni o'zi chaqiradigan hamma asinxron ishga ham uzatsin.

9. Mashqlar

1-mashq (oson): Bekor qilinadigan kut

Promise yordamchilari darsidagi kut(ms) ni kut(ms, { signal } = {}) ko'rinishiga keltiring: signal bekor bo'lsa — taymer tozalansin va Promise signal.reason bilan rad etilsin; signal oldindan bekor bo'lsa — darhol rad etilsin. Sinang: kut(500, { signal: AbortSignal.timeout(100) }) — catch da xato.name ni chiqaring.

Yechim
js
function kut(ms, { signal } = {}) {
  return new Promise((resolve, reject) => {
    if (signal?.aborted) {
      reject(signal.reason);
      return;
    }
    function bekorQil() {
      clearTimeout(taymer);
      reject(signal.reason);
    }
    const taymer = setTimeout(() => {
      signal?.removeEventListener("abort", bekorQil);
      resolve();
    }, ms);
    signal?.addEventListener("abort", bekorQil, { once: true });
  });
}

async function sinab() {
  await kut(100);
  console.log("100 ms kutildi");
  try {
    await kut(500, { signal: AbortSignal.timeout(100) });
  } catch (xato) {
    console.log(xato.name);
  }
  try {
    await kut(100, { signal: AbortSignal.abort() });
  } catch (xato) {
    console.log(xato.name);
  }
}

sinab();

Konsolda:

text
100 ms kutildi
TimeoutError
AbortError

Uch holat: signalsiz (oddiy kutish), vaqt tugadi va oldindan bekor qilingan signal. Funksiya uchala qoidani bajaradi: boshida tekshiradi, abort'ni tinglaydi, tugaganda tinglovchini olib tashlaydi.

2-mashq (o'rta): Osh damlash — darhol to'xtash bilan

«Ko'p qadamli ish» dagi oshDamla ni yaxshilang: signalni 1-mashqdagi kut ga ham uzating, shunda bekor qilish qadam o'rtasida ham darhol ishlasin. Keyin uni AbortSignal.any bilan chaqiring: mehmon 150 ms da ketadi (new Error("Mehmon ketdi") sababi bilan), umumiy chegara — 1000 ms. Kutilgan natija: "guruch yuvildi" chiqadi, "zirvak tayyor" esa chiqmaydi.

Yechim
js
function kut(ms, { signal } = {}) {
  return new Promise((resolve, reject) => {
    signal?.throwIfAborted();
    function bekorQil() {
      clearTimeout(taymer);
      reject(signal.reason);
    }
    const taymer = setTimeout(() => {
      signal?.removeEventListener("abort", bekorQil);
      resolve();
    }, ms);
    signal?.addEventListener("abort", bekorQil, { once: true });
  });
}

async function oshDamla(signal) {
  const qadamlar = ["guruch yuvildi", "zirvak tayyor", "damlandi"];
  for (const qadam of qadamlar) {
    await kut(100, { signal });
    console.log(" ", qadam);
  }
  return "osh tayyor";
}

async function sinab() {
  const pult = new AbortController();
  setTimeout(() => pult.abort(new Error("Mehmon ketdi")), 150);
  const signal = AbortSignal.any([
    pult.signal,
    AbortSignal.timeout(1000),
  ]);
  try {
    console.log(await oshDamla(signal));
  } catch (xato) {
    console.log("To'xtadi:", xato.message);
  }
}

sinab();

Konsolda:

text
  guruch yuvildi
To'xtadi: Mehmon ketdi

Endi 150 ms da ikkinchi kut ning taymeri darhol tozalandi — "zirvak tayyor" yozilmadi. throwIfAborted sikldan olib tashlandi: kut ning o'zi boshida tekshiradi. AbortSignal.any ning reason i — pultning o'z sababi, shuning uchun message — "Mehmon ketdi".

3-mashq (qiyin): oxirgisiYutadi o'rami

«Oxirgi so'rov yutadi» naqshini qayta ishlatiladigan qiling. oxirgisiYutadi(fn) funksiyasini yozing: u fn(argument, { signal }) ko'rinishidagi funksiyani oladi va yangi funksiya qaytaradi. Yangi funksiya har chaqirilganda oldingi chaqiruvni bekor qilsin. Bekor qilingan chaqiruvning Promise'i AbortError bilan rad etilsin. Faylni kurs/mashqlar/11/12-abort/oxirgisiYutadi.mjs ga saqlang.

Sinash uchun «Oxirgi so'rov yutadi» dagi natija blokidan menyudanQidir ni oling.

Yechim
js
function oxirgisiYutadi(fn) {
  let pult = null;
  return (argument) => {
    pult?.abort();
    pult = new AbortController();
    return fn(argument, { signal: pult.signal });
  };
}

const menyu = ["osh", "manti", "somsa", "shashlik", "sho'rva"];

function menyudanQidir(soz, { signal }) {
  const ms = 800 - Math.min(soz.length, 3) * 200;
  return new Promise((resolve, reject) => {
    signal.throwIfAborted();
    const taymer = setTimeout(() => {
      resolve(menyu.filter((taom) => taom.startsWith(soz)));
    }, ms);
    signal.addEventListener("abort", () => {
      clearTimeout(taymer);
      reject(signal.reason);
    }, { once: true });
  });
}

const qidir = oxirgisiYutadi(menyudanQidir);

for (const soz of ["s", "sh", "sho"]) {
  qidir(soz)
    .then((natija) => console.log(`«${soz}» →`, natija.join()))
    .catch((xato) => console.log(`«${soz}»:`, xato.name));
}

Konsolda:

text
«s»: AbortError
«sh»: AbortError
«sho» → sho'rva

pult closure'da yashaydi (Closure amaliyotda) — har oxirgisiYutadi chaqiruvining o'z pulti bor. Shuning uchun ikki xil qidiruv maydoni bir-birining so'rovini bekor qilmaydi. AbortError lar catch ga birinchi bo'lib keladi: ular abort() paytida, sinxron kod ichida rad etilgan.

Shu g'oya — eski so'rovni pult bilan bekor qilish — API klient qatlami darsida haqiqiy server so'rovlariga qo'llanadi: vazifalar ning api.js metodlari signal opsiyasini qabul qiladi.

bash
git add 11/12-abort/oxirgisiYutadi.mjs
git commit -m "11/12: oxirgisiYutadi — eski so'rovni bekor qilish"

10. Real ishda

  • Qidiruv va filtrlar. "Oxirgi so'rov yutadi" — har qidiruv maydoni, avtoto'ldirish va filtr panelining standart talabi. Kod sharhida (code review — hamkasbingiz kodingizni o'qib, fikr bildiradi) birinchi so'raladigan savollardan: "eski so'rov kech kelsa nima bo'ladi?"
  • Freymvorklar. React'da komponent yopilganda uning so'rovlari AbortController bilan to'xtatiladi; TanStack Query (serverdan ma'lumot olishni boshqaradigan kutubxona) har so'rovga signalni o'zi beradi. Ularni kursning keyingi qismlarida ko'ramiz, hozir bilish shart emas.
  • Vaqt chegarasi. Har tashqi so'rovga AbortSignal.timeout — production'dagi (foydalanuvchilar ishlatayotgan haqiqiy ilovadagi) qat'iy qoida. vazifalar ning api.js ida ham shunday qilamiz (API klient qatlami).
  • Node serverlari. Mijoz ulanishni uzsa, server shu mijoz uchun boshlagan bazaga so'rovlar va tashqi API chaqiruvlarini signal orqali to'xtatadi — keraksiz ish qilinmaydi.
  • Intervyu. "Promise'ni bekor qilib bo'ladimi?" — to'g'ri javob: "Promise'ning o'zini yo'q, lekin uning ortidagi ishni AbortController bilan to'xtatish mumkin" va qanday yozilishi.

Xulosa

  • Promise'ni bekor qilib bo'lmaydi — ishni AbortController (pult) va uning signal i (chiroq) orqali to'xtatasiz.
  • abort(sabab?) — signal bir marta bekor bo'ladi, "abort" hodisasi sinxron keladi, reason — sukut bo'yicha AbortError.
  • O'z funksiyangiz: { signal } opsiyasi; boshida tekshirish, abort'ni tinglab to'xtash, tugaganda tinglovchini olib tashlash; reject(signal.reason).
  • AbortSignal.timeout(ms) — vaqt chegarasi, TimeoutError; AbortSignal.any([...]) — birontasi bekor bo'lsa.
  • catch da AbortError — kutilgan holat, xato emas; har yangi ish — yangi pult.

Keyingi dars: Asinxron iteratsiya — vaqt o'tib bo'lak-bo'lak keladigan ma'lumotni for await...of va async generator bilan aylanish.

Manbalar

  • DOM Living Standard: "Interface AbortController", "Interface AbortSignal" (timeout, any, throwIfAborted) — dom.spec.whatwg.org
  • MDN: "AbortController", "AbortSignal", "AbortSignal.timeout()", "AbortSignal.any()" — developer.mozilla.org
  • Node.js hujjatlari: "AbortController", "Timers Promises API" — nodejs.org/api
  • web-features (Baseline): aborting, abortsignal-timeout, abortsignal-any — github.com/web-platform-dx/web-features
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
AbortController: asinxron ishni bekor qilish — AbortSignal, timeout va any — IlmHamroh