vue3使用指南
vue3使用指南
主要介绍vue3的使用,同时会涉及到vue2,并会讲解其中的一些差异。
安装
CDN引入
如果想快速体验,可以直接通过cdn进行引入。
<div id="app">{{msg}}</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script >
Vue.createApp({
data() {
return {
msg: 'Vue3'
}
}
}).mount('#app')
</script>
通过 CDN 引入 Vue 时,由于不涉及到构建步骤,可以使得设置更加简单,并且可以用于增强静态的 HTML 或与后端框架集成。但是这也意味着无法使用SFC(单文件)语法。
使用es模块构建
现代浏览器大多都已原生支持 ES 模块,可以像这样通过 CDN 以及原生 ES 模块使用 Vue。
<div id="app">{{msg}}</div>
<script type="module">
import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
createApp({
data() {
return {
msg: 'Vue3'
}
}
}).mount('#app')
</script>
使用import maps
<div id="app">{{msg}}</div>
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<script type="module">
import {createApp} from 'vue'
createApp({
data() {
return {
msg: 'Hello Vue!'
}
}
}).mount('#app')
</script>
工程化
使用vue@latest方式
Vue 官方的项目脚手架工具,内部使用的是vite进行处理。
npm init vue@latest
这个指令会安装并执行create-vue,之后按照提示安装依赖并运行即可。

请注意,生成的项目中的示例组件使用的是组合式 API 和 <script setup>,而非选项式 API。
使用@vue/cli方式
虽然vue现在已经不推荐使用该方式,内部使用的webpack进行处理,但是依旧提供了vue3模板的下载。
安装@vue/cli
npm i @vue/cli -g
# 创建项目
vue create xxx

可以选择官方的vue3模板,也可以自行配置。

使用vite创建
# npm 6.x
npm init vite@latest <project-name> --template vue
# npm 7+,需要加上额外的双短横线
npm init vite@latest <project-name> -- --template vue

创建实例
在vue2中,通过引入vue文件,进行实例的创建。
import Vue from 'vue'
new Vue({
...
})
而在vue3中做了改变,被移动到了由新的 createApp 方法所创建的应用实例上。
import { createApp } from 'vue'
const app = createApp({})
组合式API
在vue2.x中,一个组件中通常包含了data、computed、methods、watch等组件选项来组织逻辑,当组件变得越来越大时,逻辑关注点的列表也会增长,尤其对于那些一开始没有编写这些组件的人来说,这会导致组件难以阅读和理解。
这种碎片化使得理解和维护复杂组件变得困难。选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。
为了使得能将同一个逻辑的相关代码单独放在一起,vue3引入了组合式API的概念,使用setup来编写组件式API
来简单看看一个例子理解一下组合式API。
<template>
<!-- 计数器 -->
<p>{{obj.count}}</p>
<button @click="add">+1</button>
<!-- v-if展示区域 -->
<button @click="show">显示</button>
<button @click="hide">隐藏</button>
<div v-if="showDivFlag">一个被控制显隐的div</div>
</template>
<script>
import { reactive, ref } from 'vue'
// 计数器逻辑
function count() {
const data = {
count: 0
}
const obj = reactive(data);
function add() {
obj.count++;
}
return {
obj,
add
}
}
// v-if展示区域逻辑
function vifDiv() {
const showDivFlag = ref(true)
function show() {
showDivFlag.value = true
}
function hide() {
showDivFlag.value = false
}
return {
showDivFlag,
show,
hide
}
}
export default {
setup() {
const { obj, add } = count();
const {showDivFlag, show, hide} = vifDiv();
return {
obj,
add,
showDivFlag,
show,
hide
}
},
}
</script>
在setup内对数据进行响应式,之后将data、methods返回,这样,模板中就可以使用这些。比起vue2中选项式API,使用setup能将各个模块逻辑单独放在一起,避免在methods中定义一大堆的方法。
以上例子使用vue2的选项式API:
<template>
<!-- 计数器 -->
<p>{{obj.count}}</p>
<button @click="add">+1</button>
<!-- v-if展示区域 -->
<button @click="show">显示</button>
<button @click="hide">隐藏</button>
<div v-if="showDivFlag">一个被控制显隐的div</div>
</template>
<script>
export default {
data() {
return {
obj: {
count: 0
},
showDivFlag: true
}
},
methods: {
add() {
this.obj.count++;
},
show() {
this.showDivFlag = true;
},
hide() {
this.showDivFlag = false;
}
}
}
</script>
组合式 API 的核心思想是直接在函数作用域内定义响应式状态变量,并将从多个函数中得到的状态组合起来处理复杂问题。这种形式更加自由,也需要开发者对 Vue 的响应式系统有更深的理解才能高效使用。相应的,它的灵活性也使得组织和重用逻辑的模式变得更加强大。
两种 API 风格都能够覆盖大部分的应用场景。它们只是同一个底层系统所提供的两套不同的接口。实际上,选项式 API 是在组合式 API 的基础上实现的。关于 Vue 的基础概念和知识在它们之间都是通用的。
setup()
组件中的一个选项,处于生命周期函数 beforeCreate 钩子函数之前的函数,props 被解析之后执行。同时也是组合式API的入口。
接受两个参数:
props: 父组件的传值
如果想在setup中使用props,必须在props对prop进行声明,未声明的prop的无法在setup中的props中解构出来(可以通过context.attrs获取)。声明了的prop即使父组件没有传,在setup的props仍然可以获取,但是值为undefined或者props中定义的默认值。props: { key: String, value: { type: String, default: 'default' } }, setup(props) { console.log(props); }setup函数中的props是响应式的,当传入新的prop时,它将被更新。但是,因为props是响应式的,你不能使用ES6解构,因为它会消除prop的响应性。可以使用toRefs来解构。setup(props) { const {key, value} = toRefs(props); }context: 上下文,context是一个普通的JavaScript对象,也就是说,它不是响应式的,这意味着你可以安全地对context使用ES6解构。
context有4个属性:attrs: 非响应式对象,跟vue2的this.$attrs一样,用于接收父组件传的但是子组件中未声明的值。emit: 跟vue2的this.$emit一样 用于向父组件触发事件。slots:非响应式对象,跟vue2的this.$slots一样,用于获取插槽的信息。expose:用于当在setup中使用渲染函数,而无法将组件中的data这些暴露给外部组件(外部组件通过ref获取)。// 子组件 <script> import { h, ref } from 'vue' export default { setup(props, { expose }) { const count = ref(0) const increment = () => ++count.value expose({ increment }) return () => h('div', count.value) } } </script> <!-- 父组件 --> <son ref="son"></son> <script> export default { mounted() { console.log(this.$refs['son'].increment()) } } </script>
执行
setup时,组件实例尚未被创建(在setup()内部,this不会是该活跃实例的引用,即不指向vue实例,Vue为了避免开发者错误的使用,直接将setup函数中的this修改成了undefined)
从setup()中返回的对象上的property返回并可以在模板中被访问时,它将自动展开为内部值。不需要在模板中追加.value
setup函数只能是同步的不能是异步的
<script setup>其实就是 setup 函数的一个语法糖。标签内的代码会被编译成setup()函数里的内容(这样也能使得组件实例在被创建的时候才会执行,而普通的<script>在组件引入时就执行)。
下面举几个例子看看语法糖的简便:
1. 变量和方法
<script setup>
import {ref} from 'vue';
import {minusNum} from 'xxx';
const num = ref(1);
function addNum() {
num.value++;
}
</script>
<script>
import {ref} from 'vue';
import {minusNum} from 'xxx';
setup() {
const num = ref(1);
function addNum() {
num.value++;
}
return {
num,
addNum,
minusNum
}
}
</script>
使用语法糖不需要将变量和方法(包括外部文件中的方法)返回即可使用。
2. 组件注册、自定义指令
<template>
<h1 v-direct>directive</h1>
</template>
<script setup>
import { Component1 } from 'xxx';
import { vDirect } from 'xxx';
</script>
<script>
import { Component1 } from 'xxx';
import { direct } from 'xxx';
export default {
components: {
Component1
},
directives: {
direct
}
}
</script>
使用语法糖组件就不需要在 component 注册,在组件中定义指令虽然也不需要在 directives 中注册,但是需要遵循 vNameOfDirective 这样的命名规范,否则并不会注册成指令。
3. 父子组件通信
<script setup>
import { defineProps, defineEmits } from 'vue'
const emit = defineEmits(['addNum']);
const addNumEmit = () => {
emit('addNum', 1);
}
const props = defineProps({
num: {
type: Number,
default: 0
}
})
console.log(props.num);
</script>
<script>
export default {
props: {
num: {
type: Number,
default: 0
}
},
setup (props, { emit }) {
console.log(props.num)
const addNumEmit = () => {
emit('addNum', 1)
}
return {
addNumEmit
}
}
}
</script>
可以与普通script一起使用
<script setup> 可以和普通的 <script> 一起使用。
<script>
export default {
mounted() {
console.log('mounted');
}
}
</script>
<script setup>
import { onMounted } from 'vue';
onMounted(() => {
console.log('setup mounted')
})
</script>
setup中定义的生命周期钩子比普通script先执行。
生命周期
在setup中通过onXXX来注册生命周期:
选项式 API 的生命周期选项和组合式 API 之间的映射
因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,因此setup内部的没有beforeCreate、created对应的钩子映射。
beforeMount->onBeforeMountmounted->onMountedbeforeUpdate->onBeforeUpdateupdated->onUpdatedbeforeUnmount->onBeforeUnmountunmounted->onUnmountederrorCaptured->onErrorCapturedrenderTracked->onRenderTrackedrenderTriggered->onRenderTriggeredactivated->onActivateddeactivated->onDeactivated
这些函数接受一个回调函数,当钩子被组件调用时将会被执行:
export default {
setup() {
// mounted
onMounted(() => {
console.log('Component is mounted!')
})
}
}
选项式API与组合式API混用
选项式API与组合式API是允许混用的(需要Vue3或vue2.7后的版本)。如下:
<script>
import { reactive } from 'vue';
export default {
data() {
return {
value: 1
}
},
props: {
num: {
type: Number,
default: 0
}
},
setup() {
let name = reactive('leo');
return {
name
}
},
methods: {
getName() {
console.log(this.name);
}
}
}
</script>
在混用的时候需要注意由于setup函数里的this指向undefined,因此不能使用选项式中定义的data、methods这些。
组件
组件注册
全局注册
vue2中是直接使用vue的原型中component进行注册.
import Vue from 'vue';
Vue.component('xxx', xxx);
在vue3中,则是在实例中进行添加
import { createApp } from 'vue'
const app = createApp({})
app.component('xxx', xxx);
局部注册
前面提到了对于<script setup>不需要进行声明,直接引入即可使用,否则跟vue2一样也需要在components中注册。
<template>
<Component1 />
</template>
<script setup>
import { Component1 } from 'xxx';
</script>
<script>
import { Component1 } from 'xxx';
export default {
components: {
Component1
}
}
</script>
响应式API
reactive
reactive用于将数据变成响应式数据。调用reactive后返回的对象是响应式副本而非原始对象。其原理就是将传入的数据包装成一个Proxy对象。
import { reactive, watchEffect } from 'vue';
const data = {
count: 0
};
const obj = reactive(data);
data === obj // false
watchEffect(() => {
// 用于响应性追踪
console.log(obj.count);
});
setTimeout(() => {
obj.count++;
}, 2000);
响应式转换是“深层”的——它影响所有嵌套
property。在基于ES2015 Proxy的实现中,返回的proxy是不等于原始对象的。建议只使用响应式proxy,避免依赖原始对象。
reactive用于复杂数据类型,比如对象和数组等,当传入基础数据类型时,默认情况下修改数据,界面不会自动更新,如果想更新,可以通过重新赋值的方式。
readonly
接受一个对象 (响应式或纯对象) 或 ref 并返回原始对象的只读代理。只读代理是深层的:任何被访问的嵌套 property 也是只读的,不能改变值,否则是报错。
import { reactive, watchEffect, readonly } from 'vue';
const data = {
count: 0
};
const obj = reactive(data);
const copy = readonly(obj);
watchEffect(() => {
// 用于响应性追踪
console.log(obj.count)
});
setTimeout(() => {
obj.count++;
copy.count++;
}, 2000);
ref
接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value值,指向该内部值。跟reactive类似,也是将数据变成响应式。
import { ref, watchEffect } from 'vue';
const count = ref(0);
const obj = ref({
count: 0
})
console.log(obj)
watchEffect(() => {
console.log(count.value);
})
watchEffect(() => {
console.log('obj.value.count: ', obj.value.count);
})
count.value++;
obj.value.count++;
ref和reactive的区别
ref是把值类型添加一层包装,使其变成响应式的引用类型的值。ref(0) --> reactive( { value:0 })reactive则是引用类型的值变成响应式的值。
两者的区别只是在于是否需要添加一层引用包装,对于对象而言,添加一层包装后会被reactive处理为深层的响应式对象,在调用unref后就能看到其实对象是一个Reactive对象。
像上面的例子,使用ref同样可以将对象响应化,不过访问的时候需要调用value.去访问内部属性。所以对于对象而言,最好使用reative去响应化处理。
watch
vue3中组合式api watch 与vue2中选项式的watch完全等效。watch 需要监听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在监听源发生变化时被调用。
与 watchEffect 相比,watch 允许我们:
- 惰性地执行副作用;
- 更具体地说明应触发监听器重新运行的状态;
- 访问被监听状态的先前值和当前值。
监听单一源
源数据可以是一个reactive,也可以直接是一个 ref
语法
watch( name , callback, options ) ;
name: 需要监听的属性或者返回值的getter函数callback: 属性改变后执行的方法,接受两个参数-
newVal: 新值
-
oldVal: 旧值
options: 配置项,可配置如下-
deep:Boolean, 是否深度监听
-
immediate:Boolean,是否立即执行
// 侦听reactive
const state = reactive({ count: 0, value: 1 })
// 只监听对象中的count
watch(
// 使用getter函数保证只监听了state中的count
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 侦听state中所有属性
watch(state, (newVal, oldVal) => {
// ...
})
// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
监听多个源
使用数组来同时侦听多个源数据,当任一源数据发生改变都会触发watch
const state = reactive({ count: 0, value: 1 });
const count = ref(0)
watch([state, count], ([newState, newCount], [oldState, oldCount]) => {
console.log(newState);
console.log(newCount);
})
state.count++;
count.value++;
深度监听
在watch的第三个参数中传入deep: true
const state = reactive({ count: 0, value: { status: false } });
watch(() => state, (newVal, oldVal) => {
console.log('不会触发')
})
watch(() => state, (newVal, oldVal) => {
console.log('触发深度监听')
}, {
deep: true
})
state.value.status = true;
由于getter方法只返回state,对没有对内部的对象进行监听,因此内部对象的属性发生改变不会触发watch。
当没有使用getter方法而是传入state这个reactive数据,则不需要设置deep: true都会进行深度监听。
立即执行
在watch的第三个参数中传入immediate: true,当传入数据就会执行一次。
watchEffect
立即执行传入的一个函数,响应式追踪其依赖,在其依赖变更时重新运行该函数。
它会监听引用数据类型的所有属性,不需要具体到某个属性,一旦运行就会立即监听,组件卸载的时候会停止监听
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0
setTimeout(() => {
count.value++
// -> logs 1
}, 100)
停止监听
当 watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。
在一些情况下,也可以显式调用返回值以停止侦听
const stop = watchEffect(() => {
/* ... */
})
// later
stop()
清除副作用
有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除 (即完成之前状态已改变了) 。所以侦听副作用传入的函数可以接收一个 onInvalidate 函数作入参,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:
- 副作用即将重新执行时
- 侦听器被停止 (如果在
setup()或生命周期钩子函数中使用了watchEffect,则在组件卸载时)
watchEffect(onInvalidate => {
const token = performAsyncOperation(id.value)
onInvalidate(() => {
// id has changed or watcher is stopped.
// invalidate previously pending async operation
token.cancel()
})
})
computed
接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象。
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误
或者,接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象。
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0