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

مدخل إلى عمّال Workers جافا سكريبت


ابراهيم الخضور

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

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

لهذا السبب ظهرت تقنية العمال workers لتمنحك القدرة على إنجاز بعض المهام ضمن خيوط معالجة مختلفة، إذ تبدأ حينها مهمة معينة ثم تنتقل (وهي لا تزال قيد المعالجة) لتعمل على أخرى (كأن تعالج مدخلات المستخدم).

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

وهناك ثلاث أنواع مختلفة من العمال:

  • عمال مختصون Dedicated workers.
  • عمال مشتركون Shared workers.
  • عمال خدمة Service workers.

سنتعرف على النوع الأول من خلال مثال تطبيقي، ثم نناقش النوعين اﻵخرين باختصار.

استخدام عمال ويب

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

مولّد اﻷعداد اﻷولية المتزامن

لنلق نظرة في البداية إلى شيفرة جافا سكريبت الموافقة:

function generatePrimes(quota) {
  function isPrime(n) {
    for (let c = 2; c <= Math.sqrt(n); ++c) {
      if (n % c === 0) {
        return false;
      }
    }
    return true;
  }

  const primes = [];
  const maximum = 1000000;

  while (primes.length < quota) {
    const candidate = Math.floor(Math.random() * (maximum + 1));
    if (isPrime(candidate)) {
      primes.push(candidate);
    }
  }

  return primes;
}

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

document.querySelector("#reload").addEventListener("click", () => {
  document.querySelector("#user-input").value =
    'Try typing in here immediately after pressing "Generate primes"';
  document.location.reload();
});

ستتوقف استجابة البرنامج يعد أن نستدعي الدالة ()generatePrimes.

مولّد أرقام أولية باستخدام عامل ويب Web worker

عليك أولًا قبل أن تشرع العمل معنا تنزيل نسخة من ملفات البرنامج، وهي أربعة ملفات:

  • index.html
  • style.css
  • main.js
  • generate.js

ستجد أن الملفين "index.html" و "style.css" مكتملان. إليك أولًا ملف HTML:

<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Prime numbers</title>
    <script src="main.js" defer></script>
    <link href="style.css" rel="stylesheet" />
  </head>

  <body>
    <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>

    <textarea id="user-input" rows="5" cols="62">
Try typing in here immediately after pressing "Generate primes"
    </textarea>

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

وهذا محتوى ملف CSS:

textarea {
  display: block;
  margin: 1rem 0;
}

كما ستجد الملفين "main.js" و "generate.js" فارغين وسنضع الشيفرة الرئيسية في الملف "main.js " وشيفرة العامل ضمن الملف "generate.js".

إذا ما نلاحظه أولًا كيفية فصل شيفرة العامل عن الشيفرة الرئيسية، كما سترى أننا أضفنا فقط الشيفرة اﻷساسية إلى صفحة الويب "index.html" ضمن العنصر <script>.

انسخ اﻵن الشيفرة التالية إلى الملف "main.js":

//"generate.js" أنشئ عاملًا جديدًا، ونسند إليه الشيفرة الموجودة في الملف
const worker = new Worker("./generate.js");
//أرسل رسالة إلى العامل "Generate primes" عندما ينقر المستخدم
//"quota" وتضم أيضًا القيمة "generate" اﻷمر الموجود في الرسالة هو
//وهي عدد اﻷرقام اﻷولية التي نولّدها

document.querySelector("#generate").addEventListener("click", () => {
  const quota = document.querySelector("#quota").value;
  worker.postMessage({
    command: "generate",
    quota,
  });
});
//عندما يعيد العامل ارسال إلى خيط المعالجة الرئيسي
//حدّث صندوق الخرج برسال إلى المستخدم تتضمن عدد اﻷعداد الأولية التي ولدناها
// والمأخوذة من بيانات الرسالة اﻷصلية

worker.addEventListener("message", (message) => {
  document.querySelector("#output").textContent =
    `Finished generating ${message.data} primes!`;
});

document.querySelector("#reload").addEventListener("click", () => {
  document.querySelector("#user-input").value =
    'Try typing in here immediately after pressing "Generate primes"';
  document.location.reload();
});
  • ننشئ بداية عاملًا باستخدام البانية ()Worker، ونمرر إليها عنوان URL يشير إلى سكريبت العامل.تُنفَّذ شيفرة العامل بمجرد إنشاءه.

  • وكما هو الحال في النسخة المتزامنة من التطبيق نضيف معالجًا للحدث click خاصًا بالزر "Generate primes". لكن وبدلًا من استدعاء الدالة ()generatePrimes، نرسل رسالة إلى العامل باستخدام التابع ()worker.postMessage الذي يأخذ وسيطًا واحدًا. لهذا نمرر له كائن JSON يضم خاصيتين:

  • command: وتضم قيمة نصية تخبر العامل ما عليه فعله (في حال كان باستطاعته تنفيذ أكثر من شيء).

  • quota: عدد اﻷعداد اﻷولية التي يولّدها.

  • نضيف تاليًا معالج الحدث message إلى العامل، لكي يبلغنا العامل من انتهاء عمله ويعيد أية نتائج نريدها. يأخذ معالج الحدث بياناته من الخاصية data العائدة للرسالة ويطبعها ضمن عنصر الخرج (البيانات هنا هي نفسها قيمة الخاصية quota، لذا لا حاجة لها عمليًا ووضعناها لعرض مبدأ العمل فقط).

  • أضفنا اخيرة شيفرة معالج حدث النقر click للزر "Reload"، وهي مشابهة تمامًا لشيفرة النسخة المتزامنة.

انقل اﻵن الشيفرة التالية إلى الملف "generate.js":

// Listen for messages from the main thread.
// If the message command is "generate", call `generatePrimes()`
addEventListener("message", (message) => {
  if (message.data.command === "generate") {
    generatePrimes(message.data.quota);
  }
});

// Generate primes (very inefficiently)
function generatePrimes(quota) {
  function isPrime(n) {
    for (let c = 2; c <= Math.sqrt(n); ++c) {
      if (n % c === 0) {
        return false;
      }
    }
    return true;
  }

  const primes = [];
  const maximum = 1000000;

  while (primes.length < quota) {
    const candidate = Math.floor(Math.random() * (maximum + 1));
    if (isPrime(candidate)) {
      primes.push(candidate);
    }
  }

  // When we have finished, send a message to the main thread,
  // including the number of primes we generated.
  postMessage(primes.length);
}

وتذكّر أن هذه الشيفرة ستُنفَّذ بمجرد إنشاء عامل جديد.

يترصّد العامل بداية الرسائل التي ترسلها الشيفرة الرئيسية من خلال الدالة ()addEventListener وهي دالة عامة في العامل. وتضم الخاصية data الموجودة ضمن معالج حدث الرسالة message نسخة من الوسيط الذي تمرره الشيفرة الرئيسية. فإذا مررت الشيفرة الرئيسية اﻷمر generate، نستدعي حينها الدالة ()generatePrimes ونمرر لها القيمة quota من الحدث message.

تشبه الدالة ()generatePrimes مقابلتها في النسخة المتزامنة من التمرين ما عدا أننا نرسل رسالة إلى السكريبت الرئيسي عند الانتهاء بدلًا من إعادة قيمة. ونستخدم في هذه الحالة التابع ()postMessage والذي يشبه الدالة ()addEventListener بأنه عام في شيفرة العامل أيضًا. وكما رأينا، يستمع السكريبت الرئيسي إلى الرسالة ويُحدّث شجرة DOM عند استقبال الرسالة.

ملاحظة: لتشغيل هذا الموقع، عليك تشغيل خادم محلي على حاسوبك،إذ لا يُسمح بتحميل شيفرة العامل من الوجهة.

وإن صادفتك أي مشاكل في إنشاء نسختك من التمرين` بإمكانك الاطلاع على النسخة المكتملة) منه على جت-هب أو تجربته مباشرة.

أنواع أخرى من العمال workers

يُدعى العامل الذي أنشأناه في المثال السابق بالعامل المخصص dedicated worker. ويعني ذلك أنه استخدم من قبل سكريبت واحد. وهنالك نوعين آخرين هما:

  • العمال المشتركون shared workers: ويمكن مشاركتهم بين أكثر من سكريبت أثناء تنفيذها في نوافذ مختلفة للمتصفح.
  • عمّال الخدمة service workers: ويعملون كخوادم وكيلة proxy servers أو لتخزين الموارد مؤقتًا كي تعمل صفحة ويب عندما لا يكون المتصفح متصلا بالشبكة، فهي مكوّن أساسي من مكوّنات تطبيقات الويب المتقدمة Progressive Web Apps

الخلاصة

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

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

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

اقرأ أيضًا:


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...