创建Vue
应用的注意点:
1.
.mount()
方法应该始终在整个应用配置和资源注册完成后被调用。同时请注意,不同于其他资源注册方法。-放到最后面去挂载!2.当根组件没有设置
template
选项时,Vue
将自动使用容器的innerHTML
作为模板。
多个应用实例
应用实例并不只限于一个。
createApp
API 允许你在同一个页面中创建多个共存的 Vue 应用,而且每个应用都拥有自己的用于配置和全局资源的作用域。
如果你正在使用
Vue
来增强服务端渲染 HTML,并且只想要Vue
去控制一个大型页面中特殊的一小部分,应避免将一个单独的Vue
应用实例挂载到整个页面上,而是应该创建多个小的应用实例,将它们分别挂载到所需的元素上去。
const app1 = createApp({
/* ... */
})
app1.mount('#container-1')
const app2 = createApp({
/* ... */
})
app2.mount('#container-2')
模板语法注意点:
绑定在表达式中的方法在组件每次更新时都会被重新调用,因此不应该产生任何副作用,比如改变数据或触发异步操作。
<span :title="toTitleDate(date)">
{{ formatDate(date) }}
</span>
受限的全局访问
模板中的表达式将被沙盒化,仅能够访问到有限的全局对象列表。该列表中会暴露常用的内置全局对象,比如
Math
和Date
。没有显式包含在列表中的全局对象将不能在模板内表达式中访问,例如用户附加在
window
上的属性。然而,你也可以自行在app.config.globalProperties
上显式地添加它们,供所有的Vue
表达式使用。
动态参数
同样在指令参数上也可以使用一个 JavaScript
表达式,需要包含在一对方括号内:
<!--
注意,参数表达式有一些约束,
参见下面“动态参数值的限制”与“动态参数语法的限制”章节的解释
-->
<a v-bind:[attributeName]="url"> ... </a>
<!-- 简写 -->
<a :[attributeName]="url"> ... </a>
这里的 attributeName
会作为一个 JavaScript
表达式被动态执行,计算得到的值会被用作最终的参数。举例来说,如果你的组件实例有一个数据属性 attributeName
,其值为 "href"
,那么这个绑定就等价于 v-bind:href
。
相似地,你还可以将一个函数绑定到动态的事件名称上:
<a v-on:[eventName]="doSomething"> ... </a>
<!-- 简写 -->
<a @[eventName]="doSomething">
动态参数值的限制
动态参数中表达式的值应当是一个字符串
,或者是 null
。特殊值 null
意为显式移除该绑定。其他非字符串的值会触发警告。
动态参数语法的限制
<!-- 这会触发一个编译器警告 -->
<a :['foo' + bar]="value"> ... </a>
还应该避免使用大写,因为浏览器会将它们强制转换成小写的
响应式基础
DOM更新时机nextTick
若要
等待一个状态改变后的 DOM 更新完成
,你可以使用 nextTick() 这个全局API
:
import { nextTick } from 'vue'
export default {
methods: {
increment() {
this.count++
nextTick(() => {
// 访问更新后的 DOM
})
}
}
}
响应式对象VS原始对象
值得注意的是,reactive()
返回的是一个原始对象的 Proxy,它和原始对象是不相等的
const raw = {}
const proxy = reactive(raw)
// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false
只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue
的响应式系统的最佳实践是 仅使用你声明对象的代理版本。
这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理:
reactive()的对象的局限性
因为 Vue
的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失:
通过reactive设置的属性可以通过直接通过属性名来使用!
let state = reactive({ count: 0 })
// 上面的引用 ({ count: 0 }) 将不再被追踪(响应性连接已丢失!)
state = reactive({ count: 1 })
用==ref()==定义响应式变量
ref()相对于reactive()可谓是自由了很多!
Vue
提供了一个ref()
方法来允许我们创建可以使用任何值类型
的响应式 refref()
将传入参数的值包装为一个带.value
属性的 ref 对象:所以在每次使用的时候,我们都要用.value方式!- 和响应式对象的属性类似,ref 的
.value
属性也是响应式的。同时,当值为对象类型时,会用reactive()
自动转换它的.value
。
一个包含对象类型值的 ref 可以响应式地替换整个对象
const objectRef = ref({ count: 0 })
// 这是响应式的替换
objectRef.value = { count: 1 }
ref 被传递给函数或是从一般对象上被解构时,不会丢失响应性:
const obj = {
foo: ref(1),
bar: ref(2)
}
// 该函数接收一个 ref
// 需要通过 .value 取值
// 但它会保持响应性
callSomeFunction(obj.foo)
// 仍然是响应式的
const { foo, bar } = obj
ref在模板中的解包
当 ref 在模板中==作为顶层属性==被访问时,它们会被自动“解包”,所以不需要使用 .value
。下面是之前的计数器例子,用 ref()
代替:
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">
{{ count }} <!-- 无需 .value -->
</button>
</template>
Tips:
请注意,仅当 ref 是模板渲染上下文的顶层属性时才适用自动“解包”。 例如, foo
是顶层属性,但 object.foo
不是。
所以我们给出以下 object:
js
const object = { foo: ref(1) }
下面的表达式将不会像预期的那样工作:
template
{{ object.foo + 1 }}
渲染的结果会是一个 [object Object]
,因为 object.foo
是一个 ref 对象。我们可以通过将 foo
改成顶层属性来解决这个问题:
js
const { foo } = object
template
{{ foo + 1 }}
现在渲染结果将是 2
。
需要注意的是,如果一个 ref 是文本插值—就是不是JS表达式(即一个 {{ }}
符号)计算的最终值,它也将被解包。因此下面的渲染结果将为 1
:
template
{{ object.foo }}
这只是文本插值的一个方便功能,相当于 {{ object.foo.value }}
。
ref在响应式对象中的解包
当一个 ref
被嵌套在一个响应式对象中,作为属性被访问或更改时,它会自动解包,因此会表现得和一般的属性一样:通过对象来调用这个属性的时候,可以不使用value!
js
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
如果将一个新的 ref 赋值给一个关联了已有 ref 的属性,那么它会替换掉旧的 ref:
js
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// 原始 ref 现在已经和 state.count 失去联系
console.log(count.value) // 1
只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包。
数组和集合类型的解包
跟响应式对象不同,当 ref 作为响应式数组或像 Map
这种原生集合类型的元素被访问时,不会进行解包。
js
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)
响应性语法糖(新特性–目前在实验中)
相对于普通的 JavaScript 变量,我们不得不用相对繁琐的 .value
来获取 ref 的值。这是一个受限于 JavaScript 语言限制的缺点。然而,通过编译时转换,我们可以让编译器帮我们省去使用 .value
的麻烦。Vue 提供了一种编译时转换,使用$
vue
<script setup>
let count = $ref(0)
function increment() {
// 无需 .value
count++
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
计算属性
computed返回值为一个计算属性ref。和其它的ref相似,你可以通过.value
访问计算结果。计算属性 ref 也会在模板中自动解包,因此在模板表达式中引用时无需添加 .value
。
响应式依赖:
Vue 的计算属性会自动追踪响应式依赖,如果它依赖的那个属性为响应式的数据,那么它本身也变成了一个响应式的数据!
计算属性缓存vs方法:
有时候,我们定义一个方法,然后在模板中使用,获取到的效果与计算属性是一致的。但计算属性它是根据依赖而进行缓存的,一个计算属性仅会在其响应式依赖更新时才重新计算,
这也解释了为什么下面的计算属性永远不会更新,因Date.now()
并不是一个响应式依赖:
const now = computed(() => Date.now())
而方法,在每次重新渲染的情况下都会执行,这就造成了性能的损耗!
可写计算属性
计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性
,你可以通过同时提供 getter
和 setter
来创建:
<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>
现在当你再运行 fullName.value = 'John Doe'
时,setter 会被调用而 firstName
和 lastName
会随之更新。
类与样式的绑定
Vue 专门为 class
和 style
的 v-bind
用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。
绑定对象
<div :class="{ active: isActive }"></div>
你可以在对象中写多个字段来操作多个 class。此外,:class
指令也可以和一般的 class
attribute 共存。举例来说,下面这样的状态:
<div
class="static"
:class="{ active: isActive, 'text-danger': hasError }"
></div>
这也会渲染出相同的结果。我们也可以绑定一个返回对象的计算属性。这是一个常见且很有用的技巧:
const isActive = ref(true)
const error = ref(null)
const classObject = computed(() => ({
active: isActive.value && !error.value,
'text-danger': error.value && error.value.type === 'fatal'
}))
在组件上使用
<!-- 子组件模板 -->
<p class="foo bar">Hi!</p>
<!-- 在使用组件时 -->
<MyComponent class="baz boo" />
<p class="foo bar baz boo">Hi</p>
如果你的组件有多个根元素,你将需要指定哪个根元素来接收这个 class。你可以通过组件的 $attrs
属性来实现指定:
$attrs
可以访问到父节点上的类名
条件渲染
上的v-if
如果我们想要切换多个元素,可以使用template将他们包裹起来。这只是一个不可见的包装器元素,最后渲染的结果并不会包含这个 <template>
元素
v-show
不支持在 <template>
元素上使用,也不能和 v-else
搭配使用。
列表渲染
v-for来遍历对象
第一个参数表示的是键值,第二个参数表示的是键,第三个参数表示的是索引!
v-for里面使用范围值
v-for
可以直接接受一个整数值。在这种用例中,会将该模板基于 1...n
的取值范围重复多次。
<span v-for="n in 10">{{ n }}</span>
注意此处 n
的初值是从 1
开始而非 0
。
template上面的v-for
在template
标签里面渲染一个包含多个元素的块
v-for
与v-if
v-if的优先级要高于v-for,也就是说:当v-if中要用到v-for中的值,v-if是访问不到的!
<!--
这会抛出一个错误,因为属性 todo 此时
没有在该实例上定义
-->
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo.name }}
</li>
解决方案:
在外面包一层template使用v-for,可以解决这个优先级的问题!
v-for中绑定key值与不绑定key值的区别
假如我们有一个数组 arr = [1,2,3,4],我们要在2后面插入一个值9;
如果绑定了key值,那么会是这样的情况:
只会更新一个dom元素
如果没有绑定key值,那么在此后面的元素都要更新!
更高效地更新虚拟dom
key
绑定的值期望是一个基础类型的值,例如字符串或 number 类型。不要用对象作为 v-for
的 key
数组变化侦测
变更方法#
Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
替换一个数组#
变更方法,顾名思义,就是会对调用它们的原数组进行变更。相对地,也有一些不可变 (immutable) 方法,例如 filter()
,concat()
和 slice()
,这些都不会更改原数组,而总是返回一个新数组。当遇到的是非变更方法时,我们需要将旧的数组替换为新的:
// `items` 是一个数组的 ref
items.value = items.value.filter((item) => item.message.match(/Foo/))
你可能认为这将导致 Vue
丢弃现有的 DOM 并重新渲染整个列表——幸运的是,情况并非如此。Vue
实现了一些巧妙的方法来最大化对 DOM 元素的重用,因此用另一个包含部分重叠对象的数组来做替换,仍会是一种非常高效的操作。
事件处理
在内联事件处理器中访问事件参数
1.传一个$event变量
2.使用内联箭头函数
<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>
<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
Submit
</button>
function warn(message, event) {
// 这里可以访问原生事件
if (event) {
event.preventDefault()
}
alert(message)
}
事件修饰符
为解决这一问题,Vue
为 v-on
提供了事件修饰符。修饰符是用 .
表示的指令后缀,包含以下这些:
.stop
.prevent
.self
.capture
.once
.passive
按键修饰符
…
表单输入绑定
在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript
中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:
原生写法:
<input
:value="text"
@input="event => text = event.target.value">
语法糖:
<input v-model="text">
侦听器
在组合式 API
中,我们可以使用 watch
函数在每次响应式状态发生变化时触发回调函数:
侦听数据源类型
watch的第一个参数可以是不同形式的数据源,但不能直接侦听响应式对象里面的数据。
第一个参数可以是:
- 一个函数,返回一个值
- 一个ref
- 一个响应式对象
- 或是由以上类型的值组成的数组
第二个参数:侦听源发生变化时要触发的回调函数。
当侦听多个来源时,回调函数接受两个数组,分别对应源数组中的新值和旧值
( [ newValue1, newValue2 ] , [ oldValue1 , oldValue2 ]) => {/* code */
第三个参数:可选对象,可以支持一下这些选项
- immediate:侦听器创建时立即触发回调
deep
:如果源是一个对象,会强制深度遍历
,以便在深层级发生变化时触发回调函数
- flush:调整回调函数的刷新时机—访问被vue更新后的DOM
onTrack
/onTrigger
:调试侦听器的依赖
const obj = reactive({ count: 0 })
// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
解决方法:
需要用一个返回该属性的getter函数
// 提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)
深层监听器
直接给 watch()
传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发
:
小Tips:
深度侦听需要遍历被侦听对象中的所有嵌套的属性,
当用于大型数据结构时,开销很大
。因此请只在必要时才使用它,并且要留意性能。
watchEffect
watch()
是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。
看一个请求数据的小例子:
const url = ref('https://...')
const data = ref(null)
async function fetchData() {
const response = await fetch(url.value)
data.value = await response.json()
}
// 立即获取
fetchData()
// ...再侦听 url 变化
watch(url, fetchData)
代码可以简写
为:
watchEffect(async () => {
const response = await fetch(url.value)
data.value = await response.json()
})
watchEffect()
会立即执行一遍回调函数,如果这时函数产生了副作用,Vue 会自动追踪副作用的依赖关系,自动分析出响应源
Watch
和watchEffect
的区别:
这两者主要的区别是追踪响应式依赖的方式:
watch
只追踪明确侦听的数据源
watchEffect
则会在副作用发生期间追踪依赖,他会在同步过程中,追踪到所有能够访问到的响应式属性。依赖关系不那么明确!
后置刷新的 watchEffect()
有个更方便的别名watchPostEffect
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
})
模板引用
Vue
声明式渲染为你抽象了对大部分DOM的直接操作,但在某些情况,我们仍然需要直接访问底层DOM元素。要实现这一点,我们有一种ref
属性
<input ref="input">
访问模板引用
为了通过组合式 API
获得该模板引用,我们需要声明一个同名的 ref
:
<script setup>
import { ref, onMounted } from 'vue'
// 定义的那个变量需要和DOM元素中ref的属性值相等!
// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="input" />
</template>
注意,你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问 input
,在初次渲染时会是 null
。这是因为在初次渲染前这个元素还不存在呢!
watchEffect(() => {
if (input.value) {
input.value.focus()
} else {
// 此时还未挂载,或此元素已经被卸载(例如通过 v-if 控制)
}
})
v-for
中的模板引用
当在 v-for
中使用模板引用时,对应的 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
数组并不保证与源数组相同的顺序
。
函数模板引用
除了使用字符串值作名字,ref
attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:
template
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">
注意我们这里需要使用动态的 :ref
绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el
参数会是 null
。你当然也可以绑定一个组件方法而不是内联函数。
组件上的ref
模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例
:
Tips:遵守标准!
- 大多数情况下,应该只在绝对需要时才使用组件引用
- 大多数情况下,你应该首先使用标准的
props
和emit
接口来实现父子组件交互。
有一个例外的情况,使用了 <script setup>
的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup>
的子组件中的任何东西,除非子组件在其中通过 defineExpose
宏显式暴露:
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({
a,
b
})
</script>
当父组件通过模板引用获取到了该组件的实例时,得到的实例类型为 { a: number, b: number }
(ref 都会自动解包,和一般的实例一样)。
上面的意思也就是说,如果每个子组件都使用了setup语法,那么如果想通过ref访问里面的内容,则需要手动地去暴露能够访问的变量。利用
defineProps
去暴露!