آموزش استفاده از AbortController در React

در این مقاله، به‌صورت گام‌به‌گام با نحوه استفاده از APIهای کاربردی

AbortController
AbortController و
AbortSignal
AbortSignal در محیط‌های فرانت‌اند و بک‌اند آشنا خواهیم شد. تمرکز اصلی ما بر استفاده از AbortController در React برای مدیریت و لغو درخواست‌های asynchronous خواهد بود، اما نحوه پیاده‌سازی آن در Node.js را نیز بررسی خواهیم کرد.

معرفی API AbortController و کاربردهای آن در مدیریت عملیات asynchronous

API مربوط به

AbortController از نسخه ۱۵ به Node.js افزوده شد. این API ابزاری کاربردی برای متوقف‌سازی فرآیندهای asynchronous محسوب می‌شود و عملکردی مشابه رابط
AbortController
AbortController در محیط مرورگر دارد.

برای استفاده از این API، ابتدا باید یک نمونه از کلاس

AbortController
AbortController ایجاد کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const controller = new AbortController();
const controller = new AbortController();
const controller = new AbortController();

آشنایی با متد abort و ویژگی signal

نمونه‌ای که از کلاس

AbortController
AbortController ساخته می‌شود، شامل دو ویژگی است:

  • متد
    abort
    abort
  • ویژگی
    signal
    signal

فراخوانی متد

abort
abort، یک event با نام
abort
abort منتشر می‌کند تا به API قابل لغو که به controller متصل است، اطلاع دهد که عملیات باید متوقف شود. همچنین هنگام فراخوانی
abort
abort می‌توانیم یک دلیل دلخواه نیز برای لغو عملیات ارسال کنیم. اگر دلیلی مشخص نشود، به‌صورت پیش‌فرض خطای
AbortError
AbortError صادر خواهد شد.

استفاده از متد addEventListener برای شنیدن abort event

برای شنیدن event مربوط به

abort
abort، باید با استفاده از متد
addEventListener
addEventListener، یک event listener به ویژگی
signal
signal اضافه کنیم تا در زمان وقوع event، کدی اجرا شود. برای حذف این listener نیز می‌توانیم از متد
removeEventListener
removeEventListener استفاده کنیم.

کد زیر نحوه اضافه کردن و حذف کردن event listener

abort
abort را با استفاده از این دو متد نشان می‌دهد:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const controller = new AbortController();
const { signal } = controller;
const abortEventListener = (event) => {
console.log(signal.aborted); // true
console.log(signal.reason); // Hello World
};
signal.addEventListener("abort", abortEventListener);
controller.abort("Hello World");
signal.removeEventListener("abort", abortEventListener);
const controller = new AbortController(); const { signal } = controller; const abortEventListener = (event) => { console.log(signal.aborted); // true console.log(signal.reason); // Hello World }; signal.addEventListener("abort", abortEventListener); controller.abort("Hello World"); signal.removeEventListener("abort", abortEventListener);
const controller = new AbortController();
const { signal } = controller;

const abortEventListener = (event) => {
  console.log(signal.aborted); // true
  console.log(signal.reason); // Hello World
};

signal.addEventListener("abort", abortEventListener);
controller.abort("Hello World");
signal.removeEventListener("abort", abortEventListener);

بررسی ویژگی‌های reason و aborted در signal

ویژگی

signal
signal در controller دارای دو ویژگی کلیدی
reason
reason و
aborted
aborted است. ویژگی
reason
reason، دلیلی است که هنگام فراخوانی
abort
abort به آن ارسال می‌شود. مقدار اولیه این ویژگی
undefined
undefined است، اما پس از لغو عملیات، به مقدار دلیل ارائه‌شده (یا
AbortError
AbortError در صورت عدم ارائه دلیل) تغییر خواهد کرد.

همچنین ویژگی

aborted
aborted که به‌صورت پیش‌فرض مقدار
false
false دارد، پس از فراخوانی متد
abort
abort به
true
true تغییر می‌کند.

برخلاف مثال‌های آموزشی ساده، در عمل استفاده از

AbortController
AbortController به این صورت است که ویژگی
signal
signal را به APIهایی ارسال می‌کنیم که قابلیت لغو دارند. حتی می‌توان یک
signal
signal را به چندین عملیات asynchronous تخصیص داد؛ این APIها در صورت فراخوانی
abort
abort توسط controller، عملیات خود را متوقف خواهند کرد.

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

signal
signal را به آن‌ها بدهیم تا در زمان فراخوانی متد
abort
abort، فرآیندشان به‌درستی متوقف شود.

اما اگر قصد پیاده‌سازی عملکردی دلخواه و مبتنی بر Promise داشته باشیم که قابلیت لغو داشته باشد، باید به‌صورت دستی یک event listener برای

abort
abort اضافه کرده و هنگام وقوع آن، عملیات مورد نظر را متوقف کنیم.

دلایل استفاده از AbortController در پروژه‌های Node.js و مرورگر

زبان جاوااسکریپت به‌صورت پیش‌فرض single-threaded است. با توجه به محیط اجرایی، موتور جاوااسکریپت پردازش‌های asynchronous مانند درخواست‌های شبکه‌ای، دسترسی به سیستم فایل و سایر وظایف زمان‌بر را به برخی APIهای دیگر واگذار می‌کند تا به‌صورت asynchronous اجرا شوند.

به‌طور معمول انتظار داریم که نتیجه یک عملیات asynchronous یا موفقیت‌آمیز باشد یا با خطا مواجه شود. با این حال، ممکن است عملیات زمان بیشتری نسبت به حد انتظار بگیرد یا حتی در برخی موارد دیگر، اصلاً به نتیجه آن نیازی نداشته باشیم.

بنابراین منطقی است که عملیاتی که بیش از حد طول کشیده یا نتیجه‌اش دیگر مورد نیاز نیست را لغو کنیم. اما تا پیش از ارائه AbortController، این کار به صورت native در جاوااسکریپت بسیار دشوار بود.

با معرفی

AbortController
AbortController در نسخه ۱۵ Node.js، امکان لغو برخی عملیات asynchronous به‌صورت native فراهم شد.

نحوه استفاده از AbortController در محیط Node.js

AbortController
AbortController یکی از قابلیت‌های نسبتاً جدید در Node.js است. بنابراین در حال حاضر تنها برخی از APIهای asynchronous از آن پشتیبانی می‌کنند. این APIها شامل نسخه جدید Fetch API، تایمرها،
fs.readFile
fs.readFile،
fs.writeFile
fs.writeFile،
http.request
http.request و
https.request
https.request می‌باشند.

در این بخش، نحوه استفاده از

AbortController
AbortController در کنار برخی APIهای داخلی را بررسی می‌کنیم. از آنجا که روش استفاده در اکثر آن‌ها مشابه است، تنها به دو مورد پرکاربرد یعنی Fetch و
fs.readFile
fs.readFile بسنده خواهیم کرد.

استفاده از AbortController برای لغو عملیات Fetch در Node.js و مرورگر 

در گذشته، کتابخانه

node-fetch ابزار اصلی برای ارسال درخواست‌های HTTP در Node.js محسوب می‌شد. اما با معرفی API داخلی fetch در نسخه‌های جدید Node.js، این روند در حال تغییر است.

fetch یکی از APIهای native در Node است که می‌توانیم عملکرد آن را با استفاده از

AbortController
AbortController کنترل کنیم. همان‌طور که پیش‌تر نیز اشاره شد، برای لغو عملیات باید ویژگی
signal
signal را به این‌گونه APIها که از Promise پشتیبانی می‌کنند، ارسال نماییم.

کد زیر نشان می‌دهد که چطور می‌توانیم از

AbortController
AbortController همراه با fetch در Node.js استفاده کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const url = "https://jsonplaceholder.typicode.com/todos/1";
const controller = new AbortController();
const signal = controller.signal;
const fetchTodo = async () => {
try {
const response = await fetch(url, { signal });
const todo = await response.json();
console.log(todo);
} catch (error) {
if (error.name === "AbortError") {
console.log("Operation timed out");
} else {
console.error(err);
}
}
};
fetchTodo();
controller.abort();
const url = "https://jsonplaceholder.typicode.com/todos/1"; const controller = new AbortController(); const signal = controller.signal; const fetchTodo = async () => { try { const response = await fetch(url, { signal }); const todo = await response.json(); console.log(todo); } catch (error) { if (error.name === "AbortError") { console.log("Operation timed out"); } else { console.error(err); } } }; fetchTodo(); controller.abort();
const url = "https://jsonplaceholder.typicode.com/todos/1";

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

const fetchTodo = async () => {
  try {
    const response = await fetch(url, { signal });
    const todo = await response.json();
    console.log(todo);
  } catch (error) {
    if (error.name === "AbortError") {
      console.log("Operation timed out");
    } else {
      console.error(err);
    }
  }
};

fetchTodo();

controller.abort();

مثال بالا به‌صورت ساده نحوه استفاده از

AbortController
AbortController را با API fetch در Node نمایش می‌دهد. اما در پروژه‌های واقعی، به‌ندرت پیش می‌آید که بلافاصله پس از شروع عملیات، آن را لغو نماییم.

همچنین لازم است تاکید کنیم که fetch هنوز در Node.js یک ویژگی آزمایشی محسوب می‌شود و ممکن است در نسخه‌های آینده تغییراتی در آن ایجاد شود.

کنترل لغو عملیات خواندن فایل با استفاده از AbortController و متد fs.readFile 

در بخش قبلی نحوه استفاده از

AbortController
AbortController همراه با API fetch را بررسی کردیم. به‌همان صورت، می‌توانیم از این قابلیت با سایر APIهای قابل لغو نیز بهره ببریم.

برای این منظور، کافی است ویژگی

signal
signal مربوط به controller را به تابع موردنظر (مثلاً
fs.readFile
fs.readFile) ارسال نماییم. مثال زیر نحوه استفاده از
AbortController
AbortController را همراه با
fs.readFile
fs.readFile نشان می‌دهد:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const fs = require("node:fs");
const controller = new AbortController();
const { signal } = controller;
fs.readFile("data.txt", { signal, encoding: "utf8" }, (error, data) => {
if (error) {
if (error.name === "AbortError") {
console.log("Read file process aborted");
} else {
console.error(error);
}
return;
}
console.log(data);
});
controller.abort();
const fs = require("node:fs"); const controller = new AbortController(); const { signal } = controller; fs.readFile("data.txt", { signal, encoding: "utf8" }, (error, data) => { if (error) { if (error.name === "AbortError") { console.log("Read file process aborted"); } else { console.error(error); } return; } console.log(data); }); controller.abort();
const fs = require("node:fs");

const controller = new AbortController();
const { signal } = controller;

fs.readFile("data.txt", { signal, encoding: "utf8" }, (error, data) => {
  if (error) {
    if (error.name === "AbortError") {
      console.log("Read file process aborted");
    } else {
      console.error(error);
    }
    return;
  }
  console.log(data);
});

controller.abort();

از آنجا که سایر APIهای قابل لغو نیز تقریباً به همین شیوه با

AbortController
AbortController کار می‌کنند، در این مقاله به بررسی آن‌ها نمی‌پردازیم.

بررسی ساختار و نقش کلاس AbortSignal در فرآیند لغو عملیات asynchronous 

هر نمونه‌ای از کلاس AbortController دارای یک نمونه متناظر از کلاس

AbortSignal
AbortSignal است که از طریق ویژگی
signal
signal قابل دسترسی می‌باشد.

با این حال، کلاس

AbortSignal
AbortSignal دارای قابلیت‌هایی مانند متد استاتیک
AbortSignal.timeout
AbortSignal.timeout نیز هست که می‌توانیم به‌صورت مستقل از AbortController نیز از آن‌ها استفاده کنیم.

کلاس

AbortSignal
AbortSignal از کلاس
EventTarget
EventTarget ارث‌بری کرده است و می‌تواند event
abort
abort را دریافت کند. بنابراین می‌توانیم با استفاده از متدهای
addEventListener
addEventListener و
removeEventListener
removeEventListener listenerهایی برای این event اضافه یا حذف نماییم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const controller = new AbortController();
const { signal } = controller;
signal.addEventListener(
"abort",
() => {
console.log("First event handler");
},
{ once: true }
);
signal.addEventListener(
"abort",
() => {
console.log("Second event handler");
},
{ once: true }
);
controller.abort();
const controller = new AbortController(); const { signal } = controller; signal.addEventListener( "abort", () => { console.log("First event handler"); }, { once: true } ); signal.addEventListener( "abort", () => { console.log("Second event handler"); }, { once: true } ); controller.abort();
const controller = new AbortController();
const { signal } = controller;

signal.addEventListener(
  "abort",
  () => {
    console.log("First event handler");
  },
  { once: true }
);
signal.addEventListener(
  "abort",
  () => {
    console.log("Second event handler");
  },
  { once: true }
);

controller.abort();

همان‌طور که در مثال بالا مشاهده می‌کنیم، می‌توانیم تعداد دلخواهی handler برای event

abort
abort تعریف کنیم. با فراخوانی متد
abort
abort، تمام این listenerها فعال می‌شوند.

برای جلوگیری از memory leak، حذف event handler پس از لغو عملیات یک رویه استاندارد محسوب می‌شود.

همچنین به‌جای حذف دستی handler با

removeEventListener
removeEventListener، می‌توانیم هنگام افزودن آن از آرگومان اختیاری
{ once: true }
{ once: true } استفاده کنیم. در این صورت، event تنها یک بار اجرا می‌شود و سپس به‌صورت خودکار حذف خواهد شد.

نحوه اعمال time out در عملیات asynchronous با استفاده از AbortSignal 

علاوه بر استفاده در کنار AbortController، کلاس AbortSignal دارای متدهای کاربردی خاص خود نیز هست. یکی از این متدها، متد استاتیک

AbortSignal.timeout
AbortSignal.timeout می‌باشد.

همان‌طور که از نام آن پیداست، از این متد می‌توان برای لغو عملیات asynchronous پس از گذشت زمان مشخصی استفاده کرد. این متد، یک عدد بر حسب میلی‌ثانیه دریافت کرده و یک سیگنال بازمی‌گرداند که می‌توانیم از آن برای لغو عملیات قابل لغو استفاده نماییم.

در ادامه، نحوه استفاده از این متد را در کنار API fetch مشاهده خواهیم کرد.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const signal = AbortSignal.timeout(200);
const url = "https://jsonplaceholder.typicode.com/todos/1";
const fetchTodo = async () => {
try {
const response = await fetch(url, { signal });
const todo = await response.json();
console.log(todo);
} catch (error) {
if (error.name === "AbortError") {
console.log("Operation timed out");
} else {
console.error(err);
}
}
};
fetchTodo();
const signal = AbortSignal.timeout(200); const url = "https://jsonplaceholder.typicode.com/todos/1"; const fetchTodo = async () => { try { const response = await fetch(url, { signal }); const todo = await response.json(); console.log(todo); } catch (error) { if (error.name === "AbortError") { console.log("Operation timed out"); } else { console.error(err); } } }; fetchTodo();
const signal = AbortSignal.timeout(200);
const url = "https://jsonplaceholder.typicode.com/todos/1";

const fetchTodo = async () => {
  try {
    const response = await fetch(url, { signal });
    const todo = await response.json();
    console.log(todo);
  } catch (error) {
    if (error.name === "AbortError") {
      console.log("Operation timed out");
    } else {
      console.error(err);
    }
  }
};

fetchTodo();

می‌توانیم از

AbortSignal.timeout
AbortSignal.timeout به شیوه‌ای مشابه برای سایر APIهای قابل لغو نیز استفاده کنیم.

روش لغو هم‌زمان چند عملیات asynchronous با استفاده از یک نمونه AbortSignal 

زمانی که با بیش از یک abort signal سر و کار داریم، می‌توانیم آن‌ها را با استفاده از متد

AbortSignal.any()
AbortSignal.any() ترکیب کنیم. این قابلیت زمانی بسیار مفید است که بخواهیم چندین روش مختلف برای لغو یک عملیات داشته باشیم.

در مثال زیر، دو controller ایجاد خواهیم کرد:

اولی توسط کاربر API کنترل می‌شود و دومی برای اهداف داخلی مانند تعیین محدودیت زمانی به کار می‌رود. اگر هرکدام از این دو سیگنال عملیات را لغو کند، event listener مربوطه حذف خواهد شد:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Create two separate controllers for different concerns
const userController = new AbortController();
const timeoutController = new AbortController();
// Set up a timeout that will abort after 5 seconds
setTimeout(() => timeoutController.abort(), 5000);
// Register an event listener that can be aborted by either signal
document.addEventListener('click', handleUserClick, {
signal: AbortSignal.any([userController.signal, timeoutController.signal])
});
// Create two separate controllers for different concerns const userController = new AbortController(); const timeoutController = new AbortController(); // Set up a timeout that will abort after 5 seconds setTimeout(() => timeoutController.abort(), 5000); // Register an event listener that can be aborted by either signal document.addEventListener('click', handleUserClick, { signal: AbortSignal.any([userController.signal, timeoutController.signal]) });
// Create two separate controllers for different concerns
const userController = new AbortController();
const timeoutController = new AbortController();

// Set up a timeout that will abort after 5 seconds
setTimeout(() => timeoutController.abort(), 5000);

// Register an event listener that can be aborted by either signal
document.addEventListener('click', handleUserClick, {
  signal: AbortSignal.any([userController.signal, timeoutController.signal])
});

اگر هر یک از سیگنال‌ها در گروه ترکیبی، عملیات را لغو کند، سیگنال ترکیبی بلافاصله غیرفعال می‌شود و سایر eventهای لغو بعد از آن نادیده گرفته می‌شوند. این روش باعث می‌شود مسئولیت‌ها به‌خوبی از هم جدا و قابل مدیریت باشند.

مدیریت جریان داده‌ها در Node.js با کمک AbortSignal برای لغو عملیات

AbortSignals
AbortSignals این امکان را فراهم می‌کند که جریان‌ها را به‌آسانی متوقف کنیم. برای مثال، اگر بخواهیم یک جریان را متوقف کنیم چون به مقدار موردنظر خود رسیده‌ایم، یا قصد داریم کاری کاملاً متفاوت انجام دهیم، می‌توانیم از
AbortSignal
AbortSignal به شکل زیر استفاده نماییم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const abortController = new AbortController();
const { signal } = abortController;
const uploadStream = new WritableStream({
/* implementation */
}, { signal });
// To abort:
abortController.abort();
const abortController = new AbortController(); const { signal } = abortController; const uploadStream = new WritableStream({ /* implementation */ }, { signal }); // To abort: abortController.abort();
const abortController = new AbortController();
const { signal } = abortController;

const uploadStream = new WritableStream({
  /* implementation */
}, { signal });

// To abort:
abortController.abort();

در مثال بالا، یک

AbortController
AbortController ایجاد می‌کنیم که سیگنال حاصل از آن را به گزینه‌های constructor
WritableStream
WritableStream پاس می‌دهیم. سپس با فراخوانی متد
abort()
abort() روی controller، پردازش جریان را به‌صورت کامل لغو می‌کنیم.

پیاده‌سازی یک asynchronous API و قابل لغو با استفاده از AbortController و AbortSignal 

همان‌طور که در بخش‌های قبلی اشاره شد، چندین API داخلی asynchronous از

AbortController
AbortController پشتیبانی می‌کنند. با این حال، می‌توانیم یک API سفارشی مبتنی بر Promise نیز بسازیم که با
AbortController
AbortController قابل لغو باشد.

مشابه APIهای داخلی، API ما باید ویژگی

signal
signal مربوط به یک نمونه از کلاس
AbortController
AbortController را به عنوان ورودی دریافت کند. این روش در طراحی APIهایی که از Abort پشتیبانی می‌کنند، به‌عنوان یک استاندارد رایج شناخته می‌شود:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const myAbortableApi = (options = {}) => {
const { signal } = options;
if (signal?.aborted === true) {
throw new Error(signal.reason);
}
const abortEventListener = () => {
// Abort API from here
};
if (signal) {
signal.addEventListener("abort", abortEventListener, { once: true });
}
try {
// Run some asynchronous code
if (signal?.aborted === true) {
throw new Error(signal.reason);
}
// Run more asynchronous code
} finally {
if (signal) {
signal.removeEventListener("abort", abortEventListener);
}
}
};
const myAbortableApi = (options = {}) => { const { signal } = options; if (signal?.aborted === true) { throw new Error(signal.reason); } const abortEventListener = () => { // Abort API from here }; if (signal) { signal.addEventListener("abort", abortEventListener, { once: true }); } try { // Run some asynchronous code if (signal?.aborted === true) { throw new Error(signal.reason); } // Run more asynchronous code } finally { if (signal) { signal.removeEventListener("abort", abortEventListener); } } };
const myAbortableApi = (options = {}) => {
  const { signal } = options;

  if (signal?.aborted === true) {
    throw new Error(signal.reason);
  }

  const abortEventListener = () => {
    // Abort API from here
  };
  if (signal) {
    signal.addEventListener("abort", abortEventListener, { once: true });
  }
  try {
    // Run some asynchronous code
    if (signal?.aborted === true) {
      throw new Error(signal.reason);
    }
    // Run more asynchronous code
  } finally {
    if (signal) {
      signal.removeEventListener("abort", abortEventListener);
    }
  }
};

در مثال بالا، ابتدا بررسی می‌کنیم که آیا ویژگی

aborted
aborted در
signal
signal مقدار
true
true دارد یا نه. اگر این ویژگی
true
true باشد، یعنی متد
abort
abort روی controller فراخوانی شده و باید یک خطا throw کنیم.

همان‌طور که در بخش‌های قبلی نیز توضیح داده شد، می‌توانیم event listener

abort
abort را با متد
addEventListener
addEventListener ثبت کنیم. برای جلوگیری از memory leak، گزینه
{ once: true }
{ once: true } را به عنوان سومین آرگومان به
addEventListener
addEventListener می‌دهیم تا پس از اجرای event handler،
abort
abort به‌صورت خودکار حذف شود.

همچنین، در بلاک

finally
finally نیز event listener را با استفاده از
removeEventListener
removeEventListener حذف می‌کنیم تا در صورتی که تابع
myAbortableApi
myAbortableApi بدون لغو کامل شود، event listener همچنان متصل باقی نماند و باعث مصرف غیرضروری حافظه نشود.

استفاده از AbortController در فریم‌ورک React برای کنترل عملیات asynchronous 

API مربوط به AbortController برای توسعه‌دهندگان React بسیار مفید است، اما استفاده از آن در این محیط کمی متفاوت می‌باشد.

برای مثال، در مواقعی که نیاز به استفاده از event listenerها داریم، باید با دقت از

addEventListener
addEventListener استفاده کرده و سپس در یک تابع cleanup،
removeEventListener
removeEventListenerهای مربوطه را حذف کنیم.

هرچند این روش در عمل مؤثر است، اما می‌تواند خسته‌کننده باشد و احتمال بروز خطاهای تایپی در آن بالاست. در ادامه، یک مثال واقعی را بررسی می‌کنیم.

فرض کنید در حال توسعه یک داشبورد هستیم که حرکت ماوس را ردیابی می‌کند، به کلیدهای میان‌بر کیبورد واکنش نشان می‌دهد، موقعیت اسکرول را بررسی می‌کند و نسبت به تغییر اندازه پنجره پاسخ می‌دهد. این‌ها چهار event listener متفاوت هستند که باید به‌درستی مدیریت شوند.

اگر یک سناریوی معمولی را در نظر بگیریم، کد ما به این شکل خواهد بود:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
useEffect(() => {
// Define all your handler functions
const handleMouseMove = (e) => { /* update state */ };
const handleKeyPress = (e) => { /* update state */ };
const handleScroll = () => { /* update state */ };
const handleResize = () => { /* update state */ };
// Add all the listeners
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('keydown', handleKeyPress);
window.addEventListener('scroll', handleScroll);
window.addEventListener('resize', handleResize);
// Return a cleanup function that removes them all
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('keydown', handleKeyPress);
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', handleResize);
};
}, []);
useEffect(() => { // Define all your handler functions const handleMouseMove = (e) => { /* update state */ }; const handleKeyPress = (e) => { /* update state */ }; const handleScroll = () => { /* update state */ }; const handleResize = () => { /* update state */ }; // Add all the listeners document.addEventListener('mousemove', handleMouseMove); document.addEventListener('keydown', handleKeyPress); window.addEventListener('scroll', handleScroll); window.addEventListener('resize', handleResize); // Return a cleanup function that removes them all return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('keydown', handleKeyPress); window.removeEventListener('scroll', handleScroll); window.removeEventListener('resize', handleResize); }; }, []);
useEffect(() => {
  // Define all your handler functions
  const handleMouseMove = (e) => { /* update state */ };
  const handleKeyPress = (e) => { /* update state */ };
  const handleScroll = () => { /* update state */ };
  const handleResize = () => { /* update state */ };

  // Add all the listeners
  document.addEventListener('mousemove', handleMouseMove);
  document.addEventListener('keydown', handleKeyPress);
  window.addEventListener('scroll', handleScroll);
  window.addEventListener('resize', handleResize);

  // Return a cleanup function that removes them all
  return () => {
    document.removeEventListener('mousemove', handleMouseMove);
    document.removeEventListener('keydown', handleKeyPress);
    window.removeEventListener('scroll', handleScroll);
    window.removeEventListener('resize', handleResize);
  };
}, []);

مقایسه روش سنتی و استفاده از AbortController برای مدیریت event listener

این کد کار می‌کند، اما باید referenceهای تمام توابع را نگه داریم تا بتوانیم همان reference را در

addEventListener
addEventListener و
removeEventListener
removeEventListener استفاده کنیم. حال اگر از AbortController استفاده نماییم، می‌توانیم به این صورت عمل کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
// Define all your handler functions
const handleMouseMove = (e) => { /* update state */ };
const handleKeyPress = (e) => { /* update state */ };
const handleScroll = () => { /* update state */ };
const handleResize = () => { /* update state */ };
// Add all the listeners with the signal
document.addEventListener('mousemove', handleMouseMove, { signal });
document.addEventListener('keydown', handleKeyPress, { signal });
window.addEventListener('scroll', handleScroll, { signal });
window.addEventListener('resize', handleResize, { signal });
// Just one line for cleanup!
return () => controller.abort();
}, []);
useEffect(() => { const controller = new AbortController(); const { signal } = controller; // Define all your handler functions const handleMouseMove = (e) => { /* update state */ }; const handleKeyPress = (e) => { /* update state */ }; const handleScroll = () => { /* update state */ }; const handleResize = () => { /* update state */ }; // Add all the listeners with the signal document.addEventListener('mousemove', handleMouseMove, { signal }); document.addEventListener('keydown', handleKeyPress, { signal }); window.addEventListener('scroll', handleScroll, { signal }); window.addEventListener('resize', handleResize, { signal }); // Just one line for cleanup! return () => controller.abort(); }, []);
useEffect(() => {
  const controller = new AbortController();
  const { signal } = controller;

  // Define all your handler functions
  const handleMouseMove = (e) => { /* update state */ };
  const handleKeyPress = (e) => { /* update state */ };
  const handleScroll = () => { /* update state */ };
  const handleResize = () => { /* update state */ };

  // Add all the listeners with the signal
  document.addEventListener('mousemove', handleMouseMove, { signal });
  document.addEventListener('keydown', handleKeyPress, { signal });
  window.addEventListener('scroll', handleScroll, { signal });
  window.addEventListener('resize', handleResize, { signal });

  // Just one line for cleanup!
  return () => controller.abort();
}, []);

با استفاده از AbortController می‌توانیم تنها با یک خط کد تمام event listenerها را، بدون توجه به تعداد آن‌ها پاک‌سازی کنیم. این روش بسیار تمیزتر است و احتمال بروز خطا را به‌شدت کاهش می‌دهد.

استفاده از AbortController در درخواست‌های fetch در React

بیشتر نمونه‌هایی که به‌صورت آنلاین درباره استفاده از AbortController در React منتشر شده‌اند، معمولاً درون یک هوک

useEffect() پیاده‌سازی شده‌اند. با این حال، برای بهره‌گیری از AbortController در React، الزاماً نیازی به استفاده از useEffect نیست.

در واقع، می‌توان به‌صورت مستقیم و مداوم از

AbortController
AbortController در زمان ارسال درخواست‌های fetch استفاده کرد.

برای مثال، فرض کنید در یک پروژه نیاز به پیاده‌سازی قابلیت جستجوی live داریم؛ به این صورت که هم‌زمان با تایپ کاربر، درخواست‌هایی به سمت سرور ارسال می‌شوند.

چالش اینجاست که اگر کاربر سریع تایپ کند، ممکن است چندین درخواست هم‌زمان به سرور ارسال شود. در چنین حالتی، گاهی پاسخ درخواست‌های قدیمی‌تر دیرتر از درخواست‌های جدیدتر برمی‌گردند و باعث بروز مشکلاتی نظیر نمایش نتایج نامرتبط یا تداخل بین نتایج جدید و قدیمی می‌شوند.

در این شرایط، استفاده از AbortController در React می‌تواند این مشکل را به‌خوبی حل کند:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
-language="js">// Key implementation of AbortController for API requests in React
import { useRef, useState } from 'react';
// Component with search functionality
const SearchComponent = () => {
const controllerRef = useRef<AbortController>();
const [query, setQuery] = useState<string>();
const [results, setResults] = useState<Array<any> | undefined>();
async function handleOnChange(e: React.SyntheticEvent) {
const target = e.target as typeof e.target & {
value: string;
};
// Update the query state
setQuery(target.value);
setResults(undefined);
// Cancel any previous in-flight request
if (controllerRef.current) {
controllerRef.current.abort();
}
// Create a new controller for this request
controllerRef.current = new AbortController();
const signal = controllerRef.current.signal;
try {
const response = await fetch('/api/search', {
method: 'POST',
body: JSON.stringify({
query: target.value
}),
signal
});
const data = await response.json();
setResults(data.results);
} catch(e) {
// Silently catch aborted requests
// For production, you might want to check if error is an AbortError
}
}
return (
<div>
<input type="text" onChange={handleOnChange} />
{/* Results rendering */}
</div>
);
};
-language="js">// Key implementation of AbortController for API requests in React import { useRef, useState } from 'react'; // Component with search functionality const SearchComponent = () => { const controllerRef = useRef<AbortController>(); const [query, setQuery] = useState<string>(); const [results, setResults] = useState<Array<any> | undefined>(); async function handleOnChange(e: React.SyntheticEvent) { const target = e.target as typeof e.target & { value: string; }; // Update the query state setQuery(target.value); setResults(undefined); // Cancel any previous in-flight request if (controllerRef.current) { controllerRef.current.abort(); } // Create a new controller for this request controllerRef.current = new AbortController(); const signal = controllerRef.current.signal; try { const response = await fetch('/api/search', { method: 'POST', body: JSON.stringify({ query: target.value }), signal }); const data = await response.json(); setResults(data.results); } catch(e) { // Silently catch aborted requests // For production, you might want to check if error is an AbortError } } return ( <div> <input type="text" onChange={handleOnChange} /> {/* Results rendering */} </div> ); };
-language="js">// Key implementation of AbortController for API requests in React
import { useRef, useState } from 'react';

// Component with search functionality
const SearchComponent = () => {
  const controllerRef = useRef<AbortController>();
  const [query, setQuery] = useState<string>();
  const [results, setResults] = useState<Array<any> | undefined>();

  async function handleOnChange(e: React.SyntheticEvent) {
    const target = e.target as typeof e.target & {
      value: string;
    };

    // Update the query state
    setQuery(target.value);
    setResults(undefined);

    // Cancel any previous in-flight request
    if (controllerRef.current) {
      controllerRef.current.abort();
    }

    // Create a new controller for this request
    controllerRef.current = new AbortController();
    const signal = controllerRef.current.signal;

    try {
      const response = await fetch('/api/search', {
        method: 'POST',
        body: JSON.stringify({
          query: target.value
        }),
        signal
      });

      const data = await response.json();
      setResults(data.results);
    } catch(e) {
      // Silently catch aborted requests
      // For production, you might want to check if error is an AbortError
    }
  }

  return (
    <div>
      <input type="text" onChange={handleOnChange} />
      {/* Results rendering */}
    </div>
  );
};

با استفاده از هوک

useRef، توانستیم referenceای به درخواست جاری نگه داریم و هر بار که مقدار ورودی تغییر می‌کند، آن را لغو کرده و درخواست جدیدی ارسال کنیم.

جمع‌بندی

در حالت عادی، یک فرآیند asynchronous ممکن است با موفقیت به پایان برسد، با شکست مواجه شود یا بیش از حد انتظار زمان ببرد. بنابراین، منطقی است که بتوانیم در مواقعی که عملیات asynchronous دیگر موردنیاز نیست یا بیش از حد طول کشیده، آن را به‌صورت دستی متوقف کنیم.

API مربوط به

AbortController
AbortController دقیقاً برای پاسخ به همین نیاز طراحی شده است.

این API به‌صورت سراسری در Node.js و مرورگر در دسترس است و نیازی به import کردن آن وجود ندارد. یک نمونه از کلاس

AbortController
AbortController دو قابلیت کلیدی را فراهم می‌کند:

  • متد

    abort
    abort برای لغو عملیات

  • ویژگی

    signal
    signal برای ارائه سیگنال لغو به API مورد نظر

ویژگی

signal
signal در واقع نمونه‌ای از کلاس
AbortSignal
AbortSignal است. هر بار که از
AbortController
AbortController یک نمونه ساخته می‌شود، به‌صورت خودکار یک نمونه متناظر از
AbortSignal
AbortSignal نیز ایجاد می‌گردد که از طریق ویژگی
signal
signal قابل دسترسی است.

ما ویژگی

signal
signal را به asynchronous API مورد نظر ارسال می‌کنیم و هر زمان نیاز به توقف عملیات بود، با فراخوانی متد
abort
abort روی controller، فرآیند لغو را آغاز می‌کنیم.

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

AbortController
AbortController و
AbortSignal
AbortSignal یک API سفارشی و قابل لغوی طراحی کنیم که کنترل کامل عملیات را در اختیار ما قرار دهد.

تنها نکته مهمی که باید همواره در نظر داشت، رعایت بهترین شیوه‌های مدیریت منابع برای جلوگیری از memory leak است؛ موضوعی که در طول این مقاله به‌طور کامل مورد بررسی قرار گرفت.

دیدگاه‌ها:

افزودن دیدگاه جدید