vue2 - Day03 - (生命周期、组件、组件通信)

news2025/2/26 15:58:10

文章目录

  • 一、生命周期
    • 1. 创建阶段
    • 2. 挂载阶段
    • 3. 更新阶段
    • 4. 销毁阶段
    • 5. 错误捕获
    • 总结
  • 二、组件
    • 2.1 注册
      • 1. 全局注册 - 公共的组件。
      • 2. 局部注册
      • 总结
    • 2.2 三大重要的组成部分
      • 1. 模板 (Template)
        • 主要功能:
        • 说明:
      • 2. 脚本 (Script)
        • 主要功能:
        • 说明:
      • 3. 样式 (Style)
        • 主要功能:
        • 说明:
      • 三个部分的关系
        • 解析:
      • 总结
    • 2.3 scoped原理
      • `scoped` 的原理
        • 编译后的代码:
      • 为什么 `scoped` 能起作用?
      • `scoped` 样式的限制
      • `scoped` 子组件样式穿透
        • 父组件:
        • 子组件(ChildComponent.vue):
      • 总结
    • 2.4 在 Vue 2 中,`data` 是一个函数而不是一个对象。
      • 为什么 `data` 是一个函数?
        • 1. Vue 组件是可复用的
        • 2. 返回新的数据对象
      • 为什么 `data` 需要是一个函数
      • 例子
        • 解释:
      • `data` 作为函数的好处
      • 为什么不能直接使用对象?
      • 总结
  • 三、组件通信
    • 3.1 父子组件通信
      • 父组件传递数据给子组件 (Props)
      • 子组件向父组件传递数据
      • 说明:
    • 3.2 兄弟组件通信
      • 父组件作为中介
    • 3.3 跨层级组件通信
      • (1) Event Bus (事件总线)
      • (2) Vuex (全局状态管理)
    • 3.4 总结
    • 3.5 props 校验
      • `props` 校验的基础语法
      • 常见的 `props` 校验规则
      • 组合校验规则
      • 校验规则的执行时机
      • 代码
      • 总结
    • 3.6 `props` 和 `data`:单向数据流
      • 1. `props` 和 `data` 的区别
        • (1) `props`
        • (2) `data`
      • 2. 单向数据流的概念
      • 3. 如何遵循单向数据流
        • (1) 父组件向子组件传递数据 (Props)
        • (2) 子组件通过事件通知父组件 (Event Emitting)
      • 4. 为什么需要单向数据流?
        • (1) 数据追踪更简单
        • (2) 调试和维护更容易
        • (3) 减少了副作用
        • (4) 便于构建组件库
      • 5. Vuex:管理复杂应用的单向数据流
      • 总结
  • 四、非父子组件通信
    • 通过 `provide` 和 `inject` 进行跨层级通信
      • 1. `provide` 和 `inject` 的基本原理
      • 2. 使用场景
      • 3. 基本用法
        • 3.1 祖先组件使用 `provide` 提供数据
        • 3.2 后代组件使用 `inject` 接收数据
      • 4. 工作原理
      • 5. 举例说明
        • 5.1 跨层级数据共享
        • 5.2 跨层级函数共享
      • 6. `provide` 和 `inject` 的注意事项
      • 7. 总结
  • 五 补充
    • 5.1 `v-model`
      • 1. 双向绑定的实现
      • 2. 具体实现过程
        • 2.1 `v-model` 在输入控件上的绑定
        • 2.2 监听事件和更新数据
        • 2.3 `v-model` 的内部实现
      • 3. `.sync` 和 `.lazy` 修饰符
      • 4. 总结
    • 5.2 ref 和 $refs
      • 1. `ref`
        • 1.1 使用 `ref` 的基本语法
      • 2. `$refs`
        • 2.1 访问 `$refs` 中的 DOM 元素或组件
        • 2.2 `ref` 和 `$refs` 的使用场景
        • 2.3 `$refs` 的响应性
        • 2.4 `ref` 的特殊情况
      • 3. 总结
    • 5.3 Vue异步更新、$nextTick
      • 1. Vue 异步更新
        • 为什么 Vue 使用异步更新?
        • 示例:异步更新数据
      • 2. `$nextTick`
        • 使用 `$nextTick`
      • 3. `$nextTick` 的应用场景
      • 4. `$nextTick` 和 Vue 的更新队列
      • 5. 总结

一、生命周期

1. 创建阶段

  • beforeCreate

      该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务器端进行。
    
    beforeCreate() {
      console.log('beforeCreate');
    }
    
  • created
    在实例创建完成后立即调用,数据观测和事件配置都已经完成。这时可以访问到 datacomputedmethods 等数据和方法,但尚未挂载 DOM。

    created() {
      console.log('created');
    }
    

2. 挂载阶段

  • beforeMount
    在挂载开始之前被调用,相关的 render 函数首次被调用,DOM 尚未渲染。

    beforeMount() {
      console.log('beforeMount');
    }
    
  • mounted
    在挂载完成后立即调用,el 被替换成 DOM 后可以访问到 DOM 元素。

    mounted() {
      console.log('mounted');
    }
    

3. 更新阶段

  • beforeUpdate
    当数据更新且视图重新渲染之前被调用。在这里访问更新前的 DOM 状态。

    beforeUpdate() {
      console.log('beforeUpdate');
    }
    
  • updated
    数据更新且视图重新渲染后调用。此时可以访问到更新后的 DOM。

    updated() {
      console.log('updated');
    }
    

4. 销毁阶段

  • beforeDestroy
    在实例销毁之前调用。此时组件的 DOM 和数据绑定尚未清理,可以在此阶段做一些清理工作,比如移除事件监听器等。

    beforeDestroy() {
      console.log('beforeDestroy');
    }
    
  • destroyed
    实例销毁之后调用。组件的所有绑定和事件监听都被移除,所有的子组件也都被销毁。

    destroyed() {
      console.log('destroyed');
    }
    

5. 错误捕获

  • errorCaptured
    当子组件的错误被捕获时调用。该钩子接收三个参数:错误信息 (err)、错误来源组件 (vm)、错误所在的生命周期钩子 (info)。

    errorCaptured(err, vm, info) {
      console.error('Error captured:', err);
      return false;  // 如果返回 false,错误不会再向上传播
    }
    

总结

Vue 的生命周期提供了钩子函数,使得你可以在合适的时机进行某些操作。不同阶段的生命周期钩子适用于不同的场景,比如在 created 中获取数据、在 mounted 中设置 DOM 操作、在 beforeDestroy 中进行清理等。

二、组件

在 Vue 2 中,根组件(Root Component)是整个 Vue 应用的入口组件,它是 Vue 实例的最顶层组件,所有的其他组件都是在根组件的基础上注册和嵌套的。根组件通常负责整个应用的初始化、配置和全局数据管理。

  • 根组件是 Vue 应用的入口:在一个 Vue 应用中,只有一个根组件,它是整个应用的最外层组件。所有的子组件都是由根组件管理和调度的。
  • Vue 实例挂载在根组件上:Vue 的应用实例是挂载到根组件上的,根组件通过 el 将 Vue 实例挂载到页面的 DOM 元素上。
  • 根组件通常包含其他子组件:根组件会注册其他组件,并将它们通过模板语法嵌套在自身的模板中。
    在这里插入图片描述

2.1 注册

在 Vue 2 中,组件的注册可以分为 **局部注册** 和 **全局注册** 两种方式。

1. 全局注册 - 公共的组件。

全局注册的步骤:

  1. main.jsapp.js 中导入组件。
  2. 使用 Vue.component() 方法进行注册。
// main.js
import Vue from 'vue'
import App from './App.vue'
import MyComponent from './components/MyComponent.vue'

// 全局注册组件
Vue.component('my-component', MyComponent)

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

这样,MyComponent 组件就可以在应用的任何地方使用了,使用时直接通过 <my-component></my-component> 引用。

2. 局部注册

局部注册的组件只能在其所在的父组件中使用。
只在某个特定组件内使用的组件。

局部注册的步骤:

  1. 在父组件中导入子组件。
  2. 在父组件的 components 选项中注册该组件。
// ParentComponent.vue
<template>
  <div>
    <my-component></my-component>
  </div>
</template>

<script>
import MyComponent from './MyComponent.vue'

export default {
  components: {
    MyComponent // 局部注册
  }
}
</script>

这样,MyComponent 只会在 ParentComponent 组件中可用,而不能在其他地方直接使用。

总结

  • 全局注册:通过 Vue.component()main.js 中注册,组件可以在整个应用中使用。
  • 局部注册:在父组件中通过 components 选项注册,组件只在父组件中有效。

通常来说,如果某个组件只在少数几个地方使用,建议使用 局部注册,以便减少命名冲突和提升代码的可维护性。全局注册适用于通用的、需要在多个地方使用的组件,如 Button, Modal 等。

2.2 三大重要的组成部分

在 Vue 2 中,组件主要由 模板 (Template)脚本 (Script)样式 (Style) 即:视图、逻辑和样式

1. 模板 (Template)

模板是 Vue 组件的视图部分,负责定义组件的 HTML 结构。Vue 使用声明式渲染方式来绑定数据和 DOM,模板部分包含了 HTML 语法和 Vue 特有的指令(如 v-bind, v-if, v-for 等)。它不仅能展示数据,还能响应用户的交互。

主要功能:
  • 动态渲染数据:模板能够渲染动态数据,通过插值语法({{ }})或者指令来绑定数据到 DOM。
  • 事件处理:使用 v-on 指令或 @ 简写来绑定事件,处理用户的交互(如点击、输入等)。
  • 条件渲染和循环渲染:使用 v-if, v-for, v-show 等指令来控制 DOM 的渲染和显示。
<template>
  <div>
    <h1>{{ title }}</h1> <!-- 数据绑定 -->
    <button @click="changeMessage">Change Message</button> <!-- 事件绑定 -->
    
    <!-- 条件渲染 -->
    <p v-if="isVisible">This is visible if condition is true.</p>
    
    <!-- 列表渲染 -->
    <ul>
      <li v-for="item in items" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>
说明:
  • {{ title }}:插值表达式,用于将数据绑定到 DOM 元素。
  • @click="changeMessage":事件绑定,绑定按钮的点击事件,触发 changeMessage 方法。
  • v-if:条件渲染,只有 isVisibletrue 时,<p> 标签才会渲染。
  • v-for:循环渲染,遍历 items 数组,并为每个 item 渲染一个 <li> 元素。

2. 脚本 (Script)

脚本部分包含 Vue 组件的业务逻辑,包括数据、方法、计算属性、生命周期钩子等。通过 export default 定义一个 Vue 组件对象,Vue 会将这些数据和方法与模板部分绑定。

主要功能:
  • 数据 (data):定义组件的响应式数据。
  • 方法 (methods):定义组件的行为或逻辑,用户的点击事件。
  • 计算属性 (computed):计算基于响应式数据的派生状态,常用于替代方法来优化性能。
  • 生命周期钩子:例如 created, mounted, updated, destroyed 等,在组件不同生命周期阶段执行代码。
<script>
export default {
  data() {
    return {
      title: 'Hello, Vue!',
      isVisible: true,
      items: [
        { id: 1, name: 'Item 1' },
        { id: 2, name: 'Item 2' },
        { id: 3, name: 'Item 3' }
      ]
    };
  },
  methods: {
    changeMessage() {
      this.title = 'Message has been changed!';
    }
  },
  computed: {
    itemCount() {
      return this.items.length;
    }
  },
  mounted() {
    console.log('Component is mounted!');
  }
}
</script>
说明:
  • data():返回组件的响应式数据,这些数据可以在模板中使用。
  • methods:包含可以在模板中调用的方法(如 changeMessage)。
  • computed:计算属性 itemCount,它基于 items 数组的长度返回一个值,computed 会缓存计算结果,只有 items 发生变化时才会重新计算。
  • mounted():生命周期钩子函数,组件挂载到 DOM 后会执行该方法。

3. 样式 (Style)

样式部分用于定义组件的视觉效果和布局。Vue 允许你在组件中直接写 CSS 样式,也支持预处理器如 SASS/SCSS 和 LESS 等。Vue 组件的样式有两种应用方式:全局样式和局部样式。

主要功能:
  • 局部样式:通过在 <style> 标签上添加 scoped 属性,使样式只作用于当前组件,避免影响到全局其他组件。
  • 全局样式:如果不使用 scoped 属性,样式将影响到全局的其他组件。
  • 使用预处理器:可以在 <style> 标签中使用 SASS/SCSS、LESS 等预处理器,提升样式的灵活性和可维护性。
<style scoped>
  h1 {
    color: blue;
  }
  button {
    background-color: lightgray;
    border: none;
    padding: 10px;
  }
</style>
说明:
  • scoped:局部样式,只会作用于当前组件的元素,不会影响到其他组件。
  • background-color: lightgray;:样式定义了按钮的背景颜色。

三个部分的关系

  • 模板:负责定义组件的视图结构。
  • 脚本:定义组件的行为、数据和生命周期,提供逻辑处理。
  • 样式:定义组件的样式和外观,控制组件的视觉效果。
<template>
  <div>
    <h1>{{ title }}</h1>
    <button @click="toggleVisibility">Toggle Visibility</button>
    <p v-if="isVisible">This paragraph will be toggled on click.</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: 'Vue Component Example',
      isVisible: true
    };
  },
  methods: {
    toggleVisibility() {
      this.isVisible = !this.isVisible;
    }
  }
}
</script>

<style scoped>
h1 {
  color: green;
}
button {
  background-color: #42b983;
  color: white;
  border: none;
  padding: 10px;
  cursor: pointer;
}
button:hover {
  background-color: #35495e;
}
</style>
解析:
  • 模板部分:展示了一个标题、按钮和一个可切换显示的段落。
  • 脚本部分:定义了 titleisVisible 数据,提供了 toggleVisibility 方法来切换 isVisible 的值。
  • 样式部分:为标题和按钮添加了样式,使其具有更好的视觉效果。

总结

  1. 模板 (Template):定义 HTML 结构,使用 Vue 特有的指令来动态渲染视图。
  2. 脚本 (Script):定义组件的行为,处理数据、方法和生命周期,提供组件的逻辑。
  3. 样式 (Style):定义组件的样式,支持局部样式和预处理器。

2.3 scoped原理

在 Vue 组件中,scoped 是一个 <style> 标签的特殊属性,它用来实现 局部样式。当在 <style> 标签中使用 scoped 属性时,样式只会作用于当前组件的元素,而不会影响到全局的其他组件或页面的元素。这样可以避免不同组件之间的样式冲突,确保组件的样式是隔离的。

scoped 的原理

scoped 的工作原理依赖于 Vue 的 CSS作用域隔离。它通过 自动添加样式选择器的唯一属性 来实现样式的局部化。

  1. 样式选择器加上独特的属性
    当你在 Vue 组件的 <style> 标签上使用 scoped 时,Vue 会在编译期间,给该组件的每个样式选择器添加一个独特的属性(通常是基于组件的唯一 ID),从而使这些样式只作用于该组件内的元素。

  2. 自动生成唯一的标识符
    Vue 会为每个组件生成一个唯一的标识符(比如 data-v-xxxxxxx),并将这个标识符作为选择器的一部分添加到每个 CSS 规则中。

  3. 修改生成的 HTML 元素
    Vue 会为组件中的 HTML 元素添加与样式选择器相匹配的属性(如 data-v-xxxxxxx)。这些属性帮助样式选择器只作用于当前组件的 DOM 元素。

<template>
  <div class="example">
    <p>This is a scoped example!</p>
  </div>
</template>

<script>
export default {
  name: 'ScopedComponent'
}
</script>

<style scoped>
.example {
  color: red;
}
</style>
编译后的代码:
<template>
  <div class="example" data-v-1234567>
    <p data-v-1234567>This is a scoped example!</p>
  </div>
</template>

<script>
export default {
  name: 'ScopedComponent'
}
</script>

<style scoped>
.example[data-v-1234567] {
  color: red;
}
</style>

scoped 会让 Vue 自动为 .example<p> 元素添加一个 data-v-1234567 属性。该属性的值是由 Vue 自动生成的唯一标识符,用于确保样式只作用于当前组件中的元素。最终生成的 CSS 规则变为 .example[data-v-1234567],因此只有拥有 data-v-1234567 的元素会应用该样式。

为什么 scoped 能起作用?

  • 样式选择器的作用范围变小:在样式选择器中加入 data-v-xxxxxxx 这种唯一标识符时,CSS 的作用范围变得非常精确,确保样式只会作用于具有相同标识符的 DOM 元素。
  • 生成的标识符是独立的:由于 Vue 为每个组件生成独立的标识符,确保了样式不会与其他组件的样式发生冲突。例如,两个组件中的 .example 类会被编译成不同的选择器,避免了样式覆盖问题。

scoped 样式的限制

虽然 scoped 样式可以隔离组件的样式,但它并不是万能的,也有一些限制和注意事项:

  1. 不能作用于全局样式
    scoped 样式只能影响当前组件内的元素,不能影响到外部组件或页面的全局元素。当某些样式作用于全局,可以使用普通的 <style> 标签,或者通过 CSS 类全局引入样式。

  2. 伪类和伪元素的特殊情况
    scoped 不适用于伪类(如 :hover)和伪元素(如 ::after)。不过,这通常并不是一个问题,因为 Vue 会自动将伪类和伪元素与组件的作用域一起处理。

  3. 深度选择器 ::v-deep
    需要在组件的样式中影响子组件的样式(例如样式穿透),可以使用 ::v-deep 选择器。这个选择器会将样式应用到子组件的元素中,而不受 scoped 限制。

    <style scoped>
    ::v-deep .child-class {
      color: blue;
    }
    </style>
    
  4. 不能影响外部的全局 CSS 文件
    当在外部引用了全局样式文件,scoped 不能对它们进行局部化控制。如果需要针对某个组件内的元素应用全局样式,最好将样式放在组件外部的 <style> 标签或 CSS 文件中。

scoped 子组件样式穿透

假设有一个父组件和一个子组件,当父组件的样式可以影响到子组件的内部样式。可以使用 ::v-deep 来实现样式的穿透。

父组件:
<template>
  <div>
    <child-component></child-component>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  }
}
</script>

<style scoped>
/* 使用 ::v-deep 让父组件的样式影响到子组件 */
::v-deep .child-class {
  color: red;
}
</style>

(1)或者使用>>>
在这里插入图片描述
(2) /deep/
在这里插入图片描述

子组件(ChildComponent.vue):
<template>
  <div class="child-class">
    xx
  </div>
</template>

<script>
export default {
  name: 'ChildComponent'
}
</script>

<style scoped>
/* 这个样式只会应用在子组件中 */
.child-class {
  font-size: 20px;
}
</style>

::v-deep 使得父组件的样式能够影响到子组件中 .child-class 类的文本颜色。
在这里插入图片描述

总结

  • 原理scoped 通过为每个组件生成唯一的标识符,并将该标识符添加到样式选择器和 DOM 元素中,从而确保样式只作用于当前组件的元素。
  • 优点:避免了不同组件之间样式的冲突,实现了组件化的样式隔离。
  • 局限性:不能影响全局样式,不能作用于伪类/伪元素,也不能穿透子组件的样式,除非使用 ::v-deep

scoped 是 Vue 中非常强大的功能,它帮助开发者在多个组件之间实现样式的局部化,减少了 CSS 冲突的风险。

2.4 在 Vue 2 中,data 是一个函数而不是一个对象。

为什么 data 是一个函数?

1. Vue 组件是可复用的

Vue 组件是可以多次实例化的,且每个实例都有自己的独立状态。由于 Vue 是基于组件化的结构,它允许多个组件实例存在,而每个实例都应该有自己独立的 data 数据。将 data 定义为一个函数而非直接对象,确保了每个组件实例都有自己的独立数据副本。

  • 如果 data 是对象的话,所有的组件实例将共享同一份数据,这样就无法保证每个实例的独立性。
  • data 定义为函数可以避免这个问题。每次创建新的组件实例时,data 函数都会被调用,从而返回一个新的对象,确保每个组件实例都有自己的数据副本。
2. 返回新的数据对象

在 Vue 组件中,data 是一个函数,它返回一个对象。这个对象包含了该组件的所有响应式数据。这是为了保证每个组件实例都有自己独立的 data,避免多个实例共享同一份数据。

为什么 data 需要是一个函数

// 错误的写法
Vue.component('my-component', {
  data: {
    message: 'Hello, World!'
  }
});

在上面的代码中,data 直接是一个对象。问题是,如果多个组件实例使用这个组件,它们将共享同一个 data 对象,所有实例的数据都将相互影响。

正确的写法是将 data 作为一个函数:

// 正确的写法
Vue.component('my-component', {
  data() {
    return {
      message: 'Hello, World!'
    };
  }
});

在这个正确的写法中,data 是一个返回对象的函数。每次创建组件实例时,Vue 会调用 data() 函数,返回一个新的对象,从而确保每个组件实例拥有自己的独立数据副本。

例子

<template>
  <div>
    <p>{{ message }}</p>
    <button @click="changeMessage">Change Message</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello, Vue!'
    };
  },
  methods: {
    changeMessage() {
      this.message = 'Message has been changed!';
    }
  }
}
</script>
解释:
  1. data() 是一个函数:每个组件实例都会调用 data() 方法,返回一个包含 message 的对象。每个组件实例有自己的 message 数据,不会相互影响。
  2. 数据的响应性message 是一个响应式数据,组件会自动监听它的变化,当调用 changeMessage 方法时,message 被更新,视图也会随之更新。

data 作为函数的好处

  1. 避免数据共享:Vue 会为每个组件实例调用 data() 函数,确保每个实例都有一个独立的 data 对象。

  2. 支持多次实例化:多个组件实例可以独立使用自己的数据副本,而不会互相干扰。

  3. 响应式系统的运作:Vue 会将 data() 返回的对象变成响应式数据,确保视图能够自动更新。

为什么不能直接使用对象?

如果 data 是一个对象而不是一个函数,Vue 会把这个对象直接作为组件实例的共享数据。这样,在多个组件实例中,这个共享对象就会在各个实例之间共享,导致数据污染或意外的相互干扰。

例如,以下代码是错误的:

Vue.component('my-component', {
  data: {
    message: 'Hello, Vue!'
  }
});

这会导致所有使用该组件的实例共享同一个 data 对象,因此一个组件实例中数据的改变会影响到其他实例。为了避免这种情况,Vue 设计上选择将 data 设置为一个函数,使得每个组件实例都能拥有独立的 data

总结

  • data 是一个函数而非对象,这是因为每个 Vue 组件实例应该拥有自己独立的状态。如果 data 是对象,多个组件实例将共享同一个数据对象,导致数据冲突。
  • data() 是为了确保每个组件实例都有自己独立的数据副本,这样可以避免数据共享和污染的问题。
  • data() 返回的对象是响应式的,Vue 会追踪其变化并自动更新视图。

通过这种方式,Vue 实现了组件的数据隔离和响应式数据更新机制,从而增强了组件的可复用性和数据的独立性。

三、组件通信

在 Vue 2 中,组件之间的通信是 Vue 应用中非常重要的部分。不同组件之间的通信方式可以根据组件的关系(如父子组件、兄弟组件或跨层级组件)而有所不同。

3.1 父子组件通信

父组件传递数据给子组件 (Props)

在 Vue 中,父组件可以通过 props 向子组件传递数据。props 是一种从父组件向子组件传递数据的方式,它是单向数据流的体现,即父组件的数据可以通过 props 传递给子组件,但子组件不能直接修改父组件的数据。

父组件传递数据给子组件

<!-- ParentComponent.vue -->
<template>
  <div>
    <child-component :message="parentMessage"></child-component>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      parentMessage: 'Hello from Parent!'
    };
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  props: ['message']
}
</script>

说明:

  • 在父组件中,使用 :message="parentMessage" 传递了数据。
  • 在子组件中,通过 props 接收父组件传递的数据并渲染。

子组件向父组件传递数据

子组件向父组件传递数据,通常是通过自定义事件实现的。子组件通过 $emit 向父组件发送事件,父组件监听这个事件并执行相应的操作。

子组件向父组件传递数据

<!-- ParentComponent.vue -->
<template>
  <div>
    <child-component @message-changed="handleMessageChanged"></child-component>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  methods: {
    handleMessageChanged(newMessage) {
      console.log('Message from child:', newMessage);
    }
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <button @click="sendMessageToParent">Send Message</button>
  </div>
</template>

<script>
export default {
  methods: {
    sendMessageToParent() {
      this.$emit('message-changed', 'Hello from Child!');
    }
  }
}
</script>

说明:

  • 子组件通过 this.$emit('message-changed', 'Hello from Child!') 向父组件发送 message-changed 事件并附带数据。
  • 父组件通过 @message-changed="handleMessageChanged" 监听这个事件,并处理来自子组件的数据。

3.2 兄弟组件通信

兄弟组件间的通信可以通过 父组件 进行中介(即,父组件充当传递数据的桥梁)。兄弟组件之间无法直接互相通信,因此可以通过父组件将数据从一个兄弟组件传递到另一个。

父组件作为中介

<!-- ParentComponent.vue -->
<template>
  <div>
    <sibling-one @message-changed="updateMessage"></sibling-one>
    <sibling-two :message="message"></sibling-two>
  </div>
</template>

<script>
import SiblingOne from './SiblingOne.vue';
import SiblingTwo from './SiblingTwo.vue';

export default {
  components: {
    SiblingOne,
    SiblingTwo
  },
  data() {
    return {
      message: ''
    };
  },
  methods: {
    updateMessage(newMessage) {
      this.message = newMessage;
    }
  }
}
</script>
<!-- SiblingOne.vue -->
<template>
  <div>
    <button @click="sendMessage">Send Message to Sibling 2</button>
  </div>
</template>

<script>
export default {
  methods: {
    sendMessage() {
      this.$emit('message-changed', 'Message from Sibling One');
    }
  }
}
</script>
<!-- SiblingTwo.vue -->
<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  props: ['message']
}
</script>

说明:

  • SiblingOne 通过 $emit 向父组件发送事件,父组件接收这个事件并更新 message
  • SiblingTwo 接收父组件传递的 message,并将其显示出来。

3.3 跨层级组件通信

跨层级组件通信可以通过 Event BusVuex 实现。

(1) Event Bus (事件总线)

Event Bus 是通过一个中央的 Vue 实例作为消息中心,允许多个组件之间通过发布订阅模式进行通信。事件总线常用于兄弟组件、父子组件等之间的通信。

Event Bus 实现跨层级通信

// eventBus.js
import Vue from 'vue';
export const EventBus = new Vue();
<!-- SenderComponent.vue -->
<template>
  <button @click="sendMessage">Send Message</button>
</template>

<script>
import { EventBus } from './eventBus';

export default {
  methods: {
    sendMessage() {
      EventBus.$emit('message-sent', 'Hello from Sender!');
    }
  }
}
</script>
<!-- ReceiverComponent.vue -->
<template>
  <div>{{ receivedMessage }}</div>
</template>

<script>
import { EventBus } from './eventBus';

export default {
  data() {
    return {
      receivedMessage: ''
    };
  },
  created() {
    EventBus.$on('message-sent', (message) => {
      this.receivedMessage = message;
    });
  },
  destroyed() {
    EventBus.$off('message-sent'); // 清理事件监听器
  }
}
</script>

说明:

  • SenderComponent 通过 EventBus.$emit 发送事件。
  • ReceiverComponent 通过 EventBus.$on 监听该事件并接收消息。

(2) Vuex (全局状态管理)

Vuex 是 Vue 的官方状态管理库,用于在大型应用中管理和共享组件之间的状态。Vuex 适用于组件间需要共享数据,且这个数据需要跨越多个层级的场景。

Vuex 基础使用:

  1. 安装 Vuex:

    npm install vuex
    
  2. 配置 Vuex 存储:

    // store.js
    import Vue from 'vue';
    import Vuex from 'vuex';
    
    Vue.use(Vuex);
    
    export const store = new Vuex.Store({
      state: {
        message: ''
      },
      mutations: {
        setMessage(state, message) {
          state.message = message;
        }
      },
      actions: {
        updateMessage({ commit }, message) {
          commit('setMessage', message);
        }
      }
    });
    
  3. 在组件中使用 Vuex:

<!-- SenderComponent.vue -->
<template>
  <button @click="sendMessage">Send Message</button>
</template>

<script>
export default {
  methods: {
    sendMessage() {
      this.$store.dispatch('updateMessage', 'Hello from Sender!');
    }
  }
}
</script>
<!-- ReceiverComponent.vue -->
<template>
  <div>{{ message }}</div>
</template>

<script>
export default {
  computed: {
    message() {
      return this.$store.state.message;
    }
  }
}
</script>

说明:

  • 使用 Vuex 管理状态,在 SenderComponent 组件中更新状态,在 ReceiverComponent 组件中读取共享的状态。

3.4 总结

  1. 父子组件通信

    • 父组件通过 props 向子组件传递数据。
    • 子组件通过 $emit 向父组件传递数据。
  2. 兄弟组件通信

    • 通过父组件作为中介,父组件使用事件处理器更新数据并传递给其他兄弟组件。
  3. 跨层级组件通信

    • 通过 Event Bus(事件总线)来实现跨组件通信。
    • 通过 Vuex 进行全局状态管理,适用于更复杂的组件通信需求。

选择合适的通信方式取决于你的应用结构、组件之间的关系以及数据流的复杂度。在大型应用中,通常会使用 Vuex 来管理全局状态,而在小型或中型应用中,props$emit 可能已经足够满足需求。

3.5 props 校验

在 Vue 中,props 是父组件向子组件传递数据的主要方式。为了确保传递给子组件的数据符合预期的格式,Vue 提供了 props 校验 功能。通过在子组件中定义 props 的类型和规则,我们可以让 Vue 在开发环境下自动检查父组件传递的 props 是否符合指定的类型和约束条件。

props 校验的基础语法

在 Vue 中,我们可以通过在子组件的 props 选项中定义一个对象来指定属性的类型和其他验证规则。这些验证规则会在开发模式下进行检查并给出警告(如果数据不符合规则)。

props: {
  // prop1 是一个字符串类型
  prop1: {
    type: String,
    required: true, // 表示这个 prop 是必填的
    default: 'default value' // 默认值
  },
  
  // prop2 是一个数字类型,且有自定义验证规则
  prop2: {
    type: Number,
    validator(value) {
      // 如果 prop2 的值小于 10,验证失败
      if (value < 10) {
        console.warn('prop2 should be greater than or equal to 10');
        return false;
      }
      return true;
    }
  }
}

常见的 props 校验规则

  1. type
    type 用来指定 prop 的类型。常见的类型有:StringNumberBooleanArrayObjectFunctionSymbol 等。Vue 会自动检查传递的数据是否符合指定的类型。

    props: {
      name: {
        type: String,
        required: true
      },
      age: {
        type: Number,
        default: 18
      }
    }
    
  2. required
    required 属性用来指定该 prop 是否是必需的。如果 required 设置为 true,Vue 会确保该 prop 必须传递给子组件。

    props: {
      name: {
        type: String,
        required: true
      }
    }
    
  3. default
    default 用来指定该 prop 的默认值。如果父组件没有传递该 prop,则会使用默认值。

    props: {
      name: {
        type: String,
        default: 'Anonymous'
      }
    }
    

    注意:default 只能用于非 requiredprops,因为 requiredprops 必须由父组件提供值。

  4. validator
    validator 用来实现自定义的校验逻辑。它是一个函数,接受 value 参数,返回 truefalse。如果返回 false,则表示验证失败,并在开发模式下发出警告。

    props: {
      age: {
        type: Number,
        validator(value) {
          // 验证 prop `age` 是否在合理的范围内
          if (value < 0 || value > 120) {
            console.warn('Invalid age value');
            return false;
          }
          return true;
        }
      }
    }
    

    注意validator 只会在开发模式下有效,它不会在生产环境中执行。

  5. 自定义多个类型的 prop 校验

    如果一个 prop 可以是多个类型(例如,StringNumber),可以使用数组来定义 type

    props: {
      value: {
        type: [String, Number],
        required: true
      }
    }
    

    value 可以是字符串类型或数字类型,Vue 会验证它是否符合这两者中的任意一种。

组合校验规则

可以在 props 中同时使用多个校验规则。

props: {
  name: {
    type: String,
    required: true,   // 必填
    default: 'Guest'  // 默认值
  },
  age: {
    type: Number,
    required: true,
    validator(value) {
      if (value < 0 || value > 120) {
        console.warn('Age must be between 0 and 120');
        return false;
      }
      return true;
    }
  }
}

校验规则的执行时机

  • 校验只在 开发模式 下进行。在生产环境中,Vue 会移除这些校验逻辑,以提高性能。
  • 如果 props 校验失败,Vue 会在开发者工具控制台输出警告信息,告诉你哪个 prop 验证失败了,并给出相关的错误信息。
  • validator 函数只在开发环境中生效,当需要对生产环境进行严格的校验,可以考虑通过其他方式处理。

代码

<!-- ParentComponent.vue -->
<template>
  <div>
    <child-component :name="parentName" :age="parentAge"></child-component>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      parentName: 'John',
      parentAge: 25
    };
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <p>Name: {{ name }}</p>
    <p>Age: {{ age }}</p>
  </div>
</template>

<script>
export default {
  props: {
    name: {
      type: String,
      required: true,
      default: 'Anonymous'
    },
    age: {
      type: Number,
      required: true,
      validator(value) {
        if (value < 0 || value > 120) {
          console.warn('Age should be between 0 and 120');
          return false;
        }
        return true;
      }
    }
  }
}
</script>

总结

在 Vue 中,props 校验可以帮助确保父组件传递给子组件的数据符合预期的类型和格式。

  • type:指定类型;
  • required:指定是否为必填项;
  • default:指定默认值;
  • validator:自定义验证规则。

使用这些规则,Vue 会在开发模式下自动进行校验,确保传递的数据是有效的,并通过警告提醒开发者。在生产环境中,校验会被移除,以提高性能。

3.6 propsdata:单向数据流

在 Vue 中,propsdata 是两种核心的数据机制,分别用于父子组件间的数据传递和组件内部的数据存储。理解这两者的关系及其如何支持 单向数据流 是理解 Vue 数据流动的关键。

1. propsdata 的区别

(1) props
  • props 是父组件传递给子组件的数据。
  • 它是一种 单向数据流 的方式,数据只能从父组件流向子组件。子组件无法直接修改从父组件传递的 props,但可以通过事件或其他方式与父组件进行交互。
  • props 的值是由父组件提供的,一旦传递给子组件,子组件可以通过 props 来读取这些值。
<!-- ParentComponent.vue -->
<template>
  <div>
    <child-component :message="parentMessage"></child-component>
  </div>
</template>

<script>
export default {
  data() {
    return {
      parentMessage: 'Hello from Parent'
    };
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  props: ['message']
}
</script>

parentMessage 通过 props 被传递给了子组件 ChildComponent,子组件通过 {{ message }} 来访问并显示这个数据。这是一种单向数据流:从父组件到子组件

(2) data
  • data 是组件内部的状态,它存储了组件的响应式数据。
  • 这些数据是 局部的,只能在组件内部使用或通过方法进行修改,不会直接影响其他组件的状态。
<!-- MyComponent.vue -->
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="changeMessage">Change Message</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello from data'
    };
  },
  methods: {
    changeMessage() {
      this.message = 'Message changed!';
    }
  }
}
</script>

message 是一个在组件内部定义的 data,它是响应式的,子组件可以通过 this.message 访问和修改它。点击按钮后,message 的值会被更新,Vue 会自动重新渲染视图。

2. 单向数据流的概念

单向数据流 是指数据只能在应用中以一个方向流动。在 Vue 中,数据流动的方向是从 父组件到子组件,通过 props 进行传递。Vue 的响应式系统保证了父组件的数据变更会自动反映到子组件中,而子组件无法直接修改父组件的数据。

这种设计理念的核心目的是:

  • 可维护性:数据流动只有一个方向,避免了多个组件之间的数据相互依赖和复杂的更新逻辑,使得应用更易于调试和维护。
  • 简化管理:当数据的流动是单向时,组件的状态和行为更容易追踪,父组件负责管理全局状态,子组件则负责展示和交互。

3. 如何遵循单向数据流

(1) 父组件向子组件传递数据 (Props)

父组件将数据通过 props 传递给子组件,子组件只能读取这些数据,不能修改它们。这种方式确保了数据流动的方向是单向的。

<!-- Parent.vue -->
<template>
  <div>
    <child-component :message="parentMessage"></child-component>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: { ChildComponent },
  data() {
    return {
      parentMessage: 'Message from Parent'
    };
  }
}
</script>
<!-- Child.vue -->
<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  props: {
    message: String
  }
}
</script>

parentMessage 通过 props 传递给了 ChildComponent,而子组件 ChildComponent 只是读取了这个值,不能修改它。

(2) 子组件通过事件通知父组件 (Event Emitting)

当子组件需要与父组件交互(例如修改父组件的数据)时,子组件不能直接修改父组件的数据,而是通过 自定义事件 通知父组件进行修改。父组件接收到事件后,执行相应的操作。

<!-- Parent.vue -->
<template>
  <div>
    <child-component @updateMessage="handleUpdateMessage"></child-component>
    <p>{{ parentMessage }}</p>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: { ChildComponent },
  data() {
    return {
      parentMessage: 'Initial Message'
    };
  },
  methods: {
    handleUpdateMessage(newMessage) {
      this.parentMessage = newMessage;
    }
  }
}
</script>
<!-- Child.vue -->
<template>
  <div>
    <button @click="sendMessageToParent">Change Parent Message</button>
  </div>
</template>

<script>
export default {
  methods: {
    sendMessageToParent() {
      this.$emit('updateMessage', 'Updated Message from Child');
    }
  }
}
</script>

子组件通过 this.$emit 向父组件发送 updateMessage 事件,父组件通过监听这个事件来更新自己的状态。这仍然是 单向数据流,因为父组件的状态变化是通过事件触发的,而不是直接由子组件修改的。

4. 为什么需要单向数据流?

(1) 数据追踪更简单

在单向数据流中,数据总是从父组件流向子组件,避免了多方向的数据传递问题,这样可以让开发者更容易追踪数据的变化来源。

(2) 调试和维护更容易

单向数据流使得 Vue 应用的行为更加可预测。当应用变得复杂时,我们可以通过父组件和子组件的关系清楚地理解数据是如何变化的,避免了状态不一致和不必要的复杂性。

(3) 减少了副作用

当子组件不能直接修改父组件的数据时,避免了多个组件之间不经意间互相影响的数据更改,减少了副作用。父组件可以集中管理数据,而子组件则专注于展示和交互。

(4) 便于构建组件库

由于 Vue 提倡单向数据流,开发者可以更容易地创建和维护可复用的组件。组件之间的关系是通过 props 和事件来建立的,父组件控制着数据流动,而子组件则是 纯粹的展示组件,不会修改外部的状态。

5. Vuex:管理复杂应用的单向数据流

在大型应用中,单一的父子组件数据流可能会变得不够用,尤其是当多个组件需要共享和修改相同的状态时。为了处理这种情况,Vue 提供了 Vuex,它是一个专门为 Vue 设计的状态管理库,可以帮助管理和共享跨组件的数据。

  • Vuex 强制应用的数据流仍然是单向的。通过 state 来集中存储数据,mutations 用于修改数据,actions 用于触发 mutations,getters 用于计算派发的数据。
  • Vuex 保证数据流动的单向性,使得应用的状态变得更加可预测和可维护。

总结

  • props 是用于父组件向子组件传递数据的机制,确保了数据流动是单向的。
  • data 是组件内部的状态,只有组件内部可以修改它。
  • 单向数据流 是指数据从父组件流向子组件,而子组件不能直接修改父组件的数据。如果需要修改父组件的数据,子组件应该通过自定义事件向父组件发送请求,或者使用像 Vuex 这样的全局状态管理解决方案。

单向数据流在 Vue 中的应用有助于提高应用的可维护性、可预测性和调试性,使得组件的行为更加明确和稳定。

四、非父子组件通信

通过 provideinject 进行跨层级通信

在 Vue 中,provideinject 是一对 API,允许在组件树中实现跨层级的数据共享,通常用于避免通过 propsevents 传递数据,尤其是当数据需要在多个组件之间共享时。

这两个 API 允许 祖先组件 提供数据,而 后代组件 无论在组件树中有多少层级,都可以直接访问这些数据。这样做的好处是能够避免多层级的 props 传递,减少了组件间的耦合性。

1. provideinject 的基本原理

  • provide:在祖先组件中提供一个数据或方法,这个数据是可以被后代组件访问的。
  • inject:在后代组件中接收祖先组件提供的数据。

这种机制主要是用来处理父组件和子组件之间不直接的通信,尤其适合跨越多层级的通信(例如跨越中间组件)。provideinject 可以在不涉及 props 的情况下,使数据流在组件之间传递。

2. 使用场景

  • 插件和全局设置:插件或库中的设置、主题配置等。
  • 跨层级的数据共享:避免多层级的 props 传递,尤其在一些复杂的组件树中,传递 props 会显得很冗余。
  • 避免中间层组件的重复传递:一些中间层组件只是负责将 props 继续传递到更深的组件,并不使用它们,可以通过 provideinject 来避免中间层的重复传递。

3. 基本用法

3.1 祖先组件使用 provide 提供数据

provide 让你可以提供一个对象、数据或方法,后代组件可以通过 inject 访问这些数据。

// GrandparentComponent.vue
<template>
  <div>
    <parent-component></parent-component>
  </div>
</template>

<script>
import ParentComponent from './ParentComponent.vue';

export default {
  components: { ParentComponent },
  provide() {
    // 提供数据
    return {
      message: 'Hello from Grandparent!',
      updateMessage: this.updateMessage
    };
  },
  methods: {
    updateMessage(newMessage) {
      this.message = newMessage;
    }
  }
};
</script>

GrandparentComponent 使用 provide 提供了 messageupdateMessage,这将允许其后代组件访问这些数据。

3.2 后代组件使用 inject 接收数据

inject 允许后代组件直接访问祖先组件通过 provide 提供的数据,无论组件层级有多深。

// ChildComponent.vue
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="changeMessage">Change Message</button>
  </div>
</template>

<script>
export default {
  inject: ['message', 'updateMessage'],  // 注入祖先组件提供的数据和方法
  methods: {
    changeMessage() {
      this.updateMessage('Message updated by Child!');
    }
  }
};
</script>

ChildComponent.vue 中,inject 会接收 GrandparentComponent 提供的 messageupdateMessage 方法。message 显示在页面中,而 changeMessage 方法会调用 updateMessage 来更新 message

4. 工作原理

  • 数据流动方向:数据流从祖先组件通过 provide 流向所有后代组件,这个过程是单向的。
  • 响应式:提供的数据是响应式的,即当数据改变时,所有使用 inject 注入该数据的组件会自动重新渲染。注意,这种响应性是基于 Vue 的响应式系统的,数据本身需要是 Vue 的响应式对象。
  • 作用域provideinject跨层级的,但它们在组件树中仅限于祖先和后代组件之间的传递,不能跨越同级组件。如果两个组件没有父子关系,无法直接使用 provideinject

5. 举例说明

5.1 跨层级数据共享
<!-- GrandparentComponent.vue -->
<template>
  <div>
    <parent-component></parent-component>
  </div>
</template>

<script>
import ParentComponent from './ParentComponent.vue';

export default {
  components: { ParentComponent },
  provide() {
    return {
      userName: 'John Doe'  // 提供的数据
    };
  }
};
</script>
<!-- ParentComponent.vue -->
<template>
  <div>
    <child-component></child-component>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: { ChildComponent }
};
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <p>{{ userName }}</p>
  </div>
</template>

<script>
export default {
  inject: ['userName'],  // 注入祖先组件提供的数据
};
</script>

GrandparentComponent 提供了一个 userName,而 ChildComponent 直接注入了这个数据。即使 ParentComponent 不直接使用 userNameChildComponent 依然能够访问到它。

5.2 跨层级函数共享
<!-- GrandparentComponent.vue -->
<template>
  <div>
    <parent-component></parent-component>
  </div>
</template>

<script>
import ParentComponent from './ParentComponent.vue';

export default {
  components: { ParentComponent },
  provide() {
    return {
      logMessage: this.logMessage
    };
  },
  methods: {
    logMessage(message) {
      console.log('Message from Grandparent:', message);
    }
  }
};
</script>
<!-- ParentComponent.vue -->
<template>
  <div>
    <child-component></child-component>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: { ChildComponent }
};
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <button @click="triggerLog">Log Message</button>
  </div>
</template>

<script>
export default {
  inject: ['logMessage'],  // 注入祖先组件提供的方法
  methods: {
    triggerLog() {
      this.logMessage('Hello from ChildComponent!');
    }
  }
};
</script>

GrandparentComponent 提供了一个方法 logMessage,并且 ChildComponent 可以直接调用它。在点击按钮时,ChildComponent 调用了 logMessage 方法,输出信息到控制台。

6. provideinject 的注意事项

  • 数据的响应性provide 提供的数据是响应式的,但必须确保传递的数据是 Vue 可响应的对象。如果是非响应式对象,Vue 无法自动更新组件。
  • 命名约定provideinject 的值需要遵循一致的命名约定,如果组件层级较多,使用命名空间可以帮助管理这些数据。
  • 仅限于父子关系provideinject 是用于父子关系的跨层级通信,不能用于同级组件之间的通信。如果需要同级组件之间通信,可以考虑使用 Event BusVuex
  • 生命周期inject 的数据在组件生命周期内是有效的,但当组件销毁时,数据也会被清除。需要特别注意清理事件监听和其他可能的副作用。

7. 总结

  • provideinject 机制非常适用于跨层级的数据共享,尤其是在多个中间组件没有必要传递数据时。
  • 通过 provideinject,你可以减少不必要的 props 传递,从而让你的组件更加简洁和解耦。
  • 这种方式适用于一些特殊场景,比如全局设置、跨层级的共享数据,或者插件化的设计。

虽然 provideinject 很有用,但对于应用状态比较复杂的情况,Vuex 仍然是更推荐的全局状态管理工具。

五 补充

5.1 v-model

1. 双向绑定的实现

v-model 通过在数据和 DOM 之间建立双向绑定来工作。具体来说,当我们在 Vue 的模板中使用 v-model 时,它会自动绑定到一个表单控件(如 <input><select><textarea>)的值(value)和 Vue 实例中的数据。

  • 在 DOM 端,v-model 会监听表单控件的变化(比如用户输入或者选择)。
  • 在 Vue 实例的 js 端,它会把控件的变化同步到 Vue 数据中,保持数据和视图的一致性。

2. 具体实现过程

2.1 v-model 在输入控件上的绑定

当你在 Vue 模板中使用 v-model 绑定一个表单控件时,它会根据控件类型(例如 <input><textarea><select>)进行不同的处理:

  • 对于 <input><textarea><select>v-model 会绑定到这些元素的值。

    对于 <input> 元素,它通常绑定 value 属性,并监听 input 事件。每当用户在输入框中输入内容时,input 事件会被触发,Vue 会更新绑定的数据。

    <input v-model="message">
    

    在这个例子中,message 是 Vue 实例的数据,v-model 会自动地将 message 绑定到 <input>value 属性上,并监听 input 事件来更新 message

2.2 监听事件和更新数据
  • 事件监听:当表单控件的内容发生变化时,v-model 会监听控件的事件(例如 input 事件)。对于 <input> 元素来说,Vue 会监听 input 事件,并将事件触发时的值更新到 Vue 实例中的数据。

    <input v-model="message">
    

    这背后实际上等同于:

    <input :value="message" @input="message = $event.target.value">
    
  • 数据更新:当 Vue 实例中的数据变化时,视图会自动更新。例如,如果你通过某些操作修改了 message 的值,Vue 会自动更新绑定的输入框的内容。

2.3 v-model 的内部实现

Vue 2 内部通过 Object.defineProperty()gettersetter 来实现数据的响应式系统。v-model 会依赖 Vue 的数据响应式机制,当数据发生变化时,自动触发视图更新。

例如,当你通过 v-model 绑定一个输入框并修改 Vue 实例中的数据时,Vue 会通过其响应式系统来确保输入框的值更新。

3. .sync.lazy 修饰符

v-model 也支持一些修饰符,可以改变其默认的行为:

  • .sync 修饰符:当父子组件的 prop 更新时,默认会触发事件($emit)。如果你想在子组件内部修改 prop 并同步更新到父组件,可以使用 .sync 修饰符。

  • .lazy 修饰符:默认情况下,v-model 会在 input 事件时更新数据,而 .lazy 修饰符会改变这个行为,使得数据更新延迟到 change 事件时。

    <input v-model.lazy="message">
    

    这意味着只有当用户失去焦点或者选择内容时,message 才会更新。

  • .number 修饰符:用于自动将输入的值转换为数字。例如,当用户输入 "123" 时,v-model 会自动将其转换为 123(数字类型)。

    <input v-model.number="age">
    
  • .trim 修饰符:会自动去除输入值的前后空格。

    <input v-model.trim="message">
    

4. 总结

v-model 实现了表单控件的双向数据绑定,通过监听控件的事件(如 input)并更新 Vue 实例的数据,反之也会在数据发生变化时更新控件的显示值。它的核心原理是通过 Vue 的响应式系统实现的,确保视图与数据的一致性。

5.2 ref 和 $refs

在 Vue 中,ref$refs 都与引用 DOM 元素或组件实例相关,但它们的使用方式和作用是不同的。

1. ref

ref 是 Vue 模板语法中的一个特殊属性,用于在模板中为某个 DOM 元素或组件实例指定引用标识符。这些引用会在 Vue 实例的 $refs 对象中创建一个对应的属性。

1.1 使用 ref 的基本语法
  • 在模板中,我们为某个 DOM 元素或组件添加 ref 属性,赋予它一个标识符。

    <!-- 为 DOM 元素添加 ref -->
    <div ref="myDiv">Hello, world!</div>
    
    <!-- 为组件添加 ref -->
    <my-component ref="myComponent"></my-component>
    

在 Vue 实例的生命周期中,所有使用了 ref 的 DOM 元素或组件都会被挂载到 $refs 对象中。

2. $refs

$refs 是 Vue 实例的一个对象,它包含了所有带有 ref 属性的 DOM 元素和子组件实例。它是 Vue 实例上提供的一个便捷方式,用来直接访问这些 DOM 元素或组件实例。

2.1 访问 $refs 中的 DOM 元素或组件
  • 通过 $refs 可以访问到在模板中用 ref 引用的 DOM 元素或组件实例。

    // 访问 DOM 元素
    this.$refs.myDiv  // 返回 <div ref="myDiv">...</div>
    
    // 访问组件实例
    this.$refs.myComponent  // 返回 <my-component ref="myComponent">...</my-component>
    
2.2 ref$refs 的使用场景
  • 访问 DOM 元素:如果需要操作某个元素的属性、样式,或执行一些 DOM 操作(如聚焦、滚动等),可以使用 ref 来引用该元素,然后通过 $refs 访问它。

    <input ref="inputField">
    <button @click="focusInput">Focus the input</button>
    
    methods: {
      focusInput() {
        this.$refs.inputField.focus();
      }
    }
    
  • 访问子组件实例:如果要调用子组件中的方法或访问子组件的数据,也可以使用 ref 来引用该组件,并通过 $refs 访问它。

    <child-component ref="child"></child-component>
    <button @click="callChildMethod">Call child method</button>
    
    methods: {
      callChildMethod() {
        this.$refs.child.someChildMethod();
      }
    }
    
2.3 $refs 的响应性
  • 不具备响应性$refs 不是响应式的,也就是说,如果 $refs 中的某个引用发生变化,Vue 不会自动重新渲染组件或 DOM 元素。$refs 是在组件挂载后通过 ref 获取的引用对象,只有在组件完全挂载后才能访问。
2.4 ref 的特殊情况
  • v-for 中使用 ref:当 ref 用在 v-for 循环中时,$refs 会返回一个数组(或对象),其中包含所有相同 ref 值的引用。

    <div v-for="(item, index) in items" :key="index" :ref="'item' + index">
      {{ item }}
    </div>
    
    mounted() {
      console.log(this.$refs); // 输出一个包含所有 'item0', 'item1', ... 的对象
    }
    

3. 总结

  • ref 是一个在模板中设置的属性,用于给 DOM 元素或子组件实例设置引用标识符。
  • $refs 是 Vue 实例的一个对象,包含了所有带有 ref 的 DOM 元素或组件实例,可以通过 $refs 直接访问这些元素或组件实例。
  • 使用 ref$refs 可以让你在 Vue 中直接操作 DOM 或访问子组件的方法,适用于一些不适合通过数据绑定来控制的操作。
  • 注意 $refs 不具备响应性,不会触发视图更新,因此应谨慎使用。

5.3 Vue异步更新、$nextTick

在 Vue 中,异步更新和**$nextTick** 是非常重要的概念,它们与 Vue 的响应式系统、DOM 更新机制密切相关。理解这些概念有助于更好地掌握 Vue 的数据更新和视图渲染的时机。

1. Vue 异步更新

Vue 的数据更新通常是异步的,这意味着当你修改 Vue 实例的数据时,Vue 会将这些更改加入一个更新队列,而不是立即更新 DOM。这样做的目的是为了优化性能,避免多次触发 DOM 操作。

为什么 Vue 使用异步更新?
  • 性能优化:如果每次数据变化时都立即更新 DOM,可能会导致不必要的性能开销。Vue 会在一个事件循环的末尾进行批量 DOM 更新,避免多次渲染,减少重排(reflow)和重绘(repaint)的次数。
  • 事件循环:js 是单线程的,Vue 的更新机制利用了事件循环(Event Loop),在下一个 “tick”(时机)更新 DOM。
示例:异步更新数据
data: {
  message: 'Hello'
},
methods: {
  updateMessage() {
    this.message = 'World';  // 更新数据
    console.log(this.message);  // 打印 'World'
  }
}

在上面的代码中,this.message = 'World' 会立即更新 Vue 实例的数据,但是在调用 console.log(this.message) 时,打印的还是更新前的值 'Hello',因为 DOM 并未立即更新。

2. $nextTick

为了处理 Vue 的异步更新机制,Vue 提供了 $nextTick 方法。它允许你在数据更新并且 DOM 重新渲染后,执行一个回调函数。

$nextTick 是一个异步方法,它会等待 DOM 更新完成后再执行回调,因此可以确保你获取到更新后的 DOM 状态。

使用 $nextTick

假设你希望在数据变化之后,立即执行某些操作(如访问更新后的 DOM)。

this.message = 'World';  // 更新数据
this.$nextTick(() => {
  console.log(this.$refs.myDiv.innerText);  // 获取更新后的 DOM
});

这里,$nextTick 会等到数据更新并且 DOM 完全渲染之后,再执行回调中的代码,确保你访问的是更新后的 DOM。

3. $nextTick 的应用场景

  • 访问更新后的 DOM:当你在修改数据之后,需要访问到更新后的 DOM 元素(例如获取元素的尺寸、位置等),可以使用 $nextTick 来确保 DOM 已经渲染完成。

    this.items.push('new item');  // 更新数据
    this.$nextTick(() => {
      console.log(this.$refs.list.scrollTop);  // 获取更新后的元素的 scrollTop
    });
    
  • 执行某些需要 DOM 更新的操作:比如调用第三方库(如 jQuery、D3 等)在 DOM 更新后执行某些操作。

  • 动画和过渡效果:在改变 DOM 状态后,如果需要执行动画或过渡效果,通常要确保 DOM 更新已经完成。此时,可以使用 $nextTick 来确保动画代码是在正确的时机执行的。

    this.visible = true;  // 改变可见性
    this.$nextTick(() => {
      this.$refs.box.classList.add('fade-in');  // 在 DOM 更新后添加动画类
    });
    

4. $nextTick 和 Vue 的更新队列

  • Vue 在更新数据时,会将所有的 DOM 更新操作推入一个队列中,$nextTick 的回调函数会在下一个事件循环(即队列中的 DOM 更新完成后)执行。这意味着 Vue 会尽量将多次数据更新合并为一次 DOM 更新,从而减少不必要的性能消耗。

5. 总结

  • Vue 异步更新:Vue 在数据更新时会异步更新 DOM,避免频繁的 DOM 操作带来性能问题。

  • $nextTick:用于在数据变化和 DOM 更新后,执行一个回调函数,确保你访问的是更新后的 DOM 状态。常用于访问或操作 DOM 元素、执行动画、与第三方库交互等场景。

    this.$nextTick(() => {
      // 在 DOM 更新后执行的代码
    });
    

通过使用 $nextTick,精确地控制代码执行时机,确保与 Vue 的异步更新机制配合得当。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2264355.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java日常工作开发高并发问题

前言 本篇文章将是以工作中经常遇到的问题&#xff0c;和面试中经常遇到的java问题进行描写。内容包括微服架构&#xff0c;java并发编程以及相应的中间件的高级知识。本文所有的问题都在描述多线程编程的高级知识。 一. 面试题 1.Sychronized和ReentrantLock有哪些不同点? …

【Python】【数据分析】深入探索 Python 数据可视化:Matplotlib 绘图库完整教程

目录 引言一、什么是 Matplotlib&#xff1f;1.1 Matplotlib 的安装1.2 Matplotlib 的基本功能 二、Matplotlib 的基础绘图2.1 绘制折线图2.2 绘制柱状图2.3 绘制散点图2.4 绘制饼图 三、高级功能与定制3.1 设置图表样式3.2 使用子图3.3 保存图表 四、Matplotlib 流程图4.1 Mer…

【代码随想录|动态规划背包问题】

一、背包问题分类 01背包&#xff1a;n种物品&#xff0c;每种物品只有一个 完全背包&#xff1a;n种物品&#xff0c;每种物品有无限个 多重背包&#xff1a;n种物品&#xff0c;每种物品的个数各不相同 二、01背包问题三道题 卡码网46题.携带研究材料&#xff08;二维背包…

第1章 命题逻辑

2024年12月22日 一稿 1.1 现代逻辑学的基本研究方法 1.2 命题及其表示法 1.2.1 命题的概念 定义1.1 命题是一个可以判断真假的陈述句。 1.2.2 联结词 非 与 或 蕴含 等价 1.3 命题公式与语句形式化 1.3.1 命题公式的定义 1.3.2 公式的层次 1.3.3 语句形式化 1…

Unity-Editor扩展GUI基本实现一个可拖拉放的格子列表

短短几百行代码,好吧,又是“参考”了国外的月亮 操作,还真地挺自然的。。。。。。国外的实现有点小牛 拖拉,增加+ 一个Element 鼠标左键长按,可以出提示 鼠标右键,清除Element, 有点小bug,不是很自然地完全清除, using System.Collections; using System.Collecti…

解决vscode ssh远程连接服务器一直卡在下载 vscode server问题

目录 方法1&#xff1a;使用科学上网 方法2&#xff1a;手动下载 方法3 在使用vscode使用ssh远程连接服务器时&#xff0c;一直卡在下载"vscode 服务器"阶段&#xff0c;但MobaXterm可以正常连接服务器&#xff0c;大概率是网络问题&#xff0c;解决方法如下: 方…

重拾设计模式--外观模式

文章目录 外观模式&#xff08;Facade Pattern&#xff09;概述定义 外观模式UML图作用 外观模式的结构C 代码示例1C代码示例2总结 外观模式&#xff08;Facade Pattern&#xff09;概述 定义 外观模式是一种结构型设计模式&#xff0c;它为子系统中的一组接口提供了一个统一…

jvm栈帧结构

JVM(Java虚拟机)中的虚拟机栈是线程私有的,用于支持Java虚拟机进行方法调用和方法执行。而栈帧(Stack Frame)则是虚拟机栈的基本元素,每一个方法从调用开始至执行结束的整个过程,都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。栈帧的内部结构主要包括以下几个部分:…

2009 ~ 2019 年 408【计算机网络】大题解析

2009 年 路由算法&#xff08;9’&#xff09; 讲解视频推荐&#xff1a;【BOK408真题讲解-2009年&#xff08;催更就退网版&#xff09;】 某网络拓扑如下图所示&#xff0c;路由器 R1 通过接口 E1 、E2 分别连接局域网 1 、局域网 2 &#xff0c;通过接口 L0 连接路由器 R2 &…

Flamingo论文介绍:把视觉特征向语言模型看齐

今天介绍一篇经典的多模态论文&#xff0c;来自NeurIPS 2022的《Flamingo: a Visual Language Model for Few-Shot Learning》 &#xff0c;论文地址&#xff1a;https://arxiv.org/pdf/2103.00020 文章目录 一、Motivate二、Method三、模块细节&#xff1a;Perceiver Resampl…

【VSCode】常用插件汇总

1 Path Autocomplete&#xff08;路径提示的插件&#xff09; 步骤一&#xff1a;在vscode的扩展搜索中直接搜索Path Autocomplete&#xff0c;直接安装 步骤二&#xff1a;配置 配置 VS Code settings.json "path-autocomplete.pathMappings": {"": &q…

STM32F103 | Embedded IDE03 - 使用OpenOCD在STM32F103项目时出现下载固件失败

导言 在上一篇备忘录介绍使用OpenOCD的stlink-v2.cfg接口下载固件&#xff0c;在STM32F407的项目上很顺利。但是&#xff0c;在stm32f103上会出现下载失败。 在网上搜了一下&#xff0c;这位博主的文章解决了这个问题: https://www.iotword.com/26738.html 一、修改stm32f1x.c…

易语言 OCR 文字识别

一.引言 文字识别&#xff0c;也称为光学字符识别&#xff08;Optical Character Recognition, OCR&#xff09;&#xff0c;是一种将不同形式的文档&#xff08;如扫描的纸质文档、PDF文件或数字相机拍摄的图片&#xff09;中的文字转换成可编辑和可搜索的数据的技术。随着技…

Linux 网络维护相关命令简介

目录 零. 概要一. ping二. ip命令2.1 ip address2.2 ip route2.3 ip neighbour 三. traceroute四. DNS查询4.1 nslookup4.2 dig 五. ss 查看网络连接状态 零. 概要 ⏹在Linux系统中有2套用于网络管理的工具集 net-tools 早期网络管理的主要工具集&#xff0c;缺乏对 IPv6、网…

vscode中同时运行两个python文件(不用安装插件)

如何在vscode中同时运行两个python文件呢&#xff1f;今天在工作中遇到了这个问题。 查了网上的方法是安装coder runner插件&#xff0c;后来发现自身就有这个功能。所以记录一下,方便后续查找: 这是我的第一个文件&#xff0c;点击右上角的运行旁边的小箭头&#xff0c;有一…

matlab绘图时设置左、右坐标轴为不同颜色

目录 一、需求描述 二、实现方法 一、需求描述 当图中存在两条曲线&#xff0c;需要对两条曲线进行分别描述时&#xff0c;应设置左、右坐标轴为不同颜色&#xff0c;并设置刻度线&#xff0c;且坐标轴颜色需要和曲线颜色相同。 二、实现方法 2.1、实现目标&#xff1a; 1…

解决Apache/2.4.39 (Win64) PHP/7.2.18 Server at localhost Port 80问题

配置一下apache里面的配置文件&#xff1a;httpd.conf 和 httpd.vhosts.conf httpd.conf httpd-vhosts.conf 重启服务 展示&#xff1a; 浏览器中中文乱码问题&#xff1a;

RunCam WiFiLink连接手机图传测试

RunCam WiFiLink中文手册从这里下载 一、摄像头端 1.连接天线&#xff08;易忘&#xff09; 2.打开摄像头前面的盖子&#xff08;易忘&#xff09; 3.接上直流电源&#xff0c;红线为正&#xff0c;黑线为负 4.直流电源设置电压为14v&#xff0c;电流为3.15A&#xff0c; 通…

用JAVA做了一个登录窗体练习

目 录 说明运行后的效果代码 说明 做了一个登录窗体作为练习&#xff0c;分享给大家&#xff0c;其中涉及到窗体、图板、随机数等内容&#xff0c;为了方便和我一样的小白可以看的比较明白&#xff0c;所以尽量详细的标注了注释&#xff0c;希望能帮到同样在学习路上的朋友 运…

《开启微服务之旅:Spring Boot 从入门到实践》(一)

Spring Boot Spring Boot 入门 Spring Boot 简介&#xff08;脚手架&#xff09; 简化Spring应用开发的一个框架&#xff1b; 整个Spring技术栈的一个大整合&#xff1b; J2EE开发的一站式解决方案&#xff1b; 优点&#xff1a;快速创建独立运行的spring项目以及与主流…