Vue笔记

Vue3 笔记

1. 环境搭建

  • 安装 18.0 或更高版本的 Node.js

  • 创建项目

    # 在指定目录下执行以下命令, 用来创建Vue项目
    npm create vue@latest
    
  • 运行项目

    # 进入Vue项目的根目录
    cd <your-project-name>
    # 安装依赖
    npm install
    # 运行项目
    npm run dev
    
  • 打包Vue项目,此命令会在 ./dist 文件夹中为你的应用创建一个生产环境的构建版本

    npm run build
    

2. Vue3目录结构

project/
|—— public/              # 于存放静态资源文件的目录,这些文件不会经过打包处理
|   |—— index.html
|—— src/
|   |—— assets/          # 放置静态资源文件,如图片、样式等
|   |—— components/      # 放置可复用的 Vue 组件
|   |—— views/           # 放置页面级组件
|   |—— router/          # 放置路由配置文件
|   |—— store/           # 放置 Vuex 状态管理文件
|   |—— services/        # 放置与后端交互的服务文件
		|—— ApiService.js # 用于定义后端 API 请求的方法,包括 GET、POST..
		|—— AuthService.js # 用于处理用户认证和授权相关的操作,比如登录、注册
|   |—— utils/           # 放置工具函数
|   |—— App.vue          # 根组件
|   |—— main.js          # 项目入口文件
|—— package.json         # 项目配置文件
|—— babel.config.js      # Babel 配置文件
|—— vue.config.js        # Vue CLI 配置文件
|—— index.html			 # 整个应用的入口文件

3. idnex.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
	<!-- 引入js入口文件 -->
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

4. main.js

// 从静态资源文件夹中导入了一个名为 main.css 的样式
import './assets/main.css'

// 从 Vue 库中导入了 createApp 函数,用于创建一个 Vue 应用实例
import { createApp } from 'vue'

// 从 Pinia 库中导入了 createPinia 函数,用于创建一个 Pinia 应用状态管理实例
import { createPinia } from 'pinia'

// 导入了根组件 App.vue,这是整个应用程序的根组件
import App from './App.vue'

// 导入了路由配置文件,用于设置 Vue Router (页面跳转)
// 导入router 文件夹中 export default router 暴露出来的实例对象
import router from './router'

// 使用 createApp 函数创建了一个 Vue 应用实例对象
const app = createApp(App)

// 调用 app.use 方法来安装并使用 Pinia 应用状态管理实例
app.use(createPinia())

// 调用 app.use 方法来安装并使用 Vue Router
app.use(router)

// 将 Vue 应用实例挂载到index.html页面 具有 id="app" 的 HTML 元素上
app.mount('#app')

5. App.vue

<!-- 根组件JS脚本 -->
<script setup>
	// 引入 vue-router 中的路由组件,用于页面跳转和显示
	import {
		// 它可以被用作普通的 <a> 标签来进行页面之间的导航
		RouterLink,
		// 用于渲染当前路由匹配到的组件的组件
		// 当用户访问不同的 URL 时,RouterView 会根据当前路由匹配到的组件来动态地渲染对应的内容
		RouterView
	} from 'vue-router'
	
	// 导入可复用组件 HelloWorld
	import HelloWorld from './components/HelloWorld.vue'
</script>

<!-- 根组件模板内容 -->
<template>
	<!-- 使用自定义的组件 -->
	<HelloWorld msg="dd did it!" />
	
	<!-- 使用路由跳转组件,相当于a标签 -->
	<!-- to属性的值为router文件夹中index.js注册的对应的组件路由-->
	<RouterLink to="/">Home</RouterLink>
	<!-- 使用路由跳转组件 -->
	<RouterLink to="about">About</RouterLink>
	
	<!-- 用于显示 RouterLink 跳转的页面内容 -->
	<RouterView />
</template>

<!-- 根组件样式 -->
<style scoped>
	
</style>

6. router/index.js

// 从 vue-router 包中导入了 createRouter 和 createWebHistory 函数
import { createRouter, createWebHistory } from 'vue-router'
// 导入自定义视图组件,用于在页面中显示一个区域的内容
import HomeView from '../views/HomeView.vue'

// 使用 createRouter 函数创建了一个新的路由实例对象
const router = createRouter({
	// 指定了路由的历史模式为基于浏览器 history 的模式
	// 并且传入了 import.meta.env.BASE_URL 作为基础 URL
	// 这将影响路由的根路径和路由的匹配规则
  history: createWebHistory(import.meta.env.BASE_URL),
	// 定义了应用程序中的路由规则
  routes: [
    {
			// RouterLink 的 to 属性对应的url,会匹配到 component 对应的组件
      path: '/',
			// 路由名称,RouterLink 的 to 属性也可以使用name属性作为值跳转到对应的组件
			// 但是必须这样写 :to="{ name: 'home' }"
      name: 'home',
			// 绑定路径对应的组件
      component: HomeView
    },
    {
      path: '/about',
      name: 'about',
			// 绑定组件的另一种方式,箭头函数体 里面返回需要导入的组件
      component: () => import('../views/AboutView.vue')
    }
  ]
})

// 导出了这个创建好的路由实例对象,以便在应用程序的其他地方进行使用
export default router

7. stores/counter.js

// 引入了 ref 和 computed 这两个用于定义响应式数据和计算属性的函数
import { ref, computed } from 'vue'
// 引入了 Pinia 库中的 defineStore 方法,用于定义一个状态管理模块
import { defineStore } from 'pinia'

// 定义了一个名为 useCounterStore 的状态管理模块,
// 并使用 defineStore 方法对其进行定义
// 实例化一个状态管理对象,里面的属性和方法,会保存数据状态
// 最后通过 export 直接导出这个状态管理对象
// 其他地方可以通过 import { useCounterStore } from '...' 来引入这个模块
export const useCounterStore = defineStore('counter', () => {
	// 定义一个常量引用,他的值是一个对象,初始化了一个数据  (类似于浏览器session)
	// ref函数可以传递任意类型的数据,比如传递对象初始化数据 
	// const person = ref({ name: 'Alice', age: 30 })
	// 可以通过访问 count.value 属性来获取或修改这些引用的值
  const count = ref(0)
	// 定义一个常量引用,用于计算响应式引用的值
	// 当 count 方法变化时,computed() 函数会自动自行,并将值返回给计算常量引用
  // computed() 方法接收一个函数,这个函数的返回值会赋值给 doubleCount 常量
	const doubleCount = computed(() => count.value * 2)
	// 定义一个方法,方调用这个方法时 count对象里属性的值加1
  function increment() {
    count.value++
  }
	// 将这些属性和方法返回
  return { count, doubleCount, increment }
})


// // 其他页面导入时 import { useCounterStore } from './counter.js'
// export { useCounterStore }
// // 其他页面导入时 import useCounterStore from './counter.js'
// export default useCounterStore 

8. views/AboutView.vue

<!-- 使用 <script setup> 时,你可以直接声明变量、引入组件选项,定义响应式状态等 -->
<script setup>
	import { useCounterStore } from '../stores/counter.js'
	import { ref } from 'vue' 
	// 实例化状态管理对象
	const constStore = useCounterStore()
	// 将状态里面的值再次封装为常量引用
	const counts = ref(constStore.count)
	
	// 增加一个方法,按钮点击事件
	const increment = () => {
		// 调用状态管理对象的累加方法,对状态里面的 count值进行修改
		constStore.increment()
		// 将状态管理对象中的常量引用,赋值给本地的常量引用,以响应式更新页面的数据
		counts.value = constStore.count
	}
</script>

<template>
  <div class="about">
    <h1>This is an about page</h1>
		<!-- 页面中可以直接获得常量引用的值不用加 .value -->
		<h2>{{ counts }}</h2>
		<!-- 添加Vue单击事件 -->
		<button @click="increment">加一</button>
  </div>
</template>

<style>
@media (min-width: 1024px) {
  .about {
    min-height: 100vh;
    display: flex;
    align-items: center;
  }
}
</style>

9. main.js 中实例化一个Vue应用

  • 每个 Vue 应用都是通过 createApp函数创建一个新的 应用实例

  • 传入 createApp 的对象实际上是一个组件

  • createApp API 允许你在同一个页面中创建多个共存的 Vue 应用

    import { createApp } from 'vue'
    // 导入了根组件 App.vue,这是整个应用程序的根组件
    import App from './App.vue'
    
    // 实例化vue应用并传参
    const app = createApp(App)
    
    // 将 Vue 应用实例挂载到index.html页面 具有 id="app" 的 HTML 元素上
    app.mount('#app')
    
  • 根组件的模板通常是组件本身的一部分

    但也可以直接通过在挂载容器内编写模板来单独提供

    <div id="app">
      <button @click="count++">{{ count }}</button>
    </div>
    
    import { createApp } from 'vue'
    
    const app = createApp({
      data() {
        return {
          count: 0
        }
      }
    })
    
    app.mount('#app')
    

10. 模版语法

  • 最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法 (即双大括号)

  • 双大括号标签会被替换为相应组件实例中 msg 属性的值。同时每次 msg 属性更改时它也会同步更新

    <span>Message: {{ msg }}</span>
    
  • 属性名也可以是动态的 只需要加上中括号接口绑定为动态的变量

    <!-- 绑定事件名称 -->
    <a @[eventName]="doSomething"> ... </a>
    
    <!-- 绑定属性名称 -->
    <a :[attributeName]="url"> ... </a>
    

11. 响应式基础

  • 声明响应式状态 ref() 接收参数,并将其包裹在一个带有 .value 属性的 ref 对象中返回

  • Ref 可以持有任何类型的值,包括深层嵌套的对象、数组等

    import { ref } from 'vue'
    
    const count = ref(0)
    
  • 示例

    <script setup>
      // 使用 ref 创建响应式变量
      import { ref, onMounted } from 'vue';
      // 初始化响应数据
      var count = ref(1);
        
      // 定义方法  相当于在vue2 的method里面编写 
      const increment = () => {
        count.value++;
      };
    
      // 在 onMounted 生命周期钩子中执行 mounted 回调函数
      onMounted(() => {
        // 打印 count 的值
        console.log(count); // => 1
        // 修改 count 的值
        count.value = 3;
        // 调用方法
    	increment();
      });
    </script>
    
    <template>
      Count is: {{ count }}
    </template>
    
  • js文件中编写, 示例

    import { ref } from 'vue'
    
    // 导入对象
    export default {
      setup() {
        const count = ref(0)
    
        function increment() {
          // 在 JavaScript 中需要 .value
          count.value++
        }
    
        // 不要忘记同时暴露 increment 函数
        return {
          count,
          increment
        }
      }
    }
    

另一种声明响应式状态的方式,即使用 reactive() API

  • reactive() 将使对象本身具有响应性

  • 有限的值类型:它只能传入对象类型存储 (对象、数组和如 MapSet 这样的集合类型)

    <template>
        <button @click="state.count++">
          {{ state.count }}
        </button>
    </template>
    
    <script setup>
    import { reactive } from 'vue'
    // 初始化响应式数据,和ref不同的是将使对象本身也具有响应性
    const state = reactive({ count: 0 })
    </script>
    

12. 计算属性

概念

  • 如果在模板中写太多逻辑,会让模板变得臃肿,难以维护

  • 所以引入一个计算属性用于计算响应式变量的变化

  • 禁止改变其他状态、getter 中做异步请求或者更改 DOM

  • 一个计算属性的声明中描述的是如何根据其他值派生一个值

  • 因此 getter 的职责应该仅为计算和返回该值

    <script setup>
    import { reactive, computed } from 'vue'
    
    const author = reactive({
      name: 'John Doe',
      books: [
        'Vue 2 - Advanced Guide',
        'Vue 3 - Basic Guide',
        'Vue 4 - The Mystery'
      ]
    })
    
    // 使用computed方法用于计算响应式数据的值返回不同的内容
    // 只要 author.books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果, 而不用重复执行 getter 函数
    const publishedBooksMessage = computed(() => {
      return author.books.length > 0 ? 'Yes' : 'No'
    })
    </script>
    
    <template>
      <p>Has published books:</p>
      <span>{{ publishedBooksMessage }}</span>
    </template>
    
  • 可以通过同时提供 getter 和 setter 来创建 计算属性,以修改计算属性的值

  • 现在当你再运行 fullName.value = 'John Doe' 时,setter 会被调用而 firstNamelastName 会随之更新

    <script setup>
    import { ref, computed } from 'vue'
    
    const firstName = ref('John')
    const lastName = ref('Doe')
    
    const fullName = computed({
      // getter
      get() {
        return firstName.value + ' ' + lastName.value
      },
      // 避免直接修改计算属性值 (只为了解)
      set(newValue) {
        // 注意:我们这里使用的是解构赋值语法
        [firstName.value, lastName.value] = newValue.split(' ')
      }
    })
    </script>
    

13. Class与Style绑定

概念

  • 可以给 :class (v-bind:class 的缩写) 传递一个对象来动态切换 class 样式

    <!-- :class="{ css样式名称: 是否使用这个css样式(布尔值) }" -->
    <div :class="{ active: isActive }"></div>
    
    <script setup>
    import { ref } from 'vue';
    // 响应式属性
    const isActive = ref(true)
    </script>
    
  • 绑定的对象并不一定需要写成内联字面量的形式,也可以直接绑定一个对象

    <div :class="classObject"></div>
    
    <script setup>
    import { reactive } from 'vue';
    // 响应式对象
    const classObject = reactive({
      active: true,  // 决定是否使用 css样式 active
      // 对与类名有特殊字符的需要加上引号
      'text-danger': false  // 不适用 css样式 text-danger
    })
    </script>
    
  • 可以绑定一个返回对象的计算属性

    <div :class="classObject"></div>
    
    <script setup>
    import { ref,computed } from 'vue';
    const isActive = ref(true)
    const error = ref(null)
    
    const classObject = computed(() => ({
      // 当isActive的值为true,并且error的值为空时,使用这个css样式
      active: isActive.value && !error.value,
      'text-danger': error.value && error.value.type === 'fatal'
    }))
    </script>
    
  • 可以给 :class 绑定一个数组来渲染多个 CSS class

    <div :class="[activeClass, errorClass]"></div>
    
    <script setup>
    import { ref } from 'vue';
    const activeClass = ref('active')
    const errorClass = ref('text-danger')
    </script>
    
  • 如果你也想在数组中有条件地渲染某个 class,你可以使用三元表达式

    <div :class="[isActive ? activeClass : '', errorClass]"></div>
    
    <script setup>
    import { ref } from 'vue';
    const isActive = ref(true)
    const activeClass = ref('active')
    const errorClass = ref('text-danger')
    </script>
    

Style绑定内联样式绑定

  • :style 支持绑定 JavaScript 对象值,对应的是 html的style属性

    <!-- :style="{ color: 响应式属性, fontSize: 响应式属性 + 'px' }" -->
    <div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
    
    <script setup>
    import { ref } from 'vue';
    const activeColor = ref('red')
    const fontSize = ref(30)
    </script>
    
  • 直接绑定一个样式对象

    <div :style="styleObject"></div>
    
    <script setup>
    import { reactive } from 'vue';
    // 响应式对象
    const styleObject = reactive({
      color: 'red',
      fontSize: '13px'
    })
    </script>
    
  • 返回样式对象的计算属性

    <div :style="styleObject"></div>
    
    <script setup>
    import {reactive,computed} form 'vue'
    // 计算属性返回的样式对象
    const styleObject = computed(() => ({
      color: 'red',
      fontSize: '13px'
    }))
    </script>
    
  • 绑定数组 ::style 绑定一个包含多个样式对象的数组

    <div :style="[baseStyles, overridingStyles]"></div>
    
    <script setup>
    import { ref } from 'vue';
    const baseStyles = ref({color: 'red',fontSize: '16px'})
    const overridingStyles = ref({fontWeight: 'bold' })
    </script>
    

14. 条件渲染

概念

  • v-if 是一个指令,他必须依附于某个元素

  • v-if 指令用于条件性地渲染一块内容, 当表达式返回真值时才被渲染

  • 注意事项:当 v-ifv-for 同时存在于一个元素上的时候,v-if 会首先被执行 ,所有不推荐在同一个元素上使用这两个指令

    <h1 v-if="awesome">Vue is awesome!</h1>
    
    <script setup>
    import { ref } from 'vue';
    const awesome = ref(true)
    </script>
    
  • 可以使用 v-elsev-if 添加一个“else 区块”

    <!-- 每当点击按钮时 awesome 的值取反 -->
    <button @click="awesome = !awesome">Toggle</button>
    <h1 v-if="awesome">Vue is awesome!</h1>
    <h1 v-else>Oh no</h1>
    
    <script setup>
    import { ref } from 'vue';
    const awesome = ref(true)
    </script>
    
  • v-else-if 提供的是相应于 v-if 的“else if 区块”

    <div v-if="type === 'A'">
      A
    </div>
    <div v-else-if="type === 'B'">
      B
    </div>
    <div v-else-if="type === 'C'">
      C
    </div>
    <div v-else>
      Not A/B/C
    </div>
    
    <script setup>
    import { ref } from 'vue';
    const type = ref('B')
    </script>
    
  • 按条件显示一个元素的指令是 v-show

  • v-show 会在 DOM 渲染中保留该元素

  • v-show 仅切换了该元素上名为 display 的 CSS 属性

  • v-show 不支持在 <template> 元素上使用,也不能和 v-else 搭配使用

  • 无论 v-show 的条件是 true 还是 false,元素都会被渲染到 DOM 中,只是通过 display: none; 样式来控制显示与隐藏

    <h1 v-show="true">Hello!</h1>
    

15. v-for

概念

  • 使用 v-for 对数据进行动态渲染

  • Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新

    • push()
    • pop()
    • shift()
    • unshift()
    • splice()
    • sort()
    • reverse()
  • 使用 v-for 的标签也会被循环生成

    <!-- 循环生成多个 li -->
    <li v-for="item in items">
      {{ item.message }}
    </li>
    
    <script setup>
    const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
    </script>
    
  • v-for 也支持使用可选的第二个参数表示当前项的位置索引

    <li v-for="(item, index) in items">
      {{ index }} - {{ item.message }}
    </li>
    
    <script setup>
    const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
    </script>
    
  • 对于多层嵌套的 v-for,每个 v-for 作用域都可以访问到父级作用域

    <li v-for="item in items">
      <span v-for="childItem in item.children">
        {{ item.message }} {{ childItem }}
      </span>
    </li>
    
  • 循环渲染对象数据

    <script setup>
    import { ref } from 'vue'
    const items = ref({message: 'Foo' , fdf: 'Bar'})
    </script>
    
    <template>
    	<ul>
            <li v-for="(value, key, index) in items">
            {{ key }} - {{ value }}
            </li>
    	</ul>
    </template>
    
  • v-for 可以直接接受一个整数值,注意此处 n 的初值是从 1 开始而非 0

    <span v-for="n in 10">{{ n }}</span>
    

通过 key 管理状态

  • 给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素

  • 需要为每个元素对应的块提供一个唯一的 key属性

    <div v-for="item in items" :key="item.id">
      <!-- 内容 -->
    </div>
    

16. 事件处理

概念

  • 内联事件处理器:事件被触发时执行的内联 JavaScript 语句

    <button @click="count++">Add 1</button>
    <p>Count is: {{ count }}</p>
    
    <script setup>
    const count = ref(0)
    </script>
    
  • 绑定事件函数

    <script setup>
    import { ref } from 'vue'
    
    const name = ref('Vue.js')
    
    const greet = (event) => {
      alert(`Hello ${name.value}!`)
      // `event` is the native DOM event
      if (event) {
        alert(event.target.tagName)
      }
    }
    </script>
    
    <template>
    	<button @click="greet">Greet</button>
    </template>
    
  • 在内联处理器中直接调用方法

    <script setup>
    function say(message) {
      alert(message)
    }
    </script>
    
    <template>
    	<button @click="say('hello')">Say hello</button>
    	<button @click="say('bye')">Say bye</button>
    </template>
    
  • 在内联事件处理器中访问原生 DOM 事件

    <!-- 将DOM原生的时间对象作为参数传给 warn方法 -->
    <button @click="warn('Form cannot be submitted yet.', $event)">
      Submit
    </button>
    
    <script setup>
    function warn(message, event) {
      // 这里可以访问原生事件
      if (event) {
        event.preventDefault()
      }
      alert(message)
    }
    </script>
    
  • v-on 提供了事件修饰符,修饰符是用 . 表示的指令后缀

    • .stop 单击事件将停止传递
    • .prevent 提交事件将不再重新加载页面
    • .self
    • .capture
    • .once 一次性按钮
    • .passive
    <!-- 单击事件将停止传递 -->
    <a @click.stop="doThis"></a>
    
    <!-- 提交事件将不再重新加载页面 -->
    <form @submit.prevent="onSubmit"></form>
    
    <!-- 修饰语可以使用链式书写 -->
    <a @click.stop.prevent="doThat"></a>
    
    <!-- 也可以只有修饰符 -->
    <form @submit.prevent></form>
    
    <!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
    <!-- 例如:事件处理器不来自子元素 -->
    <div @click.self="doThat">...</div>
    
    <!-- 添加事件监听器时,使用 `capture` 捕获模式 -->
    <!-- 例如:指向内部元素的事件,在被内部元素处理前,先被外部处理 -->
    <div @click.capture="doThis">...</div>
    
    <!-- 点击事件最多被触发一次 -->
    <a @click.once="doThis"></a>
    
    <!-- 滚动事件的默认行为 (scrolling) 将立即发生而非等待 `onScroll` 完成 -->
    <!-- 以防其中包含 `event.preventDefault()` -->
    <div @scroll.passive="onScroll">...</div>
    
    <!-- 仅在 `key` 为 `Enter` 时调用 `submit` -->
    <input @keyup.enter="submit" />
    

17. 表单输入绑定

概念

  • 实现表单元素与数据的双向绑定,简化数据状态管理

  • 自动处理不同表单元素的输入事件,避免手动编写事件监听器

  • 提高开发效率,减少重复的 DOM 操作

  • 通过使用 v-model,可以更轻松地管理表单数据和页面视图之间的关系

    <script setup>
    import { ref } from 'vue'
    
    const message = ref('')
    </script>
    
    <template>
      <p>Message is: {{ message }}</p>
    	<!-- v-model 相当于value属性  -->
    	<input v-model="message" placeholder="edit me" />
    </template>
    
  • 单一复选框绑定 决定是否选中

    对于复选框而言,当复选框被选中时,ss 数据属性的值会变为 true;当复选框未被选中时,ss 数据属性的值会变为 false

    <script setup>
    import { ref } from 'vue'
    
    const ss = ref('dd')
    </script>
    
    <template>
    	<input type="checkbox" id="checkbox" v-model="ss" />
    	<label for="checkbox">{{ ss }}</label>
    </template>
    
  • 多个复选框也可以绑定同一个数据,到的全选的效果

    <script setup>
    import { ref } from 'vue'
    
    const checkedNames = ref([])
    </script>
    
    <template>
      <div>Checked names: {{ checkedNames }}</div>
    
      <input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
      <label for="jack">Jack</label>
     
      <input type="checkbox" id="john" value="John" v-model="checkedNames" />
      <label for="john">John</label>
     
      <input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
      <label for="mike">Mike</label>
    </template>
    
  • 单选框绑定数据

    v-model绑定数据,当单选框被选中是,该单选框的value值会赋值为绑定的变量

    <script setup>
    import { ref } from 'vue'
    
    const picked = ref('One')
    </script>
    
    <template>
      <div>Picked: {{ picked }}</div>
    	<!-- v-model绑定数据,当单选框被选中是,该单选框的value值会赋值为绑定的变量 -->
    	<input type="radio" id="one" value="One" v-model="picked" />
    	<label for="one">One</label>
    
    	<input type="radio" id="two" value="Two" v-model="picked" />
      <label for="two">Two</label>
    </template>
    
  • 下拉框绑定数据

    当下拉选项被选中是,它的文本内容会赋值给绑定的变量

    <script setup>
    import { ref } from 'vue'
    
    const selected = ref('')
    </script>
    
    <template>
      <span> Selected: {{ selected }}</span>
    
      <select v-model="selected">
        <option disabled value="">Please select one</option>
        <option>A</option>
        <option>B</option>
        <option>C</option>
      </select>
    </template>
    
  • 下拉框绑定数组 (多选)

    <script setup>
    import { ref } from 'vue'
    
    const selected = ref([])
    </script>
    
    <template>
      <div>Selected: {{ selected }}</div>
    
      <select v-model="selected" multiple>
        <option>A</option>
        <option>B</option>
        <option>C00</option>
      </select>
    </template>
    
    <style>
    select[multiple] {
      width: 100px;
    }
    </style>
    
  • 值绑定

    <!-- `picked` 在被选择时是字符串 "a" -->
    <input type="radio" v-model="picked" value="a" />
    
    <!-- `toggle` 只会为 true 或 false -->
    <input type="checkbox" v-model="toggle" />
    
    <!-- `selected` 在第一项被选中时为字符串 "abc" -->
    <select v-model="selected">
      <option value="abc">ABC</option>
    </select>
    
    <input
      type="checkbox"
      v-model="toggle"
      :true-value="dynamicTrueValue"
      :false-value="dynamicFalseValue" />
    
    <input type="radio" v-model="pick" :value="first" />
    <input type="radio" v-model="pick" :value="second" />
    
    <select v-model="selected">
      <!-- 内联对象字面量 -->
      <option :value="{ number: 123 }">123</option>
    </select>
    
  • 修饰符

    <!-- 在 回车 事件后同步更新而不是 输入时 -->
    <input v-model.lazy="msg" />
    
    <!-- 输入自动转换为数字 -->
    <input v-model.number="age" />
    
    <!-- 去除用户输入内容中两端的空格 -->
    <input v-model.trim="msg" />
    

18. 生命周期

  • onMounted 钩子可以用来在组件完成初始渲染并创建 DOM 节点后调用

    <script setup>
    import { ref, onMounted } from 'vue'
    const el = ref()
    onMounted(() => {
      // el.value 获得标签元素节点对象
      el.value.innerHTML = "Hello:)"
    })
    </script>
    
    <template>
      <!-- 将元素节点绑定到响应变量el中 -->
      <div ref="el"></div>
    </template>
    
  • onUpdated() 函数:组件因为响应式状态变更而更新其 DOM 树之后调用

    <script setup>
    import { ref, onUpdated } from 'vue'
    
    const count = ref(0)
    
    onUpdated(() => {
      // 当组件的数据发生变化时执行一次该函数,打印改变后的响应变量值
      console.log(count.value)
    })
    </script>
    
    <template>
      <button @click="count++">{{ count }}</button>
    </template>
    
  • onUnmounted 钩子函数只会在整个组件被移除(从 DOM 中移除)时触发,而不是在组件中的某个 HTML 标签被移除时触发

    <script setup>
    import { onMounted, onUnmounted } from 'vue'
    
    let intervalId
    onMounted(() => {
      // 组件挂载时执行一个计时器
      intervalId = setInterval(() => {
        // ...
      })
    })
    
    // 组件卸载时清除这个计时器对象
    onUnmounted(() => clearInterval(intervalId))
    </script>
    
  • onBeforeMount() 组件被挂载之前被调用

    onBeforeMount(() => {
      ...
    })
    
  • onBeforeUpdate() 在组件即将因为响应式状态变更而更新其 DOM 树之前调用

    onBeforeUpdate(() => {
      ...
    })
    
  • onBeforeUnmount() 在组件实例被卸载之前调用

    onBeforeUnmount(() => {
      ...
    })
    
  • onErrorCaptured() 在捕获了后代组件传递的错误时调用

    function onErrorCaptured(callback: ErrorCapturedHook): void
    
    type ErrorCapturedHook = (
      err: unknown,
      instance: ComponentPublicInstance | null,
      info: string
    ) => boolean | void
    
  • onRenderTracked() 当组件渲染过程中追踪到响应式依赖时调用

    function onRenderTracked(callback: DebuggerHook): void
    
    type DebuggerHook = (e: DebuggerEvent) => void
    
    type DebuggerEvent = {
      effect: ReactiveEffect
      target: object
      type: TrackOpTypes /* 'get' | 'has' | 'iterate' */
      key: any
    }
    

19. 侦听器

概念

  • 在状态变化时执行一些操作(计算属性中禁止的操作),例如更改 DOM,或是根据异步操作的结果去修改另一处的状态

  • watch 的第一个参数可以是不同形式的“数据源” (如ref)

  • 注意,你不能直接侦听响应式对象的属性值,因为 watch() 得到的参数是一个 具体值

    <script setup>
    import { ref, watch } from 'vue'
    
    const question = ref('')
    const answer = ref('Questions usually contain a question mark. ;-)')
    const loading = ref(false)
    
    // 可以直接侦听一个 ref
    watch(question, async (newQuestion, oldQuestion) => {
      if (newQuestion.includes('?')) {
        loading.value = true
        answer.value = 'Thinking...'
        try {
          const res = await fetch('https://yesno.wtf/api')
          answer.value = (await res.json()).answer
        } catch (error) {
          answer.value = 'Error! Could not reach the API. ' + error
        } finally {
          loading.value = false
        }
      }
    })
    </script>
    
    <template>
      <p>
        Ask a yes/no question:
        <input v-model="question" :disabled="loading" />
      </p>
      <p>{{ answer }}</p>
    </template>
    
  • watch 默认是懒执行的:仅当数据源变化时,才会执行回调

  • 我们希望在创建侦听器时,立即执行一遍回调

  • 通过传入 immediate: true 选项来强制侦听器的回调立即执行

    watch(
      source,
      (newValue, oldValue) => {
        // 立即执行,且当 `source` 改变时再次执行
      },
      { immediate: true }
    )
    
  • 如果希望回调只在源变化时触发一次,请使用 once: true 参数

    watch(
      source,
      (newValue, oldValue) => {
        // 当 `source` 变化时,仅触发一次
      },
      { once: true }
    )
    
  • watchEffect() 允许我们自动跟踪回调的响应式依赖 ( 自动侦听回调函数中的响应数据 )

  • watchEffect(),我们不再需要明确传递 源数据

  • 当回调函数中的响应数据发生变化时执行一次 watchEffect() 函数

  • watchEffect()回调会立即执行一次,不需要指定 immediate: true

    // 回调函数使用了异步
    watchEffect(async () => {
      const response = await fetch(
        `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
      )
      // await 等待异步请求完毕再获取得到的数据,否则 response 的内容可能为空
      data.value = await response.json()
    })
    
  • 如果想在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM,你需要指明 flush: 'post'参数

    watch(source, callback, {flush: 'post'})
    
    watchEffect(callback, {flush: 'post'})
    
  • 停止侦听器

    • 如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏

    • 要手动停止一个侦听器,请调用 watchwatchEffect 返回的函数

      <script setup>
      import { watchEffect } from 'vue'
      
      // 它会自动停止
      watchEffect(() => {})
      
      let unwatch;
      // ...这个则不会!
      setTimeout(() => {
        unwatch = watchEffect(() => {})
      }, 100)
      // 停止异步侦听器
      unwatch();
      </script>
      
    • 请尽可能选择同步创建。如果需要等待一些异步数据,你可以使用条件式的侦听逻辑

      // 需要异步请求得到的数据
      const data = ref(null)
      
      watchEffect(() => {
        if (data.value) {
          // 数据加载后执行某些操作...
        }
      })
      

20. 模版引用

  • 但在某些情况下,我们仍然需要直接访问底层 DOM 元素

  • 实现这一点,我们可以使用特殊的 ref 属性

  • 注意,你只可以在组件挂载后才能访问模板引用

    <input ref="input">
    
    <script setup>
    import { ref,onMounted } from 'vue'
    // 获得封装有节点对象的ref
    // 必须和模板里的 ref 同名
    const input = ref()
    // 获得元素节点对象
    onMounted(() => {
      input.value.focus()
    })
    </script>
    
  • 如果不使用 <script setup>,需确保从 setup() 返回 ref

    export default {
      setup() {
        const input = ref(null)
        // ...
        return {
          input
        }
      }
    }
    
  • 如果你需要侦听一个模板引用 ref 的变化,确保考虑到其值为 null 的情况

    watchEffect(() => {
      if (input.value) {
        input.value.focus()
      } else {
        // 此时还未挂载,或此元素已经被卸载(例如通过 v-if 控制)
      }
    })
    
  • 当在 v-for 中使用模板引用时,对应的 ref 中包含的值是一个数组

  • 它将在元素被挂载后包含对应整个列表的所有元素

  • ref 数组并不保证与源数组相同的顺序

    <script setup>
    import { ref, onMounted } from 'vue'
    
    const list = ref([
      /* ... */
    ])
    
    const itemRefs = ref([])
    
    onMounted(() => console.log(itemRefs.value))
    </script>
    
    <template>
      <ul>
        <li v-for="item in list" ref="itemRefs">
          {{ item }}
        </li>
      </ul>
    </template>
    
  • ref attribute 还可以绑定为一个函数,会在每次组件更新时都被调用

  • 当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null

  • 该函数会收到元素引用作为其第一个参数

    <input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">
    
  • 模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例

    <script setup>
    import { ref, onMounted } from 'vue'
    import Child from './Child.vue'
    
    const child = ref(null)
    
    onMounted(() => {
      // child.value 是 <Child /> 组件的实例
    })
    </script>
    
    <template>
      <Child ref="child" />
    </template>
    
  • 组件上的ref

    • 使用了 <script setup> 的组件是默认私有的:

    • 一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,

    • 除非子组件在其中通过 defineExpose 宏显式暴露

      <script setup>
      import { ref } from 'vue'
      
      const a = 1
      const b = ref(2)
      
      // 像 defineExpose 这样的编译器宏不需要导入
      defineExpose({
        a,
        b
      })
      </script>
      

21.组件和传值

概念

  • 组件允许我们将 UI 划分为独立的、可重用的部分

  • 组件常常被组织成层层嵌套的树状结构

  • 定义一个组件:将 Vue 组件定义在一个单独的 .vue 文件中,这被叫做单文件组件

    <!-- ButtonCounter.vue -->
    <script setup>
    import { ref } from 'vue'
    
    const count = ref(0)
    </script>
    
    <template>
      <button @click="count++">You clicked me {{ count }} times.</button>
    </template>
    
  • 在其他页面导入并使用

    <script setup>
    // 导入组件
    import ButtonCounter from './ButtonCounter.vue'
    </script>
    
    <template>
      <h1>Here is a child component!</h1>
      <ButtonCounter />
      <!-- 如果是在 DOM 中书写该模板 -->
      <button-counter></button-counter>
      <button-counter></button-counter>
      <button-counter></button-counter>
    </template>
    

props 父组件向子组件传值

  • Props 是一种特别的 attributes,你可以在子组件上声明注册

  • defineProps 是一个仅 <script setup> 中可用的编译宏命令,

  • 并不需要显式地导入

    <!-- 父组件 -->
    <template>
      <!-- 父组件通过子组件定义的 props属性将数据 传递给子组件 -->
      <ChildComponent message="Hello from parent!" />
    </template>
    
    <!-- 子组件 -->
    <template>
      <div>{{ message }}</div>
    </template>
    
    <script setup>
    // 编译宏命令生成一个名为 message 的 props属性,
    // 用于接收父组件传递过来的值
    defineProps({'message': String})
    </script>
    
    
    <script>
    // 不用setup的写法
    /*
    export default {
      props: {
        message: String
      }
    }
    */
    </script>
    
  • $emit(自定义事件属性名) 子组件触发的事件由父组件来实现

    <!-- 父组件 -->
    <script setup>
    import { ref } from 'vue'
    <!-- 引入组件 -->
    import BlogPost from './BlogPost.vue'
      
    const posts = ref([
      { id: 1, title: 'My journey with Vue' },
      { id: 2, title: 'Blogging with Vue' },
      { id: 3, title: 'Why Vue is so fun' }
    ])
    const postFontSize = ref(1)
    
    const emitfun = () => {
      postFontSize.value += 0.2
    };
    
    </script>
    
    <template>
    	<div :style="{ fontSize: postFontSize + 'em' }">
        <BlogPost
          :title="posts[0].title"
          @enlarge-text="emitfun"
        />
      </div>
    </template>
    
    <!-- 子组件 -->
    <script setup>
    defineProps(['title'])
    // 向父组件抛出一个自定义事件属性名称,由父组件来实现
    defineEmits(['enlarge-text'])
    </script>
    
    <template>
      <div class="blog-post">
        <h4>{{ title }}</h4>
        <!-- 将单击事件传递到自定义的事件名称,由父组件来添加自定义的属性 -->
        <!-- 从而使用自定义的事件名称 绑定实现的函数 -->
        <button @click="$emit('enlarge-text')">Enlarge text</button>
      </div>
    </template>
    
  • emit 将子组件的数据传输给父组件

    父组件

    <script setup>
    import { ref } from 'vue'
    import BlogPost from './BlogPost.vue'
      
    const posts = ref([
      { id: 1, title: 'My journey with Vue' },
      { id: 2, title: 'Blogging with Vue' },
      { id: 3, title: 'Why Vue is so fun' }
    ])
    
    // 接收一个参数,这个参数就是子组件自定义事件传过来的相关联的数据
    function myfun(data){
    	alert(data.age);
    }
    </script>
    
    <template>
    	<!-- @custom-event="myfun" 使用子组件中自定的事件属性,传入一个函数获得该事件对应的数据 -->
    	<BlogPost
      	:title="posts[0].title"
    	@custom-event="myfun"  
    	></BlogPost>
    </template>
    

    子组件

    <script setup>
    import { ref, watch, watchEffect } from 'vue';
    
    defineProps(['title'])
    const data = ref({'age': 18})
    const emit = defineEmits(['custom-event'])
    // watchEffect 监听data数据,如果发生了变化,就将子组件的数据传递给父组件
    watchEffect(() => {
      emit('custom-event', data.value)
    })
        
    // watch监听data数据,如果发生了变化,就将子组件的数据传递给父组件
    // watch(data.value,() => {
    //   emit('custom-event', data.value)
    // })
    </script>
    
    <template>
      <h4 >{{ title }}</h4>
      <!-- 数据双向绑定,并使用修饰符lazy -->
      <input v-model.lazy="data.age">
    </template>
    

插槽 <slot /> 标签

  • 一些情况下我们会希望能和 HTML 元素一样向组件中传递内容

    组件中使用 <slot />标签占位,让其他组件使用时可以在里面添加html标签或文本

    <AlertBox>
      Something bad happened.
    </AlertBox>
    
    <template>
      <div class="alert-box">
        <strong>This is an Error for Demo Purposes</strong>
        <!-- 组件标签里面的内容会添加到这个里 -->
        <slot />
      </div>
    </template>
    
    <style scoped>
    .alert-box {
      /* ... */
    }
    </style>
    

动态组件

  • 有些场景会需要在两个组件间来回切换,比如 Tab 界面

  • 通过 Vue 的 <component> 元素和特殊的 is attribute 可以 实现

    <!-- currentTab 改变时组件也改变 -->
    <component :is="tabs[currentTab]"></component>
    
  • <component> 元素和特殊的 is 实现tab切换

<template>
  <div>
    <button @click="currentTab = 'TabA'">Show Tab A</button>
    <button @click="currentTab = 'TabB'">Show Tab B</button>
    
    <component :is="tabs[currentTab]"></component>
  </div>
</template>

<script setup>
// shallowRef: 与 ref 不同的是,shallowRef 
// shallowRef 只对对象的第一层属性进行响应式处理,不会递归处理其内部的属性
// markRaw: 有时候我们希望某些对象或属性保持非响应式,
// 这时就可以使用 markRaw 来达到这个目的
import { shallowRef, markRaw } from 'vue';
// 导入组件
import TabA from './currentTab.vue';
import TabB from './currentTab2.vue';

// shallowRef 创建一个响应式数据
const tabs = shallowRef({
  // markRaw(TabA) 让组件保持非响应式的
  TabA: markRaw(TabA),
  TabB: markRaw(TabB)
});

const currentTab = shallowRef('TabA');
</script>
  • 另一种方式

    <template>
      <div>
        <button @click="currentTab = 'TabA'">Show Tab A</button>
        <button @click="currentTab = 'TabB'">Show Tab B</button>
        <button @click="currentTab = 'TabC'">Show Tab C</button>
        
        <component :is="currentTab === 'TabA' ? TabA : currentTab === 'TabB' ? TabB : TabC"></component>
      </div>
    </template>
    
    <script setup>
    import { ref } from 'vue';
    import TabA from './TabA.vue';
    import TabB from './TabB.vue';
    import TabC from './TabC.vue';
    
    const currentTab = ref('TabA');
    </script>
    
  • <tr is="vue:blog-post-row"></tr> 表示将 blog-post-row 组件动态地渲染为 <tr> 元素

    <table>
      <tr is="vue:blog-post-row"></tr>
    </table>
    

22. 注册全局组件

概念

  • 一个 Vue 组件在使用前需要先被“注册”

  • 这样 Vue 才能在渲染模板时找到其对应的实现

  • 组件注册有两种方式:全局注册和局部注册

  • 在main.js中注册全局组件

    import MyComponent from './App.vue'
    import { createApp } from 'vue'
    
    const app = createApp({})
    // 组件全局组件,在任何地方后可以使用这个组件
    // 第一个参数为组件标签名称,第二个参数为实际的组件对象
    app.component('MyComponent', MyComponent)
    
    // 可以被链式调用
    app
      .component('ComponentA', ComponentA)
      .component('ComponentB', ComponentB)
      .component('ComponentC', ComponentC)
    
  • 局部组件

    • 全局注册虽然很方便,但有以下几个问题

    • 没有被使用的组件无法在生产打包时被自动移除

    • 在父组件中使用子组件时,不太容易定位子组件的实现

    • 和使用过多的全局变量一样,这可能会影响应用长期的可维护性

    • 局部注册的组件需要在使用它的父组件中显式导入,

    • 并且只能在该父组件中使用

      <script setup>
      import ComponentA from './ComponentA.vue'
      </script>
      
      <template>
        <ComponentA />
      </template>
      
    • 如果没有使用 <script setup>,则需要使用 components 选项来显式注册

      import ComponentA from './ComponentA.js'
      
      export default {
        components: {
          ComponentA
        },
        setup() {
          // ...
        }
      }
      

23. 组件Props

概念

  • props 父组件向子组件传值

    • Props 是一种特别的 attributes,你可以在子组件上声明注册

    • 注意:获得的 prop 数据 是只读的!

    • defineProps 是一个仅 <script setup> 中可用的编译宏命令,

    • 并不需要显式地导入

      <!-- 父组件 -->
      <template>
        <!-- 父组件通过子组件定义的 props属性将数据 传递给子组件 -->
        <!-- 实际上任何类型的值都可以作为 props 的值被传递 -->
        <ChildComponent message="Hello from parent!" />
      </template>
      
      <!-- 子组件 -->
      <template>
        <div>{{ message }}</div>
      </template>
      
      <script setup>
      // 编译宏命令生成一个名为 message 的 props属性,
      // 用于接收父组件传递过来的值
      const props = defineProps({'message': String})
      alert(props.message)
      </script>
      
      
      <script>
      // 不用setup的写法
      /*
      export default {
        props: {
          message: String
        }
      }
      */
      </script>
      
  • 使用一个对象绑定多个 prop

    <!-- 实际上等价于 <BlogPost :id="post.id" :title="post.title" /> -->
    <BlogPost v-bind="post" />
    
    <script>
    const post = {
      id: 1,
      title: 'My Journey with Vue'
    }
    </script>
    
  • 要声明对 props 的校验,你可以向 defineProps() 宏提供一个带有 props 校验选项的对象

    defineProps({
      // 基础类型检查
      // (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
      propA: Number,
      // 多种可能的类型
      propB: [String, Number],
      // 必传,且为 String 类型
      propC: {
        type: String,
        required: true
      },
      // Number 类型的默认值
      propD: {
        type: Number,
        default: 100
      },
      // 对象类型的默认值
      propE: {
        type: Object,
        // 对象或数组的默认值
        // 必须从一个工厂函数返回。
        // 该函数接收组件所接收到的原始 prop 作为参数。
        default(rawProps) {
          return { message: 'hello' }
        }
      },
      // 自定义类型校验函数
      // 在 3.4+ 中完整的 props 作为第二个参数传入
      propF: {
        validator(value, props) {
          // The value must match one of these strings
          return ['success', 'warning', 'danger'].includes(value)
        }
      },
      // 函数类型的默认值
      propG: {
        type: Function,
        // 不像对象或数组的默认,这不是一个
        // 工厂函数。这会是一个用来作为默认值的函数
        default() {
          return 'Default function'
        }
      }
    })
    

24. 组件事件 $emit

概念

  • $emit(自定义事件属性名) 子组件触发的事件由父组件来实现

    <!-- 父组件 -->
    <script setup>
    import { ref } from 'vue'
    <!-- 引入组件 -->
    import BlogPost from './BlogPost.vue'
      
    const posts = ref([
      { id: 1, title: 'My journey with Vue' },
      { id: 2, title: 'Blogging with Vue' },
      { id: 3, title: 'Why Vue is so fun' }
    ])
    const postFontSize = ref(1)
    
    const emitfun = () => {
      postFontSize.value += 0.2
    };
    
    </script>
    
    <template>
    	<div :style="{ fontSize: postFontSize + 'em' }">
        <BlogPost
          :title="posts[0].title"
          @enlarge-text="emitfun"
        />
      </div>
    </template>
    
    <!-- 子组件 -->
    <script setup>
    defineProps(['title'])
    // 向父组件抛出一个自定义事件属性名称,由父组件来实现
    defineEmits(['enlarge-text'])
    </script>
    
    <template>
      <div class="blog-post">
        <h4>{{ title }}</h4>
        <!-- 将单击事件传递到自定义的事件名称,由父组件来添加自定义的属性 -->
        <!-- 从而使用自定义的事件名称 绑定实现的函数 -->
        <button @click="$emit('enlarge-text')">Enlarge text</button>
      </div>
    </template>
    
  • emit 将子组件的数据传输给父组件

    父组件

    <script setup>
    import { ref } from 'vue'
    import BlogPost from './BlogPost.vue'
      
    const posts = ref([
      { id: 1, title: 'My journey with Vue' },
      { id: 2, title: 'Blogging with Vue' },
      { id: 3, title: 'Why Vue is so fun' }
    ])
    
    // 接收一个参数,这个参数就是子组件自定义事件传过来的相关联的数据
    function myfun(data){
    	alert(data.age);
    }
    </script>
    
    <template>
    	<!-- @custom-event="myfun" 使用子组件中自定的事件属性,传入一个函数获得该事件对应的数据 -->
    	<BlogPost
      	:title="posts[0].title"
    	@custom-event="myfun"  
    	></BlogPost>
    </template>
    

    子组件

    <script setup>
    import { ref, watch, watchEffect } from 'vue';
    
    defineProps(['title'])
    const data = ref({'age': 18})
    const emit = defineEmits(['custom-event'])
    // watchEffect 监听data数据,如果发生了变化,就将子组件的数据传递给父组件
    watchEffect(() => {
      emit('custom-event', data.value)
    })
        
    // watch监听data数据,如果发生了变化,就将子组件的数据传递给父组件
    // watch(data.value,() => {
    //   emit('custom-event', data.value)
    // })
    </script>
    
    <template>
      <h4 >{{ title }}</h4>
      <!-- 数据双向绑定,并使用修饰符lazy -->
      <input v-model.lazy="data.age">
    </template>
    
  • 子组件通过按钮向父组件传值

    <button @click="$emit('increaseBy', 1)">
      Increase by 1
    </button>
    
    <!-- 接收传过来的事件并获得数据 -->
    <MyButton @increase-by="(n) => count += n" />
    

25. 组件 v-model

概念

  • const model = defineModel()

  • 获得 父组件通过v-model ,绑定在子组件标签上的数据

    <!-- Child.vue -->
    <script setup>
    // 获得 父组件通过v-model  ,绑定在子组件标签上的数据
    const model = defineModel()
    </script>
    
    <!-- Parent.vue -->
    <!-- 将数据绑定到子组件中,让子组件可以访问这个数据 -->
    <Child v-model="count" />
    

26. 标签属性透传

概念

  • 将父组件的html属性传递给子组件,

  • 该属性没有被该组件声明为 props 或 emits 的 attribute

  • 会对 classstyle 合并

  • 如我们有一个 <MyButton> 组件,它的模板长这样

    <!-- <MyButton> 的模板 -->
    <button class="btn">click me</button>
    
  • 一个父组件使用了这个组件,并且传入了 class

    <MyButton class="large" />
    
  • 最后渲染出的 DOM 结果是

    <button class="btn large">click me</button>
    
  • 同样的规则也适用于 v-on 事件监听器

  • click 监听器会被添加到 <MyButton> 的根元素,

  • 即那个原生的 <button> 元素之上

  • 当原生的 <button> 被点击,会触发父组件的 onClick 方法

  • 如果原生 button 元素自身也通过 v-on 绑定了一个事件监听器,

  • 则这个监听器和从父组件继承的监听器都会被触发

    <MyButton @click="onClick" />
    
  • 禁用 Attributes 继承

  • 如果你不想要一个组件自动地继承 attribute,

  • 可以在组件选项中设置 inheritAttrs: false

  • 通过设置 inheritAttrs 选项为 false

  • 可以完全控制透传进来的 attribute 被如何使用

    <script setup>
    defineOptions({
      inheritAttrs: false
    })
    // ...setup 逻辑
    </script>
    
  • 这些透传进来的 attribute 可以在模板的表达式中直接用 $attrs 访问到

    <span>{{ $attrs }}</span>
    
  • 可以通过设定 inheritAttrs: false使用 v-bind="$attrs" 来实现

  • 透传 attribute 都应用在内部的 <button> 上而不是外层的 <div>

    <div class="btn-wrapper">
      <button class="btn" v-bind="$attrs">click me</button>
    </div>
    
  • 多根节点的 Attributes 继承

  • 多个根节点的组件没有自动 attribute 透传行为

  • 如果 $attrs 没有被显式绑定,将会抛出一个运行时警告

  • $attrs 被显式绑定,则不会有警告

<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
  • 在 JavaScript 中访问透传 Attributes

    <script setup>
    import { useAttrs } from 'vue'
    
    const attrs = useAttrs()
    </script>
    
    <!-- 如果没有使用 <script setup>, -->
    <!-- attrs 会作为 setup() 上下文对象的一个属性暴露 -->
    <script>
    export default {
      setup(props, ctx) {
        // 透传 attribute 被暴露为 ctx.attrs
        console.log(ctx.attrs)
      }
    }
    </script>
    
    

27. 插槽 <slot /> 标签

  • 一些情况下我们会希望能和 HTML 元素一样向组件中传递内容

    组件中使用 <slot />标签占位,让其他组件使用时可以在里面添加html标签或文本

    <AlertBox>
      Something bad happened.
    </AlertBox>
    
    <template>
      <div class="alert-box">
        <strong>This is an Error for Demo Purposes</strong>
        <!-- 组件标签里面的内容会添加到这个里 -->
        <slot />
      </div>
    </template>
    
    <style scoped>
    .alert-box {
      /* ... */
    }
    </style>
    
  • 在外部没有提供任何内容的情况下,可以为插槽指定默认内容

    <button type="submit">
      <slot>
        Submit <!-- 默认内容 -->
      </slot>
    </button>
    

具名插槽

  • <slot> 元素可以有一个特殊的 attribute name

  • 用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容

    <div class="container">
      <header>
        <!-- 为插槽指定名称 -->  
        <slot name="header"></slot>
      </header>
      <main>
        <slot></slot>
      </main>
      <footer>
        <slot name="footer"></slot>
      </footer>
    </div>
    
    <!-- 父组件 -->
    <BaseLayout>
      <!-- 将内容插入到指定名称的插槽中 -->
      <template #header>
        <!-- header 插槽的内容放这里 -->
      </template>
      <!-- #default 表示没有名称的插槽 -->
      <template #default>
        <p>A paragraph for the main content.</p>
        <p>And another one.</p>
      </template>
    </BaseLayout>
    
  • 动态插槽名

    <base-layout>
      <template v-slot:[dynamicSlotName]>
        ...
      </template>
    
      <!-- 缩写为 -->
      <template #[dynamicSlotName]>
        ...
      </template>
    </base-layout>
    
  • 作用域插槽

  • 同时使用父组件域内和子组件域内的数据

  • 通过子组件标签上的 v-slot 指令,直接接收到了一个插槽 props 对象

    <!-- <MyComponent> 的模板 -->
    <div>
      <!-- 将数据传给父组件 -->  
      <slot :text="greetingMessage" :count="1"></slot>
    </div>
    
    <!-- 父组件 -->
    <MyComponent v-slot="slotProps">
      {{ slotProps.text }} {{ slotProps.count }}
    </MyComponent>
    
    <MyComponent v-slot="{ text, count }">
      {{ text }} {{ count }}
    </MyComponent>
    
  • 具名作用域插槽

    <slot name="header" message="hello"></slot>
    
    
    <!-- 父组件 -->
    <MyComponent>
      <template #header="headerProps">
        {{ headerProps }}
      </template>
    
      <template #default="defaultProps">
        {{ defaultProps }}
      </template>
    
      <template #footer="footerProps">
        {{ footerProps }}
      </template>
    </MyComponent>
    

28. 依赖注入

概念

  • 要为组件后代提供数据,需要使用到 provide() 函数

    import { ref,provide } from 'vue'
    const msg = ref('add')
    provide('message',msg)
    
  • 要注入上层组件提供的数据,需使用 inject() 函数

    <script setup>
    import { inject } from 'vue'
    
    const message = inject('message')
    
    // 如果没有祖先组件提供 "message"
    // `value` 会是 "默认值"
    const value = inject('message', '默认值')
    </script>
    
  • 除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖

    import { createApp } from 'vue'
    
    const app = createApp({})
    
    app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
    

29. 异步组件

概念

  • Vue 提供了 defineAsyncComponent 方法来实现

  • 拆分应用为更小的块,并仅在需要时再从服务器加载相关组件

    import { defineAsyncComponent } from 'vue'
    
    const AsyncComp = defineAsyncComponent(() => {
      return new Promise((resolve, reject) => {
        // ...从服务器获取组件
        resolve(/* 获取到的组件 */)
      })
    })
    // ... 像使用其他一般组件一样使用 `AsyncComp`
    
  • 我们也可以用它来导入 Vue 单文件组件

  • 得到的 AsyncComp 是一个外层包装过的组件

  • 仅在页面需要它渲染时才会调用加载内部实际组件的函数

  • 所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载

    import { defineAsyncComponent } from 'vue'
    
    const AsyncComp = defineAsyncComponent(() =>
      import('./components/MyComponent.vue')
    )
    
  • 与普通组件一样,异步组件可以使用 app.component()全局注册

    app.component('MyComponent', defineAsyncComponent(() =>
      import('./components/MyComponent.vue')
    ))
    
  • 也可以直接在父组件中直接定义它们

    <script setup>
    import { defineAsyncComponent } from 'vue'
    
    const AdminPage = defineAsyncComponent(() =>
      import('./components/AdminPageComponent.vue')
    )
    </script>
    
    <template>
      <AdminPage />
    </template>
    
  • 异步操作不可避免地会涉及到加载和错误状态,

  • 因此 defineAsyncComponent() 也支持在高级选项中处理这些状态

    const AsyncComp = defineAsyncComponent({
      // 加载函数
      loader: () => import('./Foo.vue'),
    
      // 加载异步组件时使用的组件
      loadingComponent: LoadingComponent,
      // 展示加载组件前的延迟时间,默认为 200ms
      delay: 200,
    
      // 加载失败后展示的组件
      errorComponent: ErrorComponent,
      // 如果提供了一个 timeout 时间限制,并超时了
      // 也会显示这里配置的报错组件,默认值是:Infinity
      timeout: 3000
    })
    

30. 组合式函数

  • 一个组合式函数可以调用一个或多个其他的组合式函数

  • 这使得我们可以像使用多个组件组合成整个应用一样

  • 用多个较小且逻辑独立的单元来组合形成复杂的逻辑

  • 这正是为什么我们决定将实现了这一设计模式的 API 集合命名为组合式 API

  • 在组件中使用组合式 API 实现鼠标跟踪功能

    <script setup>
    import { ref, onMounted, onUnmounted } from 'vue'
    
    const x = ref(0)
    const y = ref(0)
    
    function update(event) {
      x.value = event.pageX
      y.value = event.pageY
    }
    
    onMounted(() => window.addEventListener('mousemove', update))
    onUnmounted(() => window.removeEventListener('mousemove', update))
    </script>
    
    <template>Mouse position is at: {{ x }}, {{ y }}</template>
    
  • 如果我们想在多个组件中复用这个相同的逻辑呢?

  • 我们可以把这个逻辑以一个组合式函数的形式提取到外部文件中

    // mouse.js
    import { ref, onMounted, onUnmounted } from 'vue'
    
    // 按照惯例,组合式函数名以“use”开头
    export function useMouse() {
      // 被组合式函数封装和管理的状态
      const x = ref(0)
      const y = ref(0)
    
      // 组合式函数可以随时更改其状态。
      function update(event) {
        x.value = event.pageX
        y.value = event.pageY
      }
    
      // 一个组合式函数也可以挂靠在所属组件的生命周期上
      // 来启动和卸载副作用
      onMounted(() => window.addEventListener('mousemove', update))
      onUnmounted(() => window.removeEventListener('mousemove', update))
    
      // 通过返回值暴露所管理的状态
      return { x, y }
    }
    
  • 下面是它在组件中使用的方式

    <script setup>
    import { useMouse } from './mouse.js'
    
    const { x, y } = useMouse()
    </script>
    
    <template>Mouse position is at: {{ x }}, {{ y }}</template>
    

31.内置组件 keep-alive

概念

  • 默认情况下,一个组件实例在被替换掉后会被销毁

  • 这会导致它丢失其中所有已变化的状态

  • 当这个组件再一次被显示时,会创建一个只带有初始状态的新实例

  • 我们想要组件能在被“切走”的时候保留它们的状态

  • 要解决这个问题,我们可以用 <KeepAlive> 内置组件将这些动态组件包装起来

    <!-- 非活跃的组件将会被缓存! -->
    <KeepAlive>
      <component :is="activeComponent" />
    </KeepAlive>
    
  • 通过传入 max prop 来限制可被缓存的最大组件实例数

    <KeepAlive :max="10">
      <component :is="activeComponent" />
    </KeepAlive>
    
  • <KeepAlive> 默认会缓存内部的所有组件实例

  • 但我们可以通过 includeexclude prop 来定制该行为

    <!-- 以英文逗号分隔的字符串 -->
    <KeepAlive include="a,b">
      <component :is="view" />
    </KeepAlive>
    
    <!-- 正则表达式 (需使用 `v-bind`) -->
    <KeepAlive :include="/a|b/">
      <component :is="view" />
    </KeepAlive>
    
    <!-- 数组 (需使用 `v-bind`) -->
    <KeepAlive :include="['a', 'b']">
      <component :is="view" />
    </KeepAlive>
    

32. 使用Element Plus

  • 执行命令安装

    npm install element-plus --save
    
  • main.js 中全局引入

    import ElementPlus from 'element-plus'  // 导入模块
    import 'element-plus/dist/index.css'  // 引入Element样式
    import App from './App.vue'
    
    const app = createApp(App)
    // 全局使用ElementPlus
    app.use(ElementPlus)
    app.mount('#app')
    
  • 组件页面中就可以使用Element组件了

    <el-button type="primary">Primary</el-button>
    

33. 使用axios

  • 安装 axios

    npm install axios
    
  • 在需要发送请求的页面中导入axios模块, 并发送请求

    <script setup>
    import axios from 'axios';
    // 发送get请求,当组件创建时会发送一次该请求
    axios.get('https://api.example.com/data')
      .then(response => {
        console.log(response.data);
      })
      .catch(error => {
        console.error(error);
      });
    
    // 传入请求数据,发送post请求
    axios.post('https://api.example.com/post-data', { data: 'example' })
      .then(response => {
        // 获得响应数据
        console.log(response.data);
      })
      .catch(error => {
        console.error(error);
      });
    </script>
    
  • 13
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vermouth-1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值