浅析VUE防抖与节流


Posted in Vue.js onNovember 24, 2020

防抖和节流到底是啥

函数防抖(debounce)

解释:当持续触发某事件时,一定时间间隔内没有再触发事件时,事件处理函数才会执行一次,如果设定的时间间隔到来之前,又一次触发了事件,就重新开始延时。

案例:持续触发scroll事件时,并不立即执行handle函数,当1000毫秒内没有触发scroll事件时,才会延时触发一次handle函数。

function debounce(fn, wait) {
 let timeout = null
 return function() {
 if(timeout !== null) clearTimeout(timeout)  
 timeout = setTimeout(fn, wait);
 }
}
function handle() { 
 console.log(Math.random())
}
window.addEventListener('scroll', debounce(handle, 1000))

addEventListener的第二个参数实际上是debounce函数里return回的方法,let timeout = null 这行代码只在addEventListener的时候执行了一次 触发事件的时候不会执行,那么每次触发scroll事件的时候都会清除上次的延时器同时记录一个新的延时器,当scroll事件停止触发后最后一次记录的延时器不会被清除可以延时执行,这是debounce函数的原理

函数节流(throttle)

解释:当持续触发事件时,有规律的每隔一个时间间隔执行一次事件处理函数。

案例:持续触发scroll事件时,并不立即执行handle函数,每隔1000毫秒才会执行一次handle函数。

function throttle(fn, delay) { 
 var prev = Date.now()   
 return function() {    
 var now = Date.now()    
 if (now - prev > delay) {     
  fn()    
  prev = Date.now()    
 }   
 }  
}  
function handle() {   
 console.log(Math.random())  
}
window.addEventListener('scroll', throttle(handle, 1000))

原理和防抖类似,每次执行fn函数都会更新prev用来记录本次执行的时间,下一次事件触发时判断时间间隔是否到达预先的设定,重复上述操作。

防抖和节流都可以用于 mousemove、scroll、resize、input等事件,他们的区别在于防抖只会在连续的事件周期结束时执行一次,而节流会在事件周期内按间隔时间有规律的执行多次。

浅析VUE防抖与节流

Vue中实践

在vue中实现防抖无非下面这两种方法

  • 封装utils工具
  • 封装组件

封装utils工具

把上面的案例改造一下就能封装一个简单的utils工具

utils.js

let timeout = null
function debounce(fn, wait) {
 if(timeout !== null) clearTimeout(timeout)
 timeout = setTimeout(fn, wait)
}
export default debounce

app.js

<input type="text" @input="debounceInput($event)">

import debounce from './utils'
export default {
 methods: {
 debounceInput(E){
  debounce(() => {
  console.log(E.target.value)
  }, 1000)
 }
 }
}

封装组件

至于组件的封装我们要用到$listeners、$attrs这两个属性,他俩都是vue2.4新增的内容,官网的介绍比较晦涩,我们来看他俩到底是干啥的:

$listeners: 父组件在绑定子组件的时候会在子组件上绑定很多属性,然后在子组件里通过props注册使用,那么没有被props注册的就会放在$listeners里,当然不包括class和style,并且可以通过 v-bind=”$attrs” 传入子组件的内部组件。

$listeners: 父组件在子组件上绑定的不含.native修饰器的事件会放在$listeners里,它可以通过 v-on=”$listeners” 传入内部组件。

简单来说$listeners、$attrs他俩是做属性和事件的承接,这在对组件做二次封装的时候非常有用。

我们以element-ui的el-input组件为例封装一个带防抖的debounce-input组件

debounce-input.vue

<template>
 <input v-bind="$attrs" @input="debounceInput"/>
</template>
<script>
export default {
 data() {
 return {
  timeout: null
 }
 },
 methods: {
 debounceInput(value){
  if(this.timeout !== null) clearTimeout(this.timeout)  
  this.timeout = setTimeout(() => {
  this.$emit('input', value)
  }, 1000)
 }
 }
}
</script>

app.vue

<template>
 <debounce-input placeholder="防抖" prefix-icon="el-icon-search" @input="inputEve"></debounce-input>
</template>
<script>
import debounceInput from './debounce-input'
export default {
 methods: {
 inputEve(value){
  console.log(value)
 }
 },
 components: {
 debounceInput
 }
}
</script>

上面组件的封装用了$attrs,虽然不需要开发者关注属性的传递,但是在使用上还是不方便的,因为把input封装在了内部这样对样式的限定也比较局限。有接触过react高阶组件的同学可能有了解,react高阶组件本质上是一个函数通过包裹被传入的React组件,经过一系列处理,最终返回一个相对增强的React组件。那么在vue中可以借鉴这种思路吗,我们来了解一下vue的函数式组件。

函数式组件

什么是函数式组件?

函数式组件是指用一个Function来渲染一个vue组件,这个组件只接受一些 prop,我们可以将这类组件标记为 functional,这意味着它无状态 (没有响应式数据),也没有实例 (没有this上下文)。

一个函数式组件大概向下面这样:

export default () => {
  functional: true, 
  props: { 
    // Props 是可选的
  },
  // 为了弥补缺少的实例, 提供第二个参数作为上下文
  render: function (createElement, context) {
    return vNode
  }
}

注意:在 2.3.0 之前的版本中,如果一个函数式组件想要接收 prop,则 props 选项是必须的。在 2.3.0 或以上的版本中,你可以省略 props 选项,所有组件上的特性都会被自动隐式解析为 prop。但是你一旦注册了 prop 那么只有被注册的 prop 会出现在 context.prop 里。

render函数的第二个参数context用来代替上下文this他是一个包含如下字段的对象:

  • props:提供所有 prop 的对象
  • children: VNode 子节点的数组
  • slots: 一个函数,返回了包含所有插槽的对象
  • scopedSlots: (2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。
  • data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
  • parent:对父组件的引用
  • listeners: (2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名。
  • injections: (2.3.0+) 如果使用了 inject 选项,则该对象包含了应当被注入的属性。

vm.$slots API 里面是什么

slots用来访问被插槽分发的内容。每个具名插槽 有其相应的属性 (例如:v-slot:foo 中的内容将会在 vm.$slots.foo 中被找到)。default 属性包括了所有没有被包含在具名插槽中的节点,或 v-slot:default 的内容。

slots() 和 children 对比

你可能想知道为什么同时需要 slots() 和 children。slots().default 不是和 children 类似的吗?在一些场景中,是这样——但如果是如下的带有子节点的函数式组件呢?

<my-functional-component>
  <p v-slot:foo>
    first
  </p>
  <p>second</p>
</my-functional-component>

对于这个组件,children 会给你两个段落标签,而 slots().default 只会传递第二个匿名段落标签,slots().foo 会传递第一个具名段落标签。同时拥有 children 和 slots(),因此你可以选择让组件感知某个插槽机制,还是简单地通过传递 children,移交给其它组件去处理。

一个函数式组件的使用场景

假设有一个a组件,引入了 a1,a2,a3 三个组件,a组件的父组件给a组件传入了一个type属性根据type的值a组件来决定显示 a1,a2,a3 中的那个组件。这样的场景a组件用函数式组件是非常方便的。那么为什么要用函数式组件呢?一句话:渲染开销低,因为函数式组件只是函数。

用函数式组件的方式来实现防抖

因为业务关系该防抖组件的封装同时支持 input、button、el-input、el-button 的使用,如果是input类组件对input事件做防抖处理,如果是button类组件对click事件做防抖处理。

const debounce = (fun, delay = 500, before) => {
  let timer = null
  return (params) => {
    timer && window.clearTimeout(timer)
    before && before(params)
    timer = window.setTimeout(() => {
      // click事件fun是Function input事件fun是Array
      if (!Array.isArray(fun)) {
        fun = [fun]
      }
      for (let i in fun) {
        fun[i](params)
      }
      timer = null
    }, parseInt(delay))
  }
}
export default {
  name: 'Debounce',
  functional: true, // 静态组件 当不声明functional时该组件同样拥有上下文以及生命周期函数
  render(createElement, context) {
    const before = context.props.before
    const time = context.props.time
    const vnodeList = context.slots().default
    if (vnodeList === undefined){
      console.warn('<debounce> 组件必须要有子元素')
      return null
    }
    const vnode = vnodeList[0] || null // 获取子元素虚拟dom
    if (vnode.tag === 'input') {
      const defaultFun = vnode.data.on.input
      const debounceFun = debounce(defaultFun, time, before) // 获取节流函数
      vnode.data.on.input = debounceFun
    } else if (vnode.tag === 'button') {
      const defaultFun = vnode.data.on.click
      const debounceFun = debounce(defaultFun, time, before) // 获取节流函数
      vnode.data.on.click = debounceFun
    } else if (vnode.componentOptions && vnode.componentOptions.tag === 'el-input') {
      const defaultFun = vnode.componentOptions.listeners.input
      const debounceFun = debounce(defaultFun, time, before) // 获取节流函数
      vnode.componentOptions.listeners.input = debounceFun
    } else if (vnode.componentOptions && vnode.componentOptions.tag === 'el-button') {
      const defaultFun = vnode.componentOptions.listeners.click
      const debounceFun = debounce(defaultFun, time, before) // 获取节流函数
      vnode.componentOptions.listeners.click = debounceFun
    } else {
      console.warn('<debounce> 组件内只能出现下面组件的任意一个且唯一 el-button、el-input、button、input')
      return vnode
    }
    return vnode
  }
}

原理也很简单就是在vNode中拦截on下面的click、input事件做防抖处理,这样在使用上就非常简单了。

自定义指令 directive

我们来思考一个问题,函数式组件封装防抖的关节是获取vNode,那么我们通过自定义指令同样可以拿到vNode,甚至还可以得到原生的Dom,这样用自定义指令来处理会更加方便。。。。。。

自定义指令:https://cn.vuejs.org/v2/guide/custom-directive.html

main.js

Vue.directive("dinput", {
 bind: function(el, binding, vnode) {
  let timeout = null;
  el.addEventListener("input", function() {
   if (timeout !== null) clearTimeout(timeout);
   timeout = setTimeout(function() {
    vnode.context[binding.expression]();
   }, 1000);
  });
 }
});

vue

<input type="text" v-dinput="myfunc"/>

js

export default {
 name: "App",
 data: function() {
  return {
   loginuser: null
  };
 },
 methods: {
  myfunc() {
   console.info("myfunc");
  }
 }
}

这种方式的缺点

调用方法时无法传参

以上就是浅析VUE防抖与节流的详细内容,更多关于VUE 防抖与节流的资料请关注三水点靠木其它相关文章!

Vue.js 相关文章推荐
如何使用 vue-cli 创建模板项目
Nov 19 Vue.js
vue+elementUI动态增加表单项并添加验证的代码详解
Dec 17 Vue.js
Vue组件简易模拟实现购物车
Dec 21 Vue.js
vuex的使用和简易实现
Jan 07 Vue.js
学习 Vue.js 遇到的那些坑
Feb 02 Vue.js
开发一个封装iframe的vue组件
Mar 29 Vue.js
vue点击弹窗自动触发点击事件的解决办法(模拟场景)
May 25 Vue.js
Vue鼠标滚轮滚动切换路由效果的实现方法
Aug 04 Vue.js
前端vue+express实现文件的上传下载示例
Feb 18 Vue.js
Vue监视数据的原理详解
Feb 24 Vue.js
vue cli4中mockjs在dev环境和build环境的配置详情
Apr 06 Vue.js
Vue router配置与使用分析讲解
Dec 24 Vue.js
解决vue页面刷新,数据丢失的问题
Nov 24 #Vue.js
Vue 的 v-model用法实例
Nov 23 #Vue.js
VUE+Element实现增删改查的示例源码
Nov 23 #Vue.js
Vue实现购物小球抛物线的方法实例
Nov 22 #Vue.js
vue自定义插件封装,实现简易的elementUi的Message和MessageBox的示例
Nov 20 #Vue.js
详解vue 组件注册
Nov 20 #Vue.js
vue-drawer-layout实现手势滑出菜单栏
Nov 19 #Vue.js
You might like
笑谈配置,使用Smarty技术
2007/01/04 PHP
php实现留言板功能(会话控制)
2017/05/23 PHP
JS中彻底删除JSON对象组成的数组中的元素
2020/09/22 PHP
基于Jquery实现的一个图片滚动切换
2012/06/21 Javascript
JQuery给元素添加/删除节点比如select
2013/04/02 Javascript
关闭ie窗口清除Session的解决方法
2014/01/10 Javascript
JS对象与json字符串格式转换实例
2014/10/28 Javascript
jQuery 3.0 的变化及使用方法
2016/02/01 Javascript
SelecT下拉框选中和取值的解决方法
2016/11/22 Javascript
Angular ng-repeat遍历渲染完页面后执行其他操作详细介绍
2016/12/13 Javascript
基于jquery日历价格、库存等设置插件
2020/07/05 jQuery
node实现简单的反向代理服务器
2017/07/26 Javascript
Vue组件之全局组件与局部组件的使用详解
2017/10/09 Javascript
Node.js中的cluster模块深入解读
2018/06/11 Javascript
浅谈webpack+react多页面开发终极架构
2018/11/11 Javascript
在python的WEB框架Flask中使用多个配置文件的解决方法
2014/04/18 Python
Python通过select实现异步IO的方法
2015/06/04 Python
Python基于pygame实现的弹力球效果(附源码)
2015/11/11 Python
有趣的python小程序分享
2017/12/05 Python
python自动下载图片的方法示例
2020/03/25 Python
Python中socket网络通信是干嘛的
2020/05/27 Python
Keras 快速解决OOM超内存的问题
2020/06/11 Python
Django 权限管理(permissions)与用户组(group)详解
2020/11/30 Python
python中HTMLParser模块知识点总结
2021/01/25 Python
CSS3轻松实现清新 Loading 效果的简单实例
2016/06/06 HTML / CSS
10个很棒的 CSS3 开发工具 推荐
2011/05/16 HTML / CSS
美国嘻哈文化生活方式品牌:GLD
2018/04/15 全球购物
美国基督教约会网站:ChristianCafe.com
2020/02/04 全球购物
2014年上半年工作自我评价
2014/01/18 职场文书
GMP办公室主任岗位职责
2014/03/14 职场文书
遗嘱继承公证书
2014/04/09 职场文书
目标管理责任书
2014/04/15 职场文书
爱国主义演讲稿
2014/05/07 职场文书
应届生求职自荐信范文
2015/03/04 职场文书
反腐倡廉心得体会2016
2016/01/13 职场文书
Python帮你解决手机qq微信内存占用太多问题
2022/02/15 Python