الدخول الأمامي (vue) إلى دورة الكفاءة: الدخول لتعلم JavaScript لا يوفر أي عمليات لإدارة الذاكرة. بدلاً من ذلك، تتم إدارة الذاكرة بواسطة JavaScript VM من خلال عملية استصلاح الذاكرة تسمى تجميع البيانات المهملة .
بما أننا لا نستطيع فرض عملية جمع القمامة، فكيف نعرف أنها تعمل؟ كم نعرف عنه؟
يتم إيقاف تنفيذ البرنامج النصي مؤقتًا أثناء هذه العملية
يقوم بتحرير الذاكرة للموارد التي يتعذر الوصول إليها
إنه غير مؤكد
ولا يقوم بفحص الذاكرة بأكملها مرة واحدة، بل يعمل في دورات متعددة
إنه أمر لا يمكن التنبؤ به ولكنه سيتم تنفيذه عند الضرورة
هل هذا يعني أنه لا داعي للقلق بشأن مشكلات تخصيص الموارد والذاكرة بالطبع لا؟ إذا لم نكن حذرين، فقد تحدث بعض حالات تسرب الذاكرة.
تسرب الذاكرة هو كتلة من الذاكرة المخصصة التي لا يمكن للبرنامج استعادتها.
توفر جافا سكريبت أداة تجميع البيانات المهملة، لكن هذا لا يعني أنه يمكننا تجنب تسرب الذاكرة. لكي تكون مؤهلاً لجمع البيانات المهملة، يجب ألا تتم الإشارة إلى الكائن في مكان آخر. إذا كنت تحتفظ بمراجع لموارد غير مستخدمة، فسيؤدي ذلك إلى منع استعادة هذه الموارد. وهذا ما يسمى الاحتفاظ بالذاكرة اللاواعية .
قد يؤدي تسرب الذاكرة إلى تشغيل أداة تجميع مجمعي البيانات المهملة بشكل متكرر أكثر. نظرًا لأن هذه العملية ستمنع تشغيل البرنامج النصي، فقد تتسبب في تجميد برنامجنا. في حالة حدوث مثل هذا التأخير، سيلاحظ المستخدمون الذين يصعب إرضاؤهم بالتأكيد أنه إذا لم يكونوا راضين عن ذلك، فسيكون المنتج غير متصل بالإنترنت لفترة طويلة. والأخطر من ذلك، أنه قد يتسبب في تعطل التطبيق بأكمله، وهو gg.
كيفية منع تسرب الذاكرة الشيء الرئيسي هو أنه يجب علينا تجنب الاحتفاظ بالموارد غير الضرورية؟ دعونا نلقي نظرة على بعض السيناريوهات الشائعة.
تستدعي طريقة setInterval()
دالة بشكل متكرر أو تنفذ جزءًا من التعليمات البرمجية، مع تأخير زمني ثابت بين كل مكالمة. تقوم بإرجاع ID
الفاصل الزمني ID
يحدد الفاصل الزمني بشكل فريد حتى تتمكن من حذفه لاحقًا عن طريق استدعاء clearInterval()
.
نقوم بإنشاء مكون يستدعي وظيفة رد الاتصال للإشارة إلى أنه انتهى بعد عدد x
من الحلقات. أنا أستخدم React في هذا المثال، لكن هذا يعمل مع أي إطار عمل FE.
استيراد React، { useRef } من 'react'؛ مؤقت ثابت = ({ cicles, onFinish }) => { const currentCicles = useRef(0); setInterval(() => { إذا (currentCicles.current >= cicles) { onFinish(); يعود؛ } currentCicles.current++; }، 500)؛ يعود ( <p>جارٍ التحميل...</p> ); } تصدير الموقت الافتراضي.
للوهلة الأولى، يبدو أنه لا توجد مشكلة. لا تقلق، فلنقم بإنشاء مكون آخر يقوم بتشغيل هذا المؤقت ونحلل أداء الذاكرة الخاصة به.
استيراد React، { useState } من 'react'؛ استيراد الأنماط من "../styles/Home.module.css" استيراد الموقت من "../components/Timer"؛ تصدير الوظيفة الافتراضية الصفحة الرئيسية () { const [showTimer, setShowTimer] = useState(); const onFinish = () => setShowTimer(false); يعود ( <p className={styles.container}> {إظهار الموقت (؟ <Timer cicles={10} onFinish={onFinish} /> ):( <button onClick={() => setShowTimer(true)}> أعد المحاولة </زر> )} </ص> ) }
بعد بضع نقرات على زر Retry
، إليك نتيجة استخدام أدوات تطوير Chrome للحصول على استخدام الذاكرة:
عندما نضغط على زر إعادة المحاولة، يمكننا أن نرى أنه تم تخصيص المزيد والمزيد من الذاكرة. وهذا يعني أنه لم يتم تحرير الذاكرة المخصصة مسبقًا. لا يزال المؤقت يعمل بدلاً من استبداله.
كيفية حل هذه المشكلة؟ القيمة المرجعة لـ setInterval
هي معرف الفاصل الزمني، والذي يمكننا استخدامه لإلغاء هذا الفاصل الزمني. في هذه الحالة بالذات، يمكننا استدعاء clearInterval
بعد إلغاء تحميل المكون.
استخدام التأثير (() => { معرف الفاصل الزمني = setInterval(() => { إذا (currentCicles.current >= cicles) { onFinish(); يعود؛ } currentCicles.current++; }، 500)؛ return () => ClearInterval(intervalId); }، [])
في بعض الأحيان، يكون من الصعب العثور على هذه المشكلة عند كتابة التعليمات البرمجية. أفضل طريقة هي تجريد المكونات.
باستخدام React هنا، يمكننا تغليف كل هذا المنطق في خطاف مخصص.
استيراد { useEffect } من 'react'؛ تصدير const useTimeout = (refreshCycle = 100، رد الاتصال) => { استخدام التأثير (() => { إذا (refreshCycle <= 0) { setTimeout(callback, 0); يعود؛ } معرف الفاصل الزمني = setInterval(() => { أتصل مرة أخرى()؛ },freshCycle); return () => ClearInterval(intervalId); }, [refreshCycle, setInterval, ClearInterval]); }; تصدير useTimeout الافتراضي؛
الآن عندما تحتاج إلى استخدام setInterval
، يمكنك القيام بذلك:
const HandleTimeout = () => ...; useTimeout(100, HandleTimeout);
يمكنك الآن استخدام useTimeout Hook
هذا دون القلق بشأن تسرب الذاكرة، وهو أيضًا من فوائد التجريد.
توفر Web API عددًا كبيرًا من مستمعي الأحداث. في وقت سابق، ناقشنا setTimeout
. الآن دعونا نلقي نظرة على addEventListener
.
في هذا المثال، قمنا بإنشاء وظيفة اختصار لوحة المفاتيح. وبما أن لدينا وظائف مختلفة في صفحات مختلفة، فسيتم إنشاء وظائف مفاتيح الاختصار المختلفة
وظيفة homeShortcuts({مفتاح}) { إذا (مفتاح === 'E') { console.log("تحرير القطعة") } } // عندما يقوم المستخدم بتسجيل الدخول على الصفحة الرئيسية، نقوم بتنفيذ document.addEventListener('keyup', homeShortcuts); // يقوم المستخدم بشيء ما ثم ينتقل إلى وظيفة الإعدادات settingsShortcuts({ key}) { إذا (مفتاح === 'E') { console.log("تحرير الإعداد") } } // عندما يقوم المستخدم بتسجيل الدخول على الصفحة الرئيسية، نقوم بتنفيذ document.addEventListener('keyup', settingsShortcuts);
لا يزال يبدو جيدًا، باستثناء أنه لم يتم تنظيف keyup
السابقة عند تنفيذ addEventListener
الثاني. بدلاً من استبدال مستمع keyup
الخاص بنا، سيضيف هذا الرمز callback
آخر. وهذا يعني أنه عند الضغط على مفتاح، فإنه يؤدي إلى وظيفتين.
لمسح رد الاتصال السابق، نحتاج إلى استخدام removeEventListener
:
document.removeEventListener('keyup', homeShortcuts);
إعادة صياغة الكود أعلاه:
وظيفة homeShortcuts({مفتاح}) { إذا (مفتاح === 'E') { console.log("تحرير القطعة") } } // يصل المستخدم إلى المنزل ونقوم بالتنفيذ document.addEventListener('keyup', homeShortcuts); // يقوم المستخدم ببعض الأشياء وينتقل إلى الإعدادات إعدادات الوظيفةالاختصارات({مفتاح}) { إذا (مفتاح === 'E') { console.log("تحرير الإعداد") } } // يصل المستخدم إلى المنزل ونقوم بالتنفيذ document.removeEventListener('keyup', homeShortcuts); document.addEventListener('keyup', settingsShortcuts);
كقاعدة عامة، كن حذرًا جدًا عند استخدام أدوات من الكائنات العامة.
المراقبون عبارة عن ميزة Web API للمتصفح والتي لا يعرفها العديد من المطورين. يعد هذا أمرًا فعالاً إذا كنت تريد التحقق من التغييرات في الرؤية أو حجم عناصر HTML.
توفر واجهة IntersectionObserver
(جزء من Intersection Observer API) طريقة لمراقبة حالة التقاطع لعنصر مستهدف مع عناصره الأصلية أو viewport
المستند ذات المستوى الأعلى بشكل غير متزامن. يُطلق على عنصر السلف ومنفذ viewport
اسم root
.
على الرغم من قوتها، يجب علينا استخدامها بحذر. بمجرد الانتهاء من مراقبة كائن ما، تذكر إلغاءه عندما لا يكون قيد الاستخدام.
ألق نظرة على الكود:
مرجع ثابت = ... ثابت مرئي = (مرئي) => { console.log(`إنه ${مرئي}`); } استخدام التأثير (() => { إذا (!المرجع) { يعود؛ } المراقب الحالي = مراقب التقاطع الجديد ( (الإدخالات) => { إذا (! الإدخالات [0].isIntersecting) { مرئي (صحيح)؛ } آخر { مرئي (خطأ) ؛ } }, { rootMargin: `-${header.height}px` }, ); Observer.current.observe(ref); }, [المرجع]);
الرمز أعلاه يبدو جيدًا. ومع ذلك، ماذا يحدث للمراقب بمجرد تفريغ المكون وعدم مسحه وتسريب الذاكرة؟ كيف نحل هذه المشكلة فقط استخدم طريقة disconnect
:
مرجع ثابت = ... ثابت مرئي = (مرئي) => { console.log(`إنه ${مرئي}`); } استخدام التأثير (() => { إذا (!المرجع) { يعود؛ } المراقب الحالي = مراقب التقاطع الجديد ( (الإدخالات) => { إذا (! الإدخالات [0].isIntersecting) { مرئي (صحيح)؛ } آخر { مرئي (خطأ) ؛ } }, { rootMargin: `-${header.height}px` }, ); Observer.current.observe(ref); return () => Observer.current?.disconnect(); }, [المرجع]);
يعد إضافة كائنات إلى النافذة خطأً شائعًا. في بعض السيناريوهات، قد يكون من الصعب العثور عليه، خاصة عند استخدام الكلمة الأساسية this
في سياق تنفيذ النافذة. ألق نظرة على المثال التالي:
وظيفة إضافة العنصر (العنصر) { إذا (! this.stack) { هذا المكدس = { عناصر: [] } } this.stack.elements.push(element); }
يبدو الأمر غير ضار، لكنه يعتمد على السياق الذي تستدعي منه addElement
. إذا قمت باستدعاء addElement من سياق النافذة، فسوف تنمو الكومة.
قد تكون هناك مشكلة أخرى تتمثل في تحديد متغير عام بشكل غير صحيح:
var a = 'example 1'; // يقتصر النطاق على المكان الذي تم إنشاء var فيه b = 'example 2'; // تمت إضافته إلى كائن النافذة
لمنع هذه المشكلة يمكنك استخدام الوضع الصارم:
"استخدام صارم"
باستخدام الوضع الصارم، فإنك تشير إلى مترجم JavaScript أنك تريد حماية نفسك من هذه السلوكيات. لا يزال بإمكانك استخدام Window عندما تحتاج إلى ذلك. ومع ذلك، يجب عليك استخدامه بطريقة واضحة.
كيف يؤثر الوضع الصارم على مثالنا السابق:
بالنسبة للدالة addElement
، يكون this
غير محدد عند استدعائه من النطاق العام
إذا لم تقم بتحديد const | let | var
على متغير، فسوف تحصل على الخطأ التالي:
خطأ مرجعي لم يتم اكتشافه: لم يتم تعريف b
عقد DOM ليست محصنة ضد تسرب الذاكرة أيضًا. يجب أن نكون حريصين على عدم حفظ الإشارات إليها. وإلا فلن يتمكن جامع البيانات المهملة من تنظيفها لأنه سيظل من الممكن الوصول إليها.
قم بإظهار ذلك بقطعة صغيرة من التعليمات البرمجية:
عناصر ثابتة = []؛ قائمة ثابتة = document.getElementById('list'); وظيفة إضافة العنصر () { // العقد النظيفة list.innerHTML = ''; const pElement= document.createElement('p'); عنصر ثابت = document.createTextNode(`إضافة عنصر ${elements.length}`); pElement.appendChild(element); list.appendChild(pElement); Elements.push(pElement); } document.getElementById('addElement').onclick = addElement;
لاحظ أن وظيفة addElement
تقوم بمسح القائمة p
وإضافة عنصر جديد إليها كعنصر فرعي. تتم إضافة هذا العنصر الذي تم إنشاؤه حديثًا إلى مصفوفة elements
.
في المرة التالية التي يتم فيها تنفيذ addElement
، ستتم إزالة العنصر من القائمة p
، لكنه غير مناسب لجمع البيانات المهملة لأنه مخزن في مصفوفة elements
.
نقوم بمراقبة الوظيفة بعد تنفيذها عدة مرات:
انظر كيف تم اختراق العقدة في لقطة الشاشة أعلاه. فكيف حل هذه المشكلة؟ سيؤدي مسح مصفوفة elements
إلى جعلها مؤهلة لجمع البيانات المهملة.
في هذه المقالة، نظرنا إلى الطرق الأكثر شيوعًا لتسرب الذاكرة. من الواضح أن جافا سكريبت نفسها لا تسرّب الذاكرة. بدلاً من ذلك، يحدث ذلك بسبب الاحتفاظ غير المقصود بالذاكرة من جانب المطور. وطالما أن الكود نظيف ولا ننسى أن نقوم بالتنظيف بعد أنفسنا، فلن يحدث أي تسرب.
يعد فهم كيفية عمل الذاكرة وجمع البيانات المهملة في JavaScript أمرًا ضروريًا. يشعر بعض المطورين بإحساس خاطئ بأنه نظرًا لأنه تلقائي، فلا داعي للقلق بشأن هذه المشكلة.