前言
记录一些vue3.x的内容
DOM更新时机
DOM的更新不是同步的。Vue会在“next tick”更新周期中缓冲所有状态的更改,以确保你不管进行了多少次状态更改,每个组件都只会被更新一次。
要等待DOM更新完成后再执行额外的代码,可以使用nextTick()全局API:
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// 现在 DOM 已经更新了
}
watch
计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。
- 基本示例
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
// 可以直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.indexOf('?') > -1) {
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
}
}
})
</script>
<template>
<p>
Ask a yes/no question:
<input v-model="question" />
</p>
<p>{{ answer }}</p>
</template>
- 监听数据源类型
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}`)
})
注意:不能直接监听响应式对象的属性值,例如:
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() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})
obj.count++
相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
)
也可以显示加上deep
选项,强制转成深层侦听器:
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true }
)
谨慎使用!深层侦听需要遍历被监听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此,应当只在必要时使用,并且要留意性能。
- 及时回调侦听器
watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但如果我们需要立即请求数据,比如请求一些原始数据,然后在相关状态更改时重新请求数据。就需要immediate: true
watch(source, (newValue, oldValue) => {
// 立即执行,且当 `source` 改变时再次执行
}, { immediate: true })
- watchEffect()
观察下面两部分代码,很明显watchEffect
的优势就是不用明确指定源,直接在回调中使用需要被监听的数据
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 })
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
这个例子中,回调会立即执行,不需要指定 immediate: true。在执行期间,它会自动追踪 todoId.value 作为依赖(和计算属性类似)。每当 todoId.value 变化时,回调会再次执行。有了 watchEffect(),我们不再需要明确传递 todoId 作为源值。
注意:watchEffect
仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪。
watch 和 watchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:
1、 watch
只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
2、 watchEffect
,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
- 回调的触发时机
侦听器能访问到的状态都是Vue更新之前额状态。如果想在侦听器回调中能访问Vue更新之后的DOM,需要flush: 'post'
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
后置刷新的 watchEffect() 有个更方便的别名 watchPostEffect():
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
})
- 停止侦听器
在setup() 或 <script setup>中创建的都是同步侦听器,与宿主组件绑定了生命周期,不用管。
但异步创建的侦听器需要手动停止,防止内存泄漏。
<script setup>
import { watchEffect } from 'vue'
// 它会自动停止
watchEffect(() => {})
// ...这个则不会!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
要手动停止一个侦听器,请调用 watch 或 watchEffect 返回的函数:
const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch()
注意,需要异步创建侦听器的情况很少,请尽可能选择同步创建。如果需要等待一些异步数据,你可以使用条件式的侦听逻辑:
// 需要异步请求得到的数据
const data = ref(null)
watchEffect(() => {
if (data.value) {
// 数据加载后执行某些操作...
}
})
模板引用
ref 是一个特殊的 attribute,和 v-for 章节中提到的 key 类似。它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。
-
基本使用
注意,你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问 input,在初次渲染时会是 null。这是因为在初次渲染前这个元素还不存在呢!
如果你需要侦听一个模板引用 ref 的变化,确保考虑到其值为 null 的情况:
watchEffect(() => {
if (input.value) {
input.value.focus()
} else {
// 此时还未挂载,或此元素已经被卸载(例如通过 v-if 控制)
}
})
- v-for中的使用
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([1, 2, 3])
const itemRefs = ref([])
onMounted(() => {
alert(itemRefs.value.map(i => i.textContent))
})
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
- 函数模块引用
除了使用字符串值作名字,ref attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">
注意我们这里需要使用动态的:ref
绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null。你当然也可以绑定一个组件方法而不是内联函数。
- 组件上的ref
模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例:
这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的 props 和 emit 接口来实现父子组件交互。
有一个例外的情况,使用了 <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 } (ref 都会自动解包,和一般的实例一样)。
组件基础
- 组件的使用
- 两种props的使用方式
- 与v-for的联动使用
在实际应用中,我们可能在父组件中会有如下的一个博客文章数组:
const posts = ref([
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
])
这种情况下,我们可以使用 v-for
来渲染它们:
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
留意,这里是使用v-bind
来传递动态prop值的。当事先不知道要渲染的确切内容时,这一点特别有用。
- defineEmits
简单来说就是在setup中使用类似$emit
的方式抛出事件,因为非模块中无法访问$emit
和 defineProps 类似,defineEmits 仅可用于 <script setup> 之中,并且不需要导入,它返回一个等同于 emit:
<script setup>
const emit = defineEmits(['enlarge-text'])
emit('enlarge-text')
</script>
export default {
emits: ['enlarge-text'],
setup(props, ctx) {
ctx.emit('enlarge-text')
}
}
- 动态组件
<script setup>
import Home from './Home.vue'
import Posts from './Posts.vue'
import Archive from './Archive.vue'
import { ref } from 'vue'
const currentTab = ref('Home')
const tabs = {
Home,
Posts,
Archive
}
</script>
<template>
<div class="demo">
<button
v-for="(_, tab) in tabs"
:key="tab"
:class="['tab-button', { active: currentTab === tab }]"
@click="currentTab = tab"
>
{{ tab }}
</button>
<component :is="tabs[currentTab]" class="tab"></component>
</div>
</template>
DOM模板解析注意事项
- 元素位置限制
某些 HTML 元素对于放在其中的元素类型有限制,例如 <ul>,<ol>,<table> 和 <select>,相应的,某些元素仅在放置于特定元素中时才会显示,例如 <li>,<tr> 和 <option>。
这将导致在使用带有此类限制元素的组件时出现问题。例如:
<table>
<blog-post-row></blog-post-row>
</table>
自定义的组件 <blog-post-row>
将作为无效的内容被忽略,因而在最终呈现的输出中造成错误。我们可以使用特殊的 is attribute
作为一种解决方案:
<table>
<tr is="vue:blog-post-row"></tr>
</table>
注意:当使用在原生 HTML 元素上时,is
的值必须加上前缀 vue:
才可以被解析为一个 Vue 组件。这一点是必要的,为了避免和原生的自定义内置元素相混淆。
Props
- 静态与动态
<BlogPost title="My journey with Vue" />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :title="post.title" />
<!-- 根据一个更复杂表达式的值动态传入 -->
<BlogPost :title="post.title + ' by ' + post.author.name" />
- 传递不同的值类型
1、 Number
<!-- 虽然 `42` 是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :likes="42" />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :likes="post.likes" />
2、 Boolean
<!-- 仅写上 prop 但不传值,会隐式转换为 `true` -->
<BlogPost is-published />
<!-- 虽然 `false` 是静态的值,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :is-published="false" />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :is-published="post.isPublished" />
3、Array
<!-- 虽然这个数组是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :comment-ids="[234, 266, 273]" />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :comment-ids="post.commentIds" />
4、 Object
<!-- 虽然这个对象字面量是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost
:author="{
name: 'Veronica',
company: 'Veridian Dynamics'
}"
/>
<!-- 根据一个变量的值动态传入 -->
<BlogPost :author="post.author" />
- 使用一个对象绑定多个prop
如果你想要将一个对象的所有属性都当作 props 传入,你可以使用没有参数的 v-bind,即只使用v-bind
而非:prop-name
。例如,这里有一个post
对象:
- 单向数据流
props因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。
1、 prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性。在这种情况下,最好是新定义一个局部数据属性,从 props 上获取初始值即可:
const props = defineProps(['initialCounter'])
// 计数器只是将 props.initialCounter 作为初始值
// 像下面这样做就使 prop 和后续更新无关了
const counter = ref(props.initialCounter)
2、 需要对传入的 prop 值做进一步的转换。在这种情况中,最好是基于该 prop 值定义一个计算属性:
const props = defineProps(['size'])
// 该 prop 变更时计算属性也会自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())
Prop校验
要声明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' }
}
},
// 自定义类型校验函数
propF: {
validator(value) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
},
// 函数类型的默认值
propG: {
type: Function,
// 不像对象或数组的默认,这不是一个
// 工厂函数。这会是一个用来作为默认值的函数
default() {
return 'Default function'
}
}
})
一些细节:
1、 所有prop默认都是可选的,除非说明了required:true。
2、 除Boolean外的未传递的可选prop将会有一个默认值undefined。
3、 Boolean 类型的未传递 prop 将被转换为 false。这可以通过为它设置 default 来更改——例如:设置为 default: undefined 将与非布尔类型的 prop 的行为保持一致。
4、 如果声明了 default 值,那么在 prop 的值被解析为 undefined 时,无论 prop 是未被传递还是显式指明的 undefined,都会改为 default 值。
如果使用了基于类型的 prop 声明,Vue 会尽最大努力在运行时按照 prop 的类型标注进行编译。举例来说,defineProps<{ msg: string }>
会被编译为 { msg: { type: String, required: true }}
。
校验中的type
也可以是自定义的类或构造函数,Vue 将会通过 instanceof 来自动检查类型是否匹配。
路由
路由基础部分这部分提过:Vue简记
由于路由导航中的组件实例是被重复使用的,并没有销毁再创建的过程。这也意味着生命周期钩子不会被调用。那么要对同一组件中参数的变化做出响应的话,可以按如下的简单操作:
1、 watch
const User = {
template: '...',
created() {
this.$watch(
() => this.$route.params,
(toParams, previousParams) => {
// 对路由变化做出响应...
}
)
},
}
2、 beforeRouteUpdate 导航守卫
const User = {
template: '...',
async beforeRouteUpdate(to, from) {
// 对路由变化做出响应...
this.userData = await fetchUser(to.params.id)
},
}
- Vue Router和组合式API
1、在setup
中访问路由和当前路由
因为我们在setup
里面没有访问this
,所以我们不能再直接访问this.$router
或this.$route
。作为替代,我们使用 useRouter 和 useRoute 函数:
2、 name的作用
name属性的意义就在于,我们可以使用这个name属性来动态地绑定当前路由,而不是只能跳转固定的path路径,也就是说,我一旦给某个路由设置了name属性,那么我在跳转的时候,如果是通过绑定的name属性进行跳转,而不是根据绑定的path路径来跳转的话,我的跳转就可以是动态的,即使这个路由path更改了,我的name也是指向当前更改后的路由
注意:如果你想导航到命名路由而不是其嵌套路由,可以命名父路由。但是,当重新加载页面的时候将始终显示嵌套的子路由,因为它被视为指向路径的导航,而不是命名路由。
编程式导航!
顾名思义,就是不借助
<router-link>
,而是使用逻辑代码的方式实现跳转。
导航到不同位置
其实属性to
和push方法的规则完全相同
-
router.push
该方法可以导航到其他URL,并且会向history栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,会回到之前的URL。
该方法的参数可以是一个字符串路径,或者一个描述地址的对象:
// 字符串路径
router.push('/users/eduardo')
// 带有路径的对象
router.push({ path: '/users/eduardo' })
// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })
// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })
// 带 hash,结果是 /about#team
router.push({ path: '/about', hash: '#team' })
注意:如果提供了path,params会被忽略(params和path不能一起使用),上述例子中的query
并不属于这种情况。取而代之的是下面例子的做法,你需要提供路由的name
或手写完整的带有参数path
:
const username = 'eduardo'
// 我们可以手动建立 url,但我们必须自己处理编码
router.push(`/user/${username}`) // -> /user/eduardo
// 同样
router.push({ path: `/user/${username}` }) // -> /user/eduardo
// 如果可能的话,使用 `name` 和 `params` 从自动 URL 编码中获益
router.push({ name: 'user', params: { username } }) // -> /user/eduardo
// `params` 不能与 `path` 一起使用
router.push({ path: '/user', params: { username } }) // -> /user
替换当前位置
类似
push
,但router.replace
不会向history添加新记录,正如其名字一样——取代当前的条目。
也可以直接在传递给router.push
的routeLocation
中增加一个属性replace:true
router.push({ path: '/home', replace: true })
// 相当于
router.replace({ path: '/home' })
横跨历史
该方法采用一个整数作为参数,表示在历史堆中前进或后退多少步,类似于
window.history.go(n)
。
// 向前移动一条记录,与 router.forward() 相同
router.go(1)
// 返回一条记录,与 router.back() 相同
router.go(-1)
// 前进 3 条记录
router.go(3)
// 如果没有那么多记录,静默失败
router.go(-100)
router.go(100)
篡改历史
你可能已经注意到,router.push
、router.replace
和 router.go
是 window.history.pushState
、window.history.replaceState
和 window.history.go
的翻版,它们确实模仿了 window.history
的 API。
params和query的用法
1、 params
- 路由配置
// 使⽤params传参,路由配置的时候 path 要带上参数
{
path: '/user/:id',
name: "users",
component: User //这个 User 是组件名称
}
- 跳转方式
// ⽅法1:
<router-link :to="{ name: 'users', params: { id: this.id}}">按钮</router-link>
// ⽅法2:
this.$router.push({name:'users',params:{id: this.id}})
// ⽅法3:
this.$router.push('/user/' + this.id)
- 页面url显示
params在浏览器地址栏中不显示参数名 - 获取参数方式
this.$route.params.id
2、 query
- 路由配置
//使⽤ query 传参这⾥不需要参⼊参数,参见上⾯的params写法
{
path: '/user',
name: "users",
component: User //这个 users 是传进来的组件名称
}
- 跳转方式
// ⽅法1:
<router-link :to="{ name: 'users', query: { id: this.id }}">按钮</router-link>
// ⽅法2:
this.$router.push({ name: 'users', query:{ id: this.id }})
// ⽅法3:
<router-link :to="{ path: '/user', query: { id: this.id }}">按钮</router-link>
// ⽅法4:
this.$router.push({ path: '/user', query:{ id: this.id }})
// ⽅法5:
this.$router.push('/user?id=' + this.id)
- 页面url显示
query在浏览器地址栏中显示参数名称 - 获取参数
this.$route.query.id
router.push
和所有其他导航方法都会返回一个Promise,这让我们可以等到导航完成后才知道是成功还是失败。
命名视图
想同级展示多个视图,而不是嵌套展示(比如slidebar和main两个视图),这个时候命名视图就排上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果
router-view
没有设置名字,那么默认为default
。
<router-view class="view left-sidebar" name="LeftSidebar"></router-view>
<router-view class="view main-content"></router-view>
<router-view class="view right-sidebar" name="RightSidebar"></router-view>
一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用components
:
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
components: {
default: Home,
// LeftSidebar: LeftSidebar 的缩写
LeftSidebar,
// 它们与 `<router-view>` 上的 `name` 属性匹配
RightSidebar,
},
},
],
})
- 嵌套命名视图
有时候也需要嵌套视图命名。
重定向和别名
从/home
重定向到/
const routes = [{ path: '/home', redirect: '/' }]
重定向的目标也可以是一个命名的路由
const routes = [{ path: '/home', redirect: { name: 'homepage' } }]
甚至是一个方法,动态返回重定向目标:
const routes = [
{
// /search/screens -> /search?q=screens
path: '/search/:searchText',
redirect: to => {
// 方法接收目标路由作为参数
// return 重定向的字符串路径/路径对象
return { path: '/search', query: { q: to.params.searchText } }
},
},
{
path: '/search',
// ...
},
]
- 相对重定向
重定向到相对位置:
const routes = [
{
// 将总是把/users/123/posts重定向到/users/123/profile。
path: '/users/:id/posts',
redirect: to => {
// 该函数接收目标路由作为参数
// 相对位置不以`/`开头
// 或 { path: 'profile'}
return 'profile'
},
},
]
- 别名
将/
别名为/home
,意味着当用户访问/home
时,URL仍然是/home
,但会被匹配为用户正在访问/
。
const routes = [{ path: '/', component: Homepage, alias: '/home' }]
路由组件传参
在组件中使用$route
会与路由紧密耦合,这限制了组件的灵活性。可以通过配置props
配置来解除这种行为,即当props=true
时,route.params
将被设置为组件的props
const User = {
// 请确保添加一个与路由参数完全相同的 prop 名
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const routes = [{ path: '/user/:id', component: User, props: true }]
- 对于有命名视图的路由,你必须为每个命名视图定义
props
配置:
- 当
props
是一个对象时,它将原样设置为组件props。当props是静态的时候很有用。
- 可以创建一个返回props的函数。这允许你将参数转换为其他类型,将静态值与基于路由的值相结合等等。
URL
/search?q=vue
将传递{query: 'vue'}
作为 props 传给 SearchUser
组件。应尽可能保持
props
函数为无状态的,因为它只会在路由发生变化时起作用。如果你需要状态来定义props,请使用包装组件,这样vue才可以对状态变化做出反应。