美文网首页编程实践你不知道的JavaScriptVue.js专区
精读vue-virtual-scroller源代码,学习制作可复

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

作者: 全栈顾问 | 来源:发表于2019-08-18 19:16 被阅读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

    相关文章

      网友评论

        本文标题:精读vue-virtual-scroller源代码,学习制作可复

        本文链接:https://www.haomeiwen.com/subject/wxqcsctx.html