أنت الآن تراني: كيف أتأجل ، وتحمل كسولًا ، وتتصرف مع تقاطعObserver

نشرت: 2022-03-10
ملخص سريع ↬ هناك حاجة إلى معلومات التقاطع لأسباب عديدة ، مثل التحميل البطيء للصور. لكن هناك الكثير. حان الوقت للحصول على فهم أفضل ووجهات نظر مختلفة حول واجهة برمجة تطبيقات Intersection Observer. مستعد؟

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

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

مطور ويب
مطور الويب الوهمي

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

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

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

IntersectionObserver: الآن تراني

سيداتي وسادتي ، لنتحدث عن واجهة برمجة تطبيقات Intersection Observer. ولكن قبل أن نبدأ ، دعونا نلقي نظرة على مشهد الأدوات الحديثة الذي قادنا إلى IntersectionObserver .

كان عام 2017 عامًا جيدًا للغاية بالنسبة للأدوات المضمنة في متصفحاتنا ، مما ساعدنا على تحسين جودة وأسلوب قاعدة التعليمات البرمجية الخاصة بنا دون بذل الكثير من الجهد. في هذه الأيام ، يبدو أن الويب يبتعد عن الحلول المتفرقة التي تعتمد على حل مختلف تمامًا عن حل نموذجي جدًا لمنهج أكثر تحديدًا لواجهات المراقب (أو "المراقبون" فقط): حصل MutationObserver المدعوم جيدًا على أفراد جدد من العائلة كانوا سريعًا المعتمد في المتصفحات الحديثة:

  • تقاطعObserver و
  • PerformanceObserver (كجزء من مواصفات المستوى 2 للجدول الزمني للأداء).

أحد أفراد العائلة المحتملين ، FetchObserver ، هو عمل قيد التقدم ويوجهنا أكثر إلى أراضي وكيل الشبكة ، لكني اليوم أود التحدث أكثر عن الواجهة الأمامية بدلاً من ذلك.

يعد IntersectionObserver و PerformanceObserver الأعضاء الجدد في عائلة المراقبين.
يعد IntersectionObserver و PerformanceObserver الأعضاء الجدد في عائلة المراقبين.

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

نصيحة احترافية : يمكنك تخطي النظرية والتعمق في آليات IntersectionObserver على الفور ، أو أبعد من ذلك ، مباشرة إلى التطبيقات الممكنة لـ IntersectionObserver .

المراقب مقابل الحدث

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

المراقب مقابل الحدث: ما الفرق؟
المراقب مقابل الحدث: ما الفرق؟

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

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

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

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

لا يُقصد بالمراقبين أن يحلوا محل الأحداث: يمكن لكليهما العيش معًا بسعادة.
لا يُقصد بالمراقبين أن يحلوا محل الأحداث: يمكن لكليهما العيش معًا بسعادة.

هيكل المراقب العام

تبدو البنية العامة للمراقب (أي من تلك المتوفرة وقت كتابة هذا التقرير) مشابهة لما يلي:

 /** * Typical Observer's registration */ let observer = new YOUR-TYPE-OF-OBSERVER(function (entries) { // entries: Array of observed elements entries.forEach(entry => { // Here we can do something with each particular entry }); }); // Now we should tell our Observer what to observe observer.observe(WHAT-TO-OBSERVE);

مرة أخرى ، لاحظ أن entries عبارة عن Array من القيم ، وليست إدخالًا واحدًا.

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

هنا ، لننهي الجزء "العام" من هذه المناقشة ونتعمق أكثر في موضوع مقال اليوم - IntersectionObserver .

تفكيك التقاطعObserver

تفكيك التقاطعObserver
تفكيك التقاطعObserver

بادئ ذي بدء ، دعنا نفرز ما هو IntersectionObserver .

وفقًا لـ MDN:

توفر واجهة برمجة تطبيقات Intersection Observer طريقة لرصد التغييرات بشكل غير متزامن في تقاطع عنصر هدف مع عنصر سلف أو مع منفذ عرض لمستند عالي المستوى.

ببساطة ، يلاحظ IntersectionObserver بشكل غير متزامن تداخل عنصر واحد مع عنصر آخر. لنتحدث عن ماهية هذه العناصر في IntersectionObserver .

IntersectionObserver تهيئة

في إحدى الفقرات السابقة ، رأينا هيكل المراقب العام. يمتد IntersectionObserver هذه البنية قليلاً. بادئ ذي بدء ، يتطلب هذا النوع من المراقب تكوينًا من ثلاثة عناصر رئيسية:

  • root : هذا هو عنصر الجذر المستخدم في الملاحظة. يحدد "إطار الالتقاط" الأساسي للعناصر التي يمكن ملاحظتها. بشكل افتراضي ، يكون root هو منفذ عرض المتصفح الخاص بك ، ولكن يمكن أن يكون أي عنصر في DOM الخاص بك (ثم تقوم بتعيين root إلى شيء مثل document.getElementById('your-element') ). ضع في اعتبارك أن العناصر التي تريد مراقبتها يجب أن "تعيش" في شجرة DOM الخاصة root في هذه الحالة.
خاصية الجذر لتكوين IntersectionObserver
تحدد خاصية root قاعدة "التقاط الإطار" لعناصرنا.
  • rootMargin : يحدد الهامش حول عنصر root الخاص بك والذي يمتد أو يقلص "إطار الالتقاط" عندما لا توفر أبعاد root الخاص بك المرونة الكافية. تشبه خيارات قيم هذا التكوين تلك الخاصة margin في CSS ، مثل rootMargin: '50px 20px 10px 40px' (أعلى ، أسفل اليمين ، يسار). يمكن اختزال القيم (مثل rootMargin: '50px' ) ويمكن التعبير عنها إما في px أو % . افتراضيًا ، rootMargin: '0px' .
خاصية rootMargin لتكوين IntersectionObserver
توسع خاصية rootMargin / تتعاقد مع "إطار الالتقاط" المحدد بواسطة root .
  • threshold : لا تريد دائمًا الاستجابة على الفور عندما يتقاطع عنصر تمت ملاحظته مع حد "إطار الالتقاط" (يُعرَّف على أنه مزيج من root و rootMargin ). threshold تحدد النسبة المئوية لمثل هذا التقاطع التي يجب أن يتفاعل المراقب عندها. يمكن تعريفها على أنها قيمة واحدة أو مجموعة من القيم. لفهم تأثير threshold بشكل أفضل (أعلم أنه قد يكون محيرًا في بعض الأحيان) ، إليك بعض الأمثلة:
    • threshold: 0 : يجب أن تتفاعل القيمة الافتراضية IntersectionObserver عندما يتقاطع أول بكسل أو آخر بكسل لعنصر تمت ملاحظته مع أحد حدود "إطار الالتقاط". ضع في اعتبارك أن IntersectionObserver الاتجاه ، مما يعني أنه سيتفاعل في كلا السيناريوهين: أ) عندما يدخل العنصر و ب) عندما يغادر "إطار الالتقاط".
    • threshold: 0.5 : يجب إطلاق المراقب عندما يتقاطع 50٪ من العنصر المرصود مع "إطار الالتقاط" ؛
    • threshold: [0, 0.2, 0.5, 1] : يجب أن يتفاعل المراقب في 4 حالات:
      • يدخل أول بكسل من عنصر تمت ملاحظته في "إطار الالتقاط": لا يزال العنصر غير موجود بالفعل داخل هذا الإطار ، أو أن آخر بكسل للعنصر المرصود يترك "إطار الالتقاط": لم يعد العنصر داخل الإطار ؛
      • 20٪ من العنصر داخل "إطار الالتقاط" (مرة أخرى ، الاتجاه لا يهم بالنسبة لـ IntersectionObserver ) ؛
      • 50٪ من العنصر داخل "إطار الالتقاط" ؛
      • 100٪ من العنصر داخل "إطار الالتقاط". هذا مخالف تمامًا threshold: 0 .
خاصية عتبة تكوين IntersectionObserver
تحدد خاصية threshold مقدار تقاطع العنصر مع "إطار الالتقاط" قبل إطلاق المراقب.

من أجل إبلاغ IntersectionObserver الخاص بنا بالتكوين المطلوب ، نقوم ببساطة بتمرير كائن config الخاص بنا إلى مُنشئ المراقب لدينا إلى جانب وظيفة رد الاتصال الخاصة بنا مثل هذا:

 const config = { root: null, // avoiding 'root' or setting it to 'null' sets it to default value: viewport rootMargin: '0px', threshold: 0.5 }; let observer = new IntersectionObserver(function(entries) { … }, config);

الآن ، يجب أن نعطي IntersectionObserver العنصر الفعلي الذي يجب مراقبته. يتم ذلك ببساطة عن طريق تمرير العنصر observe() :

 … const img = document.getElementById('image-to-observe'); observer.observe(image);

هناك أمران يجب ملاحظتهما حول هذا العنصر المرصود:

  • لقد تم ذكره سابقًا ، ولكن يستحق الذكر مرة أخرى: في حالة تعيين root كعنصر في DOM ، يجب أن يكون العنصر الذي تمت ملاحظته موجودًا داخل شجرة DOM الخاصة root .
  • يمكن أن يقبل IntersectionObserver عنصرًا واحدًا فقط للمراقبة في كل مرة ولا يدعم العرض الدفعي للملاحظات. هذا يعني أنه إذا كنت بحاجة إلى ملاحظة عدة عناصر (دعنا نقول عدة صور على الصفحة) ، فيجب عليك تكرارها جميعًا ومراقبة كل منها على حدة:
 … const images = document.querySelectorAll('img'); images.forEach(image => { observer.observe(image); });
  • عند تحميل صفحة مع تطبيق Observer ، قد تلاحظ أنه تم تشغيل رد نداء IntersectionObserver لجميع العناصر التي تمت ملاحظتها مرة واحدة. حتى تلك التي لا تتطابق مع التكوين المقدم. "حسنًا ... ليس ما كنت أتوقعه حقًا ،" هي الفكرة المعتادة عند تجربة هذا لأول مرة. لكن لا ترتبك هنا: هذا لا يعني بالضرورة أن تلك العناصر التي تمت ملاحظتها تتقاطع بطريقة ما مع "إطار الالتقاط" أثناء تحميل الصفحة.
لقطة شاشة لـ DevTools مع IntersectionObserver يتم تشغيله لجميع العناصر مرة واحدة.
سيتم تشغيل IntersectionObserver لجميع العناصر التي تمت ملاحظتها بمجرد تسجيلها ، لكن هذا لا يعني أنها تتقاطع جميعًا مع "إطار الالتقاط".

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

تقاطع أو رد اتصال الخادم

بادئ ذي بدء ، تأخذ وظيفة رد الاتصال لـ IntersectionObserver وسيطين ، وسنتحدث عنهما بترتيب معكوس بدءًا من الوسيطة الثانية . جنبًا إلى جنب مع Array الإدخالات المرصودة المذكورة أعلاه ، والتي تتقاطع مع "إطار الالتقاط" ، تحصل وظيفة رد النداء على المراقب نفسه باعتباره الوسيطة الثانية .

الإشارة إلى المراقب نفسه

 new IntersectionObserver(function(entries, SELF) {…});

الحصول على مرجع إلى المراقب نفسه مفيد في الكثير من السيناريوهات عندما تريد التوقف عن مراقبة بعض العناصر بعد أن يكتشفها IntersectionObserver لأول مرة. السيناريوهات مثل التحميل البطيء للصور ، الجلب المؤجل للأصول الأخرى ، وما إلى ذلك ، هي من هذا النوع. عندما تريد التوقف عن مراقبة عنصر ما ، يوفر IntersectionObserver طريقة unobserve(element-to-stop-observing) يمكن تشغيلها في وظيفة رد الاتصال بعد تنفيذ بعض الإجراءات على العنصر المرصود (مثل التحميل البطيء الفعلي للصورة ، على سبيل المثال ).

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

تقاطع أو خادم دخول

 new IntersectionObserver(function(ENTRIES, self) {…});

entries التي نحصل عليها في وظيفة رد الاتصال لدينا Array هي من النوع الخاص: IntersectionObserverEntry . توفر لنا هذه الواجهة مجموعة خصائص محددة مسبقًا ومحسوبة مسبقًا تتعلق بكل عنصر معين تمت ملاحظته. دعونا نلقي نظرة على أكثرها إثارة للاهتمام.

بادئ ذي بدء ، تأتي إدخالات نوع IntersectionObserverEntry مع معلومات حول ثلاثة مستطيلات مختلفة - تحديد إحداثيات وحدود العناصر المشاركة في العملية:

  • rootBounds : مستطيل لـ "إطار الالتقاط" ( root + rootMargin ) ؛
  • boundingClientRect : مستطيل للعنصر المرصود نفسه ؛
  • intersectionRect : منطقة من "إطار الالتقاط" يتقاطع مع العنصر المرصود.
مستطيلات التقاطع دخول المراقب
يتم حساب جميع المستطيلات المحيطة المضمنة في IntersectionObserverEntry.

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

خاصية أخرى لواجهة IntersectionObserverEntry التي تثير اهتمامنا هي isIntersecting . هذه خاصية ملائمة تشير إلى ما إذا كان العنصر الذي تمت ملاحظته يتقاطع حاليًا مع "إطار الالتقاط" أم لا. يمكننا بالطبع الحصول على هذه المعلومات من خلال النظر إلى intersectionRect (إذا لم يكن هذا المستطيل 0 × 0 ، فإن العنصر يتقاطع مع "إطار الالتقاط") ولكن الحصول على هذه المحسوبة مسبقًا بالنسبة لنا أمر مريح للغاية.

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

  • إذا كانت false والآن أصبحت true ، فإن العنصر يدخل "إطار الالتقاط" ؛
  • إذا كان عكس ذلك وكان false الآن بينما كان true من قبل ، فإن العنصر يترك "إطار الالتقاط".

isIntersecting هي بالضبط الخاصية التي تساعدنا في حل المشكلة التي ناقشناها سابقًا ، على سبيل المثال ، إدخالات منفصلة للعناصر التي تتقاطع حقًا مع "إطار الالتقاط" من ضجيج تلك التي تكون مجرد تهيئة الإدخال.

 let isLeaving = false; let observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { // we are ENTERING the "capturing frame". Set the flag. isLeaving = true; // Do something with entering entry } else if (isLeaving) { // we are EXITING the "capturing frame" isLeaving = false; // Do something with exiting entry } }); }, config);

ملاحظة : في Microsoft Edge 15 ، لم يتم تنفيذ خاصية isIntersecting ، وإرجاع undefined على الرغم من الدعم الكامل لـ IntersectionObserver خلاف ذلك. تم إصلاح هذا في يوليو 2017 وهو متاح منذ Edge 16.

توفر واجهة IntersectionObserverEntry خاصية راحة أخرى محسوبة مسبقًا: intersectionRatio . يمكن استخدام هذه المعلمة لنفس الأغراض مثل isIntersecting ولكنها توفر مزيدًا من التحكم الدقيق نظرًا لكونها رقم فاصلة عائمة بدلاً من القيمة المنطقية. تشير قيمة معدل intersectionRatio إلى مدى تقاطع مساحة العنصر المرصود مع "إطار الالتقاط" (نسبة منطقة intersectionRect إلى منطقة boundingClientRect ). مرة أخرى ، يمكننا إجراء هذه العملية الحسابية بأنفسنا باستخدام معلومات من تلك المستطيلات ، لكن من الجيد إجرائها لنا.

ألا تبدو مألوفة بالفعل؟ نعم ، تشبه خاصية <code> intersectionRatio </code> خاصية <code> threshold </code> لتكوين Observer. الفرق هو أن الأخير يحدد <em> متى </ em> لإطلاق Observer ، يشير الأول إلى حالة التقاطع الحقيقي (التي تختلف قليلاً عن <code> العتبة </ code> بسبب الطبيعة غير المتزامنة لـ Observer).
ألا تبدو مألوفة بالفعل؟ نعم ، خاصية intersectionRatio مشابهة لخاصية threshold لتكوين المراقب. الفرق هو أن الأخير يحدد * متى * لإطلاق Observer ، يشير الأول إلى حالة التقاطع الحقيقي (التي تختلف قليلاً عن threshold بسبب الطبيعة غير المتزامنة لـ Observer).

target هو خاصية أخرى لواجهة IntersectionObserverEntry قد تحتاج إلى الوصول إليها كثيرًا. لكن لا يوجد سحر هنا على الإطلاق - إنه فقط العنصر الأصلي الذي تم تمريره observe() مراقبك. تمامًا مثل event.target الذي اعتدت عليه عند العمل مع الأحداث.

للحصول على قائمة كاملة بخصائص واجهة IntersectionObserverEntry تحقق من المواصفات.

التطبيقات الممكنة

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

الوظائف المؤجلة

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

دائري أسفل الشاشة الأولى لتطبيقك
عندما يكون لدينا دائري أو أي وظيفة أخرى للرفع الثقيل أسفل حظيرة تطبيقنا ، فإن بدء تشغيل التمهيد / تحميله على الفور يعد إهدارًا للموارد.

يعد تشغيل الرف الدائري بشكل عام مهمة شاقة. عادةً ، تتضمن مؤقتات JavaScript ، وعمليات حسابية للتمرير تلقائيًا عبر العناصر ، وما إلى ذلك. كل هذه المهام تقوم بتحميل الخيط الرئيسي ، وعندما يتم ذلك في وضع التشغيل التلقائي ، يصعب علينا معرفة متى تحصل سلسلة المحادثات الرئيسية لدينا على هذه الضربة. عندما نتحدث عن إعطاء الأولوية للمحتوى على شاشتنا الأولى ونريد الوصول إلى First Meaningful Paint و Time To Interactive في أقرب وقت ممكن ، يصبح الخيط الرئيسي المحظور عنق الزجاجة لأدائنا.

لإصلاح المشكلة ، قد نرجئ تشغيل مثل هذا العرض الدائري حتى يدخل في إطار عرض المتصفح. في هذه الحالة ، isIntersecting لواجهة IntersectionObserverEntry .

 const carousel = document.getElementById('carousel'); let isLeaving = false; let observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { isLeaving = true; entry.target.startCarousel(); } else if (isLeaving) { isLeaving = false; entry.target.stopCarousel(); } }); } observer.observe(carousel);

هنا ، نلعب الدائرة فقط عندما تدخل في منفذ العرض الخاص بنا. لاحظ عدم وجود كائن config الذي تم تمريره إلى تهيئة IntersectionObserver : هذا يعني أننا نعتمد على خيارات التكوين الافتراضية. عندما يخرج العرض الدائري من إطار العرض ، يجب أن نتوقف عن تشغيله حتى لا ننفق الموارد على العناصر غير المهمة بعد الآن.

التحميل الكسول للأصول

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

تحميل الصور البطيئة أسفل الطية
تحميل الأصول كسول مثل الصور الموجودة أسفل الشاشة الأولى - التطبيق الأكثر وضوحًا لـ IntersectionObserver.

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

لذا ، بالعودة إلى IntersectionObserver في سيناريو التحميل البطيء ، ما الذي يجب أن ننتبه إليه؟ دعنا نتحقق من مثال بسيط للصور ذات التحميل البطيء.

شاهد تحميل القلم الكسول في IntersectionObserver بواسطة Denys Mishunov (mishunov) على CodePen.

شاهد تحميل القلم الكسول في IntersectionObserver بواسطة Denys Mishunov (mishunov) على CodePen.

حاول التمرير ببطء لتلك الصفحة إلى "الشاشة الثالثة" ومشاهدة نافذة المراقبة في الزاوية اليمنى العليا: ستعلمك بعدد الصور التي تم تنزيلها حتى الآن.

في صميم ترميز HTML لهذه المهمة ، يضع تسلسل بسيط من الصور:

 … <img data-src="https://blah-blah.com/foo.jpg"> …

كما ترى ، يجب أن تأتي الصور بدون علامات src : بمجرد أن يرى المتصفح سمة src ، سيبدأ تنزيل تلك الصورة على الفور التي تتعارض مع نوايانا. ومن ثم لا ينبغي أن نضع هذه السمة على صورنا بتنسيق HTML ، وبدلاً من ذلك ، قد نعتمد على بعض سمات data- مثل data-src هنا.

جزء آخر من هذا الحل ، بالطبع ، هو JavaScript. دعنا نركز على الأجزاء الرئيسية هنا:

 const images = document.querySelectorAll('[data-src]'); const config = { … }; let observer = new IntersectionObserver(function (entries, self) { entries.forEach(entry => { if (entry.isIntersecting) { … } }); }, config); images.forEach(image => { observer.observe(image); });

من الناحية الهيكلية ، لا يوجد شيء جديد هنا: لقد غطينا كل هذا من قبل:

  • نحصل على جميع الرسائل بسمات data-src بنا ؛
  • ضبط config : في هذا السيناريو ، تريد توسيع "إطار الالتقاط" لاكتشاف العناصر الأقل قليلاً من أسفل إطار العرض ؛
  • تسجيل IntersectionObserver مع هذا التكوين ؛
  • كرر على صورنا وأضفها جميعًا ليتم ملاحظتها من خلال هذا IntersectionObserver ؛

يحدث الجزء المثير للاهتمام داخل وظيفة رد الاتصال التي تم استدعاؤها في الإدخالات. هناك ثلاث خطوات أساسية متضمنة.

  1. بادئ ذي بدء ، نحن نعالج فقط العناصر التي تتقاطع فعلاً مع "إطار الالتقاط". يجب أن يكون هذا المقتطف مألوفًا لك الآن.

     entries.forEach(entry => { if (entry.isIntersecting) { … } });

  2. بعد ذلك ، نقوم بمعالجة الإدخال بطريقة ما عن طريق تحويل صورتنا باستخدام data-src <img src="…"> حقيقي.

     if (entry.isIntersecting) { preloadImage(entry.target); … }
    سيؤدي هذا إلى تشغيل المتصفح لتنزيل الصورة أخيرًا. preloadImage() هي وظيفة بسيطة جدًا لا تستحق الذكر هنا. فقط اقرأ المصدر.

  3. الخطوة التالية والأخيرة: نظرًا لأن التحميل البطيء هو إجراء لمرة واحدة ولسنا بحاجة إلى تنزيل الصورة في كل مرة يدخل فيها العنصر إلى "إطار الالتقاط" ، فيجب علينا إلغاء unobserve الصورة التي تمت معالجتها بالفعل. بنفس الطريقة التي يجب أن نقوم بها مع element.removeEventListener() العادية عندما لا تكون هناك حاجة إليها بعد الآن لمنع تسرب الذاكرة في الكود الخاص بنا.

     if (entry.isIntersecting) { preloadImage(entry.target); // Observer has been passed as self to our callback self.unobserve(entry.target); }

ملحوظة. بدلاً من unobserve(event.target) يمكننا أيضًا استدعاء disconnect() : إنه يفصل تمامًا IntersectionObserver ولن يلاحظ الصور بعد الآن. هذا مفيد إذا كان الشيء الوحيد الذي تهتم به هو أول إصابة لمراقبك على الإطلاق. في حالتنا ، نحتاج إلى الأوبزرفر لمواصلة مراقبة الصور ، لذلك لا ينبغي لنا قطع الاتصال بعد.

لا تتردد في اقتباس المثال واللعب بإعدادات وخيارات مختلفة. هناك شيء واحد مثير للاهتمام يجب ذكره عندما تريد التحميل البطيء للصور على وجه الخصوص. يجب عليك دائمًا الاحتفاظ بالمربع الذي تم إنشاؤه بواسطة العنصر المرصود في الاعتبار! إذا قمت بالتحقق من المثال ، ستلاحظ أن CSS للصور الموجودة في الأسطر 41-47 تحتوي على أنماط زائدة عن الحاجة ، بما في ذلك. min-height: 100px . يتم إجراء ذلك لمنح العناصر النائبة للصورة ( <img> بدون سمة src ) بعض الأبعاد الرأسية. لاجل ماذا؟

  • بدون أبعاد رأسية ، ستولد جميع علامات <img> مربعًا 0 × 0 ؛
  • نظرًا لأن علامة <img> تنشئ نوعًا من مربعات inline-block افتراضيًا ، فستتم محاذاة جميع هذه المربعات 0 × 0 جنبًا إلى جنب على نفس السطر ؛
  • هذا يعني أن IntersectionObserver الخاص بك سوف يسجل جميع الصور (أو تقريبًا ، بناءً على مدى سرعة التمرير ، كل) الصور مرة واحدة - ربما ليس بالضبط ما تريد تحقيقه.

تسليط الضوء على القسم الحالي

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

راجع قسم Pen Highlighting الحالي في IntersectionObserver بواسطة Denys Mishunov (mishunov) على CodePen.

راجع قسم Pen Highlighting الحالي في IntersectionObserver بواسطة Denys Mishunov (mishunov) على CodePen.

من الناحية الهيكلية ، يشبه المثال الخاص بالصور ذات التحميل البطيء وله نفس البنية الأساسية مع الاستثناءات التالية:

  • الآن نريد أن نلاحظ ليس الصور ، ولكن الأقسام الموجودة على الصفحة ؛
  • من الواضح أن لدينا أيضًا وظيفة مختلفة لمعالجة الإدخالات في رد الاتصال ( intersectionHandler(entry) ). لكن هذا ليس مثيرًا للاهتمام: كل ما يفعله هو تبديل فئة CSS.

ما هو مثير للاهتمام هنا هو كائن config على الرغم من:

 const config = { rootMargin: '-50px 0px -55% 0px' };

لماذا لا تسأل القيمة الافتراضية لـ 0px لـ rootMargin ؟ حسنًا ، ببساطة لأن إبراز القسم الحالي والتحميل البطيء للصورة يختلفان تمامًا فيما نحاول تحقيقه. مع التحميل البطيء ، نريد أن نبدأ التحميل قبل أن تصل الصورة إلى العرض. ولهذا الغرض ، قمنا بتوسيع "إطار الالتقاط" بمقدار 50 بكسل في الجزء السفلي. على العكس من ذلك ، عندما نريد إبراز القسم الحالي ، علينا التأكد من أن القسم مرئي بالفعل على الشاشة. وليس هذا فقط: علينا التأكد من أن المستخدم ، في الواقع ، يقرأ أو سيقرأ هذا القسم بالضبط. ومن ثم ، فإننا نريد أن يتجاوز القسم نصف منفذ العرض من الأسفل قليلاً قبل أن نعلن أنه القسم النشط. أيضًا ، نريد حساب ارتفاع شريط التنقل ، ولذا نزيل ارتفاع الشريط من "إطار الالتقاط".

التقاط الإطار للقسم الحالي
نريد أن يكتشف المراقب فقط العناصر التي تدخل في "إطار الالتقاط" بين 50 بكسل من الأعلى و 55٪ من منفذ العرض من الأسفل.

لاحظ أيضًا أنه في حالة تمييز عنصر التنقل الحالي ، لا نريد التوقف عن مراقبة أي شيء. هنا يجب أن نبقي دائمًا IntersectionObserver مسؤولاً ، ومن ثم لن تجد disconnect() ولا unobserve() هنا.

ملخص

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

لماذا يعتبر برنامج IntersectionObserver مفيدًا لك؟

  • IntersectionObserver هو واجهة برمجة تطبيقات غير متزامنة غير معطلة!
  • يستبدل IntersectionObserver مستمعينا الباهظين عند scroll أو resize الأحداث.
  • يقوم IntersectionObserver بجميع العمليات الحسابية باهظة الثمن مثل getClientBoundingRect() نيابة عنك بحيث لا تحتاج إلى ذلك.
  • يتبع IntersectionObserver النمط الهيكلي للمراقبين الآخرين ، لذلك ، من الناحية النظرية ، يجب أن يكون من السهل فهمه إذا كنت على دراية بكيفية عمل المراقبين الآخرين.

أشياء لتأخذها بالحسبان

إذا قارنا إمكانيات IntersectionObserver بعالم window.addEventListener('scroll') من حيث أتى كل شيء ، فسيكون من الصعب رؤية أي سلبيات في هذا المراقب. لذلك ، دعنا نلاحظ فقط بعض الأشياء التي يجب وضعها في الاعتبار بدلاً من ذلك:

  • نعم ، IntersectionObserver هو واجهة برمجة تطبيقات غير متزامنة غير معطلة. هذا شيء عظيم أن تعرف! ولكن الأهم من ذلك هو فهم أن الكود الذي تقوم بتشغيله في عمليات الاسترجاعات الخاصة بك لن يتم تشغيله بشكل غير متزامن افتراضيًا على الرغم من أن واجهة برمجة التطبيقات نفسها غير متزامنة. لذلك لا تزال هناك فرصة لإزالة جميع الفوائد التي تحصل عليها من IntersectionObserver إذا كانت حسابات وظيفة رد الاتصال الخاصة بك تجعل مؤشر الترابط الرئيسي غير مستجيب. لكن هذه قصة مختلفة.
  • إذا كنت تستخدم IntersectionObserver للتحميل البطيء للأصول (مثل الصور ، على سبيل المثال) ، فقم بتشغيل .unobserve(asset) بعد تحميل الأصل.
  • يمكن لـ IntersectionObserver اكتشاف التقاطعات فقط للعناصر التي تظهر في بنية تنسيق المستند. لتوضيح الأمر: يجب أن تنشئ العناصر التي يمكن ملاحظتها مربعًا وتؤثر بطريقة ما على التخطيط. فيما يلي بعض الأمثلة فقط لمنحك فهمًا أفضل:

    • العناصر مع display: none غير وارد ؛
    • opacity: 0 أو visibility:hidden تقوم بإنشاء الصندوق (حتى وإن كانت غير مرئية) لذلك سيتم اكتشافها ؛
    • عناصر موضوعة تمامًا width:0px; height:0px width:0px; height:0px على ما يرام. Though, it has to be noted that absolutely positioned elements fully positioned outside of parent's borders (with negative margins or negative top , left , etc.) and are cut out by parent's overflow: hidden won't be detected: their box is out of scope for the formatting structure.
IntersectionObserver: Now You See Me
IntersectionObserver: Now You See Me

I know it was a long article, but if you're still around, here are some links for you to get an even better understanding and different perspectives on the Intersection Observer API:

  • Intersection Observer API on MDN;
  • IntersectionObserver polyfill;
  • IntersectionObserver polyfill as npm module;
  • Lazy-Loading Images with IntersectionObserver [video] by amazing Paul Lewis;
  • Basic and short (just 01:39), but very informative introduction to IntersectionObserver [video] by Surma.

With this, I would like to make a pause in our discussion to give you an opportunity to play with this technology and realize all of its convenience. So, go play with it. The article is finally over. This time I really mean it.