اذهب إلى المحتوى

نتحدث في هذا المقال عن مفهومي البرمجة المتزامنة synchronous وغير المتزامنة asynchronous، وفوائد استخدام التقنيات غير المتزامنة، كما نتحدث بإيجاز عن المشاكل التي تعلّقت تاريخيًا بتنفيذ الدوال غير المتزامنة في جافا سكريبت.

تُعرّف البرمجة غير المتزامنة أنها تقنية تسمح لبرامجك بتنفيذ مهام مطوّلة تأخذ وقتًا طويلًا لتنفيذها، لكنها تبقى مع ذلك قادرة على الاستجابة للأحداث التي تقع أثناء تنفيذ هذه المهمة، بدلًا من الانتظار حتى تنتهي بالكامل. وستظهر نتيجة الاستجابة للحدث بمجرد انتهاء المهمة اﻷساسية وعرض نتيجة التنفيذ.

على سبيل المثال معظم الدوّال المضمّنة في المتصفحات قد تستغرق وقتًا لذلك فهي تنفذ بطريقة غير متزامنة، نذكر منها على سبيل المثال:

  • ()fetch: تستخدم لإحضار الموارد باستخدام طلبات HTTP.
  • ()getUserMedia: تستخدم للوصول إلى كاميرا أو ميكروفون المستخدم.
  • ()showOpenFilePicke: تستخدم لتطلب من المستخدم اختيار ملف.

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

سنبدأ هذا المقال بالنظر إلى ما تسببه الدوال المتزامنة التي تستغرق وقتًا طويلًا لتنفيذها، وضرورة استخدام البرمجة غير المتزامنة في هذه الحالات.

البرمجة المتزامنة

لنتأمل الشيفرة التالية:

const name = "Muhammd";
const greeting = `Hello, my name is ${name}!`;
console.log(greeting);
// "Hello, my name is Muhammd!"

في هذه الشيفرة:

  1. name: يصرّح عن نص.
  2. greeting: يصرّح عن نص آخر يستخدم المتغير name.
  3. تطبع الشيفرة بعد ذلك رسالة الترحيب على الطرفية.

ينتقل المتصفح سطرًا سطرًا في الشيفرة السابقة وينفذها بهذا الترتيب. وينتظر المتصفح نهاية تنفيذ السطر اﻷول حتى ينتقل إلى السطر التالي، فكل سطر يعتمد على السطر الذي يسبقه. ندعو هذا البرنامج برنامجًا متزامنًا synchronous program، وسيبقى متزامنًا حتى لو استخدمنا دالة منفصلة كما في المثال التالي:

function makeGreeting(name) {
 return `Hello, my name is ${name}!`;
}

const name = "Muhammd";
const greeting = makeGreeting(name);
console.log(greeting);
// "Hello, my name is Muhammd!"

فالدالة ()makeGreating هي دالة متزامنة، لأن مُستدعيها سينتظر حتى تنهي الدالة عملها وتعيد القيمة المطلوبة ثم يستأنف تنفيذ الشيفرة البرمجية.

دالة متزامنة تستغرق وقتًا في التنفيذ

ماذا لو استغرقت الدالة المتزامنة وقتًا طويلًا في التنفيذ؟

سنعرض تاليًا برنامجًا يعتمد على خوارزمية ضعيفة وغير مجدية لتوليد أعداد أولية عندما ينقر المستخدم على زر "توليد أعداد أولية Generating Primes". وكلما كبر عدد اﻷعداد الأولية الذي يحدده المستخدم ستستغرق الدالة وقتًا أكبر ﻹيجاد النتيجة.

إليك شيفرة HTML:

<label for="quota">Number of primes:</label>
<input type="text" id="quota" name="quota" value="1000000" />

<button id="generate">Generate primes</button>
<button id="reload">Reload</button>

<div id="output"></div>

وشيفرة جافا سكريبت:

const MAX_PRIME = 1000000;

function isPrime(n) {
 for (let i = 2; i <= Math.sqrt(n); i++) {
    if (n % i === 0) {
     return false;
    }
 }
 return n > 1;
}

const random = (max) => Math.floor(Math.random() * max);

function generatePrimes(quota) {
 const primes = [];
 while (primes.length < quota) {
    const candidate = random(MAX_PRIME);
    if (isPrime(candidate)) {
     primes.push(candidate);
    }
 }
 return primes;
}

const quota = document.querySelector("#quota");
const output = document.querySelector("#output");

document.querySelector("#generate").addEventListener("click", () => {
 const primes = generatePrimes(quota.value);
 output.textContent = `Finished generating ${quota.value} primes!`;
});

document.querySelector("#reload").addEventListener("click", () => {
 document.location.reload();
});

وستكون نتيجة التنفيذ كالتالي:

جرّب أن تنقر على الزر "Generate primes"، ستلاحظ أن الحاسوب سيتأخر عدة ثوان (وفقًا لسرعة حاسوبك) قبل أن يعرض رسالة الانتهاء "!Finished".

مشاكل الدوال المتزامنة

تشبه شيفرة المثال التالي ما تنفذه شيفرة المثال السابق باستثناء وجود مربع نص لتكتب فيه. جرّب أن تنقر اﻵن على الزر "Generate primes" ثم حاول أن تكتب مباشرةً شيئًا ما في مربع النص. ما ستلاحظه أن البرنامج سيفقد تجاوبه طالما أن الدالة ()generatePrimes قيد التنفيذ ولن تتمكن من الكتابة في مربع النص أو أن تنقر في أي مكان أو تفعل أي شيء.

يعود السبب في ذلك إلى أن برنامج جافا سكريبت ذو خيط معالجة وحيد single-threaded. وخيط المعالجة هو سلسلة من التعليمات يتبعها البرنامج. ولأن البرنامج وحيد الخيط، سينفذ التعليمات واحدة تلو اﻷخرى. لهذا استغرقت الدالة المتزامنة السابقة وقتًا حتى تعيد قيمتها وكان علينا الانتظار حتى تنتهي.

وما نحتاجه عادة في برنامجنا طريقة لتنفيذ اﻵتي:

  1. البدء بتنفيذ مهمة طويلة باستدعاء دالتها.
  2. أن تكون الدالة قادرة على تنفيذ مهمتها والانتهاء مباشرة، حتى يبقى البرنامج متجاوبًا مع بقية اﻷحداث.
  3. أن تنفّذ الدالة مهامها دون أن تعيق خيط المعالجة الرئيسي، كأن تبدأ مثلًا خيط معالجة جديد.
  4. إبلاغنا بنتيجة عملياتها عندما تنتهي.

هذه النقاط ما تقدمه بالفعل الدوال غير المتزامنة، وسنتابع في هذا المقال شرح كيفية إنجاز هذه الدوال في جافا سكريبت.

معالجات اﻷحداث Event Handlers

قد يذكرك شرحنا للداول غير المتزامنة بمعالجات اﻷحداث وستكون محقًا في ذلك. فمعالجات الأحداث هي نوع من البرمجة غير المتزامنة، إذ تزوّدنا بدالة (معالج الحدث) لن تُستدعى مباشرة بل بمجرّد وقوع الحدث الذي نترصّده. فإن كان الحدث هو إنهاء دالة غير المتزامنة لمهمتها، فقد يُستخدم حدث اﻹنهاء لتنبيه المستدعي بنتيجة استدعاء هذه الدالة.

وقد استخدمت بعض الواجهات البرمجية غير المتزامنة في البدايات هذا اﻷسلوب، نذكر منها الواجهة البرمجية XMLHttpRequest التي تمكنك من إرسال طلبات HTTP إلى خادم واستقبالها باستخدام جافا سكريبت. فهذه العمليات قد تستغرق وقتًا، لهذا فهي غير المتزامنة. وتتلقى من خلال هذه الواجهة تنبيهًا عن تقدم الطلب وانتهائه عن طريق ربط مترصد أحداث Event Listener إلى الكائن XMLHttpRequest.

نعرض في المثال التالي طريقة تنفيذ اﻷمر، فعندما تنقر على الزر "انقر ﻹرسال طلب Click to start request"، سننشئ كائن XMLHttpRequest جديد، ثم نترصد وقوع الحدث loadend (انتهاء التحميل). يطبع بعد ذلك معالج الحدث الرسالة "!Finished" إضافة إلى رمز الحالة Status Code.

بعد إضاف معالج الحدث، نرسل الطلب. ولاحظ أنه بعد إرسال الطلب يمكننا طباعة العبارة "Started XHR request" أي يتابع برنامجنا تنفيذ تعليماته في الفترة التي يُعالج فيها الطلب، ثم يُستدعى معالج الحدث عند اكتمال الطلب.

إليك شيفرة HTML:

<button id="xhr">Click to start request</button>
<button id="reload">Reload</button>

<pre readonly class="event-log"></pre>

وهذه شيفرة جافا سكريبت:

const log = document.querySelector(".event-log");

document.querySelector("#xhr").addEventListener("click", () => {
 log.textContent = "";

 const xhr = new XMLHttpRequest();

 xhr.addEventListener("loadend", () => {
    log.textContent = `${log.textContent}Finished with status: ${xhr.status}`;
 });

 xhr.open(
    "GET",
    "https://raw.githubusercontent.com/mdn/content/main/files/en-us/_wikihistory.json",
 );
 xhr.send();
 log.textContent = `${log.textContent}Started XHR request\n`;
});

document.querySelector("#reload").addEventListener("click", () => {
 log.textContent = "";
 document.location.reload();
});

وستكون نتيجة التنفيذ كالتالي:

يشبه اﻷمر ما شرحناه في مقال مدخل إلى اﻷحداث في جافا سكريبت، لكن بدل أن يكون الحدث الذي نترصده هو فعل ينفّذه المستخدم مثل النقر على زر، سيكون الحدث تغيّر حالة كائن ما.

دوال رد النداء Callbacks

تُعد معالجات الأحداث نوعًا خاصًا من دوال رد النداء، ودالة رد النداء هي دالة تُمرر إلى دالة أخرى، لكنها تُستدعى في الوقت المناسب. فدوال رد النداء هي الطريقة الرئيسية التي نُفّذت فيها الدوال غير المتزامنة في جافا سكريبت. لكن قد تصبح الشيفرة التي تعتمد على دوال رد النداء صعبة الفهم وخاصة عندما تستدعي تلك دوال رد النداء دوال رد النداء أخرى. ستواجه هذه الحالة كثيرًا إن احتجت لتنفيذ بعض العمليات المكونة من سلسلة من الدوال غير المتزامنة، إليك مثالًا:

function doStep1(init) {
 return init + 1;
}

function doStep2(init) {
 return init + 2;
}

function doStep3(init) {
 return init + 3;
}

function doOperation() {
 let result = 0;
 result = doStep1(result);
 result = doStep2(result);
 result = doStep3(result);
 console.log(`result: ${result}`);
}

doOperation();

لدينا هنا عملية واحدة مقسّمة إلى ثلاث خطوات تتعلق كل واحدة بسابقتها. إذ تضيف الخطوة الأولى 1 إلى الدخل وتضيف الثانية العدد 2 والثالثة تُضيف العدد 3. فلو كان الدخل هو 0 ستكون النتيجة 6 (0+1+2+3). إن هذا البرنامج سهل ومباشر وفق أسلوب البرمجة المتزامنة، لكن إن نفذناه وفق أسلوب دوال رد النداء، سنجد:

function doStep1(init, callback) {
 const result = init + 1;
 callback(result);
}

function doStep2(init, callback) {
 const result = init + 2;
 callback(result);
}

function doStep3(init, callback) {
 const result = init + 3;
 callback(result);
}

function doOperation() {
 doStep1(0, (result1) => {
    doStep2(result1, (result2) => {
     doStep3(result2, (result3) => {
      console.log(`result: ${result3}`);
     });
    });
 });
}

doOperation();

ولأنه علينا استدعاء دوال رد النداء، ستكون النتيجة دالة ()doOperation متداخلة بشدة، ومن الصعب حينها قراءتها وتنقيحها. يُعرف هذا الأمر باسم "جحيم دوال رد النداء callback hell" أو "هرم الهلاك pyramid of doom" إذ تتخذ الدوال المتداخلة شكلًا هرميًا.

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

لهذه اﻷسباب، لا تستخدم معظم الواجهات البرمجية غير المتزامنة دوال رد النداء حاليًا. وبدلًا من ذلك تبنت جافا سكريبت أساسًا جديدًا في البرمجة غير المتزامنة من خلال الوعد promise. وهذا ما سنتعرف عليه بالتفصيل في المقال التالي من السلسلة.

ترجمة -وبتصرف- للمقال: Introducing asynchronous JavaScript

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...