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

المزخرفات decorators والتمرير forwarding في جافاسكربت


صفا الفليج

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

خبيئة من خلف الستار

لنقل بأنّ أمامنا الدالة الثقيلة على المعالج ‎slow(x)‎ بينما نتائجها مستقرة، أي لنقل بأنّنا لو مرّرنا ذات ‎x‎، فسنجد ذات النتيجة دومًا.

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

ولكن، بدل إضافة هذه الميزة في دالة ‎slow()‎ نفسها، سنُنشئ دالة غالِفة تُضيف ميزة الخبيئة هذه. سنرى أسفله مدى فوائد هذا الأمر.

إليك الشيفرة أولًا، وبعدها الشرح:

function slow(x) {
  // هنا مهمّة ثقيلة تُهلك المعالج
  alert(`‎Called with ${x}‎`);
  return x;
}

function cachingDecorator(func) {
  let cache = new Map();

  return function(x) {
    if (cache.has(x)) {    // لو وجدنا هذا المفتاح في الخبيئة
      return cache.get(x); // نقرأ النتيجة منها
    }

    let result = func(x);  // وإلّا نستدعي الدالة

    cache.set(x, result);  // ثمّ نُخبّئ (نتذكّر) ناتجها
    return result;
  };
}

slow = cachingDecorator(slow);

alert( slow(1) ); // خبّأنا slow(1)
alert( "Again: " + slow(1) ); // ذات الناتج

alert( slow(2) ); // ‫خبّأنا slow(2)‎
alert( "Again: " + slow(2) ); // ذات ناتج السطر السابق

في الشيفرة أعلاه، ندعو ‎cachingDecorator‎ بالمُزخرِف (decorator): وهي دالة خاصّة تأخذ دالة أخرى مُعاملًا وتعدّل على سلوكها.

الفكرة هنا هي استدعاء ‎cachingDecorator‎ لأيّ دالة أردنا، وستُعيد لنا غِلاف الخبيئة ذاك. الفكرة هذه رائعة إذ يمكن أن نكون أمام مئات من الدوال التي يمكن أن تستغلّ هذه الميزة، وكلّ ما علينا فعله هو إضافة ‎cachingDecorator‎ عليها.

كما وأنّا نحافظ على الشيفرة أبسط بفصل ميزة الخبيئة عن مهمّة الدالة الفعلية.

ناتج ‎cachingDecorator(func)‎ هو «غِلاف» يُعيد الدالة ‎function(x)‎ التي «تُغلّف» استدعاء ‎func(x)‎ داخل شيفرة الخبيئة:

decorator-makecaching-wrapper.png

الشيفرات الخارجية لا ترى أيّ تغيير على دالة ‎slow‎ المُغلّفة. ما فعلناه هو تعزيز سلوكها بميزة الخبيئة.

إذًا نُلخّص: ثمّة فوائد عدّة لاستعمال ‎cachingDecorator‎ منفصلًا بدل تعديل شيفرة الدالة ‎slow‎ نفسها:

  • إعادة استعمال ‎cachingDecorator‎، فنُضيفه على دوال أخرى.
  • فصل شيفرة الخبيئة فلا تزيد من تعقيد دالة ‎slow‎ نفسها (هذا لو كانت معقّدة).
  • إمكانية إضافة أكثر من مُزخرف عند الحاجة (سنرى ذلك لاحقًا).

استعمال ‎func.call‎ لأخذ السياق

لا ينفع مُزخرِف الخبيئة الذي شرحناه مع توابِع الكائنات.

فمثلًا في الشيفرة أسفله، سيتوقّف عمل ‎worker.slow()‎ بعد هذه الزخرفة:

// ‫هيًا نُضف ميزة الخبيئة إلى worker.slow
let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    // أمامنا مهمّة ثقيلة على المعالج هنا 
    alert("Called with " + x);
    return x * this.someMethod(); // (*)
  }
};

// نفس الشيفرة أعلاه
function cachingDecorator(func) {
  let cache = new Map();
  return function(x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func(x); // (**)
    cache.set(x, result);
    return result;
  };
}

alert( worker.slow(1) ); // التابِع الأصلي يعمل كما ينبغي

worker.slow = cachingDecorator(worker.slow); // وقت الخبيئة

alert( worker.slow(2) ); // ‫لاااا! خطأ: تعذّرت قراءة الخاصية 'someMethod' في undefined

مكان الخطأ هو السطر ‎(*)‎ الذي يحاول الوصول إلى ‎this.someMethod‎ ويفشل فشلًا ذريعًا. هل تعرف السبب؟

السبب هو أنّ الغِلاف يستدعي الدالة الأصلية هكذا ‎func(x)‎ في السطر ‎(**)‎. وحين نستدعيها هكذا تستلم الدالة ‎this = undefined‎.

سنرى ما يشبه هذا الخطأ لو شغّلنا هذه الشيفرة:

let func = worker.slow;
func(2);

إذًا… يُمرّر الغِلاف الاستدعاء إلى التابِع الأصلي دون السياق ‎this‎، بهذا يحصل الخطأ.

وقت الإصلاح.

ثمّة تابِع دوال مضمّن في اللغة باسم func.call(context, …args) يتيح لنا استدعاء الدالة

صياغته هي:

func.call(context, arg1, arg2, ...)

يُشغّل التابِع الدالةَ ‎func‎ بعد تمرير المُعامل الأول (وهو ‎this‎) وثمّ مُعاملاتها.

للتبسيط، هذين الاستدعاءين لا يفرقان بشيء في التنفيذ:

func(1, 2, 3);
func.call(obj, 1, 2, 3)

فكلاهما يستدعي ‎func‎ بالمُعاملات ‎1‎ و‎2‎ و‎3‎. الفرق الوحيد هو أنّ ‎func.call‎ تضبط قيمة ‎this‎ على ‎obj‎ علاوةً على ذلك.

لنأخذ مثالًا. في الشيفرة أسفله نستدعي ‎sayHi‎ بسياق كائنات أخرى: يُشغّل ‎sayHi.call(user)‎ الدالةَ ‎sayHi‎ ويُمرّر ‎this=user‎، ثمّ في السطر التالي يضبط ‎this=admin‎:

function sayHi() {
  alert(this.name);
}

let user = { name: "John" };
let admin = { name: "Admin" };

// ‫نستعمل call لنمرّر مختلف الكائنات على أنّها this:
sayHi.call( user ); // this = John
sayHi.call( admin ); // this = Admin

وهنا نستدعي ‎call‎ لتستدعي ‎say‎ بالسياق والعبارة المُمرّرتين:

function say(phrase) {
  alert(this.name + ': ' + phrase);
}

let user = { name: "John" };

// ‫الكائن user يصير this وتصير Hello المُعامل الأول
say.call( user, "Hello" ); // John: Hello

في حالتنا نحن، يمكن استعمال ‎call‎ في الغِلاف ليُمرّر السياق إلى الدالة الأصلية:

let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    alert("Called with " + x);
    return x * this.someMethod(); // (*)
  }
};

function cachingDecorator(func) {
  let cache = new Map();
  return function(x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func.call(this, x); // ‫هكذا نُمرّر «this» كما ينبغي
    cache.set(x, result);
    return result;
  };
}

worker.slow = cachingDecorator(worker.slow); // والآن نُضيف الخبيئة

alert( worker.slow(2) ); // يعمل
alert( worker.slow(2) ); // ‫يعمل ولا يستدعي التابِع الأصلي (إذ القيمة مُخبّأة)

الآن يعمل كلّ شيء كما نريد.

لنوضّح الأمر أكثر، لنرى بالتفصيل المملّ تمريرات ‎this‎ من هنا إلى هناك:

  1. بعد الزخرفة، يصير ‎worker.slow‎ الغِلاف ‎function (x) { ... }‎.
  2. لذا حين نُنفّذ ‎worker.slow(2)‎، يأخذ الغِلاف القيمةَ ‎2‎ وسيطًا ويضبط ‎this=worker‎ (وهو الكائن قبل النقطة).
  3. في الغِلاف (باعتبار أنّ النتيجة لم تُخبّأ بعد)، تُمرّر ‎func.call(this, x)‎ قيمة ‎this‎ الحالية (وهي ‎worker‎) مع المُعامل الحالي (‎2‎) - كلّه إلى التابِع الأصلي.

استعمال أكثر من وسيط داخل func.apply

الآن صار وقت تعميم ‎cachingDecorator‎ على العالم. كنّا إلى هنا نستعملها مع الدوال التي تأخذ مُعاملًا واحدًا فقط.

وماذا لو أردنا تخبئة التابِع ‎worker.slow‎ الذي يأخذ أكثر من مُعامل؟

let worker = {
  slow(min, max) {
    return min + max; // نُعدّها عملية تستنزف المعالج
  }
};

// علينا تذكّر الاستدعاءات بنفس المُعامل هنا
worker.slow = cachingDecorator(worker.slow);

كنّا سابقًا نستعمل ‎cache.set(x, result)‎ (حين تعاملنا مع المُعامل الوحيد ‎x‎) لنحفظ الناتج، ونستعمل ‎cache.get(x)‎ لنجلب الناتج. أمّا الآن فعلينا تذكّر ناتج مجموعة مُعاملات ‎(min,max)‎. الخارطة ‎Map‎ لا تأخذ المفاتيح إلّا بقيمة واحدة.

ثمّة أمامنا أكثر من حلّ:

  1. كتابة بنية بيانات جديدة تشبه الخرائط (أو استعمال واحدة من طرف ثالث) يمكن استعمالها لأكثر من أمر وتسمح لنا بتخزين أكثر من مفتاح.
  2. استعمال الخرائط المتداخلة: تصير ‎cache.set(min)‎ خارطة تُخزّن الزوجين ‎(max, result)‎. ويمكن أن نأخذ الناتج ‎result‎ باستعمال ‎cache.get(min).get(max)‎.
  3. دمج القيمتين في واحدة. في حالتنا هذه يمكن استعمال السلسلة النصية ‎"min,max"‎ لتكون مفتاح ‎Map‎. ويمكن أن نقدّم للمُزخرِف دالة عنونة Hashing يمكنها صناعة قيمة من أكثر من قيمة، فيصير الأمر أسهل.

أغلب التطبيقات العملية تَعدّ الحل الثالث كافيًا، ولهذا سنستعمله هنا.

علينا أيضًا استبدال التابِع ‎func.call(this, x)‎ بالتابِع ‎func.call(this, ...arguments)‎ كي نُمرّر كلّ المُعاملات إلى استدعاء الدالة المُغلّفة لا الأولى فقط.

رحّب بالمُزخرف ‎cachingDecorator‎ الجديد، أكثر قوة وأناقة:

let worker = {
  slow(min, max) {
    alert(`‎Called with ${min},${max}‎`);
    return min + max;
  }
};

function cachingDecorator(func, hash) {
  let cache = new Map();
  return function() {
    let key = hash(arguments); // (*)
    if (cache.has(key)) {
      return cache.get(key);
    }

    let result = func.call(this, ...arguments); // (**)

    cache.set(key, result);
    return result;
  };
}

function hash(args) {
  return args[0] + ',' + args[1];
}

worker.slow = cachingDecorator(worker.slow, hash);

alert( worker.slow(3, 5) ); // يعمل
alert( "Again " + worker.slow(3, 5) ); // ‫نفس الناتج (خبّأناه)

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

أمامنا تعديلان اثنان:

  • في السطر‎(*)‎، نستدعي ‎hash‎ لتصنع مفتاحًا واحدًا من ‎arguments‎. نستعمل هنا دالة «دمج» بسيطة تحوّل المُعاملان ‎(3, 5)‎ إلى المفتاح ‎"3,5"‎. لو كانت الحالة لديك أكثر تعقيدًا، فتحتاج إلى دوال عنونة أخرى.
  • ثمّ يستعمل ‎(**)‎ التابِع ‎func.call(this, ...arguments)‎ لتمرير السياق وكلّ المُعاملات التي استلمها الغِلاف (وليس الأول فقط) - كله إلى الدالة الأصلية.

يمكننا بدل استعمال ‎func.call(this, ...arguments)‎ استغلال ‎func.apply(this, arguments)‎.

صياغة هذا التابِع المبني في اللغة func.apply هي:

func.apply(context, args)

يُشغّل التابِع الدالةَ ‎func‎ بضبط ‎this=context‎ واستعمال الكائن الشبيه بالمصفوفات ‎args‎ قائمةً بالمُعطيات للدالة.

الفارق الوحيد بين ‎call‎ و‎apply‎ هي أنّ الأوّل يتوقّع قائمة بالمُعطيات بينما الثاني يأخذ كائنًا شبيهًا بالمصفوفات يحويها.

أي أنّ الاستدعاءين الآتين متساويين تقريبًا:

func.call(context, ...args); // نمرّر الكائن قائمةً بمُعامل التوزيع
func.apply(context, args);   // ‫نفس الفكرة باستعمال apply

ولكن هناك فرق بسيط واحد:

  • يُتيح لنا مُعامل التوزيع ‎...‎ تمرير المُتعدَّد ‎args‎ قائمةً إلى ‎call‎.
  • لا يقبل ‎apply‎ إلّا مُعامل ‎args‎ شبيه بالمصفوفات.

أي أنّ هذين الاستدعاءين يُكمّلان بعضهما البعض. لو توقّعنا وصول مُتعدَّد فنستعمل ‎call‎، ولو توقّعنا شبيهًا بالمصفوفات نستعمل ‎apply‎.

أمّا الكائنات المُتعدَّدة والشبيهة بالمصفوفات (مثل المصفوفات الحقيقية)، فيمكننا نظريًا استعمال أيّ من الاثنين، إلّا أنّ ‎apply‎ سيكون أسرع غالبًا إذ أنّ مُعظم محرّكات جافاسكربت تحسّن أدائه داخليًا أكثر من ‎call‎.

يُدى تمرير كافة المُعاملات (مع السياق) من دالة إلى أخرى بتمرير الاستدعاء.

إليك أبسط صوره:

let wrapper = function() {
  return func.apply(this, arguments);
};

حين تستدعي أيّ شيفرة خارجية ‎wrapper‎ محال أن تفرّق بين استدعائها واستدعاء الدالة الأصلية ‎func‎.

استعارة التوابِع

أمّا الآن لنحسّن دالة العنونة قليلًا:

function hash(args) {
  return args[0] + ',' + args[1];
}

لا تعمل الدالة حاليًا إلّا على مُعاملين اثنين، وسيكون رائعًا لو أمكن أن ندمج أيّ عدد من ‎args‎.

أوّل حلّ نفكّر به هو استعمال التابِع arr.join:

function hash(args) {
  return args.join();
}

ولكن… للأسف فهذا لن ينفع، إذ نستدعي ‎hash(arguments)‎ بتمرير كائن المُعاملات ‎arguments‎ المُتعدَّد والشبيه بالمصفوفات… إلّا أنّه ليس بمصفوفة حقيقية.

بذلك استدعاء ‎join‎ سيفشل كما نرى أسفله:

function hash() {
  alert( arguments.join() ); // ‫خطأ: arguments.join ليست بدالة
}

hash(1, 2);

مع ذلك فما زال هناك طريقة سهلة لضمّ عناصر المصفوفة:

function hash() {
  alert( [].join.call(arguments) ); // 1,2
}

hash(1, 2);

ندعو هذه الخدعة باستعارة التوابِع.

فيها نأخذ (أي نستعير) تابِع الضمّ من المصفوفات العادية (‎[].join‎) ونستعمل ‎[].join.call‎ لتشغيله داخل سياق ‎arguments‎.

ولكن، لمَ تعمل أصلًا؟

هذا بسبب بساطة الخوارزمية الداخلية للتابِع الأصيل ‎arr.join(glue)‎ في اللغة.

أقتبس -بتصرّف خفيف جدًا- من مواصفات اللغة:

  1. لمّا أنّ ‎glue‎ هو المُعامل الأول، ولو لم تكن هناك مُعاملات فهو ‎","‎.
  2. لمّا أنّ ‎result‎ هي سلسلة نصية فارغة.
  3. أضِف ‎this[0]‎ إلى نهاية ‎result‎. أضِف ‎glue‎ و‎this[1]‎.
  4. أضِف ‎glue‎ و‎this[2]‎.
  5. …كرّر حتّى يتنهي ضمّ العناصر الـ ‎this.length‎.
  6. أعِد ‎result‎.

إذًا فهو يأخذ ‎this‎ ويضمّ ‎this[0]‎ ثمّ ‎this[1]‎ وهكذا معًا. كتب المطوّرون التابِع بهذه الطريقة عمدًا ليسمح أن تكون ‎this‎ أيّ شبيه بالمصفوفات (ليست مصادفة إذ تتبع كثير من التوابِع هذه الممارسة). لهذا يعمل التابِع حين يكون ‎this=arguments‎.

المزخرفات decorators‌ وخاصيات الدوال

استبدال الدوال أو التوابِع بأخرى مُزخرفة هو أمر آمن عادةً، ولكن باستثناء صغير: لو احتوت الدالة الأًلية على خاصيات (مثل ‎func.calledCount‎) فلن تقدّمها الدالة المُزخرفة، إذ أنّها غِلاف على الدالة الأصلية. علينا بذلك أن نحذر في هذه الحالة.

نأخذ المثال أعلاه مثالًا، لو احتوت الدالة ‎slow‎ أيّ خاصيات فلن يحتوي الغِلاف ‎cachingDecorator(slow)‎ عليها.

يمكن أن تقدّم لنا بعض المُزخرِفات خاصيات خاصة بها. فمثلًا يمكن أن يعدّ المُزخرِف كم مرّة عملت الدالة وكم من وقت أخذ ذلك، وتقدّم لنا خاصيات لنرى هذه لمعلومات.

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

ملخص

تُعدّ المزخرفات أغلفة حول الدوال فتعدّل سلوكها، بينما المهمة الأساس مرهونة بالدالة نفسها.

يمكن عدّ المُزخرِفات «مزايا» نُضيفها على الدالة، فنُضيف واحدة أو أكثر، ودون تغيير أيّ سطر في الشيفرة!

رأينا التوابِع الآتية لنعرف كيفية إعداد المُزخرِف ‎cachingDecorator‎:

  • func.call(context, arg1, arg2…) -- يستدعي ‎func‎ حسب السياق والمُعاملات الممرّرة.
  • func.apply(context, args) -- يستدعي ‎func‎ حيث يُمرّر ‎context‎ بصفته ‎this‎ والكائن الشبيه بالمصفوفات ‎args‎ في قائمة المُعاملات.

عادةً ما نكتب تمرير الاستدعاءات باستعمال ‎apply‎:

let wrapper = function() {
  return original.apply(this, arguments);
};

كما رأينا مثالًا عن استعارة التوابِع حيث أخذنا تابِعًا من كائن واستدعيناه ‎call‎ في سياق كائن آخر غيره. يشيع بين المطوّرين أخذ توابِع المصفوفات وتطبيقها على المُعاملات ‎arguments‎. لو أردت بديلًا لذلك فاستعمل كائن المُعاملات البقية إذ هو مصفوفة حقيقية.

ستجد في رحلتك المحفوفة بالمخاطر مُزخرِفات عديدة. حاوِل التمرّس عليها بحلّ تمارين هذا الفصل.

تمارين

مزخرف تجسس

الأهمية: 5

أنشِئ المُزخرِف ‎spy(func)‎ ليُعيد غِلافًا يحفظ كلّ استدعاءات تلك الدالة في خاصية ‎calls‎ داخله.

احفظ كلّ استدعاء على أنّه مصفوفة من الوُسطاء.

مثال:

function work(a, b) {
  alert( a + b ); // ‫ليست work إلّا دالة أو تابِعًا لسنا نعرف أصله
}

work = spy(work); // (*)

work(1, 2); // 3
work(4, 5); // 9

for (let args of work.calls) {
  alert( 'call:' + args.join() ); // "call:1,2", "call:4,5"
}

ملاحظة: نستفيد من هذا المُزخرِف أحيانًا لاختبار الوحدات. يمكن عدّ ‎sinon.spy‎ في المكتبة Sinon.JS صورةً متقدّمةً عنه.

الحل

سيُخزّن الغِلاف الذي أعادته spy(f)‎ كلّ الوُسطاء، بعدها يستعمل f.apply لتمرير الاستدعاء. …..

function spy(func) {

  function wrapper(...args) {
    // ‫استعملنا arg... بدلًا من arguments للحصول على مصفوفة حقيقية في wrapper.calls
    wrapper.calls.push(args);
    return func.apply(this, args);
  }

  wrapper.calls = [];

  return wrapper;
}

مزخرف تأخير

الأهمية: 5

أنشِئ المُزخرف ‎delay(f, ms)‎ ليُؤخّر كلّ استدعاء من ‎f‎ بمقدار ‎ms‎ مليثانية.

مثال:

function f(x) {
  alert(x);
}

// أنشِئ الغِلافات
let f1000 = delay(f, 1000);
let f1500 = delay(f, 1500);

f1000("test"); // ‫يعرض «test» بعد 1000 مليثانية
f1500("test"); // ‫يعرض «test» بعد 1500 مليثانية

أي أنّ المُزخرِف ‎delay(f, ms)‎ يُعيد نسخة عن ‎f‎ «تأجّلت ‎ms‎».

الدالة ‎f‎ في الشيفرة أعلاه تقبل وسيطًا واحدًا، ولكن على الحل الذي ستكتبه تمرير كلّ الوُسطاء والسياق ‎this‎ كذلك.

الحل

function delay(f, ms) {

  return function() {
    setTimeout(() => f.apply(this, arguments), ms);
  };

}

let f1000 = delay(alert, 1000);

f1000("test"); // ‫يعرض test بعد 1000 مليثانية

لاحظ بأنّا استعملنا الدالة السهمية هنا. كما نعلم فالدوال السهمية لا تملك لا ‎this‎ ولا ‎arguments‎، لذا يأخذ ‎f.apply(this, arguments)‎ كِلا ‎this‎ و‎arguments‎ من الغِلاف.

لو مرّرنا دالة عادية فسيستدعيها ‎setTimeout‎ بدون المُعاملات ويضبط ‎this=window‎ (باعتبار أنّا في بيئة المتصفّح).

مع ذلك يمكننا تمرير قيمة ‎this‎ الصحيحة باستعمال متغيّر وسيط ولكنّ ذلك سيكون تعبًا لا داعٍ له:

function delay(f, ms) {

  return function(...args) {
    let savedThis = this; // خزّنه في متغير وسيط
    setTimeout(function() {
      f.apply(savedThis, args); // استعمل الوسيط هنا
    }, ms);
  };

}

مزخرف إزالة ارتداد

اصنع المُزخرِف ‎debounce(f, ms)‎ ليُعيد غِلافًا يُمرّر الاستدعاء إلى ‎f‎ مرّة واحدة كلّ ‎ms‎ مليثانية.

بعبارة أخرى: حين ندعو الدالة «بأنّ ارتدادها أُزيل» Debounce فهي تضمن لنا بأنّ الاستدعاءات التي ستحدث في أقلّ من ‎ms‎ مليثانية بعد الاستدعاء السابق - ستُهمل.

1.jpg

إليك مثال (مأخوذ من مكتبة Lodash?

let f = _.debounce(alert, 1000);

f("a");
setTimeout( () => f("b"), 200);
setTimeout( () => f("c"), 500);
// alert("c") تنتظر 1000 م.ث بعد آخر استدعاء ثم تُنفذ

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

إن فكرت بالحل، قبل النظر للشيفرة التالية، فهو عبارة عن بضعة سطور!

الحل

function debounce(func, ms) {
  let timeout;
  return function() {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, arguments), ms);
  };
}

استدعاء ‎debounce‎ يُعيد غِلافًا. عند استدعائها، تجدول الدالة الأصلية بعد عدد الثواني المحدد وتمسح أي استدعاء سابق مُجدول.

مزخرف خنق

الأهمية: 5

أنشِئ مُزخرِف «الخنق/throttle» ‏‎throttle(f, ms)‎ ليُعيد غِلافًا يُمرّر الاستدعاء إلى ‎f‎ مرّة كلّ ‎ms‎ مليثانية. والاستدعاءات التي تحدث في فترة «الراحة» تُهمل.

الفرق بين هذه وبين ‎debounce‎ هي أنّه لو كان الاستدعاء المُهمل هو آخر الاستدعاءات أثناء فترة الراحة، فسيعمل متى انتهت تلك الفترة.

لنطالع هذا التطبيق من الحياة العملية لنعرف أهمية هذا الشيء الغريب العجيب وما أساسه أصلًا.

لنقل مثلًا أنّا نريد تعقّب تحرّك الفأرة.

يمكن أن نضبط دالة (في المتصفّح) لتعمل كلّما تحرّكت الفأرة وتأخذ مكان المؤشّر أثناء هذه الحركة. لو كنت تستعمل الفأرة فعادةً ما تعمل الدالة هذه بسرعة (ربما تكون 100 مرّة في الثانية، أي كلّ 10 مليثوان).

نريد تحديث بعض المعلومات في صفحة الوِب أثناء حركة المؤشّر.

…ولكن تحديث الدالة ‎update()‎ عملية ثقيلة ولا تنفع لكلّ حركة فأرة صغيرة. كما وليس منطقيًا أصلًا التحديث أكثر من مرّة كلّ 100 مليثانية.

لذا نُغلّف الدالة في مُزخرف: نستعمل ‎throttle(update, 100)‎ على أنّها دالة التشغيل كلّما تحرّكت الفأرة بدلًا من الدالة ‎update()‎ الأصلية. سيُستدعى المُزخرِف كثيرًا صحيح، ولكنّها لن يمرّر الاستدعاءات هذه إلى ‎update()‎ إلّا مرّة كلّ 100 مليثانية.

هكذا سيظهر للمستخدم:

  1. في أوّل تحريك للفأرة، تُمرّر نسختنا المُزخرفة من الدالة الاستدعاء مباشرةً إلى ‎update‎، وهذا مهمّ إذ يرى المستخدم كيف تفاعلت الصفحة مباشرةً مع تحريكه الفأرة.
  2. ثمّ يُحرّك المستخدم الفأرة أكثر، ولا يحدث شيء طالما لم تمرّ ‎100ms‎. نسختنا المُزخرفة الرائعة تُهمل تلك الاستدعاءات.
  3. بعد نهاية ‎100ms‎ يعمل آخر استدعاء ‎update‎ حاملًا الإحداثيات الأخيرة.
  4. وأخيرًا تتوقّف الفأرة عن الحراك. تنتظر الدالة المُزخرفة حتى تمضي ‎100ms‎ وثمّ تشغّل ‎update‎ حاملةً آخر الإحداثيات. وهكذا نُعالج آخر حركة للفأرة، وهذا مهم

مثال عن الشيفرة:

function f(a) {
  console.log(a);
}

// تمرّر f1000 الاستدعاءات إلى f مرّة كلّ 1000 مليثانية كحدّ أقصى
let f1000 = throttle(f, 1000);

f1000(1); // تعرض 1
f1000(2); // (مخنوقة، لم تمض 1000 مليثانية بعد)
f1000(3); // (مخنوقة، لم تمض 1000 مليثانية بعد)

// ‫حين تمضي 1000 مليثانية...
// ‫...تطبع 3، إذ القيمة 2 الوسطية أُهملت

ملاحظة: يجب تمرير المُعاملات والسياق ‎this‎ المُمرّرة إلى ‎f1000‎- تمريرها إلى ‎f‎ الأصلية.

الحل

function throttle(func, ms) {

  let isThrottled = false,
    savedArgs,
    savedThis;

  function wrapper() {

    if (isThrottled) { // (2)
      savedArgs = arguments;
      savedThis = this;
      return;
    }

    func.apply(this, arguments); // (1)

    isThrottled = true;

    setTimeout(function() {
      isThrottled = false; // (3)
      if (savedArgs) {
        wrapper.apply(savedThis, savedArgs);
        savedArgs = savedThis = null;
      }
    }, ms);
  }

  return wrapper;
}

يُعيد استدعاء ‎throttle(func, ms)‎ الغِلاف ‎wrapper‎.

  1. أثناء الاستدعاء الأول، يُشغّل ‎wrapper‎ ببساطة الدالة ‎func‎ ويضبط حالة الراحة (‎isThrottled = true‎).
  2. في هذه الحالة نحفظ كلّ الاستدعاءات في ‎savedArgs/savedThis‎. لاحظ بأنّ السياق والوُسطاء مهمّان ويجب حفظهما كلاهما، فنحتاجهما معًا لنُعيد ذلك الاستدعاء كما كان ونستدعيه حقًا.
  3. بعد مرور ‎ms‎ مليثانية، يعمل ‎setTimeout‎، بهذا تُزال حالة الراحة (‎isThrottled = false‎) ولو كانت هناك استدعاءات مُهملة، نُنفّذ ‎wrapper‎ بآخر ما حفظنا من وُسطاء وسياق.

لا نشغّل في الخطوة الثالثة ‎func‎ بل ‎wrapper‎ إذ نريد تنفيذ ‎func‎ إضافةً إلى دخول حالة الراحة ثانيةً وضبط المؤقّت لتصفيرها.

ترجمة -وبتصرف- للفصل Decorators and forwarding, call/apply من كتاب The JavaScript language


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

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

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



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

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

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

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


×
×
  • أضف...