本文为Vue学习笔记,内容主要来源于Vue官方教程。过程中将某些API与React做了对比,方便更好地理解以及加深记忆。
创建一个Vue应用
应用实例
Vue中有应用实例的概念,并且在应用实例上开放了很多接口,在应用配置部分会提到。React中并没有应用实例的概念。
import { createApp } from 'vue'const app = createApp({/* 根组件选项 */
})
根组件
在createApp
中传入根组件就可以生成应用实例。
const app = createApp(App)
挂载应用
调用应用实例的mount
方法挂载应用。在React中,使用createRoot(document.getElementById('app')).render(<App />)
方式来渲染应用。视角会有点差别,一个是挂载,一个是渲染。
app.mount('#app')
应用配置
应用实例上开放了config
配置接口,可以处理错误;
app.config.errorHandler = (err) => {/* 处理错误 */
}
应用实例还开放了其他方法接口,如下面的注册全局组件。
app.component('TodoDeleteButton', TodoDeleteButton)
查看了完整的API列表,该实例上共有11个方法可供使用。
多个应用实例
Vue允许用户在一个页面中创建多个应用实例,而且每个应用都拥有自己的用于配置和全局资源的作用域。教程中提到主要用于服务端渲染。
const app1 = createApp({/* ... */
})
app1.mount('#container-1')const app2 = createApp({/* ... */
})
app2.mount('#container-2')
模板语法
文本插值
使用双大括号绑定变量。React中使用大括号。
<span>Message: {{ msg }}</span>
原始 HTML
想要插入原始html,这里用到了Vue的指令即v-xxx。在React中则是使用<div dangerouslySetInnerHTML={{ __html:'<p>some raw html</p>' }} />
的方式。
<p>Using text interpolation: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
Attribute 绑定
<div v-bind:id="dynamicId"></div>
// 简写
<div :id="dynamicId"></div>
// 同名简写
<div :id></div>
动态绑定多个值
Vue的属性绑定相较于React稍微有些特别,React中文本插值和属性绑定都是用大括号,比较统一。Vue中在双引号内写表达式比较特殊。
const objectOfAttrs = {id: 'container',class: 'wrapper',style: 'background-color:green'
}
<div v-bind="objectOfAttrs"></div>
使用 JavaScript 表达式
这里仅支持单一表达式,一个简单的判断方法是是否可以合法地写在 return 后面。这个和React是一致的。
{{ number + 1 }}{{ ok ? 'YES' : 'NO' }}{{ message.split('').reverse().join('') }}<div :id="`list-${id}`"></div>
调用函数
<time :title="toTitleDate(date)" :datetime="date">{{ formatDate(date) }}
</time>
受限的全局访问
模板中的表达式将被沙盒化,仅能够访问到有限的全局对象列表。该列表中会暴露常用的内置全局对象,比如 Math 和 Date。没有显式包含在列表中的全局对象将不能在模板内表达式中访问,例如用户附加在 window 上的属性。然而,你也可以自行在 app.config.globalProperties 上显式地添加它们,供所有的 Vue 表达式使用。
指令 Directives
Vue中的指令可以用来处理属性绑定、事件绑定、条件语句等等。在React中使用{ seen && <div>see</div>}
。
<p v-if="seen">Now you see me</p>
参数 Arguments
<a v-bind:href="url"> ... </a>
<!-- 简写 -->
<a :href="url"> ... </a><a v-on:click="doSomething"> ... </a>
<!-- 简写 -->
<a @click="doSomething"> ... </a>
动态参数
Vue中提供了动态参数的API。React中没有,只能通过解构对象的方式处理,即{...{id: 'url'}}
。
<!--
注意,参数表达式有一些约束,只允许字符串和null
-->
<a v-bind:[attributeName]="url"> ... </a>
<!-- 简写 -->
<a :[attributeName]="url"> ... </a><a v-on:[eventName]="doSomething"> ... </a>
<!-- 简写 -->
<a @[eventName]="doSomething"> ... </a>
另外要注意如果写在html文件里的模板,动态参数名需要完全小写,因为浏览器会自动转换成小写。
修饰符 Modifiers
以下表示调用event.preventDefault()
方法。在React中需要在事件回调中显式调用。
<form @submit.prevent="onSubmit">...</form>
响应式基础
声明响应式状态
代码中使用ref时,注意使用.value
获取和修改值。设计.value
是为了方便通过getter和setter函数来获取和相应值的变化,因为原生JS是很难监听变量的变化的。
import { ref } from 'vue'const count = ref(0)console.log(count) // { value: 0 }
console.log(count.value) // 0count.value++
console.log(count.value) // 1
在模板中使用时,Vue会自动解包,不需要使用.value
。
<button @click="count++">{{ count }}
</button>
setup()
选项式API
在setup函数中定义状态和函数,返回给模板使用。
import { ref } from 'vue'export default {setup() {const count = ref(0)function increment() {// 在 JavaScript 中需要 .valuecount.value++}// 不要忘记同时暴露 increment 函数return {count,increment}}
}
<script setup>
组合式API
单文件组件中,使用setup标识可以避免上述写法,将所有状态和函数暴露给模板。
<script setup>
import { ref } from 'vue'const count = ref(0)function increment() {count.value++
}
</script><template><button @click="increment">{{ count }}</button>
</template>
深层响应性
非原始值将通过 reactive() 转换为响应式代理。也可以通过 shallow ref 来放弃深层响应性。
import { ref } from 'vue'const obj = ref({nested: { count: 0 },arr: ['foo', 'bar']
})function mutateDeeply() {// 以下都会按照期望工作obj.value.nested.count++obj.value.arr.push('baz')
}
DOM 更新时机
当修改了响应式状态时,DOM 会被自动更新。DOM 更新不是同步的。Vue 会在“next tick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。这个就如同React中的状态更新批处理。
要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API。这个在React中没有对应的API,一般就是使用setTimeout将要更新的状态放到下一个宏任务中更新。
import { nextTick } from 'vue'async function increment() {count.value++await nextTick()// 现在 DOM 已经更新了
}
侦听器
Vue中使用watch监听ref变化执行副作用,类似于React中的useEffect。不同的是:
- watch默认不会立即执行副作用,而useEffect会;
- watch的执行时机默认是在DOM更新之前(即同步执行),而useEffect始终在DOM更新之后(即异步执行);
- 监听的值或对象会作为参数传入到副作用函数中,且包含新旧两个参数值。
<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 = trueanswer.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 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组。
watch函数可以监听getter函数,在React中,相当于useMemo与useEffect结合使用。
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}`)}
)
深层侦听器
方式一:使用reactive
const obj = reactive({ count: 0 })watch(obj, (newValue, oldValue) => {// 在嵌套的属性变更时触发// 注意:`newValue` 此处和 `oldValue` 是相等的// 因为它们是同一个对象!
})obj.count++
方式二:使用ref+deep
watch一个ref只监听ref本身的变化,不会监听其内部属性的变化。使用deep属性可以强制开启深层响应性。
const obj = ref({count: 1})// 单个 ref
watch(obj, (newObj) => {console.log(`count is ${newObj.count}`)
},{deep: true})
在 Vue 3.5+ 中,deep 选项还可以是一个数字,表示最大遍历深度——即 Vue 应该遍历对象嵌套属性的级数。
即时回调的侦听器
immediate参数设置为true后,行为等同于React中的useEffect。
watch(source,(newValue, oldValue) => {// 立即执行,且当 `source` 改变时再次执行},{ immediate: true }
)
一次性侦听器
设置once参数为true,可以控制副作用只触发一次,具体场景后面再看。
仅支持 3.4 及以上版本
watch(source,(newValue, oldValue) => {// 当 `source` 变化时,仅触发一次},{ once: true }
)
watchEffect
watchEffect是watch(immediate:true)的简化写法,其会自动跟踪副作用函数中的响应式依赖,不需要再声明依赖。相比较深层侦听器,watchEffect的方式更加简便且高效。
watchEffect(async () => {const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)data.value = await response.json()
})
副作用清理
onWatcherCleanup只在3.5+中支持
import { watch, onWatcherCleanup } from 'vue'watch(id, (newId) => {const controller = new AbortController()fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {// 回调逻辑})onWatcherCleanup(() => {// 终止过期请求controller.abort()})
})
onCleanup可以在3.5以下版本中作为替代
watch(id, (newId, oldId, onCleanup) => {// ...onCleanup(() => {// 清理逻辑})
})watchEffect((onCleanup) => {// ...onCleanup(() => {// 清理逻辑})
})
回调的触发时机
默认情况下,侦听器回调会在父组件更新 (如有) 之后、所属组件的 DOM 更新之前被调用。这意味着如果你尝试在侦听器回调中访问所属组件的 DOM,那么 DOM 将处于更新前的状态。
如果想在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM,你需要指明flush: 'post'
选项:
watch(source, callback, {flush: 'post'
})watchEffect(callback, {flush: 'post'
})
后置刷新的 watchEffect()
有个更方便的别名 watchPostEffect()
。
import { watchPostEffect } from 'vue'watchPostEffect(() => {/* 在 Vue 更新后执行 */
})
同步侦听器
你还可以创建一个同步触发的侦听器,它会在 Vue 进行任何更新之前触发:
watch(source, callback, {flush: 'sync'
})watchEffect(callback, {flush: 'sync'
})
同步触发的 watchEffect()
有个更方便的别名 watchSyncEffect()
:
import { watchSyncEffect } from 'vue'watchSyncEffect(() => {/* 在响应式数据变化时同步执行 */
})
同步侦听器不会进行批处理,每当检测到响应式数据发生变化时就会触发。可以使用它来监视简单的布尔值,但应避免在可能多次同步修改的数据源 (如数组) 上使用。
停止侦听器
同步创建的侦听器会在组件被卸载时自动停止,异步创建的需要手动回收。
<script setup>
import { watchEffect } from 'vue'// 它会自动停止
watchEffect(() => {})// ...这个则不会!
setTimeout(() => {watchEffect(() => {})
}, 100)const unwatch = watchEffect(() => {})// ...当该侦听器不再需要时
unwatch()
</script>
模板引用
useTemplateRef 在3.5+支持
<script setup>
import { useTemplateRef, onMounted } from 'vue'// 第一个参数必须与模板中的 ref 值匹配
const input = useTemplateRef('my-input')onMounted(() => {input.value.focus()
})
</script><template><input ref="my-input" />
</template>
v-for中的模板引用
<script setup>
import { ref, useTemplateRef, onMounted } from 'vue'const list = ref([/* ... */
])const itemRefs = useTemplateRef('items')onMounted(() => console.log(itemRefs.value))
</script><template><ul><li v-for="item in list" ref="items">{{ item }}</li></ul>
</template>
ref 数组并不保证与源数组相同的顺序
函数模板引用
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">
组件Ref
选项式API写法的子组件,ref得到的组件实例同this,即拥有对子组件属性和方法的完全访问。而使用组合式API的,子组件属性和方法都是私有的,需要使用defineExpose进行暴露。这与React中的Component组件和函数组件处理组件实例的方式是一致的。
<script setup>
import { useTemplateRef, onMounted } from 'vue'
import Child from './Child.vue'const childRef = useTemplateRef('child')onMounted(() => {// childRef.value 将持有 <Child /> 的实例
})
</script><template><Child ref="child" />
</template>
<script setup>
import { ref } from 'vue'const a = 1
const b = ref(2)// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({a,b
})
</script>
组件
定义props
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script><template><h4>{{ title }}</h4>
</template>或者export default {props: ['title'],setup(props) {console.log(props.title)}
}
监听事件
模板中调用回调
<BlogPost...@enlarge-text="postFontSize += 0.1"/><!-- BlogPost.vue, 省略了 <script> -->
<template><div class="blog-post"><h4>{{ title }}</h4><button @click="$emit('enlarge-text')">Enlarge text</button></div>
</template>
代码中调用回调
<script setup>
const emit = defineEmits(['enlarge-text'])emit('enlarge-text')或
export default {emits: ['enlarge-text'],setup(props, ctx) {ctx.emit('enlarge-text')}
}
</script>
通过插槽来分配内容
<!-- AlertBox.vue -->
<template><div class="alert-box"><strong>This is an Error for Demo Purposes</strong><slot /></div>
</template><style scoped>
.alert-box {/* ... */
}
</style>
动态组件
<!-- currentTab 改变时组件也改变 -->
<component :is="tabs[currentTab]"></component>
当使用 来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过
<KeepAlive>
组件强制被切换掉的组件仍然保持“存活”的状态。
DOM 内模板解析注意事项
注意以下几点
- 大小写区分
// JavaScript 中的 camelCase
const BlogPost = {props: ['postTitle'],emits: ['updatePost'],template: `<h3>{{ postTitle }}</h3>`
}
<!-- HTML 中的 kebab-case -->
<blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>
- 闭合标签
<MyComponent />
<my-component></my-component>
- 元素位置限制
<table><blog-post-row></blog-post-row>
</table>
<table><tr is="vue:blog-post-row"></tr>
</table>