بیا ابتدا به چند نمونه واقعی از منابعی نگاه کنیم که نیاز به مدیریت دستی دارند:
هندلهای فایل (File handles): از هندل فایل برای خواندن و نوشتن بایتها در یک فایل استفاده میشود. وقتی کارتان با آن تمام شد، باید حتماً متد fileHandle.close() را صدا بزنید؛ در غیر این صورت، فایل همچنان باز میماند، حتی اگر دیگر هیچ دسترسی به آن شیء جاوااسکریپتی در کدتان نداشته باشید. همانطور که در مستندات Node.js آمده است:
اگر یک
<FileHandle>با استفاده از متدfileHandle.close()بسته نشود، سیستم تلاش میکند تا دیسکریپتور فایل را به طور خودکار ببندد و یک هشدار (Process Warning) صادر کند تا از نشت حافظه جلوگیری شود. اما لطفاً به این رفتار تکیه نکنید، زیرا ممکن است غیرقابلاعتماد باشد و فایل اصلاً بسته نشود. در عوض، همیشه به صورت صریح و دستی هندلهای فایل را ببندید. Node.js ممکن است این رفتار را در آینده تغییر دهد.
اتصالات شبکه (Network connections): برخی از اتصالات مانند WebSocket و RTCPeerConnection اگر پیامی از طریق آنها منتقل نمیشود، باید حتماً بسته شوند. در غیر این صورت اتصال باز میماند، در حالی که ظرفیت استخرهای اتصال (Connection Pools) معمولاً بسیار محدود است.
خوانندههای استریم (Stream readers): در کار با استریمها، اگر متد ReadableStreamDefaultReader.releaseLock() را صدا نزنید، استریم در حالت قفلشده باقی میماند و به هیچ خواننده (Reader) دیگری اجازه نمیدهد که دادههای آن را مصرف کند.
بیا یک مثال عینی را با استفاده از یک استریم خواندنی (Readable Stream) بررسی کنیم:
const stream = new ReadableStream({
start(controller) {
controller.enqueue("a");
controller.enqueue("b");
controller.enqueue("c");
controller.close();
},
});
async function readUntil(stream, text) {
const reader = stream.getReader();
let chunk = await reader.read();
while (!chunk.done && chunk.value !== text) {
console.log(chunk);
chunk = await reader.read();
}
// ما فراموش کردیم که قفل استریم را در اینجا آزاد کنیم (releaseLock)
}
readUntil(stream, "b").then(() => {
const anotherReader = stream.getReader();
// خطای زیر رخ میدهد:
// TypeError: ReadableStreamDefaultReader constructor can only
// accept readable streams that are not yet locked to a reader
});
در این کد، ما استریمی داریم که سه تکه داده منتشر میکند. ما استریم را تا زمانی که به حرف "b" برسیم میخوانیم. وقتی اجرای تابع readUntil تمام میشود، استریم فقط تا نیمه مصرف شده است؛ بنابراین ما باید بتوانیم با یک Reader دیگر به خواندن بقیه دادهها ادامه دهیم. اما چون آزاد کردن قفل استریم را فراموش کردیم، با اینکه متغیر reader دیگر در دسترس نیست، اما استریم همچنان قفل باقی مانده و نمیتوانیم Reader جدیدی بسازیم.
راهحل اولیه در این مورد ساده است: صدا زدن reader.releaseLock() در پایان تابع readUntil. اما با این حال، هنوز چند مشکل و چالش بزرگ پابرجا میماند:
منابع مختلف، روشهای متفاوتی برای آزاد شدن دارند. مثلاً در یکی باید close() را صدا بزنیم، در دیگری releaseLock() و در یکی دیگر disconnect(). هیچ الگوی واحد و یکپارچهای وجود ندارد که بتوان همهجا از آن استفاده کرد.
اگر فراخوانیِ reader.read() با خطا مواجه شود چه اتفاقی میافتد؟ در این صورت، اجرای تابع readUntil فوراً متوقف میشود و کد ما هرگز به خطِ reader.releaseLock() نمیرسد. میتوانیم این مشکل را با ساختار try...finally حل کنیم:
async function readUntil(stream, text) {
const reader = stream.getReader();
try {
let chunk = await reader.read();
while (!chunk.done && chunk.value !== text) {
console.log(chunk);
chunk = await reader.read();
}
} finally {
reader.releaseLock(); // حالا مطمئن هستیم که در هر شرایطی اجرا میشود
}
}
اما چالش اینجاست که شما باید هر بار که با یک منبع حیاتی سروکار دارید، یادتان بماند که این ساختار را به طور دستی بنویسید.
در مثال بالا، وقتی از بلوک try...finally خارج میشویم، کار reader تمام شده و بسته شده است، اما این متغیر همچنان در اسکوپ و محدوده تابع در دسترس باقی میماند. این یعنی ممکن است بعد از بسته شدن، تصادفاً دوباره از آن استفاده کنید و برنامه را با خطا مواجه کنید.
اگر دو Reader روی دو استریم مختلف داشته باشیم، باید یادمان بماند که هر دوی آنها را آزاد کنیم. یک تلاش محترمانه برای انجام این کار میتواند به این صورت باشد:
const reader1 = stream1.getReader();
const reader2 = stream2.getReader();
try {
// انجام کارهای مختلف با reader1 و reader2
} finally {
reader1.releaseLock();
reader2.releaseLock();
}
اما این روش دردسرهای جدیدی در مدیریت خطا ایجاد میکند! اگر روندِ stream2.getReader() خطا پرتاب کند، reader1 هرگز آزاد نمیشود. یا اگر فرآیندِ reader1.releaseLock() با خطا مواجه شود، خط دوم یعنی reader2 آزاد نخواهد شد. این یعنی برای امنیت کامل، مجبوریم هر جفتِ «گرفتن و آزاد کردن منبع» را در یک try...finally تودرتوی مجزا قرار دهیم:
const reader1 = stream1.getReader();
try {
const reader2 = stream2.getReader();
try {
// انجام کارهای مختلف با reader1 و reader2
} finally {
reader2.releaseLock();
}
} finally {
reader1.releaseLock();
}
همانطور که میبینید، یک کار به ظاهر ساده مثل صدا زدن releaseLock، چطور میتواند به سرعت کد ما را به یک ساختار تودرتو و پر از کدهای تکراری (Boilerplate code) تبدیل کند.
دقیقاً به همین دلیل است که جاوااسکریپت یک پشتیبانی بومی و یکپارچه در سطح زبان برای مدیریت منابع (Explicit Resource Management) معرفی کرده است تا این مشکل را یکبار برای همیشه حل کند.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://developer.mozilla.org/en-US/docs/Web/JavaScript