前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >什么,lodash 的防抖失效了?

什么,lodash 的防抖失效了?

作者头像
Nealyang
发布2020-12-03 11:08:42
1.5K0
发布2020-12-03 11:08:42
举报
文章被收录于专栏:全栈前端精选全栈前端精选

背景

在使用 uni-app 开发小程序时,有个填写表单的需求,包含两个输入框,看起来像这样

image-20201107143814796

两个在普通不过的输入框

因为需要复用一些样式和逻辑,所以将输入框抽象成了组件,代码简化后如下

代码语言:javascript
复制
<template>
  <view>
    <custom-textarea v-model="text1"></custom-textarea>
    <custom-textarea v-model="text2"></custom-textarea>
  </view>
</template>

<script>
import CustomTextarea from '@/pages/index/CustomTextarea'
import { api } from '@/api'
export default {
  components: { CustomTextarea },
  data() {
    return {
      text1: '',
      text2: '',
    }
  },
  watch: {
    async text1(newVal) {
      await api(newVal)
    },
    async text2(newVal) {
      await api(newVal)
    },
  },
}
</script>

子组件代码

代码语言:javascript
复制
<template>
  <textarea :value="value" class="textarea" @input="handleInput"></textarea>
</template>

<script>
import { debounce } from 'lodash'

export default {
  name: 'CustomTextarea',
  props: {
    value: {
      type: String,
    },
  },
  methods: {
    handleInput: debounce(function (e) {
      console.log('value', e.target.value)
      this.$emit('input', e.target.value)
    },1000),
  },
}
</script>

<style lang="less" scoped>
.textarea {
  border: 2rpx solid red;
}
</style>

由于在父组件中需要依赖输入的值请求接口,为避免接口频繁调用,这边引入 lodash debounce 用于防抖

这个需求在 yeyan1996 眼中没有任何难度,但在几天后却收到了部分用户反馈,说在两个输入框分别填写了值,但最终只有一个输入框有效

这时才回头想起代码中的 debounce ....

问题原因

收到用户反馈后,yeyan1996 尝试多次点击输入框,发现问题并不是必现,最终总结出了规律

填写第一个输入框后,快速对第二个输入框进行输入,才会造成最终的表单数据中只有一个值的问题

通过下图子组件的 log 可以看到,虽然 ui 界面显示两个输入框都有值,但实际只触发了第二个输入框的 log

Kapture 2020-11-07 at 14.58.57

之所以 ui 界面显示两个输入框都有值,是因为是用户直接和 textarea 控件交互,实际并没有更新控件绑定的 value 值

最终结论:第一个输入框中被 debounce 包裹的函数并没有执行

是防抖问题么?

尝试将 debounce 去掉后,果然 bug 解决了

Kapture 2020-11-07 at 15.06.41

那么,是防抖的问题么?

不妨先思考下 Vue 组件的实现原理,我在 [Vue.js进阶]从源码角度剖析Vue的生命周期 中提到过,每个 .vue 文件可以理解为一个构造函数,或者一个 Class,而在父组件中引用组件就等于对其的实例化

代码语言:javascript
复制
  <view>
    <custom-textarea v-model="text1"></custom-textarea>
    <custom-textarea v-model="text2"></custom-textarea>
  </view>

上述代码即创建了 2 个 CustomTextarea 组件的实例

熟悉面向对象的同学应该知道,构造函数实例化时,同时会创建实例的属性和方法,一般每个实例的属性都不相同,而方法因为是函数,所以会复用,已达到节省内存的效果

代码语言:javascript
复制
class Person {
  constructor(name) {
    this.name = name
  }
  eat() {}
}

const person1 = new Person('张三')
const person2 = new Person('李四')

console.log(person1.name === person2.name) // false
console.log(person1.eat === person2.eat) // true

Vue2 的组件借鉴了面向对象的原理,虽然内部的实现方式不同,但最终的行为一致,即组件的每个实例都拥有不同的 data,但会复用相同的 methods

源码地址:https://github.com/vuejs/vue/blob/dev/src/core/instance/state.js#L286

image-20201107155528333

286 行中 methods 对象是每个组件实例共用的,每实例化一个组件,会创建相同的引用,指向 methods 中的函数

未命名

上图案例中, 所有 custom-textarea 中的 handleInput 都指向同一个函数,而作为 props 的 value 字段是通过父组件传入的,并不会共享(分别为 text1/text2)

解决方案

经过上述的分析,答案显而易见,两个组件实例都指向了同一个被 debounce 包裹的 handleInput 函数

所以在输入第一个值后, 1000 毫秒内快速切换到第二个输入框进行输入,此时由于防抖效果仍存在,导致第一次的输入并没有计算在内

而第二次输入完毕后,经过 1000 毫秒,最终只会执行第二个 custom-textarea 的 handleInput

只要使得每个组件实例的 handleInput 互相独立,即可解决问题

代码语言:javascript
复制
<template>
  <textarea :value="value" class="textarea" @input="handleInput"></textarea>
</template>

<script>
import { debounce } from 'lodash'

export default {
  name: 'CustomTextarea',
  props: {
    value: {
      type: String,
    },
  },
  data() {
    return {
      handleInput: null,
    }
  },
  mounted() {
    this.handleInput = debounce(function (e) {
      console.log('value', e.target.value)
      this.$emit('input', e.target.value)
    }, 1000)
  },
}
</script>

<style lang="less" scoped>
.textarea {
  border: 2rpx solid red;
}
</style>

将 handleInput 从 methods 放到 data 中,每次初始化时创建防抖函数,此时每个组件实例的 handleInput 就不会互相干扰

Kapture 2020-11-07 at 16.44.41

大功告成???

题外话

Vue 组件中通过将 data 定义为一个函数,函数的返回值作为组件的数据来源,使得每个组件实例的数据都不相同

而 Vue 组件中 methods 是所有实例共用的,那么对于 watch/computed/生命周期,它们是否会共用的呢?

代码语言:javascript
复制
<template>
  <textarea :value="value" class="textarea" @input="handleInput"></textarea>
</template>

<script>
import { debounce } from 'lodash'

export default {
  name: 'CustomTextarea',
  props: {
    value: {
      type: String,
    },
  },
  data() {
    return {
      handleInput: null,
    }
  },
  computed: {
    computedValue() {
      return this.value
    },
  },
  watch: {
    value(newVal) {
      console.log('value', newVal)
    },
  },
  methods:{
    method(){}
  },
  mounted() {
    this.handleInput = debounce(function (e) {
      this.$emit('input', e.target.value)
    }, 1000)
  },
}
</script>

和 methods 对象相同,computed 对象的属性名是一个响应式变量,而值是一个函数,所以所有实例也会指向同一个函数,但由于这个函数需要有返回值,所以不会用防抖函数进行包裹,很少遇到函数公用导致的问题

而 watch 也和 methods 对象相同,所有组件实例共用,所以也会存在防抖的问题

至于生命周期本身就是一个函数,如果对生命周期设置了防抖,多个组件实例同时初始化时也会造成只执行一次的情况

参考资料

[Vue.js进阶]从源码角度剖析Vue的生命周期

【Vue原理】Methods - 源码版](https://segmentfault.com/a/1190000019605909)

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-11-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 全栈前端精选 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 问题原因
  • 是防抖问题么?
  • 解决方案
  • 题外话
  • 参考资料
相关产品与服务
云开发 CloudBase
云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档