4

开发过程中,经常会遇到需要处理大量数据的情况,比如列表、历史记录等,通常选择无限加载和分页导航。

传统后端渲染,一般会选择分页导航,它可以轻松跳转,甚至一次跳转几个页面,现在SPA盛行,无限滚动加载是更好的方案,可以给用户更好的体验,尤其是在移动端。

在Awesome Vue中,有如下无限滚动组件

Intersection Observer API的出现,让开发无限滚动组件变得更加简单方便。

Intersection Observer API

Intersection Observer API提供了一个可订阅的模型,可以观察该模型,以便在元素进入视口时得到通知。

创建一个观察者实例很简单,我们只需要创建一个IntersectionObserver的新实例并调用observe方法,传递一个DOM元素:


const observer = new IntersectionObserver();

const coolElement = document.querySelector("#coolElement");
observer.observe(coolElement);

接下来可以使用回调方式将参数传给InersectionObserver:


const observer = new IntersectionObserver(entries => {
  const firstEntry = entries[0];
  if (firstEntry.isIntersecting) {
    // Handle intersection here...
  }
});

const coolDiv = document.querySelector("#coolDiv");
observer.observe(coolDiv);

回调接收entries作为其参数。 这是一个数组,因为当你使用阈值时你可以有几个条目,但事实并非如此,所以只得到第一个元素。
然后可以使用firstEntry.isIntersection属性检查它是否相交。 这是进行异步请求并检索下一个页面的数据。

IntersectionObserver构造函数使用以下表示法接收选项组件作为其第二个参数:


const options = {
  root: document.querySelector("#scrollArea"),
  rootMargin: "0px",
  threshold: 1.0
};

const observer = new IntersectionObserver(callback, options);

关于options里的参数解释,截自ruanyifeng intersectionobserver_api

==root==:性指定目标元素所在的容器节点(即根元素)。注意,容器元素必须是目标元素的祖先节点

==rootMargin==:
定义根元素的margin,用来扩展或缩小rootBounds这个矩形的大小,从而影响intersectionRect交叉区域的大小。它使用CSS的定义方法,比如10px 20px 30px 40px,表示 top、right、bottom 和 left 四个方向的值。

这样设置以后,不管是窗口滚动或者容器内滚动,只要目标元素可见性变化,都会触发观察器

==threshold==:决定了什么时候触发回调函数。它是一个数组,每个成员都是一个门槛值,默认为[0],即交叉比例(intersectionRatio)达到0时触发回调函数。
比如,[0, 0.25, 0.5, 0.75, 1]就表示当目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。

由于需要使用dom元素作为观察者,在Vue中,使用mounted,React中使用componentDidMount


// Observer.vue
export default {
  data: () => ({
    observer: null
  }),
  mounted() {
    this.observer = new IntersectionObserver(([entry]) => {
      if (entry && entry.isIntersecting) {
        // ...
      }
    });

    this.observer.observe(this.$el);
  }
};
注意:我们在 [entry] 参数上使用数组解构,使用this.$el作为root以便观察

为了使其可重用,我们需要让父组件(使用Observer组件的组件)处理相交的事件。 为此,可以在它相交时发出一个自定义事件:

export default {
  mounted() {
    this.observer = new IntersectionObserver(([entry]) => {
      if (entry && entry.isIntersecting) {
        this.$emit("intersect");
      }
    });

    this.observer.observe(this.$el);
  }
  // ...
};

<template>
  <div class="observer"/>
</template>

组件销毁的时候,记得关闭observer

export default {
  destroyed() {
    this.observer.disconnect();
  }
  // ...
};

与==unobserve==不同的是,unobserve关闭当前被观察的元素,而disconnect关闭所有被观察的元素。

<!-- Observer.vue -->
<template>
  <div class="observer"/>
</template>

<script>
export default {
  props: ['options'],
  data: () => ({
    observer: null,
  }),
  mounted() {
    const options = this.options || {};
    this.observer = new IntersectionObserver(([entry]) => {
      if (entry && entry.isIntersecting) {
        this.$emit("intersect");
      }
    }, options);

    this.observer.observe(this.$el);
  },
  destroyed() {
    this.observer.disconnect();
  },
};
</script>

创建无限滚动组件Vue

假如有如下类似需求

<template>
  <div>
    <ul>
      <li class="list-item" v-for="item in items" :key="item.id">
        {{item.name}}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data: () => ({ page: 1, items: [] }),
  async mounted() {
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/comments?_page=${
        this.page
      }&_limit=50`
    );

    this.items = await res.json();
  }
};
</script>

引入Observer组件

<template>
  <div>
    <ul>
      <li class="list-item" v-for="item in items" :key="item.id">
        {{item.name}}
      </li>
    </ul>
    <Observer @intersect="intersected"/>
  </div>
</template>

<script>
import Observer from "./Observer";
export default {
  data: () => ({ page: 1, items: [] }),
  async mounted() {
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/comments?_page=${
        this.page
      }&_limit=50`
    );

    this.items = await res.json();
  },
  components: {
    Observer
  }
};
</script>

将==mounted==钩子里的异步请求移到==methods==里,并加上自增page以及合并items数据

export default {
  data: () => ({ page: 1, items: [] }),
  methods: {
    async intersected() {
      const res = await fetch(
        `https://jsonplaceholder.typicode.com/comments?_page=${
          this.page
        }&_limit=50`
      );

      this.page++;
      const items = await res.json();
      this.items = [...this.items, ...items];
    }
  }
};
this.items = [...this.items, ...items] 等价于 this.items.concat(items)

到此InfiniteScroll.vue已经完成

<!-- InfiniteScroll.vue -->
<template>
  <div>
    <ul>
      <li class="list-item" v-for="item in items" :key="item.id">{{item.name}}</li>
    </ul>
    <Observer @intersect="intersected"/>
  </div>
</template>

<script>
import Observer from "./Observer";

export default {
  data: () => ({ page: 1, items: [] }),
  methods: {
    async intersected() {
      const res = await fetch(`https://jsonplaceholder.typicode.com/comments?_page=${
        this.page
      }&_limit=50`);

      this.page++;
      const items = await res.json();
      this.items = [...this.items, ...items];
    },
  },
  components: {
    Observer,
  },
};
</script>

值得注意的是,intersection Observer api兼容性并不是太好,经本人测试,chrome上无压力,其余全不兼容,不过可以使用W3C’s Intersection Observernpm install intersection-observer,然后在Observer.vue中加入require('intersection-observer');即可。

Demo在此:https://codesandbox.io/s/kxm8...


pfan
566 声望0 粉丝

bug人生,人生bug