各位大佬,对于vue中渲染列表时,我想知道如何比较友好的产生“元素进入可视区事件”?

举例说明:
如图所示,绿色中框为用户可视区(ul),9个蓝色框分别为信息条(li)。
可视区限制一定高度,产生纵向滚动条。2-7元素在可视区中,认为用户已读。1元素在上方,则忽略即认为已读。8、9在下方,还未出现,则认为未读。

我想知道,有没有什么事件可以绑定在li上,当8、9滑动到可视区时自动产生事件将其更为已读。

目前只能想到两个方法。
其一:

<ul class="messages">
<li v-for="message in messages" v-bind:id="message.id"><li>
</ul>
<script>
export default {
name: 'App',
data(){
messages: []
},
methods:{
onScroll(){
let range = [$(".messages").offset().top,$(".messages").offset().top+$(".messages").height()];
$('.messages li').each(function(){
// 判断元素进入可视区
if($(this).offset().top >= range[0] && $(this).offset().top <= range[1]){
let id = $(this).prop('id');
for(let i in this.messages){
if(this.messages[i].id == id){
this.messages[i].status = 'read';
}
}
}
});
}
}
}
//
</script>

简单说说,这个方法,外露一个id属性,给js去遍历,然后再更新到data。
因为需要外露一个属性,感觉不是很好。

其二:
这个可以隐藏id,但是感觉也不是很好。

<ul class="messages">
<li v-for="message in messages" v-bind:id="message.id" @click="message.status='read'"><li>
</ul>
<script>
export default {
name: 'App',
data(){
messages: []
},
methods:{
onScroll(){
let range = [$(".messages").offset().top,$(".messages").offset().top+$(".messages").height()];
$('.messages li').each(function(){
// 判断元素进入可视区
if($(this).offset().top >= range[0] && $(this).offset().top <= range[1]){
$(this).click();
}
});
}
}
}
//
</script>

此方法根据方法一稍微改改,通过click事件触发已读功能。(本想用焦点进入、其他事件、自定义事件结果都不好使)
我觉得这个相比比方法一好,目前也在用。缺点是如果li如果有其他事件,就需要混写在一起,感觉不好
其次是,滚动事件每一次都要全量foreach一遍页面元素,感觉这个不是很妙。

各位sf的大佬,有没有好的解决方案呢?

我搜索时还发现一个“IntersectionObserver”相关的,由于不是专业前端,不太清楚这个也没细研究。

回答

使用 Intersection Observer,我以前写过一篇博客,可以看一下:《Intersection Observer 笔记》。简单来说,就是浏览器提供了一个原生 API 可以监控一个 DOM 的显示/隐藏,及百分比,接下来我们就可以组合使用,实现一些功能。写成代码大概是这样:

// 声明一个实例
// 因为我的视口即当前 viewport,所以这里不需要 `options`
const observer = new IntersectionObserver(entries => {
  // 遍历所有实例,如果它显示出来,即 intersectionRatio 显示比例大于 0
  // 那么就让它 `dispatch('visible')`
  entries.forEach(({target, intersectionRatio}) => {
    const event = new CustomEvent('visible', {
      detail: {
        isVisible: intersectionRatio > 0,
      },
    });
    target.dispatchEvent(event);
  });
});

// 然后可以在 Vue 里侦听这个事件
export default {
  template: '<div @visible="onVisible"></div>';
}

getBoundingClientRect用于获取某个元素相对于视窗的位置集合。集合中有top, right, bottom, left等属性。可以查一下mdn

直接用ref取就可以

点我测试

<template>
  <div class="container">
    <ul ref="ul">
      <li v-for="item in messages" :key="item.id" :ref="item.id">
        <div>{{ item.status }}</div>
        {{ item.id }}
      </li>
    </ul>
    <button @click="viewStatus">查看信息状态</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      messages: [],
      unreadIndex: 0, //未读起始索引
      ulHeight: 0,
    };
  },

  components: {},

  computed: {},
  created() {
    for (let i = 0; i < 9; i++) {
      this.messages.push({
        id: i + 1,
        status: "未读",
      });
    }
  },
  mounted() {
    this.ulHeight = this.$refs.ul.offsetHeight;
    this.checkStatus(this.unreadIndex);
    this.$refs.ul.onscroll = (e) => {
      this.checkStatus(this.unreadIndex);
    };
  },

  methods: {
    viewStatus() {
      console.log("查看所有信息状态:");
      for (let msg of this.messages) {
        console.log(`${msg.id}:${msg.status}`);
      }
    },
    checkStatus(index) {
      for (let i = index; i < this.messages.length; i++) {
        let msgOffsetop =
          this.$refs[this.messages[i].id][0].offsetTop -
          this.$refs.ul.scrollTop;
        if (msgOffsetop < this.ulHeight) {
          //进入过可视区的
          this.$set(this.messages, i, {
            id: this.messages[i].id,
            status: "已读",
          });
        } else {
          this.unreadIndex = i;
          break;
        }
      }
    },
  },
};
</script>
<style lang="less" scoped>
.container {
  text-align: center;
  ul {
    position: relative;
    list-style: none;
    padding: 0;
    width: 200px;
    height: 300px;
    margin: 50px auto;
    border: 1px solid green;
    overflow: auto;
    li {
      position: relative;
      padding: 20px 0;
      &:nth-child(odd) {
        background: #e3f9fd;
      }
      & > div {
        position: absolute;
        right: 5px;
      }
    }
  }
}
</style>

使用 getBoundingClientRect 而不是 Intersection Observer, getBoundingClientRect 有更好的兼容性。

/**
 * @description 获取元素相对与浏览器视口的位置
 * @param {Object} client document对象
 * @returns top, bottom, left, right, height, width
 */
function getClientRect(el) {
    const {
        top,
        bottom,
        left,
        right,
        height,
        width,
    } = el.getBoundingClientRect()
    return {
        top,
        bottom,
        left,
        right,
        height: height || bottom - top,
        width: width || right - left,
    }
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注

Scroll Up