Vue Virtual Scroller是一个可复用的列表组件,解决了现实列表中的一些通用功能。通过精度它的源代码,学习如何编写可复用的VUE组件。具体使用方法就不搬运了,请查看项目文档。
了解VUE插槽(slot)
Vue 将 <slot>
元素作为承载分发内容的出口。插槽内可以包含任何模板代码,包括 HTML或其它组件。
comp1模板
<!--这是一个叫comp1的VUE组件模版-->
<div>
<slot></slot>
</div>
使用comp1模板
<comp1>
hello vue
</comp1>
渲染结果
<div>
hello vue
</div>
可复用的列表组件
显然,利用插槽可以制作可复用组件。例如可以实现一个通用的列表组件,列表的行为是公共的、可复用的,列表中显示的内容使用组件时再指定。
基本实现思路是:将列表数据(items)传递给列表组件,组件中用v-for
生成列表的框架,其中每个item通过slot
展示,使用组件的代码中指定slot
的内容。
列表组件
<li v-for="item in items" :key="item.id">
<slot></slot>
</li>
这里出现1个问题,slot中的内容是和item相关的,所以替换slot的内容时必须能够访问item的数据。
<my-list :items="items">
<div>{{item.label}}</div>
</my-list>
但是这样写是无效的,因为:
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
VUE中提供了作用域插槽解决这个问题。为了让组件中数据(item)在父级的插槽内容中可用,我们可以将item作为<slot>元素的一个特性绑定上去:绑定在<slot>元素上的特性被称为插槽 prop。现在在父级作用域中,我们可以给 v-slot 带一个值来定义我们提供的插槽 prop 的名字。
<li v-for="item in items" :key="item.id">
<slot :item="item"></slot>
</li>
在父组件中默认访问子组件属性的方式:
<my-list :items="items" v-slot="slotProps">
<div>{{slotProps.item.label}}</div>
</my-list>
但是这样写比较啰嗦,可以用解构插槽prop简化:
<my-list :items="items" v-slot="{ item }">
<div>{{item.label}}</div>
</my-list>
Vue Virtual Scroller
index.js
将3个组件(RecycleScroller,DynamicScroller,DynamicScrollerItem)注册为全局组件,也就是说它们在注册之后可以用在任何新创建的 Vue 根实例 (new Vue) 的模板中。
组件RecycleScroller.vue
<template>
<div
v-observe-visibility="handleVisibilityChange"
class="vue-recycle-scroller"
:class="{
ready,
'page-mode': pageMode,
[`direction-${direction}`]: true,
}"
@scroll.passive="handleScroll"
>
<div
v-if="$slots.before"
class="vue-recycle-scroller__slot"
>
<slot
name="before"
/>
</div>
<div
ref="wrapper"
:style="{ [direction === 'vertical' ? 'minHeight' : 'minWidth']: totalSize + 'px' }"
class="vue-recycle-scroller__item-wrapper"
>
<div
v-for="view of pool"
:key="view.nr.id"
:style="ready ? { transform: `translate${direction === 'vertical' ? 'Y' : 'X'}(${view.position}px)` } : null"
class="vue-recycle-scroller__item-view"
:class="{ hover: hoverKey === view.nr.key }"
@mouseenter="hoverKey = view.nr.key"
@mouseleave="hoverKey = null"
>
<slot
:item="view.item"
:index="view.nr.index"
:active="view.nr.used"
/>
</div>
</div>
<div
v-if="$slots.after"
class="vue-recycle-scroller__slot"
>
<slot
name="after"
/>
</div>
<ResizeObserver @notify="handleResize" />
</div>
</template>
有3个插槽,分别是before,default和after。默认插槽中定义了3个插槽属性:item,index和active。
<slot
:item="view.item"
:index="view.nr.index"
:active="view.nr.used"
/>
全局变量$slots
可以访问插槽中要分发的内容,命名插槽before和after通过这个变量判断是否有要分发的内容。
<div v-if="$slots.before" class="vue-recycle-scroller__slot">
<slot name="before" />
</div>
尽管存在 prop 和事件,有的时候你仍可能需要在 JavaScript 里直接访问一个子组件。为了达到这个目的,你可以通过 ref 特性为这个子组件赋予一个 ID 引用,并通过全局变量$refs
访问。
<div
ref="wrapper"
:style="{ [direction === 'vertical' ? 'minHeight' : 'minWidth']: totalSize + 'px' }"
class="vue-recycle-scroller__item-wrapper"
>
组件中使用了自定义指令
<div
v-observe-visibility="handleVisibilityChange"
...
>
...
</div>
import { ObserveVisibility } from 'vue-observe-visibility'
handleVisibilityChange (isVisible, entry) {
if (this.ready) {
if (isVisible || entry.boundingClientRect.width !== 0 || entry.boundingClientRect.height !== 0) {
this.$emit('visible')
requestAnimationFrame(() => {
this.updateVisibleItems(false)
})
} else {
this.$emit('hidden')
}
}
}
第2参数entry
是实现IntersectionObserverEntry接口的实现。通过这个接口可以判断对象相对于根元素或视窗(viewport)是否可见。这段代码就是当列表自身的可见性发生变化时,进行相应的处理。
if (this.emitUpdate) this.$emit('update', startIndex, endIndex)
更新列表显示内容时可以出发update
事件,通过startIndex和endIndex就可以知道显示到了哪些数据,这样如果需要就可以实现按需动态加载数据。
组件DynamicScroller.vue
<template>
<RecycleScroller
ref="scroller"
:items="itemsWithSize"
:min-item-size="minItemSize"
:direction="direction"
key-field="id"
v-bind="$attrs"
@resize="onScrollerResize"
@visible="onScrollerVisible"
v-on="listeners"
>
<template slot-scope="{ item: itemWithSize, index, active }">
<slot v-bind="{ item: itemWithSize.item, index, active, itemWithSize }"/>
</template>
<template slot="before">
<slot name="before" />
</template>
<template slot="after">
<slot name="after" />
</template>
</RecycleScroller>
</template>
组件DynamicScroller把组件RecycleScroller组件包裹了一层,把自己接收的slot内容再穿入RecycleScroller组件。
在 <template> 上使用特殊的 slot-scope 特性,可以接收传递给插槽的 prop。这个用法自2.6.0版本已经废弃了,现在用v-slot
替代。
参考
https://cn.vuejs.org/v2/guide/components-slots.html
https://github.com/Akryum/vue-observe-visibility
https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserverEntry
网友评论