Skip to content

08 自定义事件

v-model 原理

  • v-model 本质上是一个语法糖。例如应用在输入框上,就是 value 属性 和 input 事件 的合写

    vue
    <template>
      <div id="app">
        <input v-model="msg" type="text" />
        <br />
        <input :value="msg" @input="msg = $event.target.value" type="text" />
      </div>
    </template>

作用

提供数据的双向绑定

  • 数据变,视图跟着变 :value
  • 视图变,数据跟着变 @input

注意

$event 用于在模板中,获取事件的形参

vue
<template>
  <div class="app">
    <!-- v-model 的底层其实就是:value 和 @input 的简写 -->
    <input type="text" v-model="msg1" />
    <br />
    <input type="text" :value="msg2" @input="msg2 = $event.target.value" />
  </div>
</template>

<script>
export default {
  data () {
    return {
      msg1: 'hello',
      msg2: 'world',
    }
  },
}
</script>

<style>
  input {
    width: 200px;
    height: 30px;
    font-size: 20px;
    padding: 0 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
    margin-bottom: 10px;
  }
</style>
js
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: (h) => h(App),
}).$mount('#app')

v-model 使用在其他表单元素上的原理

  • 不同的表单元素,v-model 在底层的处理机制是不一样的。
  • 比如给 checkbox 使用 v-model,底层处理的是 checked 属性和 change 事件。
  • 不过咱们只需要掌握应用在文本框上的原理即可

表单类组件封装

自定义组件的 v-model - 自定义事件 — Vue.js

  • 实现子组件和父组件数据的双向绑定(实现 App.vue 中的 selectId 和子组件选中的数据进行双向绑定)
  • 父组件通过 v-model 简化代码,实现子组件和父组件数据 双向绑定

思路

v-model 其实就是 :value@input 事件的简写

  • 子组件:props 通过 value 接收数据,事件触发 input
  • 父组件:v-model 直接绑定数据

相关代码

vue
<template>
  <div>
    <!-- 向父组件传递数据 -->
    <select :value="value" @change="$emit('input', $event.target.value)">
      <option value="101">北京</option>
      <option value="102">上海</option>
      <option value="103">武汉</option>
      <option value="104">广州</option>
      <option value="105">深圳</option>
    </select>
    <p>
      value 为<span>{{ value }}</span>
    </p>
  </div>
</template>

<script>
export default {
  props: {
    value: String,
  },
}
</script>

<style scoped>
  select {
    width: 200px;
    height: 30px;
    font-size: 20px;
    padding: 0 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
    margin-bottom: 10px;
  }

  span {
    background-color: #ffc0cb;
  }
</style>
vue
<template>
  <div class="app">
    <div class="box1">
      <h2>使用 v-model</h2>
      <BaseSelect v-model="selectId"></BaseSelect>
    </div>
    <!-- v-model 本质上是语法糖,等价于 :value="selectId" @input="selectId = $event" -->
    <!--
      使用 v-model 前提:
        1. 组件内部 props 必须有 value 属性,用于接收父组件传递的数据
        2. 组件内部必须有 input 事件,用于向父组件传递数据
    -->
    <div class="box2">
      <h2>不使用 v-model</h2>
      <BaseSelect :value="selectId" @input="selectId = $event"></BaseSelect>
    </div>
  </div>
</template>

<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
  components: {
    BaseSelect,
  },
  data () {
    return {
      selectId: '101',
    }
  },
}
</script>

<style scoped>
  .app {
    display: flex;
    justify-content: flex-start;
    gap: 20px;
    margin: 20px 0 0 20px;
  }
  .box1,
  .box2 {
    width: 266px;
    padding: 10px;
    border: 2px solid #ccc;
    background-color: #bfd8af;
    border-radius: 6px;
    box-shadow: 0 0 6px 6px #d4e7c5;
  }
</style>

.sync 修饰符

.sync 修饰符 — Vue.js

  • 作用

    • 可以实现 子组件父组件数据双向绑定,简化代码
    • 简单理解:子组件可以修改父组件传过来的 props 值
  • 场景

    • 封装弹框类的基础组件,visible 属性 (true 显示、false 隐藏)
  • 本质

    • .sync 修饰符 就是 :属性名@update:属性名 合写

语法

  • 父组件

    js
    // .sync 写法
    <BaseDialog :visible.sync="isShow" />
    // 完整写法
    <BaseDialog :visible="isShow" @update:visible="isShow = $event" />
  • 子组件

    js
    props: {
      visible: Boolean
    },
    
    this.$emit('update:visible', false)

代码示例

vue
<script>
export default {
  props: {
    isShow: Boolean,
  },
  methods: {
    closeDialog () {
      this.$emit('update:isShow', false)
    },
  },
}
</script>

<template>
  <div class="base-dialog-wrap" v-show="isShow">
    <div class="base-dialog">
      <div class="title">
        <h3>温馨提示:</h3>
        <button class="close" @click="closeDialog">x</button>
      </div>
      <div class="content">
        <p>你确认要退出本系统么?</p>
      </div>
      <div class="footer">
        <button @click="closeDialog">确认</button>
        <button @click="closeDialog">取消</button>
      </div>
    </div>
  </div>
</template>

<style scoped>
  .base-dialog-wrap {
    width: 300px;
    height: 200px;
    box-shadow: 2px 2px 2px 2px #ccc;
    position: fixed;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    padding: 0 10px;
  }
  .base-dialog .title {
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 2px solid #000;
  }
  .base-dialog .content {
    margin-top: 38px;
  }
  .base-dialog .title .close {
    width: 20px;
    height: 20px;
    cursor: pointer;
    line-height: 10px;
  }
  .footer {
    display: flex;
    justify-content: flex-end;
    margin-top: 26px;
  }
  .footer button {
    width: 80px;
    height: 40px;
  }
  .footer button:nth-child(1) {
    margin-right: 10px;
    cursor: pointer;
  }
</style>
vue
<script>
import BaseDialog from './components/BaseDialog.vue'
export default {
  components: {
    BaseDialog,
  },
  data () {
    return {
      isShow: false,
    }
  },
  methods: {
    openDialog () {
      this.isShow = true
      // console.log(document.querySelectorAll('.box'));
    },
  },
}
</script>

<template>
  <div class="app">
    <div class="box box1">
      <h2>使用完整写法</h2>
      <button @click="openDialog">显示退出按钮</button>
      <BaseDialog :isShow="isShow" @update:isShow="isShow = $event"></BaseDialog>
    </div>
    <!-- isShow.sync  等价于 :isShow="isShow" @update:isShow="isShow = $event" -->
    <div class="box box2">
      <h2>使用简写</h2>
      <button @click="openDialog">显示退出按钮</button>
      <BaseDialog :isShow.sync="isShow"></BaseDialog>
    </div>
  </div>
</template>

<style scoped>
  .app {
    display: flex;
    flex-wrap: wrap;
  }
  .box1,
  .box2 {
    width: 200px;
    margin: 20px;
    padding: 10px;
    border: 2px solid #ccc;
    background-color: #bfd8af;
    border-radius: 6px;
  }

  button {
    width: 150px;
    height: 36px;
    margin: 20px 0 0 20px;
    font-size: 20px;
    background-color: #ccc;
    border-radius: 5px;
    border: none;
    outline: none;
    cursor: pointer;
  }
</style>

总结

  1. 父组件如果想让子组件修改传过去的值 必须加什么修饰符?

    • 在 Vue 中,父组件通过 props 将数据传递给子组件。如果父组件希望子组件能够修改这些通过 props 传递的值,可以使用 .sync 修饰符。
    • 使用 .sync 修饰符时,子组件可以直接修改 propValue,并且父组件的 propValue 会在子组件内部修改后自动更新。这样可以更方便地实现子组件修改父组件传递的值的需求。
    vue
    <!-- ParentComponent.vue -->
    <template>
      <div>
        <!-- 使用 .sync 修饰符传递数据给子组件 -->
        <ChildComponent :propValue.sync="parentValue" />
        <p>{{ parentValue }}</p>
      </div>
    </template>
    
    <script>
      import ChildComponent from './ChildComponent.vue'
    
      export default {
        components: {
          ChildComponent,
        },
        data() {
          return {
            parentValue: 'Initial value',
          }
        },
      }
    </script>
    vue
    <!-- ChildComponent.vue -->
    <template>
      <div>
        <!-- 使用 .sync 修饰符修改父组件传递的值 -->
        <button @click="updateParentValue">Update Parent Value</button>
      </div>
    </template>
    
    <script>
      export default {
        props: {
          // 使用 .sync 修饰符声明 prop
          propValue: {
            type: String,
            default: '',
          },
        },
        methods: {
          // 使用 .sync 修饰符触发 update 事件,通知父组件更新 prop
          updateParentValue() {
            this.$emit('update:propValue', 'Updated value from child')
          },
        },
      }
    </script>
    • 在子组件中,使用 .sync 修饰符声明的 propValue 可以直接在子组件内部修改,然后通过触发 update:propValue 事件通知父组件更新。
    • 这样就实现了父组件将数据传递给子组件,并允许子组件修改传递的值的需求。
  2. 子组件要修改父组件的 props 值 必须使用什么语法?

    • 父组件

      js
      // .sync 写法
      <BaseDialog :visible.sync="isShow" />
      // 完整写法
      <BaseDialog :visible="isShow" @update:visible="isShow = $event" />
    • 子组件

      js
      props: {
        visible: Boolean
      },
      
      this.$emit('update:visible', false)

ref 和 $refs

  • 作用

    • 利用 ref$refs 可以用于 获取 dom 元素 或 组件实例
  • 特点

    • 查找范围 → 当前组件内 (更精确稳定)

ref

  • ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。

    html
    <!-- `vm.$refs.p` will be the DOM node -->
    <p ref="p">hello</p>
    
    <!-- `vm.$refs.child` will be the child component instance -->
    <child-component ref="child"></child-component>
  • v-for 用于元素或组件的时候,引用信息将是包含 DOM 节点或组件实例的数组。

关于 ref 注册时间的重要说明

  • 因为 ref 本身是作为渲染结果被创建的,在初始渲染的时候你不能访问它们 - 它们还不存在!$refs 也不是响应式的,因此你不应该试图用它在模板中做数据绑定。
  • $refs 只会在组件渲染完成之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组件的 "逃生舱"——你应该避免在模板或计算属性中访问 $refs

语法

  1. 给要获取的盒子添加 ref 属性

    html
    <div ref="chartRef">我是渲染图表的容器</div>
  2. 获取时通过 $refs 获取 this.$refs.chartRef 获取

    js
    mounted () {
      console.log(this.$refs.chartRef)
    }

注意

  • 之前只用 document.querySelect('.box') 获取的是整个页面中的盒子

代码示例

vue
<template>
  <div class="base-chart-box1" ref="baseChartBox">子组件</div>
</template>

<script>
// pnpm i -D echarts
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口
import * as echarts from 'echarts'

export default {
  mounted () {
    // 基于准备好的 dom,初始化 echarts 实例
    // document.querySelector 会查找项目中所有的元素
    // $refs 只会在当前组件查找盒子
    // var myChart = echarts.init(document.querySelector('.base-chart-box'))
    const myChart = echarts.init(this.$refs.baseChartBox)
    // 绘制图表
    myChart.setOption({
      title: {
        text: '基础柱状图 Basic Bar',
      },
      tooltip: {
        trigger: 'axis',
        axisPointer: {
          type: 'shadow',
        },
      },
      xAxis: {
        type: 'category',
        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
      },
      yAxis: {
        type: 'value',
      },
      series: [
        {
          data: [120, 200, 150, 80, 70, 110, 130],
          type: 'bar',
        },
      ],
    })
  },
}
</script>

<style scoped>
  .base-chart-box1 {
    width: 400px;
    height: 300px;
    border: 3px solid #000;
    border-radius: 6px;
  }
</style>
vue
<template>
  <div class="base-chart-box2" ref="baseChartBox">子组件</div>
</template>

<script>
// pnpm i -D echarts
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口
import * as echarts from 'echarts'

export default {
  mounted () {
    // 基于准备好的 dom,初始化 echarts 实例
    // document.querySelector 会查找项目中所有的元素
    // $refs 只会在当前组件查找盒子
    // var myChart = echarts.init(this.$refs.baseChartBox);
    const myChart = echarts.init(document.querySelector('.base-chart-box2'))
    // 绘制图表
    myChart.setOption({
      title: {
        text: '基础柱状图 Basic Bar',
      },
      tooltip: {
        trigger: 'axis',
        axisPointer: {
          type: 'shadow',
        },
      },
      xAxis: {
        type: 'category',
        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
      },
      yAxis: {
        type: 'value',
      },
      series: [
        {
          data: [120, 200, 150, 80, 70, 110, 130],
          type: 'bar',
        },
      ],
    })
  },
}
</script>

<style scoped>
  .base-chart-box2 {
    width: 400px;
    height: 300px;
    border: 3px solid #000;
    border-radius: 6px;
  }
</style>
vue
<template>
  <div class="app">
    <!--
      1. 子组件中使用 echarts.init(document.querySelector('.base-chart-box')); 会查找项目中所有的 .base-chart-box,所以会在父组件中查找到 .base-chart-box
      2. 子组件中使用 echarts.init(this.$refs.baseChartBox); 只会在当前组件(子组件)中查找 .base-chart-box
     -->
    <div class="box1">
      <h2>使用 this.$refs</h2>
      <div class="base-chart-box1">这是一个捣乱的盒子 (和子组件中的要渲染的元素类名同名)</div>
      <BaseChartA></BaseChartA>
    </div>
    <div class="box2">
      <h2>使用 document.querySelector</h2>
      <div class="base-chart-box2">这是一个捣乱的盒子 (和子组件中的要渲染的元素类名同名)</div>
      <BaseChartB></BaseChartB>
    </div>
  </div>
</template>

<script>
import BaseChartA from './components/BaseChartA.vue'
import BaseChartB from './components/BaseChartB.vue'
export default {
  components: {
    BaseChartA,
    BaseChartB,
  },
}
</script>

<style scoped>
  .app {
    display: flex;
    flex-wrap: wrap;
    gap: 20px;
  }

  .box1,
  .box2 {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;

    width: 500px;
    height: 700px;
    border: 2px solid #ccc;
    border-radius: 6px;
    background-color: #f5f5f5;
  }
  .base-chart-box1,
  .base-chart-box2 {
    width: 400px;
    height: 300px;
    border: 3px solid #000;
    border-radius: 6px;
  }
</style>

异步更新 & $nextTick

nextTick — Vue.js

  • Vue.nextTick( [callback, context] ):在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

  • 语法: this.$nextTick(函数体)

    js
    this.$nextTick(() => {
      this.$refs.inp.focus()
    })

注意

$nextTick 内的函数体 一定是箭头函数,这样才能让函数内部的 this 指向 Vue 实例

vue
<script>
export default {
  data () {
    return {
      title: '大标题',
      isShowEdit: false,
      editValue: '',
    }
  },
  methods: {
    editFn () {
      // 1. 显示文本框
      this.isShowEdit = true
      // 2. 让文本框聚焦(会等 dom 更新完之后 立马执行 nextTick 中的回调函数)
      this.$nextTick(() => {
        console.log(this.$refs.inp)
        this.$refs.inp.focus()
      })

      // this.$nextTick: 用于在下次 DOM 更新循环结束之后执行回调。它会在 Vue 组件更新完毕之后执行,可以确保你在回调中访问到最新的 DOM
      // setTimeout: 主要用于在一定的延迟之后执行一些代码,不一定与 DOM 更新相关

      // setTimeout(() => {
      //   this.$refs.inp.focus()
      // }, 0)
    },
  },
}
</script>

<template>
  <div class="app">
    <div v-if="isShowEdit">
      <input type="text" v-model="editValue" ref="inp" />
      <button @click="isShowEdit = false">确认</button>
    </div>
    <div v-else>
      <span>{{ title }}</span>
      <button @click="editFn">编辑</button>
    </div>
  </div>
</template>

<style scoped></style>