توفر JavaScript مرونة استثنائية عند التعامل مع الوظائف. يمكن تمريرها واستخدامها كأشياء، والآن سنرى كيفية إعادة توجيه المكالمات فيما بينها وتزيينها .
لنفترض أن لدينا دالة slow(x)
ثقيلة على وحدة المعالجة المركزية (CPU)، لكن نتائجها مستقرة. بمعنى آخر، لنفس x
فإنه يُرجع دائمًا نفس النتيجة.
إذا تم استدعاء الدالة كثيرًا، فقد نرغب في تخزين النتائج مؤقتًا (تذكرها) لتجنب قضاء وقت إضافي في عمليات إعادة الحساب.
ولكن بدلاً من إضافة هذه الوظيفة إلى slow()
سنقوم بإنشاء وظيفة مجمعة تضيف التخزين المؤقت. وكما سنرى، هناك العديد من الفوائد للقيام بذلك.
إليك الكود، والتفسيرات التالية:
وظيفة بطيئة (س) { // قد تكون هناك مهمة ثقيلة تستهلك وحدة المعالجة المركزية (CPU) هنا تنبيه("تم الاتصال بـ ${x}`); العودة س؛ } وظيفة التخزين المؤقت ديكور (فونك) { دع ذاكرة التخزين المؤقت = خريطة جديدة ()؛ وظيفة العودة (س) { إذا (cache.has(x)) {// إذا كان هناك مثل هذا المفتاح في ذاكرة التخزين المؤقت إرجاع ذاكرة التخزين المؤقت.get(x); // اقرأ النتيجة منه } دع النتيجة = func(x); // وإلا فاتصل بـ func Cache.set(x, result); // وذاكرة التخزين المؤقت (تذكر) النتيجة نتيجة الإرجاع؛ }; } بطيئة = cachingDecorator(slow); تنبيه(بطيء(1)); // يتم تخزين البطيء (1) مؤقتًا ويتم إرجاع النتيجة تنبيه ("مرة أخرى:" + بطيء (1) )؛ // نتيجة بطيئة (1) تم إرجاعها من ذاكرة التخزين المؤقت تنبيه(بطيء(2)); // يتم تخزين البطيء (2) مؤقتًا ويتم إرجاع النتيجة تنبيه ("مرة أخرى:" + بطيء (2) )؛ // نتيجة بطيئة (2) تم إرجاعها من ذاكرة التخزين المؤقت
في الكود أعلاه، يعد cachingDecorator
بمثابة ديكور : وظيفة خاصة تأخذ وظيفة أخرى وتغير سلوكها.
الفكرة هي أنه يمكننا استدعاء cachingDecorator
لأي وظيفة، وسيقوم بإرجاع غلاف التخزين المؤقت. هذا رائع، لأنه يمكن أن يكون لدينا العديد من الوظائف التي يمكنها استخدام مثل هذه الميزة، وكل ما يتعين علينا القيام به هو تطبيق cachingDecorator
عليها.
من خلال فصل التخزين المؤقت عن رمز الوظيفة الرئيسية، فإننا نحافظ أيضًا على بساطة الرمز الرئيسي.
نتيجة cachingDecorator(func)
هي "غلاف": function(x)
التي "تغلف" استدعاء func(x)
في منطق التخزين المؤقت:
من خلال التعليمات البرمجية الخارجية، لا تزال الوظيفة slow
الملتفة تفعل الشيء نفسه. لقد تمت إضافة جانب التخزين المؤقت إلى سلوكه.
لتلخيص ذلك، هناك العديد من الفوائد لاستخدام cachingDecorator
منفصلًا بدلاً من تغيير كود slow
نفسه:
يمكن إعادة استخدام cachingDecorator
. يمكننا تطبيقه على وظيفة أخرى.
منطق التخزين المؤقت منفصل، ولم يزيد من تعقيد slow
نفسه (إن وجد).
يمكننا الجمع بين العديد من الديكورات إذا لزم الأمر (سيتبعها مصممون آخرون).
مصمم التخزين المؤقت المذكور أعلاه غير مناسب للعمل مع أساليب الكائنات.
على سبيل المثال، في الكود أدناه، يتوقف worker.slow()
عن العمل بعد الزخرفة:
// سنجعل عامل التخزين المؤقت بطيئًا دع العامل = { بعض الطريقة () { العودة 1؛ }, بطيء (س) { // مهمة مخيفة ثقيلة على وحدة المعالجة المركزية هنا تنبيه("تم الاتصال بـ "+ x); return x * this.someMethod(); // (*) } }; // نفس الكود السابق وظيفة التخزين المؤقت ديكور (فونك) { دع ذاكرة التخزين المؤقت = خريطة جديدة ()؛ وظيفة العودة (س) { إذا (cache.has(x)) { إرجاع ذاكرة التخزين المؤقت.get(x); } دع النتيجة = func(x); // (**) Cache.set(x, result); نتيجة الإرجاع؛ }; } تنبيه(worker.slow(1)); // تعمل الطريقة الأصلية عامل.slow = cachingDecorator(worker.slow); // الآن قم بالتخزين المؤقت تنبيه(worker.slow(2)); // عفوًا! خطأ: لا يمكن قراءة الخاصية 'someMethod' غير المحددة
يحدث الخطأ في السطر (*)
الذي يحاول الوصول إلى this.someMethod
ويفشل. هل تستطيع أن ترى لماذا؟
والسبب هو أن المجمّع يستدعي الوظيفة الأصلية باسم func(x)
في السطر (**)
. وعندما يتم استدعاؤها بهذه الطريقة، تحصل الدالة على this = undefined
.
سنلاحظ عرضًا مشابهًا إذا حاولنا الركض:
دع func = عامل.slow؛ وظيفة (2)؛
لذلك، يقوم المجمع بتمرير الاستدعاء إلى الطريقة الأصلية، ولكن بدون السياق this
. ومن هنا الخطأ.
دعونا إصلاحه.
هناك طريقة وظيفية مدمجة خاصة func.call(context, …args) تسمح باستدعاء دالة تحدد this
بشكل صريح.
بناء الجملة هو:
func.call (السياق، arg1، arg2، ...)
يتم تشغيل func
مع توفير الوسيطة الأولى كـ this
، والوسيطة التالية.
لتبسيط الأمر، هاتان المكالمتان تفعلان الشيء نفسه تقريبًا:
وظيفة(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
:
الدالة sayHi() { تنبيه(هذا.اسم); } السماح للمستخدم = { الاسم: "جون" }؛ دع المشرف = { الاسم: "المسؤول" }; // استخدم الاستدعاء لتمرير كائنات مختلفة باسم "هذا" sayHi.call(user); // جون sayHi.call( المشرف ); // مسؤل
وهنا نستخدم call
to call say
مع السياق والعبارة المحددة:
وظيفة القول (عبارة) { تنبيه(this.name + ':' + العبارة); } السماح للمستخدم = { الاسم: "جون" }؛ // يصبح المستخدم هو هذا، وتصبح "Hello" هي الوسيطة الأولى say.call(user,"Hello"); // جون: مرحبا
في حالتنا، يمكننا استخدام call
في الغلاف لتمرير السياق إلى الوظيفة الأصلية:
دع العامل = { بعض الطريقة () { العودة 1؛ }, بطيء (س) { تنبيه("تم الاتصال بـ "+ x); return x * this.someMethod(); // (*) } }; وظيفة التخزين المؤقت ديكور (فونك) { دع ذاكرة التخزين المؤقت = خريطة جديدة ()؛ وظيفة العودة (خ) { إذا (cache.has(x)) { إرجاع ذاكرة التخزين المؤقت.get(x); } دع النتيجة = func.call(this, x); // تم تمرير "هذا" بشكل صحيح الآن Cache.set(x, result); نتيجة الإرجاع؛ }; } عامل.slow = cachingDecorator(worker.slow); // الآن قم بالتخزين المؤقت تنبيه(worker.slow(2)); // يعمل تنبيه(worker.slow(2)); // يعمل، ولا يستدعي النسخة الأصلية (مخبأة)
الآن كل شيء على ما يرام.
لتوضيح الأمر كله، دعونا نرى بعمق أكبر كيف يتم تمرير this
:
بعد الزخرفة، أصبحت worker.slow
الآن function (x) { ... }
.
لذا، عند تنفيذ worker.slow(2)
، يحصل الغلاف على 2
كوسيطة this=worker
(وهو الكائن قبل النقطة).
داخل الغلاف، بافتراض أن النتيجة لم يتم تخزينها مؤقتًا بعد، func.call(this, x)
يمرر الحالي this
( =worker
) والوسيطة الحالية ( =2
) إلى الطريقة الأصلية.
الآن دعونا نجعل cachingDecorator
أكثر عالمية. حتى الآن كان يعمل فقط مع وظائف الوسيطة الواحدة.
الآن كيف يتم تخزين طريقة worker.slow
متعدد الوسائط بشكل مؤقت؟
دع العامل = { بطيء (دقيقة، كحد أقصى) { العودة دقيقة + ماكس؛ // من المفترض أن تكون وحدة المعالجة المركزية مخيفة } }; // يجب أن يتذكر استدعاءات نفس الوسيطة عامل.slow = cachingDecorator(worker.slow);
في السابق، بالنسبة إلى وسيطة واحدة x
كان بإمكاننا فقط تخزين النتيجة cache.set(x, result)
و cache.get(x)
لاستعادتها. لكن علينا الآن أن نتذكر نتيجة مجموعة من الوسائط (min,max)
. تأخذ Map
الأصلية قيمة مفردة كمفتاح فقط.
هناك العديد من الحلول الممكنة:
قم بتنفيذ بنية بيانات جديدة (أو استخدم جهة خارجية) تشبه الخريطة وأكثر تنوعًا وتسمح بمفاتيح متعددة.
استخدم الخرائط المتداخلة: ستكون ذاكرة التخزين المؤقت cache.set(min)
عبارة عن Map
تخزن الزوج (max, result)
. حتى نتمكن من الحصول على result
كـ cache.get(min).get(max)
.
ضم قيمتين في واحدة. في حالتنا الخاصة يمكننا فقط استخدام سلسلة "min,max"
كمفتاح Map
. من أجل المرونة، يمكننا السماح بتوفير وظيفة تجزئة لمصمم الديكور، الذي يعرف كيفية إنشاء قيمة واحدة من العديد من القيم.
بالنسبة للعديد من التطبيقات العملية، يعتبر الإصدار الثالث جيدًا بدرجة كافية، لذا سنلتزم به.
نحتاج أيضًا إلى تمرير ليس فقط x
، بل جميع الوسائط في func.call
. لنتذكر أنه في function()
يمكننا الحصول على مصفوفة زائفة من وسائطها arguments
، لذلك يجب استبدال func.call(this, x)
بـ func.call(this, ...arguments)
.
إليك cachingDecorator
أكثر قوة:
دع العامل = { بطيء (دقيقة، كحد أقصى) { تنبيه ("تم الاتصال بـ ${min},${max}`); العودة دقيقة + ماكس؛ } }; وظيفة cachingDecorator(func, hash) { دع ذاكرة التخزين المؤقت = خريطة جديدة ()؛ وظيفة العودة () { دع المفتاح = hash(arguments); // (*) إذا (cache.has(مفتاح)) { إرجاع ذاكرة التخزين المؤقت.get(مفتاح); } Let result = func.call(this, ...arguments); // (**) Cache.set(key, result); نتيجة الإرجاع؛ }; } تجزئة الوظيفة (الوسائط) { إرجاع الوسائط[0] + ',' + args[1]; } عامل.slow = cachingDecorator(worker.slow, hash); تنبيه(worker.slow(3, 5)); // يعمل تنبيه("مرة أخرى" +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(السياق، الوسائط)
يقوم بتشغيل إعداد func
this=context
واستخدام كائن يشبه args
كقائمة من الوسائط.
الفرق الوحيد في بناء الجملة بين call
و apply
هو أن call
تتوقع قائمة من الوسائط، بينما يأخذ apply
كائنًا يشبه المصفوفة معها.
لذا فإن هاتين النداءتين متكافئتان تقريبًا:
func.call(context, ...args); func.apply(context, args);
إنهم يؤدون نفس استدعاء func
مع سياق ووسائط معينة.
لا يوجد سوى اختلاف بسيط فيما يتعلق args
:
يسمح بناء جملة الانتشار ...
بتمرير args
قابلة للتكرار كقائمة call
.
يقبل apply
فقط args
التي تشبه المصفوفة .
…وبالنسبة للكائنات القابلة للتكرار والشبيهة بالمصفوفة، مثل المصفوفة الحقيقية، يمكننا استخدام أي منها، ولكن من المحتمل أن يكون apply
أسرع، لأن معظم محركات JavaScript تعمل على تحسينها داخليًا بشكل أفضل.
يُطلق على تمرير جميع الوسائط مع السياق إلى وظيفة أخرى اسم إعادة توجيه الاتصال .
وهذا هو أبسط شكل منه:
دع المجمع = وظيفة () { إرجاع func.apply (هذا، الحجج)؛ };
عندما يستدعي كود خارجي هذا wrapper
، لا يمكن تمييزه عن استدعاء الدالة الأصلية func
.
الآن دعونا نجري تحسينًا طفيفًا آخر في وظيفة التجزئة:
تجزئة الوظيفة (الوسائط) { إرجاع الوسائط[0] + ',' + args[1]; }
اعتبارًا من الآن، يعمل فقط على وسيطتين. سيكون من الأفضل أن تتمكن من لصق أي عدد من args
.
الحل الطبيعي هو استخدام طريقة arr.join :
تجزئة الوظيفة (الوسائط) { إرجاع args.join(); }
…لسوء الحظ، هذا لن ينجح. لأننا نسمي hash(arguments)
وكائن arguments
قابل للتكرار وشبيه بالمصفوفة، ولكنه ليس مصفوفة حقيقية.
لذا فإن استدعاء join
إليه سيفشل، كما نرى أدناه:
تجزئة الوظيفة () { تنبيه (الحجج. الانضمام ())؛ // خطأ: الوسيطات. الانضمام ليست دالة } التجزئة(1, 2);
ومع ذلك، هناك طريقة سهلة لاستخدام صلة المصفوفة:
تجزئة الوظيفة () { تنبيه([].join.call(arguments)); // 1,2 } التجزئة(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
خاص لتغليف دالة. سنناقش ذلك لاحقًا في مقالة Proxy and Reflect.
الديكور عبارة عن غلاف حول دالة يغير سلوكها. لا تزال الوظيفة الرئيسية تنفذها الوظيفة.
يمكن النظر إلى أدوات الديكور على أنها "ميزات" أو "جوانب" يمكن إضافتها إلى الوظيفة. يمكننا إضافة واحد أو إضافة العديد. وكل هذا دون تغيير الكود الخاص به!
لتنفيذ cachingDecorator
، قمنا بدراسة الطرق:
func.call(context, arg1, arg2...) - يستدعي func
مع سياق ووسائط معينة.
func.apply(context, args) - يستدعي context
تمرير func
بهذا this
والوسائط المشابهة args
في قائمة من الوسائط.
عادةً ما تتم عملية تحويل المكالمات العامة باستخدام apply
:
دع المجمع = وظيفة () { إرجاع original.apply(هذا، الحجج)؛ };
لقد رأينا أيضًا مثالاً على استعارة الطريقة عندما نأخذ طريقة من كائن call
في سياق كائن آخر. من الشائع جدًا استخدام أساليب المصفوفة وتطبيقها على arguments
. البديل هو استخدام كائن معلمات الراحة الذي يمثل مصفوفة حقيقية.
هناك العديد من مصممي الديكور هناك في البرية. تحقق من مدى حصولك عليها من خلال حل مهام هذا الفصل.
الأهمية: 5
قم بإنشاء spy(func)
الذي يجب أن يُرجع غلافًا يحفظ جميع الاستدعاءات ليعمل في خاصية calls
الخاصة به.
يتم حفظ كل مكالمة كمجموعة من الوسائط.
على سبيل المثال:
وظيفة العمل (أ، ب) { تنبيه (أ + ب)؛ // العمل هو وظيفة أو طريقة عشوائية } العمل = تجسس(العمل); العمل(1, 2); // 3 العمل(4, 5); // 9 من أجل (دع وسيطات العمل. المكالمات) { تنبيه( 'اتصال:' + args.join() ); // "اتصال: 1,2"، "اتصال: 4,5" }
ملحوظة: يكون هذا الديكور مفيدًا أحيانًا لاختبار الوحدة. شكله المتقدم هو sinon.spy
في مكتبة Sinon.JS.
افتح صندوق الرمل مع الاختبارات.
يجب أن يقوم برنامج التضمين الذي تم إرجاعه بواسطة spy(f)
بتخزين جميع الوسائط ثم استخدام f.apply
لإعادة توجيه المكالمة.
وظيفة التجسس (فونك) { غلاف الوظيفة (...الوسائط) { // استخدام ...args بدلاً من الوسائط لتخزين المصفوفة "الحقيقية" في Wrapper.calls Wrapper.calls.push(args); إرجاع func.apply(this, args); } Wrapper.calls = []; غلاف العودة؛ }
افتح الحل بالاختبارات في وضع الحماية.
الأهمية: 5
قم بإنشاء delay(f, ms)
يؤخر كل استدعاء لـ f
بمقدار مللي ms
.
على سبيل المثال:
وظيفة و (خ) { تنبيه (خ)؛ } // إنشاء أغلفة دع f1000 = تأخير(f, 1000); دع f1500 = تأخير(f, 1500); f1000("اختبار"); // يظهر "اختبار" بعد 1000 مللي ثانية f1500("اختبار"); // يظهر "اختبار" بعد 1500 مللي ثانية
بمعنى آخر، يؤدي delay(f, ms)
إلى إرجاع متغير "delayed by ms
" للدالة f
.
في الكود أعلاه، f
هي دالة لوسيطة واحدة، لكن الحل الخاص بك يجب أن يمرر جميع الوسائط والسياق this
.
افتح صندوق الرمل مع الاختبارات.
الحل:
تأخير الوظيفة (و، مللي ثانية) { وظيفة العودة () { setTimeout(() => f.apply(this, الوسيطات), ms); }; } دع f1000 = تأخير(alert, 1000); f1000("اختبار"); // يظهر "اختبار" بعد 1000 مللي ثانية
يرجى ملاحظة كيفية استخدام وظيفة السهم هنا. كما نعلم، لا تمتلك وظائف السهم this
arguments
الخاصة بها، لذا f.apply(this, arguments)
تأخذ this
arguments
من الغلاف.
إذا مررنا دالة عادية، فسيستدعيها setTimeout
بدون وسائط ويكون this=window
(على افتراض أننا في المتصفح).
لا يزال بإمكاننا تمرير this
إلى اليمين باستخدام متغير وسيط، ولكن هذا أكثر تعقيدًا بعض الشيء:
تأخير الوظيفة (و، مللي ثانية) { وظيفة الإرجاع (...الوسائط) { دع saveThis = this; // قم بتخزين هذا في متغير وسيط setTimeout(وظيفة() { f.apply(savedThis, args); // استخدمه هنا }، آنسة)؛ }; }
افتح الحل بالاختبارات في وضع الحماية.
الأهمية: 5
نتيجة الديكور debounce(f, ms)
عبارة عن غلاف يعلق الاستدعاءات إلى f
حتى يكون هناك مللي ms
من عدم النشاط (لا توجد مكالمات، "فترة التهدئة")، ثم يستدعي f
مرة واحدة باستخدام أحدث الوسائط.
وبعبارة أخرى، فإن debounce
يشبه السكرتير الذي يقبل "المكالمات الهاتفية"، وينتظر حتى يكون هناك مللي ms
من الصمت. وعندها فقط يقوم بنقل أحدث معلومات المكالمة إلى "الرئيس" (يستدعي f
الفعلي).
على سبيل المثال، كان لدينا دالة f
واستبدلناها بـ f = debounce(f, 1000)
.
بعد ذلك، إذا تم استدعاء الوظيفة الملتفة عند 0 مللي ثانية و200 مللي ثانية و500 مللي ثانية، ثم لم تكن هناك أي استدعاءات، فسيتم استدعاء f
الفعلي مرة واحدة فقط، عند 1500 مللي ثانية. أي: بعد فترة التهدئة البالغة 1000 مللي ثانية من آخر مكالمة.
...وسيحصل على وسيطات المكالمة الأخيرة، ويتم تجاهل المكالمات الأخرى.
إليك الكود الخاص به (يستخدم مصمم الارتداد من مكتبة Lodash):
Let f = _.debounce(alert, 1000); و("أ"); setTimeout( () => f("b"), 200); setTimeout( () => f("c"), 500); // تنتظر الدالة المرتدة 1000 مللي ثانية بعد المكالمة الأخيرة ثم يتم تشغيلها: تنبيه("c")
الآن مثال عملي. لنفترض أن المستخدم كتب شيئًا ما، ونرغب في إرسال طلب إلى الخادم عند الانتهاء من الإدخال.
ليس هناك فائدة من إرسال الطلب لكل حرف يتم كتابته. وبدلاً من ذلك، نود الانتظار، ثم معالجة النتيجة بأكملها.
في متصفح الويب، يمكننا إعداد معالج الأحداث - وهي وظيفة يتم استدعاؤها عند كل تغيير في حقل الإدخال. عادة، يتم استدعاء معالج الحدث في كثير من الأحيان، لكل مفتاح مكتوب. ولكن إذا قمنا debounce
بمقدار 1000 مللي ثانية، فسيتم استدعاؤها مرة واحدة فقط، بعد 1000 مللي ثانية بعد آخر إدخال.
في هذا المثال المباشر، يضع المعالج النتيجة في المربع أدناه، جربها:
يرى؟ يستدعي الإدخال الثاني الدالة المرتدة، بحيث تتم معالجة محتواها بعد 1000 مللي ثانية من الإدخال الأخير.
لذا، يعد debounce
طريقة رائعة لمعالجة سلسلة من الأحداث: سواء كانت سلسلة من الضغطات على المفاتيح أو حركات الماوس أو أي شيء آخر.
ينتظر الوقت المحدد بعد المكالمة الأخيرة، ثم يقوم بتشغيل وظيفته التي يمكنها معالجة النتيجة.
وتتمثل المهمة في تنفيذ الديكور debounce
.
تلميح: هذه مجرد بضعة أسطر إذا فكرت فيها :)
افتح صندوق الرمل مع الاختبارات.
وظيفة debounce (func، مللي) { السماح للمهلة؛ وظيفة العودة () { ClearTimeout(timeout); timeout = setTimeout(() => func.apply(this, الوسيطات), مللي ثانية); }; }
دعوة debounce
ترجع غلافًا. عند استدعائها، تقوم بجدولة استدعاء الوظيفة الأصلية بعد إعطاء ms
وإلغاء المهلة السابقة.
افتح الحل بالاختبارات في وضع الحماية.
الأهمية: 5
قم بإنشاء throttle(f, ms)
- الذي يُرجع غلافًا.
عندما يتم استدعاؤه عدة مرات، فإنه يمرر الاستدعاء إلى f
بحد أقصى مرة واحدة ms
مللي ثانية.
يختلف السلوك تمامًا مقارنةً بمصمم الديكور المرتد:
يقوم debounce
بتشغيل الوظيفة مرة واحدة بعد فترة "التهدئة". جيد لمعالجة النتيجة النهائية.
لا يعمل throttle
في كثير من الأحيان أكثر مما يعطى الوقت ms
. جيد للتحديثات المنتظمة التي لا ينبغي أن تكون متكررة.
بمعنى آخر، يشبه throttle
السكرتير الذي يقبل المكالمات الهاتفية، ولكنه يزعج رئيسه (يتصل f
الفعلي) ليس أكثر من مرة واحدة لكل مللي ms
.
دعونا نتحقق من التطبيق الواقعي لفهم هذا المطلب بشكل أفضل ومعرفة مصدره.
على سبيل المثال، نريد تتبع حركات الماوس.
يمكننا في المتصفح إعداد وظيفة للتشغيل عند كل حركة للماوس والحصول على موقع المؤشر أثناء تحركه. أثناء الاستخدام النشط للماوس، تعمل هذه الوظيفة عادةً بشكل متكرر جدًا، ويمكن أن تصل إلى 100 مرة في الثانية (كل 10 مللي ثانية). نود تحديث بعض المعلومات على صفحة الويب عندما يتحرك المؤشر.
…لكن تحديث الدالة update()
ثقيل جدًا بحيث لا يمكن القيام بذلك في كل حركة صغيرة. ليس هناك أيضًا أي معنى في التحديث أكثر من مرة واحدة لكل 100 مللي ثانية.
لذلك سنقوم بتغليفها في الديكور: استخدم throttle(update, 100)
كوظيفة يتم تشغيلها عند كل حركة ماوس بدلاً من update()
. سيتم استدعاء مصمم الديكور كثيرًا، ولكن سيتم إعادة توجيه الاستدعاء إلى update()
بحد أقصى مرة واحدة كل 100 مللي ثانية.
بصريا، سوف تبدو مثل هذا:
بالنسبة لحركة الماوس الأولى، يقوم المتغير المزخرف فورًا بتمرير استدعاء update
. ومن المهم أن يرى المستخدم رد فعلنا على تحركه على الفور.
ثم مع تحرك الماوس، حتى 100ms
لا يحدث شيء. البديل المزين يتجاهل المكالمات.
في نهاية 100ms
- يحدث update
آخر للإحداثيات الأخيرة.
ثم، أخيرا، يتوقف الماوس في مكان ما. ينتظر المتغير المزخرف حتى انتهاء صلاحية 100ms
ثم يقوم بتشغيل update
بالإحداثيات الأخيرة. لذلك، من المهم جدًا أن تتم معالجة إحداثيات الماوس النهائية.
مثال على الكود:
وظيفة و (أ) { console.log(a); } // f1000 يمرر المكالمات إلى f بحد أقصى مرة واحدة لكل 1000 مللي ثانية دع f1000 = خنق (f، 1000)؛ f1000(1); // يظهر 1 f1000(2); // (الاختناق، 1000 مللي ثانية لم تنته بعد) f1000(3); // (الاختناق، 1000 مللي ثانية لم تنته بعد) // عند انتهاء مهلة 1000 مللي ثانية ... // ...المخرجات 3، تم تجاهل القيمة المتوسطة 2
يجب تمرير وسيطات PS والسياق this
تم تمريره إلى f1000
إلى f
الأصلي.
افتح صندوق الرمل مع الاختبارات.
وظيفة خنق (فونك، مللي) { دعونا يتم خنق = خطأ، تم الحفظ, saveThis; غلاف الوظيفة () { إذا (تم خنق) {// (2) saveArgs = الوسيطات؛ saveThis = this; يعود؛ } isThrottled = true; func.apply(this,حجج); // (1) setTimeout(وظيفة() { isThrottled = false; // (3) إذا (تم الحفظ) { Wrapper.apply(savedThis, saveArgs); saveArgs = saveThis = null; } }، آنسة)؛ } غلاف العودة؛ }
يؤدي استدعاء throttle(func, ms)
إلى إرجاع wrapper
.
أثناء الاستدعاء الأول، يقوم wrapper
فقط بتشغيل func
وتعيين حالة التهدئة ( isThrottled = true
).
في هذه الحالة، يتم حفظ جميع المكالمات في savedArgs/savedThis
. يرجى ملاحظة أن كلا من السياق والحجج لهما نفس القدر من الأهمية ويجب حفظهما. نحن في حاجة إليها في وقت واحد لإعادة إنتاج المكالمة.
بعد مرور مللي ms
، يتم تشغيل setTimeout
. تتم إزالة حالة التهدئة ( isThrottled = false
) وإذا تجاهلنا الاستدعاءات، فسيتم تنفيذ wrapper
باستخدام آخر الوسائط والسياق المحفوظين.
الخطوة الثالثة لا تعمل func
، بل wrapper
، لأننا لا نحتاج إلى تنفيذ func
فحسب، بل نحتاج مرة أخرى إلى الدخول في حالة التهدئة وإعداد المهلة لإعادة تعيينها.
افتح الحل بالاختبارات في وضع الحماية.