اگر تا به حال با APIها در جاوااسکریپت کار کرده باشیم، احتمالاً با AbortController در جاوااسکریپت آشنا شده‌ایم. معمولاً از این قابلیت برای لغو درخواست‌های fetch استفاده می‌کنیم، به‌ویژه در فریم‌ورک‌هایی مانند React.

با این حال، چیزی که کم‌تر شناخته شده این است که این تنها بخش کوچکی از توانایی‌های AbortController محسوب می‌شود. در این مقاله قصد داریم ویژگی‌های پیشرفته و کاربردهای شگفت‌انگیز AbortController را بررسی کنیم.

مبانی AbortController در جاوااسکریپت

پیش از ورود به ویژگی‌های پیشرفته، لازم است مبانی کار با AbortController را مرور کنیم. اگر با این API آشنا هستیم، می‌توانیم این بخش را نادیده بگیریم.

رابط AbortController به این شکل عمل می‌کند که یک signal ایجاد کرده و آن را به توابع مشخصی در جاوااسکریپت ارسال می‌کنیم. این signal به تابع اطلاع می‌دهد که آیا باید ادامه دهد یا متوقف شود. برای متوقف کردن یک تابع کافی است متد controller.abort() را فراخوانی کنیم. در این صورت هر تابعی که در حال گوش دادن به آن سیگنال باشد، بلافاصله اجرای خود را متوقف خواهد کرد.

const controller = new AbortController()
const signal = controller.signal

fetch("/api/data", { signal })
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => {
    if (err.name === "AbortError") {
      console.log("Request was aborted")
    } else {
      throw err
    }
  })

// Cancel the request
controller.abort()

رایج‌ترین کاربرد AbortController در درخواست‌های fetch است، اما می‌توانیم از آن در هر API که از گزینه signal پشتیبانی کند نیز استفاده کنیم.

استفاده از AbortController با Event Listenerها

یکی دیگر از قابلیت‌های AbortController این است که می‌توانیم از آن برای حذف Event Listenerها استفاده کنیم.

const controller = new AbortController()
const signal = controller.signal

window.addEventListener("resize", () => console.log("Resized"), {
  signal,
})

// Later, this removes the listener
controller.abort()

زمانی که یک Event Listener ایجاد می‌کنیم، امکان ارسال یک آبجکت تنظیمات به‌عنوان پارامتر سوم وجود دارد. اگر در این آبجکت ویژگی signal را همراه با سیگنال AbortController قرار دهیم، در صورت فراخوانی controller.abort()، آن Event Listener به‌صورت خودکار حذف خواهد شد؛ درست مانند زمانی که متد removeEventListener را صدا می‌زنیم.

این قابلیت به‌ویژه در فریم‌ورک‌هایی مانند React کاربرد دارد؛ جایی که نیاز داریم Event Listenerها را در useEffect پاک‌سازی کنیم یا زمانی که چندین Event Listener داریم و می‌خواهیم همه آن‌ها را به‌طور همزمان حذف نماییم.

useEffect(() => {
  const controller = new AbortController()
  const signal = controller.signal

  window.addEventListener("dragstart", () => console.log("Drag started"), {
    signal,
  })
  window.addEventListener("dragend", () => console.log("Drag ended"), {
    signal,
  })

  return () => {
    // Removes all the listeners
    controller.abort()
  }
}, [])

توابع داخلی AbortController در جاوااسکریپت

رابط AbortController چندین تابع داخلی نیز دارد که از طریق AbortSignal در دسترس قرار می‌گیرند و می‌توانند کار ما را ساده‌تر کنند.

AbortSignal.timeout()

یکی از کم استفاده‌ترین ویژگی‌های AbortController متد AbortSignal.timeout() است. این متد سیگنالی ایجاد می‌کند که پس از یک بازه زمانی مشخص، به‌صورت خودکار متوقف می‌شود:

const signal = AbortSignal.timeout(5000) // 5 seconds

fetch("/api/slow-endpoint", { signal }).catch(err => {
  if (err.name === "TimeoutError") {
    console.log("Request timed out")
  }
})

کد بالا یک درخواست fetch ایجاد می‌کند که اگر ظرف مدت ۵ ثانیه تکمیل نشود، به‌طور خودکار لغو خواهد شد. بنابراین، دیگر نیازی به مدیریت دستی timeout با استفاده از setTimeout و clearTimeout نداریم.

AbortSignal.any()

قابلیت قدرتمند دیگر، متد AbortSignal.any() است. این متد سیگنالی ایجاد می‌کند که به محض لغو شدن هر یک از سیگنال‌های ارسال‌شده، متوقف می‌شود:

const controller = new AbortController()
const signal = AbortSignal.any(
  controller.signal,
  AbortSignal.timeout(3000), // 3 seconds
)

این ویژگی زمانی بسیار مفید است که بخواهیم یک timeout و یک سیگنال لغو دستی را ترکیب کنیم. در چنین حالتی، درخواست هم در صورت رسیدن به زمان مشخص و هم در صورت لغو دستی، متوقف خواهد شد.

AbortSignal.abort()

با استفاده از AbortSignal.abort() می‌توانیم یک سیگنال ایجاد کنیم که از همان ابتدا در حالت لغو قرار داشته باشد:

const signal = AbortSignal.abort()
fetch("/api/data", { signal }).catch(err => {
  if (err.name === "AbortError") {
    console.log("Request was aborted")
  }
})

این قابلیت احتمالاً کم کاربردترین متد داخلی است، اما در برخی شرایط خاص می‌تواند مفید واقع شود.

ساخت توابع قابل لغو با AbortController در جاوااسکریپت

قدرت اصلی AbortController زمانی آشکار می‌شود که توابع اختصاصی خود را ایجاد کنیم تا از قابلیت لغو پشتیبانی کنند. این امکان باعث می‌شود APIهایی بسازیم که مانند fetch قابلیت لغو شدن داشته باشند.

برای این کار کافی است یک پارامتر سیگنال در تابع خود بپذیریم، وضعیت آن را بررسی کنیم و به ایونت abort گوش دهیم:

function doSomething(signal) {
  return new Promise((resolve, reject) => {
    // Is it already aborted?
    if (signal.aborted) {
      reject(signal.reason)
      return
    }

    // Listen for abort events
    signal.addEventListener("abort", () => {
      clearTimeout(id)
      reject(signal.reason)
    })

    // Simulate a long-running operation
    const id = setTimeout(() => resolve("Did Something"), 5000)
  })
}

به همین سادگی می‌توانیم یک API قابل لغو بسازیم. حالا می‌توانیم از این تابع همراه با AbortController استفاده کنیم.

// Cancel the operation after 3 seconds
const signal = AbortSignal.timeout(3000)

doSomething(signal)
  .then(result => console.log(result))
  .catch(err => {
    if (err.name === "AbortError") {
      console.log("Operation was aborted")
    } else {
      throw err
    }
  })

در ادامه یک تابع کمکی ساده ایجاد می‌کنیم تا فرآیند ساخت توابع قابل لغو برای ما آسان‌تر شود.

function makeAbortable(fn) {
  return signal => {
    return new Promise((resolve, reject) => {
      if (signal.aborted) {
        reject(signal.reason)
        return
      }

      signal.addEventListener("abort", () => {
        reject(signal.reason)
      })

      fn(resolve, reject, signal)
    })
  }
}

جمع‌بندی

AbortController یک API قدرتمند در جاوااسکریپت است که فراتر از لغو درخواست‌های fetch عمل می‌کند. ما می‌توانیم از آن برای پاک‌سازی Event Listenerها، ایجاد سیگنال‌های timeout و حتی ساخت توابع اختصاصی قابل لغو استفاده کنیم.