العربية

أتقن ميزة إدارة الموارد الصريحة الجديدة في JavaScript مع `using` و `await using`. تعلم أتمتة التنظيف، ومنع تسرب الموارد، وكتابة كود برمجي أنظف وأكثر قوة.

القوة الخارقة الجديدة في JavaScript: نظرة عميقة على إدارة الموارد الصريحة

في عالم تطوير البرمجيات الديناميكي، تعد إدارة الموارد بفعالية حجر الزاوية في بناء تطبيقات قوية وموثوقة وعالية الأداء. لعقود من الزمان، اعتمد مطورو JavaScript على أنماط يدوية مثل try...catch...finally لضمان تحرير الموارد الحيوية - مثل مؤشرات الملفات أو اتصالات الشبكة أو جلسات قاعدة البيانات - بشكل صحيح. على الرغم من أن هذا النهج عملي، إلا أنه غالبًا ما يكون مطولًا وعرضة للخطأ، ويمكن أن يصبح معقدًا بسرعة، وهو نمط يشار إليه أحيانًا باسم "هرم الهلاك" في السيناريوهات المعقدة.

وهنا يأتي تحول نموذجي للغة: إدارة الموارد الصريحة (ERM). هذه الميزة القوية، التي تم الانتهاء منها في معيار ECMAScript 2024 (ES2024)، والمستوحاة من بنيات مماثلة في لغات مثل C# و Python و Java، تقدم طريقة تعريفية وتلقائية للتعامل مع تنظيف الموارد. من خلال الاستفادة من الكلمات المفتاحية الجديدة using و await using، توفر JavaScript الآن حلاً أكثر أناقة وأمانًا لتحدٍ برمجي أبدي.

سيأخذك هذا الدليل الشامل في رحلة عبر إدارة الموارد الصريحة في JavaScript. سنستكشف المشكلات التي تحلها، ونحلل مفاهيمها الأساسية، ونتصفح أمثلة عملية، ونكشف عن أنماط متقدمة ستمكنك من كتابة كود برمجي أنظف وأكثر مرونة، بغض النظر عن مكان تطويرك في العالم.

الحرس القديم: تحديات تنظيف الموارد اليدوي

قبل أن نتمكن من تقدير أناقة النظام الجديد، يجب أن نفهم أولاً نقاط الضعف في النظام القديم. النمط الكلاسيكي لإدارة الموارد في JavaScript هو كتلة try...finally.

المنطق بسيط: تحصل على مورد في كتلة try، وتحرره في كتلة finally. تضمن كتلة finally التنفيذ، سواء نجح الكود في كتلة try أو فشل أو تم إرجاعه قبل الأوان.

لنتأمل سيناريو شائعًا من جانب الخادم: فتح ملف، وكتابة بعض البيانات فيه، ثم التأكد من إغلاق الملف.

مثال: عملية ملف بسيطة باستخدام try...finally


const fs = require('fs/promises');

async function processFile(filePath, data) {
  let fileHandle;
  try {
    console.log('Opening file...');
    fileHandle = await fs.open(filePath, 'w');
    console.log('Writing to file...');
    await fileHandle.write(data);
    console.log('Data written successfully.');
  } catch (error) {
    console.error('An error occurred during file processing:', error);
  } finally {
    if (fileHandle) {
      console.log('Closing file...');
      await fileHandle.close();
    }
  }
}

هذا الكود يعمل، لكنه يكشف عن عدة نقاط ضعف:

الآن، تخيل إدارة موارد متعددة، مثل اتصال بقاعدة البيانات ومؤشر ملف. سرعان ما يصبح الكود فوضى متداخلة:


async function logQueryResultToFile(query, filePath) {
  let dbConnection;
  try {
    dbConnection = await getDbConnection();
    const result = await dbConnection.query(query);

    let fileHandle;
    try {
      fileHandle = await fs.open(filePath, 'w');
      await fileHandle.write(JSON.stringify(result));
    } finally {
      if (fileHandle) {
        await fileHandle.close();
      }
    }
  } finally {
    if (dbConnection) {
      await dbConnection.release();
    }
  }
}

هذا التداخل صعب الصيانة والتوسيع. إنها إشارة واضحة إلى أن هناك حاجة إلى تجريد أفضل. هذه هي بالضبط المشكلة التي صُممت إدارة الموارد الصريحة لحلها.

تحول نموذجي: مبادئ إدارة الموارد الصريحة

تقدم إدارة الموارد الصريحة (ERM) عقدًا بين كائن المورد وبيئة تشغيل JavaScript. الفكرة الأساسية بسيطة: يمكن للكائن أن يعلن كيف يجب تنظيفه، وتوفر اللغة بناء جملة لأداء هذا التنظيف تلقائيًا عندما يخرج الكائن عن النطاق.

يتم تحقيق ذلك من خلال مكونين رئيسيين:

  1. بروتوكول القابلية للتخلص (Disposable Protocol): طريقة قياسية للكائنات لتحديد منطق التنظيف الخاص بها باستخدام رموز خاصة: Symbol.dispose للتنظيف المتزامن و Symbol.asyncDispose للتنظيف غير المتزامن.
  2. التصريحات using و await using: كلمات مفتاحية جديدة تربط موردًا بنطاق كتلة. عند الخروج من الكتلة، يتم استدعاء طريقة تنظيف المورد تلقائيًا.

المفاهيم الأساسية: Symbol.dispose و Symbol.asyncDispose

في قلب ERM يوجد رمزان جديدان معروفان. يعتبر الكائن الذي يحتوي على طريقة يكون أحد هذه الرموز مفتاحها "موردًا قابلاً للتخلص منه".

التخلص المتزامن مع Symbol.dispose

يحدد الرمز Symbol.dispose طريقة تنظيف متزامنة. هذا مناسب للموارد التي لا يتطلب تنظيفها أي عمليات غير متزامنة، مثل إغلاق مؤشر ملف بشكل متزامن أو تحرير قفل في الذاكرة.

لنقم بإنشاء غلاف لملف مؤقت يقوم بتنظيف نفسه.


const fs = require('fs');
const path = require('path');

class TempFile {
  constructor(content) {
    this.path = path.join(__dirname, `temp_${Date.now()}.txt`);
    fs.writeFileSync(this.path, content);
    console.log(`Created temp file: ${this.path}`);
  }

  // هذه هي طريقة التخلص المتزامنة
  [Symbol.dispose]() {
    console.log(`Disposing temp file: ${this.path}`);
    try {
      fs.unlinkSync(this.path);
      console.log('File deleted successfully.');
    } catch (error) {
      console.error(`Failed to delete file: ${this.path}`, error);
      // من المهم معالجة الأخطاء داخل dispose أيضًا!
    }
  }
}

أي مثيل من `TempFile` هو الآن مورد قابل للتخلص منه. لديه طريقة مفتاحها `Symbol.dispose` تحتوي على منطق حذف الملف من القرص.

التخلص غير المتزامن مع Symbol.asyncDispose

العديد من عمليات التنظيف الحديثة غير متزامنة. قد يتضمن إغلاق اتصال بقاعدة البيانات إرسال أمر `QUIT` عبر الشبكة، أو قد يحتاج عميل قائمة انتظار الرسائل إلى تفريغ مخزنه المؤقت الصادر. لهذه السيناريوهات، نستخدم Symbol.asyncDispose.

يجب أن تُرجع الطريقة المرتبطة بـ `Symbol.asyncDispose` كائن `Promise` (أو أن تكون دالة `async`).

لنمثل اتصال قاعدة بيانات وهمي يحتاج إلى إعادته إلى مجمع بشكل غير متزامن.


// مجمع اتصالات قاعدة بيانات وهمي
const mockDbPool = {
  getConnection: () => {
    console.log('DB connection acquired.');
    return new MockDbConnection();
  }
};

class MockDbConnection {
  query(sql) {
    console.log(`Executing query: ${sql}`);
    return Promise.resolve({ success: true, rows: [] });
  }

  // هذه هي طريقة التخلص غير المتزامنة
  async [Symbol.asyncDispose]() {
    console.log('Releasing DB connection back to the pool...');
    // محاكاة تأخير الشبكة لتحرير الاتصال
    await new Promise(resolve => setTimeout(resolve, 50));
    console.log('DB connection released.');
  }
}

الآن، أي مثيل `MockDbConnection` هو مورد قابل للتخلص منه بشكل غير متزامن. إنه يعرف كيفية تحرير نفسه بشكل غير متزامن عندما لا تكون هناك حاجة إليه.

البناء الجديد: using و await using في العمل

مع تعريف فئاتنا القابلة للتخلص، يمكننا الآن استخدام الكلمات المفتاحية الجديدة لإدارتها تلقائيًا. تنشئ هذه الكلمات المفتاحية تصريحات ذات نطاق كتلة، تمامًا مثل `let` و `const`.

التنظيف المتزامن مع using

تُستخدم الكلمة المفتاحية using للموارد التي تطبق `Symbol.dispose`. عندما يغادر تنفيذ الكود الكتلة التي تم فيها التصريح بـ `using`، يتم استدعاء طريقة `[Symbol.dispose]()` تلقائيًا.

لنستخدم فئة `TempFile` الخاصة بنا:


function processDataWithTempFile() {
  console.log('Entering block...');
  using tempFile = new TempFile('This is some important data.');

  // يمكنك العمل مع tempFile هنا
  const content = fs.readFileSync(tempFile.path, 'utf8');
  console.log(`Read from temp file: \"${content}\"`);

  // لا حاجة لكود تنظيف هنا!
  console.log('...doing more work...');
} // <-- يتم استدعاء tempFile.[Symbol.dispose]() تلقائيًا هنا!

processDataWithTempFile();
console.log('Block has been exited.');

سيكون الناتج:

Entering block...
Created temp file: /path/to/temp_1678886400000.txt
Read from temp file: "This is some important data."
...doing more work...
Disposing temp file: /path/to/temp_1678886400000.txt
File deleted successfully.
Block has been exited.

انظر كم هو نظيف! يتم احتواء دورة حياة المورد بأكملها داخل الكتلة. نعلن عنه، نستخدمه، ثم ننسى أمره. تتولى اللغة عملية التنظيف. هذا تحسن هائل في قابلية القراءة والأمان.

إدارة موارد متعددة

يمكنك الحصول على عدة تصريحات `using` في نفس الكتلة. سيتم التخلص منها بترتيب عكسي لإنشائها (سلوك LIFO أو "يشبه المكدس").


{
  using resourceA = new MyDisposable('A'); // تم إنشاؤه أولاً
  using resourceB = new MyDisposable('B'); // تم إنشاؤه ثانيًا
  console.log('Inside block, using resources...');
} // يتم التخلص من resourceB أولاً، ثم resourceA

التنظيف غير المتزامن مع await using

الكلمة المفتاحية await using هي النظير غير المتزامن لـ `using`. تُستخدم للموارد التي تطبق `Symbol.asyncDispose`. نظرًا لأن التنظيف غير متزامن، لا يمكن استخدام هذه الكلمة المفتاحية إلا داخل دالة `async` أو على المستوى الأعلى للوحدة (إذا كان `top-level await` مدعومًا).

لنستخدم فئة `MockDbConnection` الخاصة بنا:


async function performDatabaseOperation() {
  console.log('Entering async function...');
  await using db = mockDbPool.getConnection();

  await db.query('SELECT * FROM users');

  console.log('Database operation complete.');
} // <-- يتم استدعاء await db.[Symbol.asyncDispose]() تلقائيًا هنا!

(async () => {
  await performDatabaseOperation();
  console.log('Async function has completed.');
})();

يوضح الناتج عملية التنظيف غير المتزامنة:

Entering async function...
DB connection acquired.
Executing query: SELECT * FROM users
Database operation complete.
Releasing DB connection back to the pool...
(waits 50ms)
DB connection released.
Async function has completed.

تمامًا كما هو الحال مع `using`، يتعامل بناء `await using` مع دورة الحياة بأكملها، لكنه `awaits` بشكل صحيح عملية التنظيف غير المتزامنة. يمكنه حتى التعامل مع الموارد القابلة للتخلص المتزامن فقط - ببساطة لن ينتظرها.

الأنماط المتقدمة: `DisposableStack` و `AsyncDisposableStack`

في بعض الأحيان، لا يكون النطاق البسيط لكتلة `using` مرنًا بما فيه الكفاية. ماذا لو كنت بحاجة إلى إدارة مجموعة من الموارد ذات عمر غير مرتبط بكتلة معجمية واحدة؟ أو ماذا لو كنت تتكامل مع مكتبة قديمة لا تنتج كائنات تحتوي على `Symbol.dispose`؟

لهذه السيناريوهات، توفر JavaScript فئتين مساعدتين: `DisposableStack` و `AsyncDisposableStack`.

`DisposableStack`: مدير التنظيف المرن

كائن `DisposableStack` هو كائن يدير مجموعة من عمليات التنظيف. وهو بحد ذاته مورد قابل للتخلص، لذا يمكنك إدارة دورة حياته بأكملها باستخدام كتلة `using`.

لديه عدة طرق مفيدة:

مثال: إدارة الموارد المشروطة

تخيل دالة تفتح ملف سجل فقط إذا تم استيفاء شرط معين، لكنك تريد أن تحدث جميع عمليات التنظيف في مكان واحد في النهاية.


function processWithConditionalLogging(shouldLog) {
  using stack = new DisposableStack();

  const db = stack.use(getDbConnection()); // استخدم دائمًا قاعدة البيانات

  if (shouldLog) {
    const logFileStream = fs.createWriteStream('app.log');
    // تأجيل عملية التنظيف للمجرى
    stack.defer(() => {
      console.log('Closing log file stream...');
      logFileStream.end();
    });
    db.logTo(logFileStream);
  }

  db.doWork();

} // <-- يتم التخلص من المكدس، واستدعاء جميع وظائف التنظيف المسجلة بترتيب LIFO.

`AsyncDisposableStack`: للعالم غير المتزامن

كما قد تخمن، `AsyncDisposableStack` هو الإصدار غير المتزامن. يمكنه إدارة كل من الموارد القابلة للتخلص المتزامنة وغير المتزامنة. طريقته الأساسية للتنظيف هي `.disposeAsync()`، التي تُرجع `Promise` يتم حله عند اكتمال جميع عمليات التنظيف غير المتزامنة.

مثال: إدارة مزيج من الموارد

لإنشاء معالج طلبات خادم ويب يحتاج إلى اتصال بقاعدة البيانات (تنظيف غير متزامن) وملف مؤقت (تنظيف متزامن).


async function handleRequest() {
  await using stack = new AsyncDisposableStack();

  // إدارة مورد غير متزامن قابل للتحرير
  const dbConnection = await stack.use(getAsyncDbConnection());

  // إدارة مورد متزامن قابل للتحرير
  const tempFile = stack.use(new TempFile('request data'));

  // تبني مورد من واجهة برمجة تطبيقات قديمة
  const legacyResource = getLegacyResource();
  stack.adopt(legacyResource, () => legacyResource.shutdown());

  console.log('Processing request...');
  await doWork(dbConnection, tempFile.path);

} // <-- يتم استدعاء stack.disposeAsync(). سيقوم بانتظار التنظيف غير المتزامن بشكل صحيح.

تُعد `AsyncDisposableStack` أداة قوية لتنسيق منطق الإعداد والتفكيك المعقد بطريقة نظيفة ويمكن التنبؤ بها.

معالجة الأخطاء القوية مع `SuppressedError`

أحد التحسينات الأكثر دقة ولكن أهمية في ERM هو كيفية تعامله مع الأخطاء. ماذا يحدث إذا تم إلقاء خطأ داخل كتلة `using`، وتم إلقاء خطأ *آخر* أثناء التخلص التلقائي اللاحق؟

في عالم try...finally القديم، كان الخطأ من كتلة `finally` عادةً ما يتجاوز أو "يكبت" الخطأ الأصلي والأكثر أهمية من كتلة try. هذا غالبًا ما جعل تصحيح الأخطاء صعبًا للغاية.

يحل ERM هذه المشكلة بنوع خطأ عالمي جديد: `SuppressedError`. إذا حدث خطأ أثناء التخلص بينما ينتشر خطأ آخر بالفعل، يتم "كبت" خطأ التخلص. يتم إلقاء الخطأ الأصلي، لكنه الآن يحتوي على خاصية `suppressed` تحتوي على خطأ التخلص.


class FaultyResource {
  [Symbol.dispose]() {
    throw new Error('Error during disposal!');
  }
}

try {
  using resource = new FaultyResource();
  throw new Error('Error during operation!');
} catch (e) {
  console.log(`Caught error: ${e.message}`); // Error during operation!
  if (e.suppressed) {
    console.log(`Suppressed error: ${e.suppressed.message}`); // Error during disposal!
    console.log(e instanceof SuppressedError); // false
    console.log(e.suppressed instanceof Error); // true
  }
}

يضمن هذا السلوك أنك لن تفقد أبدًا سياق الفشل الأصلي، مما يؤدي إلى أنظمة أكثر قوة وقابلية لتصحيح الأخطاء.

حالات الاستخدام العملي عبر النظام البيئي لـ JavaScript

تطبيقات إدارة الموارد الصريحة واسعة وذات صلة بالمطورين في جميع أنحاء العالم، سواء كانوا يعملون على الواجهة الخلفية أو الواجهة الأمامية أو في الاختبار.

دعم المتصفحات وبيئات التشغيل

كميزة حديثة، من المهم معرفة أين يمكنك استخدام إدارة الموارد الصريحة. اعتبارًا من أواخر عام 2023 / أوائل عام 2024، أصبح الدعم واسع الانتشار في أحدث إصدارات بيئات JavaScript الرئيسية:

بالنسبة للبيئات الأقدم، ستحتاج إلى الاعتماد على المحولات البرمجية مثل Babel مع المكونات الإضافية المناسبة لتحويل بناء `using` وتوفير الرموز وفئات المكدس اللازمة.

الخاتمة: عصر جديد من الأمان والوضوح

إن إدارة الموارد الصريحة في JavaScript هي أكثر من مجرد سكر نحوي؛ إنها تحسين أساسي للغة يعزز الأمان والوضوح وقابلية الصيانة. من خلال أتمتة عملية تنظيف الموارد المملة والمعرضة للخطأ، فإنه يحرر المطورين للتركيز على منطق أعمالهم الأساسي.

النقاط الرئيسية هي:

عندما تبدأ مشاريع جديدة أو تعيد هيكلة الكود الحالي، فكر في تبني هذا النمط الجديد القوي. سيجعل كود JavaScript الخاص بك أنظف، وتطبيقاتك أكثر موثوقية، وحياتك كمطور أسهل قليلاً. إنه حقًا معيار عالمي لكتابة JavaScript حديثة واحترافية.