内容来源自vue官网 链接
1.响应式基础
(1)声明响应式状态
我们使用reactive()函数创建一个响应式对象或者数组
import {reactive } from 'vue'
conststate = reactive({ count: 0 })
如果需要在组件模板中使用响应式状态,需要在setup()函数中定义并且返回
import { reactive} from 'vue'
exportdefault {
// `setup` 是一个专门用于组合式 API 的特殊钩子函数
setup() {
const state = reactive({ count: 0 })
function increment() {
state.count++
}
// 暴露 state 到模板
return {
state,
increment
}
}
}
<div>{{state.count }}</div>
(2)<srcipt setup>
在setup()定义大量的状态和方法非常繁琐,我们可以使用<script setup>来简化大量代码
<script setup>
import {reactive } from 'vue'
conststate = reactive({ count: 0 })
functionincrement() {
state.count++
}
</script>
<template>
<button @click="increment">
{{ state.count }}
</button>
</template>
<script setup> 中的顶层的导入和变量声明可在同一组件的模板中直接使用。你可以理解为模板中的表达式和 <script setup> 中的代码处在同一个作用域中
(3)DOM更新时机
当你更改了响应式状体后,DOM会自动更新,但是DOM的更新不是同步的,相反,Vue 将缓冲它们直到更新周期的 “下个时机” 以确保无论你进行了多少次状态更改,每个组件都只更新一次。
若要等待一个状态改变后的 DOM 更新完成,你可以使用 nextTick() 这个全局 API:
import {nextTick } from 'vue'
functionincrement() {
state.count++
nextTick(() => {
// 访问更新后的 DOM
})
}
(4)reactive() 的局限性
reactive() API有两条限制:
仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型 无效。
因为 Vue 的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失
let state= reactive({ count: 0 })
// 上面的引用 ({ count: 0 }) 将不再被追踪(响应性连接已丢失!)
state =reactive({ count: 1 })
这也意味着当我们将响应式对象的属性赋值或解构至本地变量时,或是将该属性传入一个函数时,我们会失去响应性
(5)ref()定义响应式变量
reactive() 的种种限制归根结底是因为 JavaScript 没有可以作用于所有值类型的 “引用” 机制。为此,Vue 提供了一个 ref() 方法来允许我们创建可以使用任何值类型的响应式 ref
import {ref } from 'vue'
constcount = ref(0)
ref() 将传入参数的值包装为一个带 .value 属性的 ref 对象
constcount = ref(0)
console.log(count)// { value: 0 }
console.log(count.value)// 0
count.value++
console.log(count.value)// 1
和响应式对象的属性类似,ref 的 .value 属性也是响应式的。同时,当值为对象类型时,会用 reactive() 自动转换它的 .value
constobjectRef = ref({ count: 0 })
// 这是响应式的替换
objectRef.value= { count: 1 }
(6)响应式语法糖
相对于普通的 JavaScript 变量,我们不得不用相对繁琐的 .value 来获取 ref 的值。这是一个受限于 JavaScript 语言限制的缺点。然而,通过编译时转换,我们可以让编译器帮我们省去使用 .value 的麻烦。Vue 提供了一种编译时转换,使得我们可以像这样书写之前的“计数器”示例:
<script setup>
let count= $ref(0)
functionincrement() {
// 无需 .value
count++
}
</script>
<template>
<button @click="increment">{{count }}</button>
</template>
2.计算属性
(1)基础示例
有些时候在模板中需要写入逻辑较为复杂的代码,但是这会让模板变得臃肿,难以维护。这时候,就需要用到计算属性computed()方法了
<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage}}</span>
</template>
<script setup>
import {reactive, computed } from 'vue'
constauthor = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
// 一个计算属性 ref
constpublishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' :'No'
})
</script>
(2)计算属性缓存vs方法
通过上面的示例,不难发现我们可以在{{ }}中定义一个方法,也能实现计算属性同样的效果,那么,他们的区别在哪呢?
若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果,而不用重复执行 getter 函数。相比之下,方法调用总是会在重渲染发生时再次执行函数。
3.事件处理
(1)监听事件
我们使用v-on指令(@)来监听DOM事件,并在事件触发时执行对应的JavaScript代码。用法分为内联事件处理器和方法事件处理器:v-on:click="methodName" 或 @click="handler"
内联事件处理器
内联事件处理器通常用于简单场景,例如:
const count = ref(0)
<button
@click="count++">Add 1</button>
<p>Count is: {{ count }}</p>
方法事件处理器
随着事件处理器的逻辑变得愈发复杂,内联代码方式变得不够灵活。因此 v-on 也可以接受一个方法名或对某个方法的调用。
constname = ref('Vue.js')
functiongreet(event) {
alert(`Hello ${name.value}!`)
// `event` 是 DOM 原生事件
if (event) {
alert(event.target.tagName)
}
}
<!--`greet` 是上面定义过的方法名 -->
<button@click="greet">Greet</button>
(2)在内联事件处理器中访问事件参数
有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event 变量,或者使用内联箭头函数:
<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>
<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.',event)">
Submit
</button>
functionwarn(message, event) {
// 这里可以访问原生事件
if (event) {
event.preventDefault()
}
alert(message)
}
(3)事件修饰符
在处理事件时调用 event.preventDefault() 或 event.stopPropagation() 是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理 DOM 事件的细节会更好。
为解决这一问题,Vue 为 v-on 提供了事件修饰符。修饰符是用 . 表示的指令后缀,大致分为以下几种:
表单修饰符
事件修饰符
鼠标按键修饰符
键值修饰符
v-bind修饰符
表单修饰符
.lazy:使我们填完信息后不会里面赋值,而是等待焦点离开input后才进行赋值,也就是在change事件之后再进行信息同步
.trim:自动过滤用户输入的首字符空格,中间的空格不会被过滤
.number:自动将输入的值转为数值类型,如果输入的值不能被parseFloat()解析,则返回原来的值
事件修饰符
.stop:阻止了事件冒泡,相当于调用了event.stopPropagation()
.prevent:阻止了事件默认行为,相当于调用了preventDefault()
.self:只有触发事件的元素是自身时才触发处理函数
.once:绑定事件后只触发一次
.captrue:使事件触发从这个元素的最顶层开始触发
.passive:在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符
鼠标按键修饰符
.left:左键点击
.right:右键点击
.middle:中键点击
键盘修饰符
@keyup:松开某个按键触发
@keydown:按下某个按键触发
4.生命周期
下面是来自官网的实例生命周期的图表
![](https://i-blog.csdnimg.cn/blog_migrate/c295292b02e1df361ea8230cc35b6359.png)
下面是vue2与vue3生命周期钩子的名称区别及作用
![](https://i-blog.csdnimg.cn/blog_migrate/7bd54f6a311feb053e9ca8174d2182f3.png)
5.侦听器
(1)基本示例
计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。
watch监听某个属性的变化,一旦发生变化,就会触发对应的回调函数执行,在里面进行一些具体的操作,从而达到事件监听的效果。
watch里面具有三个参数:
第一个参数是:选择要监听的属性。
第二个参数是:设置的回调函数。即监听到变化时应该执行的函数 。
第三个参数是:可以设置deep (深度监听) 其值为true和false。还可以设置immediate (是否以当前值执行回调函数) 其值为true和false。
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)深层侦听器
在开发过程过可能有多层的数据嵌套,非常复杂,这里需要用到深层侦听器了,加上{ deep : true }就可以强制转换为深层侦听
watch(
() => state.someObject,
(newValue, oldValue) => {
},
{ deep: true }
)
(3)watchEffect()
watch()是懒执行的,是等侦听的数据源变化时才会执行的。如果希望在创建侦听器之前时就执行一遍回调函数,我们可以使用watchEffect()
具体用法
首先watchEffect传入的函数会立即执行一次,在执行的过程中收集依赖
只有收集的依赖发生变化时 函数才会再次执行
setup(){
letnum = ref(0)
watchEffect(()=>{
letxd = num.value
console,log('watchEffect函数执行了~')
})
}
(4)watch() vs watchEffect()
watch 和 watchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:
watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
6.模板引用
(1)基本示例
ref是一个特殊的属性,它允许我们在一个指定的DOM元素或子组件在挂载完成后,实现对它的直接引用
<inputtype='text' ref='input'>
exportdefault{
setup(){
const input = ref('');
return {
input,
}
}
}
// 注意:ref属性的值必须与return中导出的一致
(2)组件上的ref
模板引用也可以使用在组件上,这种情况下引用中获得的值是组件实例
<script setup>
import {ref, onMounted } from 'vue'
importChild from './Child.vue'
constchild = ref(null)
onMounted(()=> {
// child.value 是 <Child /> 组件的实例
})
</script>
<template>
<Child ref="child" />
</template>
7.组件基础
(1)定义组件
当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue 文件中,这被叫做单文件组件
<script setup>
import {ref } from 'vue'
constcount = ref(0)
</script>
<template>
<button @click="count++">Youclicked me {{ count }} times.</button>
</template>
(2)使用组件
在需要使用的文件中,在 <script setup>导入的组件,将会以默认被导出的形式暴露到外部,可以模板中直接使用
<script setup>
importButtonCounter from './ButtonCounter.vue'
</script>
<template>
<h1>Here is a child component!</h1>
<ButtonCounter />
</template>
8.传递props
在使用组件中,我们会遇到需要父组件向子组件传递某个属性的时候,props就是父组件传向子组件的一座桥梁
父组件传入属性在子组件标签上直接定义即可
<BlogPost:title="post.title"></BlogPost>
子组件接收父组件传入的属性有2种情况
使用<script setup>时,需要defineProps()来接收,里面为一个数组,数组中就是对应父组件中传入的属性
<script setup>
defineProps(['title'])
</script>
<template>
<h4>{{ title }}</h4>
</template>
不使用<script setup>时,props 必须以 props 选项的方式声明,props 象会作为 setup() 函数的第一个参数被传入:
exportdefault {
props: ['title'],
setup(props) {
console.log(props.title)
}
}
9.监听事件
在父组件向子组件传递完属性之后,我们可能会遇到父组件需要一个来自子组件的事件来进行交互
和上面一样,父组件接收事件只需要在子组件标签上定义即可
<BlogPost @enlarge-text="postFontSize += 0.1" ></BlogPost>
子组件传递事件有使用 <script setup>和不使用<script setup>两种
使用<script setup>,通过defineEmits()来抛出事件
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>
不使用<script setup>时,需在emits上定义,可以通过setup()中的第二个参数获取到,然后使用emit()进行抛出
exportdefault {
emits: ['enlarge-text'],
setup(props, ctx) {
ctx.emit('enlarge-text')
}
}
10.组件注册
一个Vue组件在使用前必须被注册才能使用,组件注册又分为全局注册和局部注册两种
(1)全局注册
全局注册Vue实例的app.component()方法,让组件在当前Vue应用中全局可用
import {createApp } from 'vue'
const app= createApp({})
app.component(
// 注册的名字
'MyComponent',
// 组件的实现
{
/* ... */
}
)
app.component() 方法可以被链式调用:
app
.component('ComponentA', ComponentA)
.component('ComponentB', ComponentB)
.component('ComponentC', ComponentC)
全局注册的组件可以在此应用的任意组件的模板中使用:
<!-- 这在当前应用的任意组件中都可用 -->
<ComponentA/>
<ComponentB/>
<ComponentC/>
所有的子组件也可以使用全局注册的组件,这意味着这三个组件也都可以在彼此内部使用。
(2)局部注册
全局注册虽然很方便,但有以下几个问题:
全局注册,但并没有被使用的组件无法在生产打包时被自动移除(也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。
全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。
相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对tree-shaking 更加友好。
在使用 <script setup> 的单文件组件中,导入的组件可以直接在模板中使用,无需注册:
<script setup>
importComponentA from './ComponentA.vue'
</script>
<template>
<ComponentA />
</template>
如果没有使用 <script setup>,则需要使用 components 选项来显式注册:
importComponentA from './ComponentA.js'
exportdefault {
components: {
ComponentA
},
setup() {
// ...
}
}
11.透传Attributes
透传Attributes指的是传递给一个组件,没有被声明为props或emit的attributes或v-on事件监听器,常见的有class、style、id
(1)Attributes 继承
当一个组件以单元素作为根节点时,会自动继承来自父组件上的透传attributes
<!--<MyButton> 的模板 -->
<button>clickme</button>
一个父组件使用了这个组件,并且传入了 class:
<MyButton class="large" />
最后渲染出的 DOM 结果是:
<button class="large">click me</button>
(2)v-on监听器继承
同样的规则也适用于 v-on 事件监听器:
<MyButton @click="onClick" />
click 监听器会被添加到 <MyButton> 的根元素,即那个原生的 <button> 元素之上。当原生的 <button> 被点击,会触发父组件的 onClick 方法。同样的,如果原生 button 元素自身也通过 v-on 绑定了一个事件监听器,则这个监听器和从父组件继承的监听器都会被触发。
(3)禁用attributes继承
如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs:false。
如果你使用了 <script setup>,你需要一个额外的 <script> 块来书写这个选项声明:
<script>
// 使用普通的 <script> 来声明选项
exportdefault {
inheritAttrs: false
}
</script>
<script setup>
//...setup 部分逻辑
</script>
最常见的需要禁用 attribute 继承的场景就是 attribute 需要应用在根节点以外的其他元素上。通过设置 inheritAttrs 选项为 false,你可以完全控制透传进来的 attribute 被如何使用。
这些透传进来的 attribute 可以在模板的表达式中直接用 $attrs 访问到。
<span>Fallthroughattribute: {{ $attrs }}</span>
这个 $attrs 对象包含了除组件所声明的 props 和 emits 之外的所有其他 attribute,例如 class,style,v-on 监听器等等。
有几点需要注意:
和 props 有所不同,透传 attributes 在 JavaScript 中保留了它们原始的大小写,所以像 foo-bar 这样的一个 attribute 需要通过 $attrs['foo-bar'] 来访问。
像 @click 这样的一个 v-on 事件监听器将在此对象下被暴露为一个函数 $attrs.onClick。
(4)在 JavaScript中访问透传 Attributes
如果需要,你可以在 <script setup> 中使用 useAttrs() API 来访问一个组件的所有透传 attribute:
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
如果没有使用 <script setup>,attrs 会作为 setup() 上下文对象的一个属性暴露:
export default {
setup(props, ctx) {
// 透传attribute 被暴露为 ctx.attrs
console.log(ctx.attrs)
}
}
12.插槽slot
(1)默认内容
在外部没有提供任何内容的情况下,可以为插槽指定默认内容比如有这样一个 <SubmitButton> 组件:
<buttontype="submit">
<slot></slot>
</button>
如果我们想在父组件没有提供任何插槽内容时在 <button> 内渲染“Submit”,只需要将“Submit”写在 <slot> 标签之间来作为默认内容:
<buttontype="submit">
<slot>
Submit <!-- 默认内容 -->
</slot>
</button>
但是如果我们提供了内容,那默认内容将被取代
(2)具名插槽
具名插槽通过特殊attribute的name,表示为这个插槽的唯一id,要为具名插槽中引入内容,需要使用一个带有v-slot的template的元素,v-slot可以缩写为#
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
(3)动态插槽名
动态指令参数在 v-slot 上也是有效的,即可以定义下面这样的动态插槽名:
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>
(4)作用域插槽
在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽。
我们也确实有办法这么做!可以像对组件传递 props 那样,向一个插槽的出口上传递 attributes:
<!--<MyComponent> 的模板 -->
<div>
<slot :text="greetingMessage":count="1"></slot>
</div>
当需要接收插槽 props 时,默认插槽和具名插槽的使用方式有一些小区别。下面我们将先展示默认插槽如何接受 props,通过子组件标签上的 v-slot 指令,直接接收到了一个插槽 props 对象:
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
(5)具名作用域插槽
具名作用域插槽的工作方式也是类似的,插槽 props 可以作为 v-slot 指令的值被访问到:v- slot:n ame="slotProps"。当使用缩写时是这样:
<MyComponent>
<template #header="headerProps">
{{ headerProps }}
</template>
<template #default="defaultProps">
{{ defaultProps }}
</template>
<template #footer="footerProps">
{{ footerProps }}
</template>
</MyComponent>
向具名插槽中传入 props:
<slot name="header" message="hello"></slot>
注意插槽上的 name 是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。因此最终 headerProps 的结果是 { message:'hello' }。
如果你混用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template> 标签。尝试直接为组件添加 v-slot 指令将导致编译错误。这是为了避免因默认插槽的 props 的作用域而困惑。举例:
<!-- 该模板无法编译 -->
<template>
<MyComponent v-slot="{ message}">
<p>{{ message }}</p>
<template #footer>
<!-- message 属于默认插槽,此处不可用 -->
<p>{{ message }}</p>
</template>
</MyComponent>
</template>
13.依赖注入
(1)prop逐级透传
通常情况下,我们传递prop都是父组件向子组件传递。但是实际开发中,子组件可能还包含其他子组件,一级一级嵌套,如果需要向更深级的组件传递,这该怎么办呢?
provide 和 inject 可以帮助我们解决这一问题。一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
(2)provide()
要为组件后代提供数据,需要使用到provide()函数
<script setup>
import {provide } from 'vue'
provide(/*注入名 */'message', /* 值 */ 'hello!')
</script>
provide() 函数接收两个参数。第一个参数被称为注入名,可以是一个字符串或是一个 Symbol。后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。
(3)inject()
需要接收上层提供的数据,需要使用inject()函数
<script setup>
import {inject } from 'vue'
constmessage = inject('message')
</script>
如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。这使得注入方组件能够通过 ref 对象保持了和供给方的响应性链接。
14.组合式函数
在Vue 应用的概念中,“组合式函数”(Composables)是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。
当构建前端应用时,我们常常需要复用公共任务的逻辑。例如为了在不同地方格式化时间,我们可能会抽取一个可复用的日期格式化函数。这个函数封装了无状态的逻辑:它在接收一些输入后立刻返回所期望的输出。复用无状态逻辑的库有很多,比如你可能已经用过的 lodash 或是 date-fns。
相比之下,有状态逻辑负责管理会随时间而变化的状态。一个简单的例子是跟踪当前鼠标在页面中的位置。在实际应用中,也可能是像触摸手势或与数据库的连接状态这样的更复杂的逻辑。