首页IT科技var let for(Varlet组件实现一个丝滑的点击水波效果详解)

var let for(Varlet组件实现一个丝滑的点击水波效果详解)

时间2025-06-13 18:24:19分类IT科技浏览3786
导读:正文 读完本篇,可以了解到如何使用一个div创建一个点击的水波效果。...

正文

读完本篇            ,可以了解到如何使用一个div创建一个点击的水波效果            。

Varlet组件库提供了一个使元素点击时生成水波扩散效果的指令:

<template> <div v-ripple>点击</div> </template>

接下来就从源码角度看看它是如何实现的                  。

首先在指令所绑定的目标元素被挂载的时候会执行如下方法:

function mounted(el: RippleHTMLElement, binding: DirectiveBinding<RippleOptions>) { // 给元素上添加一个对象记录一些数据 el._ripple = { tasker: null, ...(binding.value ?? {}), touchmoveForbid: binding.value?.touchmoveForbid ?? context.touchmoveForbid, removeRipple: removeRipple.bind(el), } // 给元素绑定了一些事件 el.addEventListener(touchstart, createRipple, { passive: true }) el.addEventListener(touchmove, forbidRippleTask, { passive: true }) el.addEventListener(dragstart, removeRipple, { passive: true }) document.addEventListener(touchend, el._ripple.removeRipple, { passive: true }) document.addEventListener(touchcancel, el._ripple.removeRipple, { passive: true }) }

主要就是绑定了一些事件                  ,处理函数一共有三个      ,从函数名中也可以大致看出其作用      。

注意看addEventListener方法的第三个参数中都设置了passive = true         ,这个选项用来告诉浏览器我们的处理函数中不会调用preventDefault方法                  ,这么做有什么好处呢?比如touch事件或scroll事件的默认行为都会触发页面的滚动         ,如果调用了preventDefault方法      ,那么就会阻止滚动                  ,但问题是浏览器并不知道我们有没有在事件处理函数中调这个方法            ,那么就必须等待函数执行完毕才知道   ,有时候函数的执行是比较耗时的                  ,这样就会导致页面卡顿               ,所以如果我们的处理函数中明确不会调用preventDefault方法,那么就通过passive标志直接告诉浏览器               ,这样浏览器就不会等待                  ,直接进行滚动   ,可以显著提升页面性能和体验         。

touchstart 事件处理

createRipple方法

先看看touchstart事件的处理方法createRipple:

function createRipple(this: RippleHTMLElement, event: TouchEvent) { // 首先获取该元素上存储的数据 const _ripple = this._ripple as RippleOptions // 先移除上一个水波 _ripple.removeRipple() // 如果禁用或者上一个水波任务还未执行则返回 if (_ripple.disabled || _ripple.tasker) { return } // 水波任务 const task = () => { // ... } // 保存定时器 _ripple.tasker = window.setTimeout(task, 60) }

当我们触摸点击一个元素的时候            ,会先移除该元素的上一个水波                  ,然后添加一个新的水波任务      ,这个任务会在一个60ms的定时器后执行         ,然后把定时器id保存起来                  ,为什么不立即执行呢         ,应该是为了能够取消吧      ,比如想在touchmove情况下不开启水波效果                  ,那么就可以通过取消这个定时器来实现            ,看一下touchmove事件的处理函数forbidRippleTask:

forbidRippleTask 方法

function forbidRippleTask(this: RippleHTMLElement) { const _ripple = this._ripple as RippleOptions // 是否需要在触摸移动时禁用水波效果 if (!_ripple.touchmoveForbid) { return } // 如果在60ms内触摸移动了就会取消定时器   ,自然水波效果就不会有了 _ripple.tasker && window.clearTimeout(_ripple.tasker) _ripple.tasker = null }

接下来看看task方法:

function createRipple(this: RippleHTMLElement, event: TouchEvent) { //... const task = () => { // 定时器任务执行了则把保存的定时器id清空 _ripple.tasker = null // 计算一些数据 const { x, y, centerX, centerY, size }: RippleStyles = computeRippleStyles(this, event) // 创建一个div const ripple: RippleHTMLElement = document.createElement(div) // 添加一个var-ripple类名 ripple.classList.add(n()) // 设置透明度为0                  ,即全透明 ripple.style.opacity = `0` // 设置位置及缩放 ripple.style.transform = `translate(${x}px, ${y}px) scale3d(.3, .3, .3)` // 设置大小 ripple.style.width = `${size}px` ripple.style.height = `${size}px` // 设置颜色 _ripple.color && (ripple.style.backgroundColor = _ripple.color) // 记录创建时间 ripple.dataset.createdAt = String(performance.now()) // 设置被点击元素的样式 setStyles(this) // 将水波元素添加到被点击元素内 this.appendChild(ripple) // 20ms后修改水波元素的样式               ,达到水波的扩散动画效果 window.setTimeout(() => { ripple.style.transform = `translate(${centerX}px, ${centerY}px) scale3d(1, 1, 1)` ripple.style.opacity = `.25` }, 20) } //... }

可以看到所谓水波就是一个div,总体的流程为先创建一个div元素               ,然后设置它的透明度为0            、初始位置                  、缩放      、大小         、背景颜色                  ,然后添加为被点击元素的子元素   ,最后在20ms以后修改div的位置                  、缩放         、透明度            ,只要设置了它的transation过渡属性即可实现过渡效果                  ,也就是水波扩散的效果      ,样式是通过类名var-ripple设置的:

:root { --ripple-cubic-bezier: cubic-bezier(0.68, 0.01, 0.62, 0.6); --ripple-color: currentColor; } .var-ripple { position: absolute;// 设置为绝对定位 transition: transform 0.2s var(--ripple-cubic-bezier), opacity 0.14s linear;// 设置过渡效果 top: 0; left: 0; border-radius: 50%;// 设置为圆形 opacity: 0; will-change: transform, opacity; pointer-events: none;// 禁止响应鼠标事件 z-index: 100; background-color: var(--ripple-color);// 背景颜色 }

可以看到水波元素为绝对定位         ,另外位置的过渡时间为200ms                  ,透明度的过渡时间为140ms                  。

接下来看看其中调用的几个函数         。

调用computeRippleStyles方法计算

首先是调用computeRippleStyles方法计算一些基本数据:

function computeRippleStyles(element: RippleHTMLElement, event: TouchEvent): RippleStyles { // 被点击元素距离屏幕顶部和左侧的距离 const { top, left }: DOMRect = element.getBoundingClientRect() // 被点击元素的宽高 const { clientWidth, clientHeight } = element // 计算水波圆的半径 const radius: number = Math.sqrt(clientWidth ** 2 + clientHeight ** 2) / 2 // 直径 const size: number = radius * 2 // ... }

水波的直径是根据勾股定理计算的:

function computeRippleStyles(element: RippleHTMLElement, event: TouchEvent): RippleStyles { // ... // 手指点击的位置相对于被点击元素的坐标 const localX: number = event.touches[0].clientX - left const localY: number = event.touches[0].clientY - top // 水波元素初始位置 const x: number = localX - radius const y: number = localY - radius // 水波元素最终位置 const centerX: number = (clientWidth - radius * 2) / 2 const centerY: number = (clientHeight - radius * 2) / 2 return { x, y, centerX, centerY, size } }

size为水波圆的直径;

手指点击的位置是水波圆初始的中心点         ,然后计算其左上角坐标x      、y为水波元素的初始位置;

水波圆的最终中心点其实就是被点击元素的中心点      ,换算成左上角坐标centerX                  、centerY即为水波元素的最终位置      。

因为水波元素为被点击元素的子元素                  ,所以这些坐标都是相对于被点击元素的左上角坐标计算的:

从绿色的圆过渡成红色的圆            ,透明度            、大小   、位置的变化就是水波的扩散效果                  。

调用setStyles方法

将水波元素添加到被点击元素内前还调用了setStyles方法:

function setStyles(element: RippleHTMLElement) { const { zIndex, position } = window.getComputedStyle(element) element.style.overflow = hidden element.style.overflowX = hidden element.style.overflowY = hidden position === static && (element.style.position = relative) zIndex === auto && (element.style.zIndex = 1) }

这个函数做的事情主要是检查和设置被点击元素的一些样式   ,首先溢出需要设置为隐藏                  ,否则水波圆的扩散就会溢出元素完整显示出来               ,这显然不好看,然后前面提到过水波元素为绝对定位               ,所以被点击元素的定位不能是静态定位                  ,最后的层级设置笔者暂时没有想出来是为了解决什么问题            。

removeRipple方法

到这里   ,当我们手触摸元素时            ,水波效果就创建完成了                  ,接下来是移除操作      ,看一下removeRipple方法:

const ANIMATION_DURATION = 250 function removeRipple(this: RippleHTMLElement) { const _ripple = this._ripple as RippleOptions const task = () => { // 获取水波元素 const ripples: NodeListOf<RippleHTMLElement> = this.querySelectorAll(`.${n()}`) if (!ripples.length) { return } // 最后一个水波 const lastRipple: RippleHTMLElement = ripples[ripples.length - 1] // 计算延迟时间 const delay: number = ANIMATION_DURATION - performance.now() + Number(lastRipple.dataset.createdAt) // 延迟后将水波的透明度设置为0 setTimeout(() => { lastRipple.style.opacity = `0` // 再次延迟后移除水波元素 setTimeout(() => lastRipple.parentNode?.removeChild(lastRipple), ANIMATION_DURATION) }, delay) } // 创建任务的定时器id存在则等待60ms _ripple.tasker ? setTimeout(task, 60) : task() }

先回顾一下创建水波的各个阶段的耗时         ,当我们第一次点击元素时                  ,等待60ms后会创建水波元素         ,然后再等待20ms后会开始进行水波的扩散效果      ,动画耗时200ms结束                  ,如果我们在60ms内进行第二次点击不会创建第二个水波            ,因为前一个水波任务还未执行   ,如果是在60ms后第二次点击                  ,会先调用removeRipplie移除上一个水波               ,然后重复第一个水波的创建流程:

每次执行removeRipple方法只需要移除当前最后一个水波即可,之前的水波会由之前的task移除   。

接下来详细看看整个过程                  。

当手指第一次触摸点击元素时会执行createRipple方法               ,方法内会先执行removeRipple方法                  ,此时_ripple.tasker不存在   ,会立即执行removeRipple的task方法            ,但是目前并没有水波元素                  ,所以这个函数会直接返回      ,removeRipple方法执行完毕               。

接下来会创建一个60ms的定时器         ,等待执行createRipple的task                  ,如果我们在60ms内就松开了手指         ,那么又会执行removeRipple方法      ,此时_ripple.tasker存在                  ,所以removeRipple的task方法也会等待60ms再执行;如果我们是在60ms后才松开手指            ,那么_ripple.tasker不存在   ,会立即执行removeRipple的task方法                  ,该方法内会获取最后一个水波元素               ,也就是刚刚创建的水波元素,然后计算delay:

delay = ANIMATION_DURATION - (performance.now() - Number(lastRipple.dataset.createdAt))

performance.now() - Number(lastRipple.dataset.createdAt)代表此刻到创建水波时过去的时间               ,ANIMATION_DURATION减去它即表示250ms还剩下的时间                  ,因为前面提到了水波从创建到扩散完成整个过程大概耗时20ms + 200ms = 220ms   ,所以延迟dealy时间            ,也就是等待水波动画完成后再让水波消失                  ,避免水波还未扩散完成就消失的情况      ,修改水波的透明度为0         ,透明度动画耗时140ms                  ,所以再等待250ms将水波元素移除。

如果在60ms内松开手指又立即再次触摸元素         ,那么又会执行createRipple方法      ,同样又会先执行removeRipple方法                  ,此时前一个创建水波的task任务还未执行            ,_ripple.tasker存在   ,所以removeRipple的task方法会等待60ms再执行                  ,这个task任务其实和松开手指时触发的task任务重复了               ,相当于两个task移除同一个水波元素,不过问题也不大               。

因为上一个水波的task还未执行,所以createRipple会直接返回                  。

如果在60ms后再次触摸元素               ,执行removeRipple时_ripple.tasker不存在                  ,会立即执行task方法   ,同样            ,这个task任务也会和松开手指触发的task任务重复   。

此时_ripple.tasker不存在                  ,所以创建第二个水波的任务会被添加到定时器里      ,当第二次松开手指时         ,执行removeRiplle会删除第二个水波            。

更多次重复触摸元素时以此类推                  ,会不断创建水波         ,水波动画结束后也会不断被删除                  。

在目标元素被卸载时会执行unmounted方法:

function unmounted(el: RippleHTMLElement) { el.removeEventListener(touchstart, createRipple) el.removeEventListener(touchmove, forbidRippleTask) el.removeEventListener(dragstart, removeRipple) document.removeEventListener(touchend, el._ripple!.removeRipple) document.removeEventListener(touchcancel, el._ripple!.removeRipple) }

主要是移除绑定的事件      。

到这里      ,水波效果的创建和移除就都介绍完了                  ,可以看到这种实现方式对目标元素还是有一定要求的            ,如果目标元素的样式布局需要设置position                  、overflow               、z-index属性为不符合要求的值   ,那么直接修改可能就会导致样式出现问题                  ,并且卸载时也没有进行恢复               ,这是不是也算是一个小bug         。

以上就是Varlet组件实现一个丝滑的点击水波效果详解的详细内容,更多关于Varlet组件点击水波的资料请关注本站其它相关文章!

声明:本站所有文章               ,如无特殊说明或标注                  ,均为本站原创发布                  。任何个人或组织   ,在未征得本站同意时            ,禁止复制、盗用               、采集                  、发布本站内容到任何网站   、书籍等各类媒体平台         。如若本站内容侵犯了原著者的合法权益                  ,可联系我们进行处理      。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
csrss.exe(csrss.exe是什么进程?有没有病毒?) 第一调查网如何注册(如何注册第一调查网才能赚到钱-简单动动手指头,就有“大神”带你“躺”着赚钱?庐山一男子轻信网上“投资理财”被骗)