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

مراقب التحول MutationObserver عبر جافاسكربت لمراقبة شجرة DOM


محمد أمين بوقرة

الكائن MutationObserver هو كائن مُضمّن built-in object يعمل على مراقبة عنصر من DOM ويطلق ردّ نداء عندما يلاحظ تغيّرا ما. سنلقي في البداية نظرة على صيغة استعماله، ونستكشف بعدها حالة استخدام واقعيّة، لرؤية متى قد يكون مفيدا.

الصيغة

يعد MutationObserver سهل الاستخدام. أوّلا، ننشئ مراقبا مع دالّة ردّ نداء:

let observer = new MutationObserver(callback);

ثمّ نربطه بعقدة في DOM:

observer.observe(node, config);

config هو كائنٌ بخيارات بوليانية تمثّل "نوع التغيرات التي يُستجاب لها":

  • childList -- التغيرات في الأبناء المباشرين للعقدة node،
  • subtree -- في جميع العناصر السليلة للعقدة node،
  • attributes -- سمات العقدة node،
  • attributeFilter -- مصفوفة بأسماء السمات، لمراقبة المحدّدة منها فقط،
  • characterData -- ما إذا تُراقَب node.data (المحتوى النصّي)،

بعض الخيارات الأخرى:

  • attributeOldValue -- إذا كانت true، تُمرّر كلا القيمتان القديمة والجديدة للسمة إلى دالة ردّ النداء (انظر أسفله) وإلّا فالجديدة فقط (تحتاج الخيار attributes
  • characterDataOldValue -- إذا كانت true، تُمرّر كلا القيمتان القديمة والجديدة لـ node.data إلى دالّة ردّ النداء (انظر أسفله) وإلّا فالجديدة فقط (تحتاج الخيار characterData).

وبذلك بعد أيّ تغيّر، تُنفّذ callback: تُمرَّر التغيّرات كوسيط أوّل على شكل قائمة من كائنات سجلّات التحوّل MutationRecord، والمراقب نفسه كوسيط ثاني.

لكائنات MutationRecord الخاصيّات التالية:

  • type -- نوع التحوّل، واحد من:
    • "attributes": تغيّرت السمة
    • "characterData": تغيّرت البيانات، تُستخدم مع العقد النصّيّة
    • "childList": أضيفت/أزيلت عناصر أبناء
  • target -- أين وقع التغيّر: يكون عنصرا بالنسبة لـ "attributes"، أوعقدة نصيّة بالنسبة لـ "characterData"، أو عنصرا بالنسبة لتحوّل "childList"
  • addedNodes/removedNodes -- العقد التي أضيفت/أزيلت
  • previousSibling/nextSibling -- الأخ السابق واللاحق للعقد التي أضيفت/أزيلت
  • attributeName/attributeNamespace -- اسم/مساحة اسم namespace بالنسبة لـ XML للسمة التي تغيّرت
  • oldValue -- القيمة السابقة، فقط للتغيّرات في السمات والعقد النصّيّة، إذا كان الخيار الموافق مضبوطا attributeOldValue characterDataOldValue

على سبيل المثال، هذا الـ <div> له السمة contentEditable. تمكّننا هذه السمة من التركيز عليه و تعديله.

<div contentEditable id="elem">Click and <b>edit</b>, please</div>

<script>
let observer = new MutationObserver(mutationRecords => {
  console.log(mutationRecords); // console.log(التغييرات)
});

// راقب كلّ شيء ما عدا السمات
observer.observe(elem, {
  childList: true, // راقب اﻷبناء المباشرين
  subtree: true, // وكذلك العناصر السليلة في اﻷسفل
  characterDataOldValue: true // مرّر البيانات القديمة إلى ردّ النداء
});
</script>

إذا أجرينا هذا المثال في متصفّح، ثمّ ركّزنا على ذلك الـ <div> وغيّرنا النصّ الذي بداخل <b>edit</b>، ستُظهر console.log تحوّلا واحدًا:

mutationRecords = [{
  type: "characterData",
  oldValue: "edit",
  target: <text node>,
  // الخاصّيّات اﻷخرى فارغة
}];

إذا أجرينا تغييرات أكثر تعقيدا، مثل إزالة <b>edit</b>، فإنّ حدث التحوّل قد يحتوي على عدّة سجلّات تحوّل:

mutationRecords = [{
  type: "childList",
  target: <div#elem>,
  removedNodes: [<b>],
  nextSibling: <text node>,
  previousSibling: <text node>
  // الخاصّيّات اﻷخرى فارغة
}, {
  type: "characterData"
  target: <text node>
  // تعتمد تفاصيل التحوّل على كيفيّة تعامل المتصفّح مع مثل هذا الحذف...
  // في عقدة واحدة ", please" و "edit " قد يجمع بين العقدتين المتجاورتين
  // أو قد يبقيهما عقدتين نصّيّتين منفصلتين
}];

وبذلك، تمكّن MutationObserver من الاستجابة لأيّة تغيّرات ضمن الشجرة الفرعيّة.

الاستخدام في الإدماج

فيما قد يفيدنا شيء كهذا؟

تصوّر حالة تحتاج فيها إلى إضافة سكربت طرف ثالث يحتوي على وظيفة مفيدة، لكنّه أيضا يقوم بشيء غير مرغوب كإظهار الإعلانات <div class="ads">Unwanted ads</div> مثلا، وبالطبع، لن توفّر سكربتات الطرف الثالث تلك آليّات لإزالتها.

باستخدام MutationObserver، يمكننا اكتشاف ظهور العنصر غير المرغوب في DOM وإزالته.

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

الاستخدام في الهندسة

هناك أيضا حالات يكون فيها MutationObserver جيّدا من المنظور الهندسيّ.

لنفترض أنّنا بصدد إنشاء موقع عن البرمجة. بالطبع، قد تحتوي المقالات و الموادّ الأخرى على قصاصات من الشيفرات المصدريّة.

تبدو هذه القصاصات من HTML هكذا:

...
<pre class="language-javascript"><code>
  // هذه هي الشيفرة
  let hello = "world";
</code></pre>
...

لمقروئيّة أفضل، وفي الوقت نفسه لتجميلها، سنستخدم في موقعنا (موقع javascript.info) مكتبة لتلوين أو إبراز صيغة جافاسكربت، مثل Prism.js. لإبراز الصيغة في القصاصات أعلاه بواسطة Prism، يُستدعى Prism.highlightElem(pre)‎، الذي يفحص محتويات عناصر pre تلك ويضيف لها وسوما خاصّة وتنسيقات بغرض الإبراز الملوّن للصيغة، مثلما ترى في اﻷمثلة التي في هذه الصفحة.

متى ينبغي أن نجري عمليّة الإبراز تلك؟ حسنا، يمكننا القيام بها عند الحدث DOMContentLoaded، أو بوضع السكربت في أسفل الصفحة. حالما يكون DOM الذي لدينا جاهزا، يمكننا البحث عن العناصر pre[class*="language"]‎ واستدعاء Prism.highlightElem عليها:

// أبرز جميع قصاصات الشيفرات على الصفحة
document.querySelectorAll('pre[class*="language"]').forEach(Prism.highlightElem);

إلى الآن كلّ شيء بسيط، صحيح؟ نعثر على قصاصات الشيفرات في HTML ونبرزها.

لنواصل الآن. لنفترض أنّنا سنجلب موادّ من الخادم ديناميكيّا. سندرس توابع لهذا الغرض لاحقا في هذا الدليل. ما يهمّ حاليّا هو أن نجلب مقال HTML من خادم الويب وعرضه حسب الطلب:

let article = /* جلب المحتوى الجديد من الخادم */
articleElem.innerHTML = article;

قد يحتوي مقال HTML الجديد article على قصاصات شيفرات. نحتاج إلى استدعاء Prism.highlightElem عليها، وإلّا فلن يتمّ إبرازها.

أين ومتى يُستدعى Prism.highlightElem للمقالات المحمّلة ديناميكيّا؟

يمكننا إلحاق ذلك الاستدعاء بالشيفرة التي تحمّل المقال، هكذا:

let article = /* جلب المحتوى الجديد من الخادم */
articleElem.innerHTML = article;

let snippets = articleElem.querySelectorAll('pre[class*="language-"]');
snippets.forEach(Prism.highlightElem);

لكن، تصوّر لو كان لدينا عدّة مواضع في الشيفرة نحمّل فيها المحتوى الخاصّ بنا - مقالات، اختبارات، مشاركات منتدى، إلى غير ذلك. هل علينا وضع استدعاء الإبراز في كلّ مكان، لإبراز الشيفرة بعد التحميل؟ هذا غير ملائم للغاية.

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

لحسن الحظ، هناك خيار آخر. يمكننا استخدام MutationObserver للقيام تلقائيّا باكشاف متى تُدرج قصاصات الشيفرات في الصفحة وإبرازها. وبالتالي سنعالج وظيفة الإبراز في مكان واحد، لنرتاح بذلك من الحاجة للإدماج.

عرض الإبراز الديناميكي

إليك المثال التالي. إذا أجريت هذه الشيفرة، ستبدأ بمراقبة العناصر في اﻷسفل وتبرز أيّ قصاصات شيفرات تظهر هناك:

let observer = new MutationObserver(mutations => {

  for(let mutation of mutations) {
    // تفحّص العقد الجديدة، هل هناك أيّ شيء لإبرازه؟

    for(let node of mutation.addedNodes) {
      // نتتبّع العناصر فقط، تجاوز العقد اﻷخرى (العقد النصّيّة مثلا)‏
      if (!(node instanceof HTMLElement)) continue;

      // تحقّق ما إذا كان العنصر المدرج قصاصة شيفرة
      if (node.matches('pre[class*="language-"]')) {
        Prism.highlightElement(node);
      }

      // أو ربّما هناك قصاصة شيفرة في مكان ما في الشجرة الفرعيّة
      for(let elem of node.querySelectorAll('pre[class*="language-"]')) {
        Prism.highlightElement(elem);
      }
    }
  }

});

let demoElem = document.getElementById('highlight-demo');

observer.observe(demoElem, {childList: true, subtree: true});

في اﻷسفل عنصر HTML وجافاسكربت لملئه ديناميكيّا باستخدام innerHTML.

example1.png

بإجراء الشيفرة السابقة (التي في اﻷعلى) يُراقب العنصر، ثمّ عند الشيفرة التي في اﻷسفل. تكتشف MutationObserver القصاصة وتبرزها، هكذا:

example2.png

تملأ الشيفرة التالية innerHTML الخاصّ بالعنصر، ما يؤدّي بـ MutationObserver إلى الاستجابة وإبراز محتوياته:

let demoElem = document.getElementById('highlight-demo');

// إدراج المحتوى الذي فيه قصاصات الشيفرات ديناميكيّا
demoElem.innerHTML = `A code snippet is below:
  <pre class="language-javascript"><code> let hello = "world!"; </code></pre>
  <div>Another one:</div>
  <div>
    <pre class="language-css"><code>.class { margin: 5px; } </code></pre>
  </div>
`;

لدينا الآن MutationObserver يمكنه تتبّع جميع عمليّات الإبراز في العناصر المراقبة أو في كامل document. يمكننا إضافة/إزالة قصاصات الشيفرات في HTML دون التفكير في ذلك.

توابع إضافية

هناك تابع لإيقاف مراقبة العقدة:

  • observer.disconnect()‎ -- يوقف المراقبة.

عندما نوقف المراقبة، قد يكون من الممكن أنّ بعض التغيّرات لم تُعالج بعد من طرف المراقب. في تلك الحالات، نستخدم

  • observer.takeRecords()‎ -- يحصل على قائمة سجّلات التحوّلات غير المعالجة - تلك التي حصلت، لكنّ ردود النداء لم تعالجها.

ويمكن أن تُستخدم هذه التوابع معا، هكذا:

// احصل على قائمة التحوّلات التي لم تُعالج
// يجب أن تُستدعى قبل قطع الاتصال
// إذا كنت تهتمّ للتحوّلات الحديثة التي قد لا تكون عولجت
let mutationRecords = observer.takeRecords();

// أوقف تتبّع التغيّرات
observer.disconnect();
...

ملاحظة: تُحذف السجلّات التي يعيدها observer.takeRecords()‎ من رتل المعالجة

لن يُستدعى ردّ النداء للسجلّات المعادة من طرف observer.takeRecords()‎.



ملاحظة: التفاعل مع جمع المهملات (garbage collection)

تستخدم المراقبات داخليّا مراجع ضعيفة (weak references) للعقد. بمعنى، إذا حُذفت عقدة ما من DOM، وصارت غير قابلة ل لوصول unreachable، فيمكن عندئذ جمعها مع المهملات.

مجرّد كون عقدة ما من DOM تحت المراقبة لا يمنع جمعها مع المهملات.


الملخص

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

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

ترجمة -وبتصرف- للمقال Mutation observer من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor.

اقرأ أيضًا


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

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

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



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

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

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

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


×
×
  • أضف...