تقدّم لنا لغة جافا سكريبت مرونة عالية غير مسبوقة في التعامل مع الدوال، إذ يمكننا تمريرها أو استعمالها على أنّها كائنات. والآن سنرى كيف نُمرّر الاستدعاءات بينها وكيف نُزخرِفها.
خبيئة من خلف الستار
لنقل بأنّ أمامنا الدالة الثقيلة على المعالج 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) is cached and the result returned
alert( "Again: " + slow(1) ); // slow(1) result returned from cache
alert( slow(2) ); // slow(2) is cached and the result returned
alert( "Again: " + slow(2) ); // slow(2) result returned from cache
في الشيفرة أعلاه، ندعو 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
من هنا إلى هناك:
- بعد الزخرفة، يصير
worker.slow
الغِلافfunction (x) { ... }
. - لذا حين نُنفّذ
worker.slow(2)
، يأخذ الغِلاف القيمةَ2
وسيطًا ويضبطthis=worker
(وهو الكائن قبل النقطة). - في الغِلاف (باعتبار أنّ النتيجة لم تُخبّأ بعد)، تُمرّر
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
لا تأخذ المفاتيح إلّا بقيمة واحدة.
ثمّة أمامنا أكثر من حلّ:
- كتابة بنية بيانات جديدة تشبه الخرائط (أو استعمال واحدة من طرف ثالث) يمكن استعمالها لأكثر من أمر وتسمح لنا بتخزين أكثر من مفتاح.
- استعمال الخرائط المتداخلة: تصير
cache.set(min)
خارطة تُخزّن الزوجين(max, result)
. ويمكن أن نأخذ الناتجresult
باستعمالcache.get(min).get(max)
. - دمج القيمتين في واحدة. في حالتنا هذه يمكن استعمال السلسلة النصية
"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)
في اللغة.
أقتبس -بتصرّف خفيف جدًا- من مواصفات اللغة:
- لمّا أنّ
glue
هو المُعامل الأول، ولو لم تكن هناك مُعاملات فهو","
. - لمّا أنّ
result
هي سلسلة نصية فارغة. - أضِف
this[0]
إلى نهايةresult
. أضِفglue
وthis[1]
. - أضِف
glue
وthis[2]
. - …كرّر حتّى يتنهي ضمّ العناصر الـ
this.length
. - أعِد
result
.
إذًا فهو يأخذ this
ويضمّ this[0]
ثمّ this[1]
وهكذا معًا. كتب المطوّرون التابِع بهذه الطريقة عمدًا ليسمح أن تكون this
أيّ شبيه بالمصفوفات (ليست مصادفة إذ تتبع كثير من التوابِع هذه الممارسة). لهذا يعمل التابِع حين يكون this=arguments
.
المُزخرِفات وخاصيات الدوال
استبدال الدوال أو التوابِع بأخرى مُزخرفة هو أمر آمن عادةً، ولكن باستثناء صغير: لو احتوت الدالة الأًلية على خاصيات (مثل 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
لتمرير الاستدعاء.
……
مُزخرِف تأخير
الأهمية: 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
مليثانية بعد الاستدعاء السابق – ستُهمل.
مثال:
let f = debounce(alert, 1000);
f(1); // يعمل مباشرةً
f(2); // يُهمل
setTimeout( () => f(3), 100); // يُهمل ( لم تمرّ إلّا 100 ملي ثانية )
setTimeout( () => f(4), 1100); // يعمل
setTimeout( () => f(5), 1500); // يُهمل (لم تمرّ الـ 1000 مليثانية من آخر تشغيل)
عمليًا في الشيفرات، نستعمل debounce
للدوال التي تستلم أو تُحدّث شيئًا ما نعرف مسبقًا بأنّ لا شيء جديد سيحدث له في هذه الفترة القصيرة، فالأفضل أن نُهمله ولا نُهدر الموارد.
الحل
function debounce(f, ms) {
let isCooldown = false;
return function() {
if (isCooldown) return;
f.apply(this, arguments);
isCooldown = true;
setTimeout(() => isCooldown = false, ms);
};
}
استدعاء debounce
يُعيد غِلافًا. ثمّة حالتين اثنتين لهذا الغِلاف:
isCooldown = false
– يمكن أن تعمل الدالة.isCooldown = true
– ننتظر انتهاء المهلة.
في أوّل استدعاء يكون isCooldown
بقيمة false
فيعمل الاستدعاء، وتتغيّر الحالة إلى true
.
نُهمل أيّ استدعاء آخر طالما isCooldown
صحيحة.
بعدها يعكس setTimeout
الحالة إلى false
بعد مرور فترة التأجيل.
مُزخرِف خنق
الأهمية: 5
أنشِئ مُزخرِف «الخنق/throttle» throttle(f, ms)
ليُعيد غِلافًا يُمرّر الاستدعاء إلى f
مرّة كلّ ms
مليثانية. والاستدعاءات التي تحدث في فترة «الراحة» تُهمل.
الفرق بين هذه وبين debounce
هي أنّه لو كان الاستدعاء المُهمل هو آخر الاستدعاءات أثناء فترة الراحة، فسيعمل متى انتهت تلك الفترة.
لنطالع هذا التطبيق من الحياة العملية لنعرف أهمية هذا الشيء الغريب العجيب وما أساسه أصلًا.
لنقل مثلًا أنّا نريد تعقّب تحرّك الفأرة.
يمكن أن نضبط دالة (في المتصفّح) لتعمل كلّما تحرّكت الفأرة وتأخذ مكان المؤشّر أثناء هذه الحركة. لو كنت تستعمل الفأرة فعادةً ما تعمل الدالة هذه بسرعة (ربما تكون 100 مرّة في الثانية، أي كلّ 10 مليثوان).
نريد تحديث بعض المعلومات في صفحة الوِب أثناء حركة المؤشّر.
…ولكن تحديث الدالة update()
عملية ثقيلة ولا تنفع لكلّ حركة فأرة صغيرة. كما وليس منطقيًا أصلًا التحديث أكثر من مرّة كلّ 100 مليثانية.
لذا نُغلّف الدالة في مُزخرف: نستعمل throttle(update, 100)
على أنّها دالة التشغيل كلّما تحرّكت الفأرة بدلًا من الدالة update()
الأصلية. سيُستدعى المُزخرِف كثيرًا صحيح، ولكنّها لن يمرّر الاستدعاءات هذه إلى update()
إلّا مرّة كلّ 100 مليثانية.
هكذا سيظهر للمستخدم:
- في أوّل تحريك للفأرة، تُمرّر نسختنا المُزخرفة من الدالة الاستدعاء مباشرةً إلى
update
، وهذا مهمّ إذ يرى المستخدم كيف تفاعلت الصفحة مباشرةً مع تحريكه الفأرة. - ثمّ يُحرّك المستخدم الفأرة أكثر، ولا يحدث شيء طالما لم تمرّ
100ms
. نسختنا المُزخرفة الرائعة تُهمل تلك الاستدعاءات. - بعد نهاية
100ms
يعمل آخر استدعاءupdate
حاملًا الإحداثيات الأخيرة. - وأخيرًا تتوقّف الفأرة عن الحراك. تنتظر الدالة المُزخرفة حتى تمضي
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
.
- أثناء الاستدعاء الأول، يُشغّل
wrapper
ببساطة الدالةfunc
ويضبط حالة الراحة (isThrottled = true
). - في هذه الحالة نحفظ كلّ الاستدعاءات في
savedArgs/savedThis
. لاحظ بأنّ السياق والوُسطاء مهمّان ويجب حفظهما كلاهما، فنحتاجهما معًا لنُعيد ذلك الاستدعاء كما كان ونستدعيه حقًا. - بعد مرور
ms
مليثانية، يعملsetTimeout
، بهذا تُزال حالة الراحة (isThrottled = false
) ولو كانت هناك استدعاءات مُهملة، نُنفّذwrapper
بآخر ما حفظنا من وُسطاء وسياق.
لا نشغّل في الخطوة الثالثة func
بل wrapper
إذ نريد تنفيذ func
إضافةً إلى دخول حالة الراحة ثانيةً وضبط المؤقّت لتصفيرها.
ترجمة -وبتصرف- للفصل Decorators and forwarding, call/apply من كتاب The JavaScript language