التعرف على واجهة برمجة تطبيقات MutationObserver

نشرت: 2022-03-10
ملخص سريع ↬ يلزم أحيانًا مراقبة التغييرات التي تطرأ على DOM في تطبيقات وأطر عمل الويب المعقدة. من خلال التفسيرات جنبًا إلى جنب مع العروض التوضيحية التفاعلية ، ستوضح لك هذه المقالة كيف يمكنك استخدام واجهة برمجة تطبيقات MutationObserver لتسهيل مراقبة تغييرات DOM نسبيًا.

في تطبيقات الويب المعقدة ، يمكن أن تكون تغييرات DOM متكررة. نتيجة لذلك ، هناك حالات قد يحتاج فيها تطبيقك إلى الاستجابة لتغيير معين في DOM.

لبعض الوقت ، كانت الطريقة المقبولة للبحث عن تغييرات في DOM عن طريق ميزة تسمى أحداث الطفرة ، والتي تم إهمالها الآن. البديل المعتمد من W3C لأحداث الطفرة هو واجهة برمجة تطبيقات MutationObserver ، وهو ما سأناقشه بالتفصيل في هذه المقالة.

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

النحو الأساسي لخادم الطفرات

يمكن استخدام MutationObserver بعدة طرق مختلفة ، والتي سأغطيها بالتفصيل في بقية هذه المقالة ، لكن الصيغة الأساسية MutationObserver تبدو كما يلي:

 let observer = new MutationObserver(callback); function callback (mutations) { // do something here } observer.observe(targetNode, observerOptions);

ينشئ السطر الأول MutationObserver جديد باستخدام مُنشئ MutationObserver() . الوسيطة التي يتم تمريرها إلى المُنشئ هي دالة رد نداء سيتم استدعاؤها على كل تغيير DOM مؤهل.

إن طريقة تحديد ما هو مؤهل لمراقب معين هي عن طريق السطر الأخير في الكود أعلاه. في هذا السطر ، أستخدم طريقة observe() الخاصة بـ MutationObserver لبدء المراقبة. يمكنك مقارنة هذا بشيء مثل addEventListener() . بمجرد إرفاق مستمع ، ستستمع الصفحة إلى الحدث المحدد. وبالمثل ، عندما تبدأ في المراقبة ، ستبدأ الصفحة في "المراقبة" MutationObserver المحدد.

المزيد بعد القفز! أكمل القراءة أدناه ↓

تأخذ طريقة observe() وسيطتين: الهدف ، الذي يجب أن يكون العقدة أو شجرة العقدة التي يجب ملاحظة التغييرات عليها ؛ وكائن خيارات ، وهو كائن MutationObserverInit يسمح لك بتعريف التكوين للمراقب.

الميزة الأساسية النهائية MutationObserver هي طريقة disconnect() . هذا يسمح لك بالتوقف عن مراقبة التغييرات المحددة ، ويبدو الأمر كما يلي:

 observer.disconnect();

خيارات لتكوين MutationObserver

كما ذكرنا ، تتطلب طريقة observe() الخاصة بـ MutationObserver وسيطة ثانية تحدد الخيارات لوصف MutationObserver . إليك كيف سيبدو كائن الخيارات مع تضمين جميع أزواج الخصائص / القيمة الممكنة:

 let options = { childList: true, attributes: true, characterData: false, subtree: false, attributeFilter: ['one', 'two'], attributeOldValue: false, characterDataOldValue: false };

عند إعداد خيارات MutationObserver ، ليس من الضروري تضمين كل هذه الخطوط. أقوم بتضمينها لأغراض مرجعية فقط ، حتى تتمكن من معرفة الخيارات المتاحة وأنواع القيم التي يمكن أن تتخذها. كما ترى ، الكل ما عدا واحد منطقي.

لكي يعمل MutationObserver ، يجب تعيين واحدة على الأقل من childList أو attributes أو البيانات characterData إلى true ، وإلا فسيتم طرح خطأ. تعمل الخصائص الأربع الأخرى جنبًا إلى جنب مع إحدى هذه الخصائص الثلاثة (المزيد حول هذا لاحقًا).

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

ملاحظة التغييرات على العناصر التابعة باستخدام قائمة الأطفال

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

يبدو HTML للقائمة كما يلي:

 <ul class="list"> <li>Apples</li> <li>Oranges</li> <li>Bananas</li> <li class="child">Peaches</li> </ul>

يشتمل JavaScript الخاص بـ MutationObserver على ما يلي:

 let mList = document.getElementById('myList'), options = { childList: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'childList') { console.log('Mutation Detected: A child node has been added or removed.'); } } } observer.observe(mList, options);

هذا ليس سوى جزء من الكود. للإيجاز ، أعرض أهم الأقسام التي تتعامل مع واجهة برمجة تطبيقات MutationObserver نفسها.

لاحظ كيف أقوم بالتكرار عبر وسيطة mutations ، وهي كائن MutationRecord يحتوي على عدد من الخصائص المختلفة. في هذه الحالة ، أقرأ خاصية type وأقوم بتسجيل رسالة تشير إلى أن المتصفح قد اكتشف طفرة مؤهلة. لاحظ أيضًا كيف أقوم بتمرير عنصر mList (إشارة إلى قائمة HTML الخاصة بي) كعنصر مستهدف (أي العنصر الذي أريد ملاحظة التغييرات عليه).

  • شاهد العرض التوضيحي التفاعلي الكامل →

استخدم الأزرار لبدء وإيقاف MutationObserver . تساعد رسائل السجل في توضيح ما يحدث. توفر التعليقات في الكود أيضًا بعض الشرح.

لاحظ بعض النقاط المهمة هنا:

  • سيتم إطلاق وظيفة رد الاتصال (التي mCallback ، لتوضيح أنه يمكنك تسميتها ما تريد) في كل مرة يتم فيها اكتشاف طفرة ناجحة وبعد تنفيذ طريقة observe() .
  • في المثال الخاص بي ، "النوع" الوحيد من الطفرات المؤهلة هو childList ، لذلك من المنطقي البحث عن هذه الطفرة عند المرور عبر سجل الطفرة. البحث عن أي نوع آخر في هذه الحالة لن يفعل شيئًا (سيتم استخدام الأنواع الأخرى في العروض التوضيحية اللاحقة).
  • باستخدام childList ، يمكنني إضافة أو إزالة عقدة نصية من العنصر المستهدف وهذا أيضًا مؤهل. لذلك ليس من الضروري أن يكون عنصرًا مضافًا أو مُزالًا.
  • في هذا المثال ، سيتم تأهيل العقد الفرعية المباشرة فقط. لاحقًا في المقالة ، سأوضح لك كيف يمكن تطبيق ذلك على جميع العقد الفرعية والأحفاد وما إلى ذلك.

مراقبة التغييرات التي تطرأ على سمات العنصر

نوع آخر شائع من الطفرات التي قد ترغب في تتبعها هو عندما تتغير سمة في عنصر محدد. في العرض التوضيحي التفاعلي التالي ، سأراقب التغييرات التي تطرأ على السمات في عنصر فقرة.

 let mPar = document.getElementById('myParagraph'), options = { attributes: true }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } } observer.observe(mPar, options);
  • جرب العرض التوضيحي →

مرة أخرى ، قمت باختصار الرمز للتوضيح ، لكن الأجزاء المهمة هي:

  • يستخدم كائن options خاصية attributes ، مضبوطة على true لإخبار MutationObserver أنني أريد البحث عن تغييرات في سمات العنصر المستهدف.
  • نوع الطفرة التي أختبرها في الحلقة الخاصة بي هي attributes ، وهي الوحيدة المؤهلة في هذه الحالة.
  • أنا أيضًا أستخدم خاصية attributeName الخاصة بكائن mutation ، والتي تتيح لي معرفة السمة التي تم تغييرها.
  • عندما أقوم بتشغيل المراقب ، أقوم بتمرير عنصر الفقرة بالإشارة ، جنبًا إلى جنب مع الخيارات.

في هذا المثال ، يتم استخدام زر لتبديل اسم فئة على عنصر HTML المستهدف. يتم تشغيل وظيفة رد الاتصال في مراقب الطفرات في كل مرة تتم فيها إضافة أو إزالة الفصل.

مراقبة تغييرات بيانات الشخصية

هناك تغيير آخر قد ترغب في البحث عنه في تطبيقك وهو حدوث طفرات في بيانات الشخصية ؛ أي التغييرات على عقدة نصية محددة. يتم ذلك عن طريق تعيين الخاصية characterData إلى true في كائن options . ها هو الكود:

 let options = { characterData: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'characterData') { // Do something here... } } }

لاحظ مرة أخرى أن type الذي يتم البحث عنه في وظيفة رد الاتصال هو characterData .

  • شاهد العرض المباشر →

في هذا المثال ، أبحث عن تغييرات في عقدة نصية معينة ، والتي أستهدفها عبر element.childNodes[0] . هذا قليل من الاختراق لكنه سيفعل في هذا المثال. النص قابل للتحرير بواسطة المستخدم عبر السمة contenteditable على عنصر فقرة.

التحديات عند مراقبة تغييرات بيانات الأحرف

إذا كنت تتلاعب بالمحتوى القابل contenteditable ، فقد تكون على دراية بوجود اختصارات لوحة مفاتيح تسمح بتحرير النص المنسق. على سبيل المثال ، تجعل CTRL-B النص غامقًا ، بينما تجعل CTRL-I النص مائلًا ، وهكذا. سيؤدي هذا إلى تقسيم عقدة النص إلى عقد نصية متعددة ، لذلك ستلاحظ أن MutationObserver سيتوقف عن الاستجابة إلا إذا قمت بتحرير النص الذي لا يزال يعتبر جزءًا من العقدة الأصلية.

يجب أن أشير أيضًا إلى أنه إذا قمت بحذف كل النص ، فلن يقوم MutationObserver بعد الآن بتشغيل رد الاتصال. أفترض أن هذا يحدث لأنه بمجرد اختفاء عقدة النص ، لم يعد العنصر الهدف موجودًا. لمكافحة هذا ، توقف العرض التوضيحي الخاص بي عن المراقبة عند إزالة النص ، على الرغم من أن الأشياء تصبح ثابتة قليلاً عند استخدام اختصارات النص المنسق.

لكن لا تقلق ، لاحقًا في هذه المقالة ، سأناقش طريقة أفضل لاستخدام خيار characterData دون الحاجة إلى التعامل مع أكبر عدد من هذه المراوغات.

مراقبة التغييرات على السمات المحددة

لقد أوضحت لك سابقًا كيفية ملاحظة التغييرات التي تطرأ على السمات في عنصر محدد. في هذه الحالة ، على الرغم من أن العرض التوضيحي يؤدي إلى تغيير اسم الفئة ، إلا أنه كان بإمكاني تغيير أي سمة في العنصر المحدد. ولكن ماذا لو كنت أرغب في ملاحظة التغييرات في سمة محددة واحدة أو أكثر مع تجاهل الآخرين؟

يمكنني القيام بذلك باستخدام الخاصية الاختيارية attributeFilter في كائن option . هذا مثال:

 let options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'] }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }

كما هو موضح أعلاه ، تقبل خاصية attributeFilter مجموعة من السمات المحددة التي أريد مراقبتها. في هذا المثال ، سيقوم MutationObserver بتشغيل رد الاتصال في كل مرة يتم فيها تعديل سمة واحدة أو أكثر من السمات hidden أو القابلة contenteditable أو المعادلة data-par .

  • شاهد العرض المباشر →

مرة أخرى أنا أستهدف عنصر فقرة معين. لاحظ القائمة المنسدلة select التي تختار السمة التي سيتم تغييرها. السمة draggable هي السمة الوحيدة التي لن تتأهل لأنني لم أحددها في خياراتي.

لاحظ في الكود أنني أستخدم خاصية اسم attributeName مرة أخرى لكائن MutationRecord لتسجيل السمة التي تم تغييرها. وبالطبع ، كما هو الحال مع العروض التوضيحية الأخرى ، لن يبدأ MutationObserver في مراقبة التغييرات حتى يتم النقر فوق الزر "ابدأ".

هناك شيء آخر يجب أن أشير إليه هنا وهو أنني لست بحاجة إلى تعيين قيمة attributes على true في هذه الحالة ؛ إنه ضمني بسبب تعيين attributesFilter عامل التصفية على صواب. لهذا السبب يمكن أن يبدو كائن الخيارات الخاص بي كما يلي ، وسيعمل بنفس الطريقة:

 let options = { attributeFilter: ['hidden', 'contenteditable', 'data-par'] }

من ناحية أخرى ، إذا قمت بشكل صريح بتعيين attributes على false مع مصفوفة attributeFilter ، فلن تعمل لأن القيمة false ستكون لها الأسبقية وسيتم تجاهل خيار التصفية.

مراقبة التغيرات التي تطرأ على العقد والشجرة الفرعية الخاصة بها

حتى الآن عند إعداد كل MutationObserver ، كنت أتعامل فقط مع العنصر المستهدف نفسه ، وفي حالة childList ، الأطفال المباشرين للعنصر. ولكن من المؤكد أنه قد تكون هناك حالة قد أرغب في ملاحظة التغييرات التي تطرأ على أحد الأمور التالية:

  • عنصر وكل عناصره الفرعية ؛
  • سمة واحدة أو أكثر على عنصر وعلى عناصره الفرعية ؛
  • جميع العقد النصية داخل عنصر.

يمكن تحقيق كل ما سبق باستخدام خاصية subtree لكائن الخيارات.

قائمة الأطفال مع شجرة فرعية

أولاً ، دعنا نبحث عن التغييرات التي تم إجراؤها على العقد الفرعية لعنصر ما ، حتى لو لم تكن أطفالًا مباشرين. يمكنني تغيير كائن الخيارات ليبدو كالتالي:

 options = { childList: true, subtree: true }

كل شيء آخر في الكود مشابه إلى حد ما childList السابقة ، إلى جانب بعض العلامات والأزرار الإضافية.

  • شاهد العرض المباشر →

توجد هنا قائمتان ، إحداهما متداخلة في الأخرى. عند بدء تشغيل MutationObserver ، ستطلق عملية رد الاتصال تغييرات على أي من القائمتين. ولكن إذا قمت بتغيير خاصية subtree مرة أخرى إلى false (الافتراضي عندما لا تكون موجودة) ، فلن يتم تنفيذ رد الاتصال عند تعديل القائمة المتداخلة.

السمات مع الشجرة الفرعية

إليك مثال آخر ، هذه المرة باستخدام subtree مع attributes و attributeFilter . هذا يسمح لي بمراقبة التغييرات التي تطرأ على السمات ليس فقط على العنصر الهدف ولكن أيضًا على سمات أي عناصر تابعة للعنصر الهدف:

 options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'], subtree: true }
  • شاهد العرض المباشر →

هذا مشابه للعرض التوضيحي للسمات السابقة ، لكن هذه المرة قمت بإعداد عنصري تحديد مختلفين. الأول يعدل السمات على عنصر الفقرة المستهدف بينما يعدل الآخر السمات على عنصر فرعي داخل الفقرة.

مرة أخرى ، إذا كنت تريد إعادة تعيين خيار subtree إلى false (أو إزالته) ، فلن يؤدي زر التبديل الثاني إلى تشغيل رد MutationObserver . وبالطبع ، يمكنني حذف عامل تصفية attributeFilter تمامًا ، MutationObserver عن التغييرات في أي سمات في الشجرة الفرعية بدلاً من تلك المحددة.

حرف البيانات مع الشجرة الفرعية

تذكر في العرض التوضيحي السابق characterData ، أنه كانت هناك بعض المشكلات مع اختفاء العقدة المستهدفة ثم MutationObserver عن العمل. في حين أن هناك طرقًا للالتفاف حول ذلك ، فمن الأسهل استهداف عنصر بشكل مباشر بدلاً من عقدة نصية ، ثم استخدام خاصية subtree لتحديد أنني أريد جميع بيانات الأحرف داخل هذا العنصر ، بغض النظر عن مدى تداخله العميق ، لتشغيله رد الاتصال MutationObserver .

ستبدو خياراتي في هذه الحالة كما يلي:

 options = { characterData: true, subtree: true }
  • شاهد العرض المباشر →

بعد أن تبدأ المراقب ، حاول استخدام CTRL-B و CTRL-I لتنسيق النص القابل للتحرير. ستلاحظ أن هذا يعمل بشكل أكثر فاعلية من المثال السابق characterData . في هذه الحالة ، لا تؤثر العقد الفرعية المفككة على المراقب لأننا نراقب جميع العقد داخل العقدة المستهدفة ، بدلاً من عقدة نصية واحدة.

تسجيل القيم القديمة

غالبًا عند ملاحظة التغييرات في DOM ، سترغب في تدوين القيم القديمة وربما تخزينها أو استخدامها في مكان آخر. يمكن القيام بذلك باستخدام بعض الخصائص المختلفة في كائن options .

السمة OldValue

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

 options = { attributes: true, attributeOldValue: true } function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }
  • شاهد العرض المباشر →

لاحظ استخدام attributeName وخصائص oldValue لكائن MutationRecord . جرب العرض التوضيحي عن طريق إدخال قيم مختلفة في حقل النص. لاحظ كيف يتم تحديث السجل ليعكس القيمة السابقة التي تم تخزينها.

حرف DataOldValue

وبالمثل ، إليك كيف ستبدو خياراتي إذا كنت أرغب في تسجيل بيانات الأحرف القديمة:

 options = { characterData: true, subtree: true, characterDataOldValue: true }
  • شاهد العرض المباشر →

لاحظ أن رسائل السجل تشير إلى القيمة السابقة. تصبح الأشياء متزعزعة قليلاً عند إضافة HTML عبر أوامر النص المنسق إلى المزيج. لست متأكدًا من السلوك الصحيح المفترض أن يكون في هذه الحالة ، لكن الأمر أكثر وضوحًا إذا كان الشيء الوحيد داخل العنصر هو عقدة نصية واحدة.

اعتراض الطفرات باستخدام takeRecords ()

هناك طريقة أخرى لكائن MutationObserver لم أذكرها بعد وهي takeRecords() . تتيح لك هذه الطريقة اعتراض أكثر أو أقل للطفرات التي يتم اكتشافها قبل معالجتها بواسطة وظيفة رد الاتصال.

يمكنني استخدام هذه الميزة باستخدام خط مثل هذا:

 let myRecords = observer.takeRecords();

هذا يخزن قائمة تغييرات DOM في المتغير المحدد. في العرض التوضيحي الخاص بي ، أقوم بتنفيذ هذا الأمر بمجرد النقر فوق الزر الذي يعدل DOM. لاحظ أن أزرار البدء والإضافة / الإزالة لا تسجل أي شيء. هذا لأنني ، كما ذكرنا ، أعترض تغييرات DOM قبل معالجتها بواسطة رد الاتصال.

لاحظ ، مع ذلك ، ما أفعله في مستمع الحدث الذي يوقف المراقب:

 btnStop.addEventListener('click', function () { observer.disconnect(); if (myRecords) { console.log(`${myRecords[0].target} was changed using the ${myRecords[0].type} option.`); } }, false);

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

عندما يتم اعتراض سجل طفرة بهذه الطريقة عن طريق استدعاء takeRecords() ، يتم إفراغ قائمة انتظار الطفرات التي عادةً ما يتم إرسالها إلى وظيفة رد الاتصال. لذلك إذا احتجت لسبب ما إلى اعتراض هذه السجلات قبل معالجتها ، takeRecords() مفيدًا.

مراقبة التغييرات المتعددة باستخدام مراقب واحد

لاحظ أنه إذا كنت أبحث عن طفرات في عقدتين مختلفتين على الصفحة ، فيمكنني القيام بذلك باستخدام نفس المراقب. هذا يعني أنه بعد استدعاء المُنشئ ، يمكنني تنفيذ طريقة observe() للعديد من العناصر كما أريد.

وهكذا بعد هذا السطر:

 observer = new MutationObserver(mCallback);

يمكنني بعد ذلك الحصول على استدعاءات observe() مع عناصر مختلفة كالوسيطة الأولى:

 observer.observe(mList, options); observer.observe(mList2, options);
  • شاهد العرض المباشر →

ابدأ المراقب ، ثم جرب أزرار الإضافة / الإزالة لكلتا القائمتين. المصيد الوحيد هنا هو أنك إذا ضغطت على أحد أزرار "التوقف" ، فسيتوقف المراقب عن المراقبة لكلتا القائمتين ، وليس فقط القائمة التي يستهدفها.

تحريك شجرة عقدة تتم ملاحظتها

آخر شيء سأشير إليه هو أن MutationObserver سيستمر في مراقبة التغييرات التي تطرأ على عقدة محددة حتى بعد إزالة تلك العقدة من عنصرها الأصلي.

على سبيل المثال ، جرب العرض التوضيحي التالي:

  • شاهد العرض المباشر →

هذا مثال آخر يستخدم childList لمراقبة التغييرات التي تطرأ على العناصر الفرعية للعنصر المستهدف. لاحظ الزر الذي يفصل القائمة الفرعية ، وهو الزر الذي يتم ملاحظته. انقر فوق الزر "ابدأ ..." ، ثم انقر فوق الزر "نقل…" لتحريك القائمة المتداخلة. حتى بعد إزالة القائمة من أصلها ، يستمر MutationObserver في مراقبة التغييرات المحددة. ليس مفاجأة كبيرة أن يحدث هذا ، لكنه شيء يجب مراعاته.

خاتمة

يغطي ذلك جميع الميزات الأساسية لواجهة برمجة تطبيقات MutationObserver . آمل أن يكون هذا الغوص العميق مفيدًا لك للتعرف على هذا المعيار. كما ذكرنا ، دعم المتصفح قوي ويمكنك قراءة المزيد حول واجهة برمجة التطبيقات هذه على صفحات MDN.

لقد وضعت جميع العروض التوضيحية لهذه المقالة في مجموعة CodePen ، إذا كنت ترغب في الحصول على مكان سهل للعبث مع العروض التوضيحية.