Vue3基本概述
内容
2020年9月18日,Vue发布了3.0版本,代号:One Piece(海贼王),周边生态原因,当时大多数开发者还处于观望状态。
现在主流组件库都已经发布了支持Vue3.0的版本,列如 Element Plus、 Vant、 Vue Use,其他生态也不断地完善中,所以Vue3是趋势。
2022你哦按2月7日开始,Vue3也将成为新的默认版本。
优点
Composition Api , 能够更好的组织 、 封装 、 复用代码 、 RFCs。
性能:打包大小减少 41%、初次渲染快 55%、更新渲染快 133%、内存减少 54%,主要原因在于Proxy,VNode,Tree Shaking support。
Better TS support ,源码。
新特性: F绕过,emt 、 Teleport 、 Suspense。
趋势:未来肯定会有越来越对多的企业用Vue3.0 + TS 进行大型项目的开发。
对于个人来说:适应市场蓄妾,学习流行的技术提升竞争力,加薪!
Vite项目创建
Vite基本使用
内容
是下一代前端开发与构建工具,热更新、打包构建速度更快,但目前周边生态还不如Webpack成熟,所以实际开发中还是建议使用Webpack。
但目前就学习Vue3 语法来说,我们可以使用更轻量级的Vite。
如何构建
# yarn create vite-app + 项目名称
npm init vite-app + 项目名称
cd 项目名称
npm install
npm run dev
- Webpack:会将所有模块提前编译、打包,不管这个模块是否被用到,随着项目越来越大,打包启动速度自然越来越慢。
- Vite:瞬间开启一个服务,并不会先编译所有文件,当浏览器用到某个文件时,Vite 服务会收到请求然后编译后响应到客户端。
// 1. 导入 createApp 函数,不再是曾经的 Vue 了
// 2. 编写一个根组件 App.vue,导入进来
// 3. 基于根组件创建应用实例,类似 Vue2 的 vm,但比 vm 更轻量
// 4. 挂载到 index.html 的 #app 容器
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
// Vue2: new Vue()、new VueRouter()、new Vuex.Store()
// Vue3: createApp()、createRouter()、createStore()
<template>
<div class="container">我是根组件</div>
</template>
<script>
export default {
name: 'App'
}
</script>
学习组合API
选项/组合API\
Vue2
优点;已于学习和使用,写代码的位置约定好。
缺点:数据和业务逻辑分散在同一个文件的N个地方,随着业务复杂度的上升,可能会出现动图左侧的代码组织方式,不利于管理和维护.
<template>
<div class="container">
<p>X 轴:{{ x }} Y 轴:{{ y }}</p>
<hr />
<div>
<p>{{ count }}</p>
<button @click="add()">自增</button>
</div>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
// !#Fn1
x: 0,
y: 0,
// ?#Fn2
count: 0
}
},
mounted() {
// !#Fn1
document.addEventListener('mousemove', this.move)
},
methods: {
// !#Fn1
move(e) {
this.x = e.pageX
this.y = e.pageY
},
// ?#Fn2
add() {
this.count++
}
},
destroyed() {
// !#Fn1
document.removeEventListener('mousemove', this.move)
}
}
</script>
优点:可以把同一功能的 ***数据 *** 和 ***业务逻辑 *** 组织到一起,方便复用和维护。
缺点:需要良好的代码组织和拆分能力,相对没有Vue2 容易上手。
注意:为了能让大家较好的过滤到Vue3.0版本,目前也是支持Vue2.x选项API的写法。
链接: why-composition-api、composition-api-doc。****
<template>
<div class="container">
<p>X 轴:{{ x }} Y 轴:{{ y }}</p>
<hr />
<div>
<p>{{ count }}</p>
<button @click="add()">自增</button>
</div>
</div>
</template>
<script>
import { onMounted, onUnmounted, reactive, ref, toRefs } from 'vue'
export default {
name: 'App',
setup() {
// !#Fn1
const mouse = reactive({
x: 0,
y: 0
})
const move = (e) => {
mouse.x = e.pageX
mouse.y = e.pageY
}
onMounted(() => {
document.addEventListener('mousemove', move)
})
onUnmounted(() => {
document.removeEventListener('mousemove', move)
})
// ?Fn2
const count = ref(0)
const add = () => {
count.value++
}
// 统一返回数据供模板使用
return {
...toRefs(mouse),
count,
add
}
}
}
</script>
steup入口函数
内容:
setup是Vue3中新增的巨剑配置项,作为组合API的入口函数。
执行时机:实例创建前调用,升值遭遇Vue2中的 beforeCreate 。
注意点:由于执行setup的时候是咧还没有created,所以在setup中是不能直接使用的data和methods中的数据的,所以Vue3干脆把setup中的this绑定了undefined,防止乱用!
虽然Vue2中的data和methods配置虽然在Vue3中也能使用,但不建议了,建议数据和方法都卸载setup函数中,并通过return惊醒返回可在模板中直接使用(一般情况下setup不能为异步函数)。
<template>
<h1 @click="say()">{{ msg }}</h1>
</template>
<script>
export default {
setup() {
const msg = 'Hello Vue3'
const say = () => {
console.log(msg)
}
return { msg, say }
}
}
</script>
setup 也可以返回一个渲染函数(问:setup 中 return 的一定只能是一个对象吗?)
<script>
import { h } from 'vue'
export default {
name: 'App',
setup() {
return () => h('h2', 'Hello Vue3')
}
}
new Vue({render: h => h(App)})
</script>
reactive包装数组
内容:
reactive是一个函数,用来将普通对象/数组包装成响应式数据使用。
实列
<template>
<ul>
<li v-for="(item, index) in arr" :key="item" @click="removeItem(index)">
{{ item }}
</li>
</ul>
</template>
<script>
export default {
name: 'App',
setup() {
const arr = ['a', 'b', 'c']
const removeItem = (index) => {
arr.splice(index, 1)
}
return {
arr,
removeItem
}
}
}
</script>
数据缺失是删除了,但是试图没更新,不是响应式的!
所以可以用reactive包装数组
<template>
<ul>
<li v-for="(item, index) in arr" :key="item" @click="removeItem(index)">
{{ item }}
</li>
</ul>
</template>
<script>
import { reactive } from 'vue'
export default {
name: 'App',
setup() {
const arr = reactive(['a', 'b', 'c'])
const removeItem = (index) => {
arr.splice(index, 1)
}
return {
arr,
removeItem
}
}
}
</script>
ref
基本使用
ref 函数,可以把简单数据类型包裹为响应式数据(复杂类型也可以),注意 JS 中操作值的时候,需要加 .value 属性,模板中正常使用即可。
<template>
<div class="container">
<div>{{ name }}</div>
<button @click="updateName">修改数据</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'App',
setup() {
const name = ref('ifer')
const updateName = () => {
name.value = 'xxx'
}
return { name, updateName }
}
}
</script>
包装复杂数据类型
注意:ref 其实也可以包裹复杂数据类型为响应式数据,一般对于数据类型未确定的情况下推荐使用 ref,例如后端返回的数据。
<template>
<div class="container">
<div>{{ data?.name }}</div>
<button @click="updateName">修改数据</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'App',
setup() {
// 初始值是 null
const data = ref(null)
setTimeout(() => {
// 右边的对象可能是后端返回的
data.value = {
name: 'ifer'
}
}, 1000)
const updateName = () => {
data.value.name = 'xxx'
}
return { data, updateName }
}
}
</script>
toRef
内容
toRef 函数的作用:转换响应式对象中某个属性为单独响应式数据,并且转换后的值和之前是关联的(ref 函数也可以转换,但值非关联)。
<template>
<div class="container">
<h2>{{ name }}</h2>
<button @click="updateName">修改数据</button>
</div>
</template>
<script>
import { reactive, toRef } from 'vue'
export default {
name: 'App',
setup() {
const obj = reactive({
name: 'ifer',
age: 10
})
const name = toRef(obj, 'name')
const updateName = () => {
// 注意:需要使用 name.value 进行修改
name.value = 'xxx'
// 对 obj.name 的修改也会影响视图的变化,即值是关联的
// obj.name = 'xxx' // ok
}
return { name, updateName }
}
}
</script>
toRefs
内容
toRefs 函数的作用:转换响应式对象中所有属性为单独响应式数据,并且转换后的值和之前是关联的。
<template>
<div class="container">
<h2>{{ name }} {{ age }}</h2>
<button @click="updateName">修改数据</button>
</div>
</template>
<script>
import { reactive, toRefs } from 'vue'
export default {
name: 'App',
setup() {
const obj = reactive({
name: 'ifer',
age: 10
})
const updateName = () => {
obj.name = 'xxx'
obj.age = 18
}
return { ...toRefs(obj), updateName }
}
}
</script>
computed
基本
作用:computed 函数用来定义计算属性。
<template>
<p>firstName: {{ person.firstName }}</p>
<p>lastName: {{ person.lastName }}</p>
<input type="text" v-model="person.fullName" />
</template>
<script>
import { computed, reactive } from 'vue'
export default {
name: 'App',
setup() {
const person = reactive({
firstName: '朱',
lastName: '逸之'
})
// 也可以传入对象,目前和上面等价
person.fullName = computed({
get() {
return person.firstName + ' ' + person.lastName
},
set(value) {
const newArr = value.split(' ')
person.firstName = newArr[0]
person.lastName = newArr[1]
}
})
return {
person
}
}
}
</script>
watch
监听 reactive 内部数据
注意 1:监听 reactive 内部数据时,强制开启了深度监听,且配置无效;监听对象的时候 newValue 和 oldValue 是全等的。
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj.hobby.eat = '面条'">click</button>
</template>
<script>
import { watch, reactive } from 'vue'
export default {
name: 'App',
setup() {
const obj = reactive({
name: 'ifer',
hobby: {
eat: '西瓜'
}
})
watch(obj, (newValue, oldValue) => {
// 注意1:监听对象的时候,新旧值是相等的
// 注意2:强制开启深度监听,配置无效
console.log(newValue === oldValue) // true
})
return { obj }
}
}
</script>
注意 2:reactive 的【内部对象】也是一个 reactive 类型的数据。
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj.hobby.eat = '面条'">click</button>
</template>
<script>
import { watch, reactive, isReactive } from 'vue'
export default {
name: 'App',
setup() {
const obj = reactive({
name: 'ifer',
hobby: {
eat: '西瓜',
},
})
// reactive 的【内部对象】也是一个 reactive 类型的数据
// console.log(isReactive(obj.hobby))
watch(obj.hobby, (newValue, oldValue) => {
console.log(newValue === oldValue) // true
})
return { obj }
},
}
</script>
注意 3:对 reactive 自身的修改则不会触发监听。
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj.hobby = { eat: '面条' }">click</button>
</template>
<script>
import { watch, reactive } from 'vue'
export default {
name: 'App',
setup() {
const obj = reactive({
name: 'ifer',
hobby: {
eat: '西瓜',
},
})
watch(obj.hobby, (newValue, oldValue) => {
// obj.hobby = { eat: '面条' }
console.log('对 reactive 自身的修改不会触发监听')
})
return { obj }
},
}
</script>
监听 ref 数据
监听一个 ref 数据
📝 监听 age 的变化,做一些操作。
<template>
<p>{{ age }}</p>
<button @click="age++">click</button>
</template>
<script>
import { watch, ref } from 'vue'
export default {
name: 'App',
setup() {
const age = ref(18)
// 监听 ref 数据 age,会触发后面的回调,不需要 .value
watch(age, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
return { age }
}
}
</script>
监听多个 ref 数据
📝 可以通过数组的形式,同时监听 age 和 num 的变化。
<template>
<p>age: {{ age }} num: {{ num }}</p>
<button @click="handleClick">click</button>
</template>
<script>
import { watch, ref } from 'vue'
export default {
name: 'App',
setup() {
const age = ref(18)
const num = ref(0)
const handleClick = () => {
age.value++
num.value++
}
// 数组里面是 ref 数据
watch([age, num], (newValue, oldValue) => {
console.log(newValue, oldValue)
})
return { age, num, handleClick }
}
}
</script>
立即触发监听
<template>
<p>{{ age }}</p>
<button @click="handleClick">click</button>
</template>
<script>
import { watch, ref } from 'vue'
export default {
name: 'App',
setup() {
const age = ref(18)
const handleClick = () => {
age.value++
}
watch(
age,
(newValue, oldValue) => {
console.log(newValue, oldValue) // 18 undefined
},
{
immediate: true
}
)
return { age, handleClick }
}
}
</script>
开启深度监听 ref 数据
解决 1:当然直接修改整个对象的话肯定是会被监听到的(注意模板中对 obj 的修改,相当于修改的是 obj.value)。
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj = { hobby: { eat: '面条' } }">修改 obj</button>
</template>
<script>
import { watch, ref } from 'vue'
export default {
name: 'App',
setup() {
const obj = ref({
hobby: {
eat: '西瓜'
}
})
watch(obj, (newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
})
return { obj }
}
}
</script>
解决 2:开启深度监听 ref 数据。
watch(
obj,
(newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
},
{
deep: true
}
)
- 解决 3:还可以通过监听 ref.value 来实现同样的效果。
🧐 因为 ref 内部如果包裹对象的话,其实还是借助 reactive 实现的,可以通过 isReactive 方法来证明。
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj.hobby.eat = '面条'">修改 obj</button>
</template>
<script>
import { watch, ref } from 'vue'
export default {
name: 'App',
setup() {
const obj = ref({
hobby: {
eat: '西瓜'
}
})
watch(obj.value, (newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
})
return { obj }
}
}
</script>
监听普通数据
监听响应式对象中的某一个普通属性值,要通过函数返回的方式进行(如果返回的是对象/响应式对象,修改内部的数据需要开启深度监听)。
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj.hobby.eat = '面条'">修改 obj</button>
</template>
<script>
import { watch, reactive } from 'vue'
export default {
name: 'App',
setup() {
const obj = reactive({
hobby: {
eat: '西瓜',
},
})
// 把 obj.hobby 作为普通值去进行监听,只能监听到 obj.hobby 自身的变化
/* watch(
() => obj.hobby,
(newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
}
) */
// 如果开启了深度监听,则能监听到 obj.hobby 和内部数据的所有变化
/* watch(
() => obj.hobby,
(newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
},
{
deep: true,
}
) */
// 能监听影响到 obj.hobby.eat 变化的操作,例如 obj.hobby = { eat: '面条' } 或 obj.hobby.eat = '面条',如果是 reactive 直接对 obj 的修改则不会被监听到(ref 可以)
watch(
() => obj.hobby.eat,
(newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
}
)
return { obj }
},
}
</script>
Vue3 生命周期
内容
- 组合 API生命周期写法,其实 选项 API 的写法在 Vue3 中也是支持。
- Vue3(组合 API)常用的生命周期钩子有 7 个,可以多次使用同一个钩子,执行顺序和书写顺序相同。
- setup、onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount、onUnmounted。
<template>
<hello-world v-if="state.bBar" />
<button @click="state.bBar = !state.bBar">destroy cmp</button>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
import { reactive } from 'vue'
export default {
name: 'App',
components: {
HelloWorld
},
setup() {
const state = reactive({
bBar: true
})
return {
state
}
}
}
</script>
<template>
<p>{{ state.msg }}</p>
<button @click="state.msg = 'xxx'">update msg</button>
</template>
<script>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
reactive
} from 'vue'
export default {
name: 'HelloWorld',
setup() {
const state = reactive({
msg: 'Hello World'
})
onBeforeMount(() => {
console.log('onBeforeMount')
})
onMounted(() => {
console.log('onMounted')
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate')
})
onUpdated(() => {
console.log('onUpdated')
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount')
})
onUnmounted(() => {
console.log('onUnmounted')
})
return {
state
}
}
}
</script>
setup 函数参数
父传子
<template>
<h1>父组件</h1>
<p>{{ money }}</p>
<hr />
<!-- 1. 父组件通过自定义属性提供数据 -->
<Son :money="money" />
</template>
<script>
import { ref } from 'vue'
import Son from './Son.vue'
export default {
name: 'App',
components: {
Son
},
setup() {
const money = ref(100)
return { money }
}
}
</script>
<template>
<h1>子组件</h1>
<p>{{ money }}</p>
</template>
<script>
export default {
name: 'Son',
// 2. 子组件通过 props 进行接收,在模板中就可以使用啦
props: {
money: {
type: Number,
default: 0
}
},
setup(props) {
// 3. setup 中也可以通过形参 props 来获取传递的数据
console.log(props.money)
}
}
</script>
子传父
<template>
<h1>父组件</h1>
<p>{{ money }}</p>
<hr />
<Son :money="money" @change-money="updateMoney" />
</template>
<script>
import { ref } from 'vue'
import Son from './Son.vue'
export default {
name: 'App',
components: {
Son
},
setup() {
const money = ref(100)
// #1 父组件准备修改数据的方法并提供给子组件
const updateMoney = (newMoney) => {
money.value -= newMoney
}
return { money, updateMoney }
}
}
</script>
<template>
<h1>子组件</h1>
<p>{{ money }}</p>
<button @click="changeMoney(1)">花 1 元</button>
</template>
<script>
export default {
name: 'Son',
props: {
money: {
type: Number,
default: 0
}
},
emits: ['change-money'],
setup(props, { emit }) {
// attrs 捡漏、slots 插槽
const changeMoney = (m) => {
// #2 子组件通过 emit 进行触发
emit('change-money', m)
}
return { changeMoney }
}
}
</script>
provide/inject
📝 把 App.vue 中的数据传递给孙组件,Child.vue。
<template>
<div class="container">
<h2>App {{ money }}</h2>
<button @click="money = 1000">发钱</button>
<hr />
<Parent />
</div>
</template>
<script>
import { provide, ref } from 'vue'
import Parent from './Parent.vue'
export default {
name: 'App',
components: {
Parent
},
setup() {
// 提供数据
const money = ref(100)
provide('money', money)
// 提供修改数据的方法
const changeMoney = (m) => (money.value -= m)
provide('changeMoney', changeMoney)
return { money }
}
}
</script>
<template>
<div>
Parent
<hr />
<Child />
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: {
Child
}
}
</script>
<template>
<div>
Parent
<hr />
<Child />
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: {
Child
}
}
</script>
Vue3 其他变更
v-model
在 Vue2 中 v-mode 指令语法糖简写的代码。
<Son :value="msg" @input="msg=$event" />
在 Vue3 中 v-model 语法糖有所调整。
<Son :modelValue="msg" @update:modelValue="msg=$event" />
<template>
<h2>count: {{ count }}</h2>
<hr />
<Son :modelValue="count" @update:modelValue="count = $event" />
<!-- <Son v-model="count" /> -->
</template>
<script>
import { ref } from 'vue'
import Son from './Son.vue'
export default {
name: 'App',
components: {
Son
},
setup() {
const count = ref(10)
return { count }
}
}
</script>
<template>
<h2>子组件 {{ modelValue }}</h2>
<button @click="$emit('update:modelValue', 100)">改变 count</button>
</template>
<script>
export default {
name: 'Son',
props: {
modelValue: {
type: Number,
default: 0
}
}
}
</script>
ref 属性
内容
获取单个 DOM。
<template>
<!-- #3 -->
<div ref="dom">我是box</div>
</template>
<script>
import { onMounted, ref } from 'vue'
export default {
name: 'App',
setup() {
// #1
const dom = ref(null)
onMounted(() => {
// #4
console.log(dom.value)
})
// #2
return { dom }
}
}
</script>
获取组件实例。
<template>
<!-- #4 -->
<button @click="changeName">修改子组件的 Name</button>
<hr />
<!-- #3 -->
<Test ref="test" />
</template>
<script>
import { ref } from 'vue'
import Test from './Test.vue'
export default {
name: 'App',
components: {
Test
},
setup() {
// #1
const test = ref(null)
const changeName = () => {
test.value.changeName('elser')
}
// #2
return { test, changeName }
}
}
</script>
<template>
<div>
<p>{{ o.name }}</p>
</div>
</template>
<script>
import { reactive } from 'vue'
export default {
setup() {
const o = reactive({ name: 'ifer' })
const changeName = (name) => {
o.name = name
}
return {
o,
changeName
}
}
}
</script>
Teleport
作用
传送,能将特定的 HTML 结构(一般是嵌套很深的)移动到指定的位置,解决 HTML 结构嵌套过深造成的样式影响或不好控制的问题。
<template>
<div class="child">
<teleport to="body">
<dialog v-if="bBar" />
</teleport>
<button @click="handleDialog">显示弹框</button>
</div>
</template>
<script>
import { ref } from 'vue'
import Dialog from './Dialog.vue'
export default {
name: 'Child',
components: {
Dialog
},
setup() {
const bBar = ref(false)
const handleDialog = () => {
bBar.value = !bBar.value
}
return {
bBar,
handleDialog
}
}
}
</script>