编程实践你不知道的JavaScriptVue.js专区

精读vue-virtual-scroller源代码,学习制作可复

2019-08-18  本文已影响1人  全栈顾问

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

上一篇 下一篇

猜你喜欢

热点阅读