Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IntersectionObserver是什么? #169

Open
FrankKai opened this issue Nov 20, 2019 · 9 comments
Open

IntersectionObserver是什么? #169

FrankKai opened this issue Nov 20, 2019 · 9 comments

Comments

@FrankKai
Copy link
Owner

FrankKai commented Nov 20, 2019

  • IntersectionObserver概览
  • IntersectionObserver构造器
  • IntersectionObserver方法
  • IntersectionObserver懒加载(vue单文件组件简版)
  • IntersectionObserver吸顶(vue单文件组件简版)
  • IntersectionObserver触底(vue单文件组件简版)
  • IntersectionObserver懒加载、吸顶、触底综合(vue单文件组件实战版)
  • 基于Intersection的开源工具 Scrollama
  • 总结
  • 参考资料
@FrankKai
Copy link
Owner Author

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。

@FrankKai
Copy link
Owner Author

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。

@FrankKai
Copy link
Owner Author

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 });

@FrankKai
Copy link
Owner Author

FrankKai commented Nov 20, 2019

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>

image

@FrankKai
Copy link
Owner Author

FrankKai commented Nov 20, 2019

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>

image

注意事项:

  • fixedTopReference是为了避免缓慢移动时add remove .fixed死循环,死循环的结果是抖动
  • fixedTopHelper是为了避免被吸顶元素没有上一个sibling元素(也就是说被吸顶元素是最上层元素)时,避免缓缓移动时add remove .fixed死循环抖动,特殊引入的标签,需要设置1个px的height
  • fixedTopHelper需要与被吸顶元素保持样式一致,以确保好的用户体验。例如在本例中将其background设置为#ccc,很好的做到了隐藏

吸顶抖动

image

@FrankKai
Copy link
Owner Author

FrankKai commented Nov 20, 2019

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>

image

@FrankKai
Copy link
Owner Author

FrankKai commented Nov 20, 2019

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>

@FrankKai
Copy link
Owner Author

基于Intersection的开源工具 Scrollama

官方提供了Basic Process,Progress,Sticky Side,Sticky Overlay几种示例。

Basic Process

image

Progress

image

Sticky Side

image

Sticky Overlay

image

在项目中可以当做适当引用。

项目地址:https://github.com/russellgoldenberg/scrollama
demo地址:https://russellgoldenberg.github.io/scrollama/basic/

@FrankKai
Copy link
Owner Author

FrankKai commented Nov 20, 2019

总结

  • 主要使用IntersectionObserver实现懒加载图片,触底,吸顶
  • vue单文件组件简版主要是用于演示,vue单文件组件实战版可用于项目
  • 虽然我这里演示的是vue单文件组件的版本,但是我相信聪明的你知道核心部分在哪里
  • IntersectionObserver可能还会有其他的用处,来日方长,慢慢探索

参考资料

https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API/Timing_element_visibility
https://juejin.im/post/5ca15c1e51882567b544ee0b
https://medium.com/walmartlabs/lazy-loading-images-intersectionobserver-8c5bff730920
https://juejin.im/post/5d665133e51d4561c83e7c83
https://github.com/russellgoldenberg/scrollama

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant