天天看點

IntersectionObserver是什麼?

  • IntersectionObserver概覽
  • IntersectionObserver構造器
  • IntersectionObserver方法
  • IntersectionObserver懶加載(vue單檔案元件簡版)
  • IntersectionObserver吸頂(vue單檔案元件簡版)
  • IntersectionObserver觸底(vue單檔案元件簡版)
  • IntersectionObserver懶加載、吸頂、觸底綜合(vue單檔案元件實戰版)
  • 基于Intersection的開源工具 Scrollama
  • 總結
  • 參考資料

IntersectionObserver概覽

  • IntersectionObserver提供了一個方式去異步觀察有一個祖先element或者top-level document viewport的目标element的交叉變化。
  • 祖先元素或者viewport被當做root節點。
  • 當IntersectionObserver建立出的時候,它會被配置到監聽root内部的給定visibility的變化。
  • 一旦IntersectionObserver建立出來,它的配置是不能變的。是以一個observer object隻能用來監測一個指定visibility值的變化。
  • 雖然隻能一對一去watch ratio,但是可以在同一個observer中watch多個target elements。也就是說一個visibility ratio可以檢測多個不同的elements。

IntersectionObserver構造器

​var observer = new IntersectionObserver(callback[, options]);​

  • 和其他構造器一樣,建立并傳回一個新的Intersection對象。
  • rootMargin如果指定一個特殊值,是為了確定文法是否正确。
  • 閥值是為了確定值在0.0到1.0之間,threshold會按照升序排列。若threshold是空,值為[0.0]。

參數

  • callback 當目标元素的透明度穿過設定的threshold值時,函數會被調用。callback接受兩個 參數。
  • entries 傳入各個threshold值的數組,比該閥值指定的百分比更明顯或者更不明顯。
  • observer 調用callback的observer執行個體。
  • options 若options沒設定。observer使用document的viewport作為root,沒有margin,0%的threshold(意味着即使有1 px的變化也會觸發回調)
  • root 被當做viewport的元素。
  • rootMargin 文法是"0px 0px 0px 0px"
  • threshold 指明被監測目标總綁定盒模型的交叉區域ratio,值在0.0到1.0之間;0.0意味着即使是1px也會被當做可見的。1.0意味着整個元素是可見的。預設threshold值是0.0。

IntersectionObserver方法

  • IntersectionObserver.disconnect() 停止observe一個目标。
  • IntersectionObserver.observe() 告訴IntersectionObserver一目标元素去observe。
  • IntersectionObserver.takeRecords() 傳回包含所有observe的對象一個數組。
  • IntersectionObserver.unobserve() 取消observe一個目标對象。

示例

下面的例子在threshold值變化在10%以上時觸發myObserverCallback。

let observer = new IntersectionObserver(myObserverCallback, { "threshold": 0.1      

IntersectionObserver懶加載(vue單檔案元件簡版)

<template>
  <div>
    <img v-for="(image, i) in images" :key="i" src :data-img-url="image"
  </div>
</template>

<script>export default {
  data() {
    return {
      images: [
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1574247890587&di=88d4066be3d57ac962a6bec37e265d37&imgtype=0&src=http%3A%2F%2F01.imgmini.eastday.com%2Fmobile%2F20170810%2F20170810151144_d41d8cd98f00b204e9800998ecf8427e_3.jpeg',
        'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=4054762707,1853885380&fm=26&gp=0.jpg',
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1574247912077&di=508a949e5e291875debf6ca844292cd4&imgtype=0&src=http%3A%2F%2F03imgmini.eastday.com%2Fmobile%2F20180827%2F20180827095359_6759372e9bd28026ee6f53b500fb4291_2.jpeg',
      ],
    };
  },
  mounted() {
    const images = document.querySelectorAll('img');

    const observerLazyLoad = new IntersectionObserver((entries) => {
      entries.forEach((item) => {
        if (item.isIntersecting) {
          item.target.src = item.target.dataset.imgUrl;
        }
      });
    });

    images.forEach((image) => {
      observerLazyLoad.observe(image);
    });
  },
};
</script>

<style lang="scss" scoped>img {
  display: block;
  height: 500px;
  margin: 30px;
}
</style>      
IntersectionObserver是什麼?

IntersectionObserver吸頂(vue單檔案元件簡版)

<template>
  <div>
    <p class="fixed-top-helper"></p>
    <p class="fixed-top-reference"></p>
    <header>頭部</header>
    <main>
      <img v-for="(image, i) in images" :key="i" :src="image"
    </main>
  </div>
</template>

<script>export default {
  data() {
    return {
      images: [
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1574247890587&di=88d4066be3d57ac962a6bec37e265d37&imgtype=0&src=http%3A%2F%2F01.imgmini.eastday.com%2Fmobile%2F20170810%2F20170810151144_d41d8cd98f00b204e9800998ecf8427e_3.jpeg',
        'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=4054762707,1853885380&fm=26&gp=0.jpg',
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1574247912077&di=508a949e5e291875debf6ca844292cd4&imgtype=0&src=http%3A%2F%2F03imgmini.eastday.com%2Fmobile%2F20180827%2F20180827095359_6759372e9bd28026ee6f53b500fb4291_2.jpeg',
      ],
    };
  },
  mounted() {
    const header = document.querySelector('header');
    const fixedTopReference = document.querySelector('.fixed-top-reference');
    fixedTopReference.style.top = `${header.offsetTop}px`;

    const observerFixedTop = new IntersectionObserver((entries) => {
      entries.forEach((item) => {
        if (item.boundingClientRect.top < 0) {
          header.classList.add('fixed');
        } else {
          header.classList.remove('fixed');
        }
      });
    });
    observerFixedTop.observe(fixedTopReference);
  },
};
</script>

<style lang="scss" scoped>.fixed-top-helper {
  height: 1px;
  background: #ccc;
}
header {
  background: #ccc;
  &.fixed {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
  }
}
main {
  img {
    display: block;
    height: 500px;
    margin: 30px;
  }
}
</style>      
IntersectionObserver是什麼?

注意事項:

  • fixedTopReference是為了避免緩慢移動時add remove .fixed死循環,死循環的結果是抖動
  • fixedTopHelper是為了避免被吸頂元素沒有上一個sibling元素(也就是說被吸頂元素是最上層元素)時,避免緩緩移動時add remove .fixed死循環抖動,特殊引入的标簽,需要設定1個px的height
  • fixedTopHelper需要與被吸頂元素保持樣式一緻,以確定好的使用者體驗。例如在本例中将其background設定為#ccc,很好的做到了隐藏

吸頂抖動

IntersectionObserver是什麼?

IntersectionObserver觸底(vue單檔案元件簡版)

<template>
  <div>
    <main>
      <img v-for="(image, i) in images" :key="i" src="image"
    </main>
    <footer>底部</footer>
  </div>
</template>

<script>export default {
  data() {
    return {
      images: [
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1574247890587&di=88d4066be3d57ac962a6bec37e265d37&imgtype=0&src=http%3A%2F%2F01.imgmini.eastday.com%2Fmobile%2F20170810%2F20170810151144_d41d8cd98f00b204e9800998ecf8427e_3.jpeg',
        'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=4054762707,1853885380&fm=26&gp=0.jpg',
        'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1574247912077&di=508a949e5e291875debf6ca844292cd4&imgtype=0&src=http%3A%2F%2F03imgmini.eastday.com%2Fmobile%2F20180827%2F20180827095359_6759372e9bd28026ee6f53b500fb4291_2.jpeg',
      ],
    };
  },
  mounted() {
    const footer = document.querySelector('footer');

    const observerTouchBottom = new IntersectionObserver((entries) => {
      entries.forEach((item) => {
        if (item.isIntersecting) {
          setTimeout(() => {
            console.log('滾動到了底部,可以發request請求資料了');
          }, 2000);
        }
      });
    });

    observerTouchBottom.observe(footer);
  },
};
</script>

<style lang="scss" scoped>main {
  img {
    display: block;
    height: 500px;
    margin: 30px;
  }
}
footer {
  background: #ccc;
}
</style>      
IntersectionObserver是什麼?

IntersectionObserver懶加載、吸頂、觸底綜合(vue單檔案元件實戰版)

上面的例子是為了脫離架構更好的揭示IntersectionObserver的用法本質,如果在實際項目中使用,還需要考慮一些其他問題。

考慮内容如下:

  • 對象拆分,下面拆分出lazyLoad,touchFooter,stickHeader三個對象并建立target和observer來分别辨別被監聽者和監聽者
  • 方法拆分,摒棄全部在mounted方法中變量的定義和指派操作,很清晰的拆分出createLazyLoadObserver,createTouchFooterObserver,createStickHeaderObserver三個方法
  • 取消監聽,建立unobserveAllIntersectionObservers方法,在beforeDestory生命周期内,調用IntersectionObserver的disconnect(),unbserve(target)取消監聽目标對象
<template>
  <div>
    <p class="fixed-top-helper"></p>
    <p class="fixed-top-reference"></p>

    <header>頭部</header>
    <main>
      <img v-for="(image, i) in images" :key="i" src :data-img-url="image"
    </main>
    <footer>底部</footer>
  </div>
</template>

<script>const images = [
  'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1574247890587&di=88d4066be3d57ac962a6bec37e265d37&imgtype=0&src=http%3A%2F%2F01.imgmini.eastday.com%2Fmobile%2F20170810%2F20170810151144_d41d8cd98f00b204e9800998ecf8427e_3.jpeg',
  'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=4054762707,1853885380&fm=26&gp=0.jpg',
  'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1574247912077&di=508a949e5e291875debf6ca844292cd4&imgtype=0&src=http%3A%2F%2F03imgmini.eastday.com%2Fmobile%2F20180827%2F20180827095359_6759372e9bd28026ee6f53b500fb4291_2.jpeg',
];
export default {
  data() {
    return {
      images,
      lazyLoad: {
        target: null,
        observer: null,
      },
      touchFooter: {
        target: null,
        observer: null,
      },
      stickHeader: {
        target: null,
        reference: null,
        observer: null,
      },
    };
  },
  mounted() {
    this.createLazyLoadObserver();
    this.createTouchFooterObserver();
    this.createStickHeaderObserver();
  },
  beforeDestroy() {
    this.unobserveAllIntersectionObservers();
  },
  methods: {
    /*
    * 建立懶加載observer并周遊監聽所有img
    */
    createLazyLoadObserver() {
      this.lazyLoad.target = document.querySelectorAll('img');

      this.lazyLoad.observer = new IntersectionObserver((entries) => {
        entries.forEach((item) => {
          if (item.isIntersecting) {
            item.target.src = item.target.dataset.imgUrl;
          }
        });
      });

      this.lazyLoad.target.forEach((image) => {
        this.lazyLoad.observer.observe(image);
      });
    },
    /*
    * 建立觸底observer并監聽footer
    */
    createTouchFooterObserver() {
      this.touchFooter.target = document.querySelector('footer');

      this.touchFooter.observer = new IntersectionObserver((entries) => {
        entries.forEach((item) => {
          if (item.isIntersecting) {
            setTimeout(() => {
              console.log('滾動到了底部,可以發request請求資料了');
            }, 2000);
          }
        });
      });

      this.touchFooter.observer.observe(this.touchFooter.target);
    },
    /*
    * 建立吸頂observer并監聽header
    * 建立reference首次防抖,.fixed-top-helper二次防抖
    */
    createStickHeaderObserver() {
      this.stickHeader.target = document.querySelector('header');
      this.stickHeader.reference = document.querySelector('.fixed-top-reference');
      this.stickHeader.reference.style.top = `${this.stickHeader.target.offsetTop}px`;

      this.stickHeader.observer = new IntersectionObserver((entries) => {
        entries.forEach((item) => {
          if (item.boundingClientRect.top < 0) {
            this.stickHeader.target.classList.add('fixed');
          } else {
            this.stickHeader.target.classList.remove('fixed');
          }
        });
      });

      this.stickHeader.observer.observe(this.stickHeader.reference);
    },
    /*
     * 取消observe所有監聽目标
     */
    unobserveAllIntersectionObservers() {
      /*
      * disconncet()可以取消所有observed目标
      * 如果調用unobserve取消監聽,稍顯備援的代碼如下:
        this.lazyLoad.target.forEach((image) => {
          this.lazyLoad.observer.unobserve(image);
        });
      */
      this.lazyLoad.observer.disconnect();
      /*
       * 由于touchFooter和stickHeader隻observe了一個目标,是以單獨unobserve即可
       * 當然disconnect()也是ok的
       */
      this.touchFooter.observer.unobserve(this.touchFooter.target);
      this.stickHeader.observer.unobserve(this.stickHeader.reference);
    },
  },
};
</script>

<style lang="scss" scoped>.fixed-top-helper {
  height: 1px;
  background: #ccc;
}
header {
  background: #ccc;
  &.fixed {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
  }
}
main {
  img {
    display: block;
    height: 500px;
    margin: 30px;
  }
}
footer {
  background: #ccc;
}
</style>      

基于Intersection的開源工具 Scrollama

官方提供了Basic Process,Progress,Sticky Side,Sticky Overlay幾種示例。

Basic Process

IntersectionObserver是什麼?

Progress

IntersectionObserver是什麼?

Sticky Side

IntersectionObserver是什麼?

Sticky Overlay

IntersectionObserver是什麼?

總結

  • 主要使用IntersectionObserver實作懶加載圖檔,觸底,吸頂
  • vue單檔案元件簡版主要是用于示範,vue單檔案元件實戰版可用于項目
  • 雖然我這裡示範的是vue單檔案元件的版本,但是我相信聰明的你知道核心部分在哪裡
  • IntersectionObserver可能還會有其他的用處,來日方長,慢慢探索

參考資料

  • 微信公衆号: 大大大前端