Vue3基础

188 阅读8分钟

Vue3基础

本文只叙述Vue3的新特性,和Vue2相同的特性、API不在这里过多叙述

以下API采用组合式 API,选项式参考官网

组合式 API 示例中使用单文件组件 + <script setup>的语法

API 风格

Vue 的组件可以按两种不同的风格书写:选项式 API 和组合式 API。

它们只是同一个底层系统所提供的两套不同的接口,实际上,选项式 API 是在组合式 API 的基础上实现的!关于 Vue 的基础概念和知识在它们之间都是通用的。

➢ 选项式 API (Options API)

○  以“组件实例”的概念为中心,使用选项式 API,可以用包含多个选项的对象来描述组件的逻辑,例如 data、methods 和 mounted。选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。

➢ 组合式 API (Composition API)

○ 通过组合式 API,可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与

○ 核心思想是直接在函数作用域内定义响应式状态变量,并将从多个函数中得到的状态组合起来处理复杂问题,这种形式更加自由。

配置

16.0 或更高版本的 Node.js

基于 Vite 的构建设置,并允许我们使用 Vue 的单文件组件 (SFC)

推荐的 IDE 配置是 Visual Studio Code + Volar 扩展。

项目初始化

npm init vue@latest 安装Vue官方的项目脚手架工具,安装并执行 create-vue
 Project name:  <your-project-name>
 Add TypeScript?  No / Yes
 Add JSX Support?  No / Yes
 Add Vue Router for Single Page Application development?  No / Yes
 Add Pinia for state management?  No / Yes
 Add Vitest for Unit testing?  No / Yes
 Add Cypress for both Unit and End-to-End testing?  No / Yes
 Add ESLint for code quality?  No / Yes
 Add Prettier for code formatting?  No / Yes

> cd <your-project-name>
> npm install
> npm run dev

基础

一、创建一个 Vue 应用

1、通过 createApp 函数创建一个新的 应用实例

import { createApp } from 'vue'
// 从一个单文件组件中导入根组件
import App from './App.vue'
const app = createApp(App)

2、挂载应用

.mount() 方法,返回值是根组件实例而非应用实例

3、应用配置

app.config.errorHandler = (err) => {
  / 处理错误 /
}
app.component('TodoDeleteButton', TodoDeleteButton)

4、多个应用实例

createApp API 允许你在同一个页面中创建多个共存的 Vue 应用,而且每个应用都拥有自己的用于配置和全局资源的作用域。

const app1 = createApp({
  / ... /
})
app1.mount('#container-1')

const app2 = createApp({
  / ... /
})
app2.mount('#container-2')

二、模板语法

参考Vue2

三、响应式基础

响应式对象其实是 JavaScript Proxy,其行为表现与一般对象相似,不同之处在于 Vue 能够跟踪对响应式对象属性的访问与更改操作

1、声明响应式状态

reactive() 函数创建一个响应式对象或数组,要在组件模板中使用响应式状态,需要在 setup() 函数中定义并返回。

js
import { reactive } from 'vue'

export default {
  setup() {
    const state = reactive({ count: 0 })

    function increment() {
      state.count++
    }

    // 不要忘记同时暴露 increment 函数
    return {
      state,
      increment
    }
  }
}
//暴露的方法通常会被用作事件监听器:
template
<button @click="increment">
  {{ state.count }}
</button>

2、<script setup>

但是在 setup() 函数中手动暴露大量的状态和方法非常繁琐

通过使用构建工具来简化该操作,当使用单文件组件(SFC)时,使用<script setup>来大幅度地简化代码。

<script setup>中的顶层的导入和变量声明可在同一组件的模板中直接使用。

可以这么理解:模板中的表达式和<script setup>中的代码处在同一个作用域中。

<script setup>
import { reactive } from 'vue'

const state = reactive({ count: 0 })

function increment() {
  state.count++
}
</script>

<template>
  <button @click="increment">
    {{ state.count }}
  </button>
</template>

3、响应式代理 vs. 原始对象

reactive() 返回的是一个原始对象的 Proxy,它和原始对象是不相等的:

const raw = {}
const proxy = reactive(raw)

// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false

// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true

// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true

// 依靠深层响应性,响应式对象内的嵌套对象依然是代理: 
proxy.nested = raw
console.log(proxy.nested === raw) // false

4、reactive() 的局限性

1)仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型 无效。

2)因为 Vue 的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。因此,当我们将响应式对象的属性赋值或解构至本地变量时,或是将该属性传入一个函数时,我们会失去响应性

5、  用 ref() 定义响应式变量

ref() 方法来允许我们创建可以使用任何值类型的响应式 ref

ref 的 .value 属性也是响应式的。同时,当值为对象类型时,会用 reactive() 自动转换它的 .value

const objectRef = ref({ count: 0 })

// 这是响应式的替换
objectRef.value = { count: 1 }

6、  ref 在模板中的解包

当 ref 在模板中作为顶层属性被访问时,它们会被自动“解包”,所以不需要使用 .value

<script setup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">
    {{ count }} <!-- 无需 .value -->
  </button>
</template>

仅当 ref 是模板渲染上下文的顶层属性时才适用自动“解包”。 例如, object 是顶层属性,但 object.foo 不是。

{{ object.foo + 1 }}
=>渲染为[object Object]1

以下写法才是顶层属性:

const { foo } = object

但如果一个 ref 是文本插值(即一个 {{ }} 符号)计算的最终值,它也将被解包。如:

{{ object.foo }}

7、  ref 在响应式对象中的解包

当一个 ref 被嵌套在一个响应式对象中,作为属性被访问或更改时,它会自动解包。

const count = ref(0)
const state = reactive({
  count
})

console.log(state.count) // 0

state.count = 1
console.log(count.value) // 1

如果将一个新的 ref 赋值给一个关联了已有 ref 的属性,那么它会替换掉旧的 ref:

const otherCount = ref(2)

state.count = otherCount
console.log(state.count) // 2
// 原始 ref 现在已经和 state.count 失去联系
console.log(count.value) // 1

8、  数组和集合类型的 ref 解包

当 ref 作为响应式数组或像 Map 这种原生集合类型的元素被访问时,不会进行解包。

const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)

四、  计算属性

使用计算属性来描述依赖响应式状态的复杂逻辑,Vue 的计算属性会自动追踪响应式依赖,且会基于其响应式依赖被缓存。

1、基础示例

computed() 方法期望接收一个 getter 函数,返回值为一个计算属性 ref。

<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'
  ]
})

// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
  return author.books.length > 0 ? 'Yes' : 'No'
})
</script>

<template>
  <p>Has published books:</p>
  <span>{{ publishedBooksMessage }}</span>
</template>

2、  可写计算属性

提供 getter 和 setter创建

getter 应只做计算而没有任何其他的副作用,不要在 getter 中做异步请求或者更改 DOM!

<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
  },
  // setter
  set(newValue) {
    // 注意:我们这里使用的是解构赋值语法
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})
</script>

五、  Class 与 Style 绑定

参考Vue2

六、条件渲染

v-if、v-else 和 v-else-if可以在 <template> 上使用 ,只是一个不可见的包装器元素,最后渲染的结果并不会包含这个 <template> 元素。

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

七、列表渲染

可以在 <template> 标签上使用 v-for 来渲染一个包含多个元素的块。

key 绑定的值期望是一个基础类型的值,例如字符串或 number 类型。

数组变化侦测:push、pop、shift、unshift、splice、sort、reverse

八、  事件处理

参考Vue2

九、  生命周期钩子

最常用的是 onMountedonUpdatedonUnmounted。所有生命周期钩子的完整参考及其用法请参考 API 索引

import { onMounted } from 'vue'

十、侦听器

在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。

1、侦听数据源类型

watch 的第一个参数可以是不同形式的“数据源”:可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组,但不能直接侦听响应式对象的属性值

const x = ref(0)
const y = ref(0)

// 单个 ref
watch(x, (newX) => {
  console.log(`x is ${newX}`)
})

// getter 函数
watch(
  () => x.value + y.value,
  (sum) => {
    console.log(`sum of x + y is: ${sum}`)
  }
)

// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x is ${newX} and y is ${newY}`)
})

2、深层侦听器

深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。

直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:

const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // 在嵌套的属性变更时触发
  // 注意:`newValue` 此处和 `oldValue` 是相等的, Proxy {count: 1}
  // 因为它们是同一个对象!
})

obj.count++

一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调,可以显式地加上 deep 选项,强制转成深层侦听器

watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // 注意:`newValue` 此处和 `oldValue` 是相等的
    // *除非* state.someObject 被整个替换了
  },
  { deep: true }
)

3、即时回调的侦听器

默认是懒执行的:仅当数据源变化时,才会执行回调。

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

watch(source, (newValue, oldValue) => {
  // 立即执行,且当 `source` 改变时再次执行
}, { immediate: true })

4、watchEffect

不再需要明确传递 todoId 作为源值,自动跟踪回调的响应式依赖,可以消除手动维护依赖列表的负担

5、回调会立即执行

watchEffect 仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪。

const todoId = ref(1)
const data = ref(null)

watch(todoId, async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
}, { immediate: true })

6、回调的触发时机

默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。

要在在侦听器回调中能访问被 Vue 更新之后的 DOM,需要指明 flush: 'post' 选项

watch(source, callback, {
  flush: 'post'
})

watchEffect(callback, {
  flush: 'post'
})

7、别名 watchPostEffect()

import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* 在 Vue 更新后执行 */
})

8、停止侦听器

同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。

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

const unwatch = watchEffect(
  async () => {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
    );
    data.value = await response.json();
  },
  {
    flush: "post",
  }
);
unwatch();

尽可能选择同步创建。如果需要等待一些异步数据,你可以使用条件式的侦听逻辑:

// 需要异步请求得到的数据
const data = ref(null)

watchEffect(() => {
  if (data.value) {
    // 数据加载后执行某些操作...
  }
})

十一、模板引用

只可以在组件挂载后才能访问模板引用。

1、访问模板引用

通过组合式 API 获得该模板引用,需要声明一个同名的 ref:

<script setup>
import { ref, onMounted } from 'vue'

// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)

onMounted(() => {
  input.value.focus()
})
</script>

<template>
  <input ref="input" />
</template>

2、v-for 中的模板引用

当在 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>

3、函数模板引用

除了使用字符串值作名字,ref attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:

<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">

4、组件上的 ref

如果一个子组件使用的是选项式 API 或没有使用 <script setup>,被引用的组件实例和该子组件的 this 完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。

使用了 <script setup> 的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露:

<script setup>
import { ref } from 'vue'

const a = 1
const b = ref(2)

// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
  a,
  b
})
</script>

当父组件通过模板引用获取到了该组件的实例时,得到的实例类型为 { a: number, b: number }

十二、组件基础

在单文件组件(SFC)中,推荐为子组件使用 PascalCase 的标签名,以此来和原生的 HTML 元素作区分。

1、传递 props

vue
<script setup>
defineProps(['title'])
</script>

<template>
  <h4>{{ title }}</h4>
</template>
defineProps 是一个仅`<script setup>`中可用的编译宏命令,并不需要显式地导入,会返回一个对象,其中包含了可以传递给组件的所有 props,声明的 props 会自动暴露给模板

js
const props = defineProps(['title'])
console.log(props.title)

如果没有使用 <script setup>,props 必须以 props 选项的方式声明,props 对象会作为 setup() 函数的第一个参数被传入:

js
export default {
  props: ['title'],
  setup(props) {
    console.log(props.title)
  }
}

2、监听事件

通过 defineEmits 宏来声明需要抛出的事件:

vue
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>

这声明了一个组件可能触发的所有事件,还可以对事件的参数进行验证。同时,这还可以让 Vue 避免将它们作为原生事件监听器隐式地应用于子组件的根元素。

和 defineProps 类似,defineEmits 仅可用于 <script setup> 之中,并且不需要导入,它返回一个等同于 emit 方法的 emit 函数。它可以被用于在组件的 <script setup> 中抛出事件,因为此处无法直接访问 emit 方法的 emit 函数。它可以被用于在组件的 <script setup> 中抛出事件,因为此处无法直接访问 emit:

如果你没有在使用 <script setup>,你可以通过 emits 选项定义组件会抛出的事件。你可以从 setup() 函数的第二个参数,即 setup 上下文对象上访问到 emit 函数:

js
export default {
  emits: ['enlarge-text'],
  setup(props, ctx) {
    ctx.emit('enlarge-text')
  }
}

3、通过插槽来分配内容

Vue 的自定义  元素实现

4、动态组件

上面的例子是通过 Vue 的  元素和特殊的 is attribute 实现的:

当使用 <component :is="..."> 来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过  组件强制被切换掉的组件仍然保持“存活”的状态。

5、DOM 模板解析注意事项

下面讨论只适用于直接在 DOM 中编写模板的情况,源于浏览器的原生 HTML 解析行为限制

如果你使用来自以下来源的字符串模板,就不需要顾虑这些限制了:

6、大小写区分

HTML 标签和属性名称是不分大小写的,所以浏览器会把任何大写的字符解释为小写,需要转换为相应等价的 kebab-case (短横线连字符) 形式:

js
// JavaScript 中的 camelCase
const BlogPost = {
  props: ['postTitle'],
  emits: ['updatePost'],
  template: `
    <h3>{{ postTitle }}</h3>
  `
}
template
<!-- HTML 中的 kebab-case -->
<blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>

7、闭合标签

我们在上面的例子中已经使用过了闭合标签 (self-closing tag):<MyComponent />

这是因为 Vue 的模板解析器支持任意标签使用 /> 作为标签关闭的标志。然而在 DOM 模板中,我们必须显式地写出关闭标签:

template
<my-component></my-component>

这是由于 HTML 只允许一小部分特殊的元素省略其关闭标签,最常见的就是  和 

当使用在原生 HTML 元素上时,is 的值必须加上前缀 vue: 才可以被解析为一个 Vue 组件。这一点是必要的,为了避免和原生的自定义内置元素相混淆。

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