国内的组件库都用太多了,于是想翻翻国外的看看有没有什么有意思的东西,看了看大致都大同小异。本文是基于vuetify组件库的源码,找了部分几个比较有意思的组件实现,对核心的实现方法和代码进行解析。
过渡样式:transition
过渡样式的主要实现思路都是使用了vue的transition进行封装。
思路一:transition的基础使用
是直接使用写在全局的scss里的样式,直接在所有需要使用的组件加上transition,进行包裹即可。这也是我们平时项目开发常用的实现方式。
//下面是对应写在全局的scss样式
.scale-rotate-transition {
&-enter-active,
&-leave-active {
transition: all .3s ease;
}
&-move {
transition: transform .6s;
}
&-enter, &-leave, &-leave-to {
opacity: 0;
transform: scale(0) rotate(-45deg);
}
}
复制代码
//直接引用即可
<transition name="scale-rotate-transition">
<p v-if="show">hello</p>
</transition>
复制代码
思路二:函数时组件封装transition
封装一个函数式组件,将transition的内容封装至render中
//函数式组件,源代码使用ts进行封装的
export function createJavascriptTransition (
name: string,
functions: Record<string, any>,
mode = 'in-out'
): FunctionalComponentOptions {
return {
name,
functional: true,
props: {
mode: {
type: String,
default: mode,
},
},
render (h, context): VNode {
//参数里的h是createElement函数,具体可以参照vue官方文档
return h(
'transition',
//mergeData是组件库封装的合并对象的写法,会以右边的对象的值为主,合并左边参数传入的对象
mergeData(context.data, {
//具体的transition的name所包含的样式都是提前写到全局的样式里面的
props: { name },
on: functions,
}),
context.children
)
},
}
}
复制代码
封装好之后再在需要添加过渡的组件中。
function testCompoennt(Vue){
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(createSimpleTransition('tab-reverse-transition'), {...},
//content是包裹的内容
[content])
},
props: {
//......
}
})
}
复制代码
然后再统一封装所有组件至install中,再将封装好的函数传入Vue.use()中进行调用
//大致是这样,源码里很多个文件传来传去的,最终就是输出一个install
const component = {
install:testCompoennt
}
// 然后用在vue.use中使用该组件即可引入
Vue.use(component)
复制代码
然后就可以在vue的单文件组件中的html中调用
<anchored-heading></anchored-heading>
引入和注册方式省略...
复制代码
懒加载组件:v-lazy
v-lazy主要功能是,给你需要懒加载的组件,当当前窗口滚动到组件的视图范围内再进行加载。
主要的实现是使用到了Intersection Observer这个方法。目前兼容性大部分主流浏览器都支持。
这个api是用来监听两个不同的元素是否相交,如果相交的时候会触发相对应的回调事件。懒加载的实现就是判断元素与屏幕可视范围相交时,再将元素显示出来即可。
源代码较多,我提取了一下,主要逻辑是以下部分
Vue.directive('lazy', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
//源代码中options是可以通过传参定制的
let options = {
root: null, //options里的root的参数默认为根元素
rootMargin: '0px',
threshold: 1.0
}
let callback =(entries) => {
entries.forEach(entry => {
console.log(entry);
//然后如果判断entry.isIntersecting为true的话,就把元素显示出来
});
};
let observer = new IntersectionObserver(callback, options);
let target = el;
observer.observe(target);
}
})
复制代码
虚拟列表:Virtual scroller
虚拟列表是长列表的优化,假设现在又一万条数据的列表,实际渲染的时候只渲染视图内的元素,然后还能照常上下滚动。
具体调用的时候代码如下
<v-virtual-scroll
:bench="benched"
:items="items"
height="300"
item-height="64"
>
<template v-slot:default="{ item }">
<!--列表元素样式-->
</template>
</v-virtual-scroll>
复制代码
然后源码具体实现如下,具体仔细的逻辑我都备注了。大致的整体实现逻辑就是
- 1.先设定好窗口的高度,以及列表里每个元素的固定高度,最外面的元素设定position:relative
- 2.计算当前滚动距离,应该窗口中应该显示列表中从第几位到第几位的元素
- 3.将传入的列表进行裁剪,只显示当前窗口需要显示的元素,每个元素的位置用absolute+top来设定位置,top根据元素的固定高度和在当前列表的index来计算
- 4.添加滚动监听,重复第2、3步
Vue.extend({
//...省略部分代码
computed: {
__bench (): number {
return parseInt(this.bench, 10)
},
__itemHeight (): number {
return parseInt(this.itemHeight, 10)
},
firstToRender (): number {
return Math.max(0, this.first - this.__bench)
},
lastToRender (): number {
return Math.min(this.items.length, this.last + this.__bench)
},
},
watch: {
height: 'onScroll', //窗口高度
itemHeight: 'onScroll', //元素高度
},
mounted () {
this.last = this.getLast(0)
},
methods: {
getChildren (): VNode[] {
//firstToRender是需要显示的首个元素的index
//lastToRender是当前显示的最后一个元素的index,只截取需要显示的部分
return this.items.slice(
this.firstToRender,
this.lastToRender,
).map(this.genChild)
},
genChild (item: any, index: number) {
index += this.firstToRender
//里面的元素用top来控制显示的位置,这样就可以保证视图范围内一直有需要列表内容显示
const top = convertToUnit(index * this.__itemHeight)
return this.$createElement('div', {
staticClass: 'v-virtual-scroll__item',
//赋予每一个单独的元素一个top
style: { top },
key: index,
}, getSlot(this, 'default', { index, item }))
},
getFirst (): number {
return Math.floor(this.scrollTop / this.__itemHeight)
},
getLast (first: number): number {
const height = parseInt(this.height || 0, 10) || this.$el.clientHeight
return first + Math.ceil(height / this.__itemHeight)
},
//监听元素的滚动事件
onScroll () {
this.scrollTop = this.$el.scrollTop //只要滚动了就会重新去调用获取当前视图第一个和最后一个元素的index
this.first = this.getFirst()
this.last = this.getLast(this.first)
},
},
//每次数据更新的时候都会重新触发render
render (h): VNode {
//content为最终渲染的内容
const content = h('div', {
staticClass: 'v-virtual-scroll__container',
style: {
//里面的height是用来设置整体内容的高度的,所以要把整个数组的长度都算进去,这样滚动条才会正常
height: convertToUnit((this.items.length * this.__itemHeight)),
},
}, this.getChildren()) //具体显示的内容从getChildren中获取
return h('div', {
staticClass: 'v-virtual-scroll',
style: this.measurableStyles,
directives: [{
name: 'scroll',
modifiers: { self: true },
value: this.onScroll,
}],
on: this.$listeners,
}, [content])
}
})
复制代码
点击元素外的检测:click outside
点击元素外的检测这个功能在很多弹窗类、冒泡类的组件经常会用到。比如下图的这种弹窗,需要你点击页面的任意非弹窗的地方,都将弹窗隐藏起来。
大致实现思路其实很简单:
- 1.通过点击事件来获取当前点击的元素
- 2.判断绑定改命令的元素的是否包含了点击元素。用node.contains(otherNode)来实现检查元素的包含关系
源码解析如下:
import { attachedRoot } from '../../util/dom' //这个方法是遍历最顶层的节点
import { VNodeDirective } from 'vue/types/vnode'
interface ClickOutsideBindingArgs {
handler: (e: Event) => void //判断点击为外面时会触发的事件
closeConditional?: (e: Event) => boolean //可以设置这个为false时,整个点击外部事件的方法都不生效
include?: () => HTMLElement[] //可以把不需要触发事件的元素传进来
}
interface ClickOutsideDirective extends VNodeDirective {
value?: ((e: Event) => void) | ClickOutsideBindingArgs
}
function defaultConditional () {
return true
}
function checkEvent (e: PointerEvent, el: HTMLElement, binding: ClickOutsideDirective): boolean {
// 因为接下来遍历的方法很耗费资源,所以在这里先检察一遍是否有传入元素,且closeConditional不为false,否则直接返回
if (!e || checkIsActive(e, binding) === false) return false
//这里是用来判断我们点击的dom元素和页面显示的dom元素是不是属于同一个树,避免点击的是shadowroot里面的节点。
const root = attachedRoot(el)
if (root instanceof ShadowRoot && root.host === e.target) return false
// elements的元素是最终用来遍历点击的元素包不包含在里面,
// 1.先把不需要触发事件的includes传进来的元素放进elements
const elements = ((typeof binding.value === 'object' && binding.value.include) || (() => []))()
// 2.然后把绑定事件的元素el放入elements
elements.push(el)
// 判断elements里的所有元素是否有点击事件e.target的元素
return !elements.some(el => el.contains(e.target as Node))
}
//检查绑定的元素是否有value和closeConditional的值,都为true才进行下一步
function checkIsActive (e: PointerEvent, binding: ClickOutsideDirective): boolean | void {
const isActive = (typeof binding.value === 'object' && binding.value.closeConditional) || defaultConditional
return isActive(e)
}
//点击外面时,回调事件的处理
function directive (e: PointerEvent, el: HTMLElement, binding: ClickOutsideDirective) {
const handler = typeof binding.value === 'function' ? binding.value : binding.value!.handler
el._clickOutside!.lastMousedownWasOutside && checkEvent(e, el, binding) && setTimeout(() => {
checkIsActive(e, binding) && handler && handler(e)
}, 0)
}
//查找根元素
function handleShadow (el: HTMLElement, callback: Function): void {
const root = attachedRoot(el)
callback(document.body)
if (root instanceof ShadowRoot) {
callback(root)
}
}
export const ClickOutside = {
inserted (el: HTMLElement, binding: ClickOutsideDirective) {
const onClick = (e: Event) => directive(e as PointerEvent, el, binding)
const onMousedown = (e: Event) => {
//!.是typescript的写法,避免元素为空
el._clickOutside!.lastMousedownWasOutside = checkEvent(e as PointerEvent, el, binding)
}
handleShadow(el, (app: HTMLElement) => {
app.addEventListener('click', onClick, true)
app.addEventListener('mousedown', onMousedown, true)
})
el._clickOutside = {
lastMousedownWasOutside: true,
onClick,
onMousedown,
}
},
unbind (el: HTMLElement) {
if (!el._clickOutside) return
handleShadow(el, (app: HTMLElement) => {
if (!app || !el._clickOutside) return
app.removeEventListener('click', el._clickOutside.onClick, true)
app.removeEventListener('mousedown', el._clickOutside.onMousedown, true)
})
delete el._clickOutside
},
}
export default ClickOutside
****
复制代码
看完觉得有收获的老哥麻烦点个赞呗~
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END