Vue Virtual Scroller
是一个可复用的列表组件,解决了现实列表中的一些通用功能。通过精度它的源代码,学习如何编写可复用的VUE组件。具体使用方法就不搬运了,请查看项目文档。
#了解VUE插槽(slot)
Vue 将
<slot>
元素作为承载分发内容的出口。插槽内可以包含任何模板代码,包括 HTML或其它组件。
comp1模板
<slot></slot>
</div>
使用comp1模板
<comp1>
hello vue
</comp1>
hello vue
#可复用的列表组件
显然,利用插槽可以制作可复用组件。例如可以实现一个通用的列表组件,列表的行为是公共的、可复用的,列表中显示的内容使用组件时再指定。
基本实现思路是:将列表数据(items)传递给列表组件,组件中用
v-for
生成列表的框架,其中每个item通过
slot
展示,使用组件的代码中指定
slot
的内容。
<li v-for="item in items" :key="item.id">
<slot></slot>
这里出现1个问题,slot中的内容是和item相关的,所以替换slot的内容时必须能够访问item的数据。
<my-list :items="items">
<div>{{item.label}}</div>
</my-list>
但是这样写是无效的,因为:
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
VUE中提供了
作用域插槽
解决这个问题。为了让组件中数据(item)在父级的插槽内容中可用,我们可以将item作为元素的一个特性绑定上去:绑定在元素上的特性被称为
插槽 prop
。现在在父级作用域中,我们可以给 v-slot 带一个值来定义我们提供的插槽 prop 的名字。
<li v-for="item in items" :key="item.id">
<slot :item="item"></slot>
在父组件中默认访问子组件属性的方式:
<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>
v-observe-visibility="handleVisibilityChange"
class="vue-recycle-scroller"
:class="{
ready,
'page-mode': pageMode,
[`direction-${direction}`]: true,
@scroll.passive="handleScroll"
v-if="$slots.before"
class="vue-recycle-scroller__slot"
name="before"
ref="wrapper"
:style="{ [direction === 'vertical' ? 'minHeight' : 'minWidth']: totalSize + 'px' }"
class="vue-recycle-scroller__item-wrapper"
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"
:item="view.item"
:index="view.nr.index"
:active="view.nr.used"
v-if="$slots.after"
class="vue-recycle-scroller__slot"
name="after"
<ResizeObserver @notify="handleResize" />
</template>
有3个插槽,分别是before,default和after。默认插槽中定义了3个插槽属性:item,index和active。
: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
访问。
ref=
"wrapper"
:style=
"{ [direction === 'vertical' ? 'minHeight' : 'minWidth']: totalSize + 'px' }"
class=
"vue-recycle-scroller__item-wrapper"
组件中使用了自定义指令
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组件。
系统架构师
13.2k
粉丝