Vue3 快速上手

Vue3 快速上手

Apr 05, 2024 ·
24 Min Read

注意:本文可能为了突出重点,有些例子是代码片段,而不是完整的代码。

改变❗

生命周期

Vue2Vue3
beforeCreatesetup
createdsetup
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestoryonBeforeUnmount
destoryedonUnmounted

注意setup 在 vue3 版本代替了 beforeCreatecreated,可以理解为 setup 执行在它们之间。

同时 setup 支持两个参数,分别为 propscontext。注意事项,代码中有注释。而且 setup 内部是没有 this,打印出来是 undefined。因此不能像 Vue 2.x 那样,通过 this.data 这种写法,所以创建了新的API,下文有说到。

setup 支持 return 一个对象,它可供模板直接使用,例如 return { count }<div>{{count}}</div>

// .vue文件代码片段
<script>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from "vue";
export default {
name: "Life",
props: {
title: String,
},
/**
* @description:
* @param {object} props 不能使用ES6解构,否则会破坏响应式。可以使用 toRef() 进行解构
* @param {object} context 非响应式,可使用ES6解构,包含attr、emit、slots
* @return {*}
*/
setup(props, context) {
// 注意:在 setup() 内部,this 不会是该活跃实例的引用,因为 setup() 是在解析其它组件选项之前被调用的,所以 setup() 内部的 this 的行为与其它选项中的 this 完全不同。这在和其它选项式 API 一起使用 setup() 时可能会导致混淆。
onBeforeMount(() => {
console.log(props);
console.log(context);
console.log("传入的title是" + props.title);
console.log("挂载前执行");
});
onMounted(() => {
console.log("挂载后执行");
});
onBeforeUpdate(() => {
console.log("更新前执行");
});
onUpdated(() => {
console.log("更新后执行");
});
onBeforeUnmount(() => {
console.log("卸载前执行");
});
onUnmounted(() => {
console.log("卸载后执行");
});
return {}
}
}
</script>

获取标签元素

在 Vue2 中,我们获取元素都是通过给元素一个 ref 属性,然后通过 this.$refs.xx 来访问的,但这在 Vue3 中已经不再适用了。

Vue 3 版本获取元素标签,有点类似 React 的感觉。

<template>
<div>
<div ref="el">div元素</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
// 创建一个DOM引用,名称必须与元素的ref属性名相同
const el = ref(null)
// 在挂载后才能通过 el 获取到目标元素
onMounted(() => {
el.value.innerHTML = '内容被修改'
})
// 把创建的引用 return 出去
return {el}
}
}
</script>

注意

在 Vue 2.x 版本,可以使用 this.$children 直接访问当前实例的子组件。在 Vue 3版本中移除,建议使用 $refs

Vue 2.x中,在 v-for 中使用 ref 是返回数组形式的 $refs,而在 Vue 3 中不再是创建一个 $refs 数组,而是需要自己 从单个 ref 绑定获取多个 ref

<template>
<div v-for="item for list" :key="item" :ref="setItemsRef"></div>
</template>
<script>
export default {
data() {
return {
itemRefs: []
}
},
methods: {
setItemsRef(el) {
el && (this.itemRefs.push(el))
}
},
beforeUpdate() {
this.itemRefs = []
}
}
</script>

自定义指令

在 vue 3中主要改变的是自定义指令的钩子函数(为了与生命周期统一)。

Vue2Vue3解析
bindbeforeMount指令绑定到元素后发生。只发生一次。
insertedmounted元素插入父 DOM 后发生
/beforeUpdate新的!这是在元素本身更新之前调用的
update移除当元素更新,但子元素尚未更新时,将调用此钩子
componentUpdatedupdated一旦组件和子级被更新,就会调用这个钩子
/beforeUnmount新的!它将在卸载元素之前调用。
unbindunmounted一旦指令被移除,就会调用这个钩子。也只调用一次
<template>
<input v-focus />
</template>
<script>
export default {
directives: {
focus: {
mounted(el) {
el.focus();
}
}
}
}
</script>

Data

data 组件选项声明不再接收纯 object,而需要 function 声明。

(为了根实例与vue模板声明同步)

// Object 声明,Vue3中不适用
const app = new Vue({
data: {
a: 1
}
})
// 而是全部改为 function 声明
const app = new Vue({
data() {
return {
a: 1
}
}
})

注意

在 Vue 3 中 Mixin 合并的时候,data 不再是深层合并,而是浅层合并。

对之前版本项目深层合并影响有点大。

const Mixin = {
data() {
return {
a: {
b: 1,
c: 2
}
}
}
}
const Comp = {
mixins: [Mixin],
data() {
return {
a: {
b: 2
}
}
}
}
// 在vue 2.x中,合并后的data
{
a: {
b: 2,
c: 2
}
}
// 在vue 3中,合并后的data
{
a: {
b: 2
}
}

🆕emits

Vue3 新增一个emits选项,类似组件中的 props 选项,用来定义组件可以向其父对象发出的事件

支持数组形式,也支持一个对象形式,定义传入事件参数的验证器

简单来说就是声明哪些事件可以给父组件,强烈建议使用

<template>
<div>
<p>{{ text }}</p>
<button v-on:click="$emit('accepted')">OK</button>
</div>
</template>
<script>
export default {
props: ['text'],
emits: ['accepted']
}
</script>

注意

❗❗ Vue 3 中已移除 .native 修饰符,一切未在 emits 中声明的事件监听器都算入 $attr,且默认绑定到 组件根节点上。

app.vue
<template>
<my-button @click="handleTap" />
</template>
<script>
export default{
methods: {
handleTap(e) {
// 会输出两次,一次是原生事件监听器监听到,一次是 $emit 触发
console.log(e)
}
}
}
</script>
<!-- MyButton.vue -->
<template>
<div>
<button @click="$emit('click', $event)">我的按钮</button>
</div>
</template>

模板

在 Vue 2.x 中 是不支持多个根节点的,往往需要我们在外层使用 div 包裹。

🆕**在 Vue 3支持多个根节点了,同时需要我们显式定义 $attr 分布 **。

<!-- vue 2.x -->
<template>
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>
<!-- vue 3 支持-->
<template>
<header>...</header>
<main v-bind="$attr">...</main>
<footer>...</footer>
</template>

创建一个实例⁉️

在 Vue 2.x 版本只支持全局导入,进而很多时候一些没用到的模块也进行导入,导致项目体积增大。 而在 Vue 3.x 版本支持按需导入。

3.x
import { createApp } from 'vue'
// 允许分开,也允许链式
createApp(App).mount('#app')
// 2.x
import Vue from 'vue'
new Vue({
el: '#app',
render: h => h(App),
store,
router
})

在Vue 2.x 版本中,当我们要使用全局API 的时候,往往我们是这么做👇

Vue.mixin(...)
Vue.directive(...)
// 但这样如果有两个根实例话,则会互相污染
const app1 = new Vue({ el: '#app-1' })
const app2 = new Vue({ el: '#app-2' })

而在 Vue3 版本中,任何全局改变 Vue 的行为都移动到实例上。

import { createApp } from 'vue'
const app = createApp(App).mount('#app')
app.mixin(...)
app.directive(...)
2.x 全局 API3.x 实例 API (app)
Vue.configapp.config
Vue.config.productionTipremoved
Vue.config.ignoredElementsapp.config.isCustomElement
Vue.componentapp.component
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.useapp.use
Vue.prototypeapp.config.globalProperties

vue3中,如果想要在多个应用中共享配置,可以使用工厂函数来创建实例。

const createApp = options => {
const app = createApp(options)
app.directive(...)
return app
}

Vue.prototype 替换为 config.globalProperties

// 在 vue 2.x 中,往往我们这样使用来达到可以在每个组件访问某个 property
Vue.prototype.$xxx = xxx
// 在 vue 3.x 版本中修改为
const app = createApp({})
app.config.globalProperties.$xxx = xxx

渲染函数

在 2.x 中, render 函数将自动接收 h 函数 (它是 createElement 的常规别名) 作为参数:

// Vue 2 渲染函数示例
export default {
render(h) {
return h('div')
}
}

在 3.x 中,h 现在是全局导入的,而不是作为参数自动传递。

// Vue 3 渲染函数示例
import { h } from 'vue'
export default {
render() {
return h('div')
}
}

挂载方式改变

// in vue 2.x
new Vue({
template: <div>这是一个模板</div>
}).$mount('#app')
// 模板代替了 挂载DOM
// 结果会渲染为
<div>这是一个模板</div>
// in vue 3.x
Vue.createApp({
template: <div>这是一个模板</div>
}).$mount('#app')
// 可以理解为在挂载的 DOM 上使用了 innerHTML
// 结果会渲染为
<div id="app">
<div>这是一个模板</div>
</div>

模板语法🈚

模板语法与2.x一致

附:2.6.0版本新增动态参数

<a v-bind:[attributeName]="url"> ... </a>
// attributeName 会被动态求值,若值为null或一切非字符串数据最终都会报警告
// 建议使用计算属性, 避免使用大写字符来命名键名

新增API⁉️

reactive

返回对象的响应式副本,它是深层的转换。

一般用于引用类型object、array。

(因为proxy转换只会对当前的对象其作用,而嵌套的对象时不会发生响应式的,则需要遍历。因此这个 API也是这个原理,遍历对象中的所有属性) 详细可以查看一下我之前写的proxy实现响应式原理

提醒:Vue 2.x 版本使用的是Object.defineProperty 来实现响应式

import { reactive } from 'vue'
const obj = {
a: 1,
b: {
c: 2,
d: {
e: 3,
}
}
}
const state = reactive(obj)
console.log(state)
console.log(state.b)
console.log(state.d)
// 结果如下
Vue3 快速上手
Vue3 快速上手

readonly

获取一个对象 (响应式或纯对象) 或 ref 并返回原始 proxy 的只读 proxy。它也是深层的,但都只是只读。

<template>
<div>
<div>可读写{{ state.a }}</div>
<div>只读{{ readState.a }}</div>
<!-- 当你点击 修改可读写读 按钮,两个div都会变化,当你点击 修改只读,控制台会报一个警告 Set operation on key "a" failed: target is readonly. -->
<button @click="add">修改可读写读</button>
<button @click="add2">修改只读</button>
</div>
</template>
<script>
import { readonly, reactive } from "vue";
export default {
setup() {
const state = reactive({ a: 1 });
const readState = readonly(state);
function add() {
state.a++;
}
function add2() {
readState.a++;
}
return {
state,
readState,
add,
add2
};
},
};
</script>

isProxy

检查对象是否是由 reactivereadonly 创建的 proxy。

isReactive

检查对象是否是 reactive 创建的响应式 proxy。

注意

如果 proxyreadonly 创建的,并且 readonly 是由普通对象创建的,则返回 false,若 readonly 是由响应式 proxy 创建的,则为 true

const state = reactive({ a: 1 })
const demo1 = readonly({ a: 2 })
console.log(isReactive(state)) // true
console.log(isReactive(demo1)) // false
console.log(isReactive(readonly(state))) // true

isReadonly

检查对象是否是由 readonly 创建的只读 proxy。

toRaw

返回 reactivereadonly 的原始对象。即改为非响应式,谨慎使用

const foo = {}
const reactiveFoo = reactive(foo)
console.log(toRaw(reactiveFoo) === foo)

markRaw

标记一个对象,使其永远不会转换为 proxy。返回对象本身。即退出响应式,深层的。

const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

shallowReactive

创建一个响应式 proxy,跟踪其自身 property 的响应性,但不执行嵌套对象的深度响应式转换 。简单来说就是浅层。就是我一开始说的 reactive 需要遍历,则它就不遍历。

<template>
<div>
<p>响应式a:{{ state2.a }}</p>
<p>非响应式b:{{ state2.b.c }}</p>
<p>
首先点击修改非响应式,点击后视图并没有发生修改,只有点击修改响应式后视图才监听到修改
</p>
<button @click="add">修改响应式</button>
<button @click="add2">修改非响应式</button>
</div>
</template>
<script>
import { reactive, shallowReactive, toRaw } from "vue";
export default {
setup() {
const obj = {
a: 1,
b: {
c: 2,
d: {
e: 3
}
}
}
const state2 = shallowReactive(obj); // shallowReactive 接受一个参数,使之成为响应式对象,但是不执行深度响应式转换,即嵌套的对象不做proxy。其实了解proxy就明白reactive 和 shallowReactive的区别
console.log(state2);
console.log(state2.b);
console.log(state2.b.d);
function add() {
state2.a++;
}
function add2() {
state2.b.c++;
// 当触发这个的时候,值已经改变了,但 shallowReactive 只会把自身属性改为响应式,而嵌套属性是非响应式的。
console.log(state2);
}
return {
state2,
add,
add2,
};
},
};
</script>

打印如下:

Vue3 快速上手
Vue3 快速上手

shallowReadonly

创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换。和上面同理,都是浅层的

ref

接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value简单来说就是用来包装一个值成为响应式

注意

setup 内部使用的时候,需要显示加上 .value。但在模板中不需要。

可以理解为 ref(any) => reactive({value: any})

一般用于基本类型string、number、boolean。

<!--
* @Author: kim
* @Date: 2020-11-30 15:08:04
* @LastEditors: kim
* @LastEditTime: 2020-11-30 16:51:21
* @Description:
-->
<template>
<div>
<p>{{ count }}</p>
<button @click="add">点击</button>
</div>
</template>
<script>
import {
ref,
reactive,
} from "vue";
export default {
setup(props, context) {
// ref 和 reactive 都是用来包装数据为一个响应式
const count = ref(0); // 每次访问的时候需要加上 .value,但在模板中不需要。一般用于基本类型string、number、boolean。
const add = () => {
count.value++;
}
return { count, add };
},
};
</script>

toRef

可以用来为源响应式对象上的 property 新创建一个 ref。然后可以将 ref 传递出去,从而保持对其源 property 的响应式连接。

const state = reactive({
foo: 1,
bar: 2
})
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3

为什么会有这个API呢?

setup 中有提到,props 不能使用 ES6 进行解构,因为会破坏其响应式。不妨碍我们想一下解构的原理,解构是复制出来,即破坏了引用关系。

这个 API 就是用来代替解构,并且保持响应式的。

toRefs

将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的ref。很好理解,toRef 对单属性,toRefs 对整个对象。简单来说就是原本是对象可以响应式的,后来对象不是响应式,但其下所有属性都是响应式,同时返回的对象支持 ES6 解构。

我们回想一下 reactive 打印出来是怎么样的👇

Vue3 快速上手
Vue3 快速上手

通过 toRefs 转换后👇

Vue3 快速上手
Vue3 快速上手
const obj = {
a: 1,
b: {
c: 2,
d: {
e: 3,
},
},
}
const state = reactive(obj)
const state2 = toRefs(state)
console.log(state2);

shallowRef

shallowReactive 是监听对象第一层的数据变化用于驱动视图更新,那么 shallowRef 则是监听 .value 的值的变化来更新视图的

<template>
<p>{{ state.a }}</p>
<p>{{ state.first.b }}</p>
<p>{{ state.first.second.c }}</p>
<!-- 点击改变2,你会发生先数据变了,但视图没有改变,只有点击改变1,视图才发生变化 -->
<button @click="change1">改变1</button>
<button @click="change2">改变2</button>
</template>
<script>
import {shallowRef} from 'vue'
export default {
setup() {
const obj = {
a: 1,
first: {
b: 2,
second: {
c: 3
}
}
}
const state = shallowRef(obj)
console.log(state);
function change1() {
// 直接将state.value重新赋值
state.value = {
a: 7,
first: {
b: 8,
second: {
c: 9
}
}
}
}
function change2() {
state.value.first.b = 8
state.value.first.second.c = 9
console.log(state);
}
return {state, change1, change2}
}
}
</script>

triggerRef

手动执行与 shallowRef关联的任何效果。简单来说就是手动刷新。

如上面那个例子中,我们在change2 加入一句代码 triggerRef() 视图也会发生改变。

provide&inject

与 Vue 2.x 中的 provideinject 作用相同。只不过 Vue 3 支持了组合式API,所以有了新的写法。

provide&inject 支持在组件中传递数据。众所周知父组件传递到子组件是使用 props,但如果嵌套这很深的组件,我们一层一层传递下去则非常烦人。

因此provide&inject 允许我们在父组件提供数据,所有子组件都可以获取到这个数据,不管层次有多深,且无需一层层传递下去。

但不要过度使用,否则会很难追踪到数据来源。当数据量多的时候建议使用 vuex 代替

<!-- 在vue 2.x中 -->
<!-- 在父组件或祖先组件中注入 -->
<script>
export default {
provide: {
location: 'North Pole',
geolocation: {
longitude: 90,
latitude: 135
}
}
}
</script>
<!-- 在子组件或子孙组件中使用 -->
<script>
export default {
inject: ['location', 'geolocation']
}
</script>

Vue3 中允许我们显示导入 provide方法和 inject 方法。

Father.vue
<!-- 比如现在有三个组件 Father.vue、Son.vue、Grandson.vue -->
<template>
<div>
<h1>我是Father</h1>
<Son/>
</div>
</template>
<script>
import { provide } from "vue";
import Son from "./Son.vue";
export default {
setup() {
provide("title", "hello world");
return {}
},
components: {
Son
}
};
</script>
<!-- Son.vue -->
<template>
<div>
<h2>我是Son</h2>
<Grandson />
</div>
</template>
<script>
import Grandson from "./Grandson.vue";
export default {
components: {
Grandson
}
};
</script>
<!-- Grandson.vue -->
<template>
<div>
<h2>我是Grandson</h2>
<div>我是来自祖父的值---{{ title }}</div>
</div>
</template>
<script>
import { inject } from "vue";
export default {
setup() {
const title = inject("title", "这是默认值");
return { title };
},
};
</script>

效果如下👇

Vue3 快速上手
Vue3 快速上手

watch&watchEffect

watchwatchEffect 都是用来监视某项数据变化从而执行指定的操作的,但用法上还是有所区别。

watchwatch( source, cb, [options] )

参数说明

Tip

watch API 与 this.$watchwatch 选项是同一样东西,只是新语法而已。

默认 watch 方法默认是浅层的监听我们指定的数据,即如果监听的数据是嵌套多层,则深层的数据改变并不会触发监听回调,如果想要监听到,则需要传入第三个参数 deep

  • watchEffect 比较,watch 允许我们:
    • 惰性地执行副作用;
    • 更具体地说明应触发侦听器重新运行的状态;
    • 访问侦听状态的先前值和当前值。

watch方法会返回一个stop方法,若想要停止监听,便可直接执行该stop函数

// 当监听 ref 类型时
setup() {
const state = ref(0)
watch(state, (newValue, oldValue) => {
console.log(`原值为${oldValue}`)
console.log(`新值为${newValue}`)
})
}
// 监听 reactive 类型时
setup() {
const state = reactive({cout: 0})
watch(() => state.count, (newValue, oldValue) => {
console.log(`原值为${oldValue}`)
console.log(`新值为${newValue}`)
})
}
// 监听多个值的时候
setup() {
const state = reactive({cout: 0, a: 2})
watch([() => state.count, () => state.a], ([newCount, newA], [oldCount, oldA]) => {
console.log(oldCount)
console.log(newCount)
console.log(oldA)
console.log(newA)
})
}

watchEffect,它与 watch 的区别主要有以下几点:

  1. 不需要手动传入依赖
  2. 每次初始化时会执行一次回调函数来自动获取依赖
  3. 无法获取到原值,只能得到变化后的值

简单来说它会自动搜索监听回调中的依赖项,只要依赖项发生改变则会自动触发

<script>
import {reactive, watchEffect} from 'vue'
export default {
setup() {
const state = reactive({ count: 0 })
watchEffect(() => {
// 比如在这里它会自动监听state.count
console.log(state.count)
})
setTimeout(() => {
state.count ++
}, 1000)
}
}
</script>

getCurrentInstance

因为 setup 函数中 this 的指向是 undefined,但我们在 Vue 2.x中经常使用的 this ,代表当前组件实例没有了吗?

getCurrentInstance 就是返回当前组件实例的。但不建议使用。

const instance = getCurrentInstance()
console.log(instance)

BUG

当你在测试环境下,console.log 打印 const { ctx } = getCurrentInstance(),大概是长这样的👇

Vue3 快速上手
Vue3 快速上手

然而当你打包后,在正式环境下打印,却长这样👇

Vue3 快速上手
Vue3 快速上手

然后接下来就是把 ctx 当成 this 使用的代码一顿报错。

临时解决方案

使用 const { proxy } = getCurrentInstance() 来代替 ctx ,但还是建议不要使用 getCurrentInstance

useStore

在 Vue2 中使用 Vuex,有时候通过 this.$store 来与获取到 Vuex 实例,但上面也说了原本 Vue2 中的 this 的获取方式不一样了,并且在 Vue3 的 getCurrentInstance().ctx 中也没有发现 $store 这个属性,那么如何获取到 Vuex 实例呢?这就要通过 vuex 中的一个方法了,即 useStore

// store 文件夹下的 index.js
import Vuex from 'vuex'
const store = Vuex.createStore({
state: {
name: '至安'
},
mutations: {
……
},
……
})
// example.vue
<script>
// 从 vuex 中导入 useStore 方法
import {useStore} from 'vuex'
export default {
setup() {
// 获取 vuex 实例
const store = useStore()
console.log(store)
}
}
</script>
Last edited Feb 15