Vue 3.x新特性总结

520 阅读4分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

开始

两种方式

vue-cli

npm i @vue/cli@next -g

Vite

# npm 6.x
npm init vite@latest my-vue-app --template vue

# npm 7+, 需要额外的双横线:
npm init vite@latest my-vue-app -- --template vue

新特性

Composition API

reactive

对于引用类型的对象,可以使用reactive来实现响应式

<script>
import { computed, reactive } from 'vue'
export default {
    setup() {
        const data = reactive({
            counter: 1,
            doubleCounter: computed(() => data.counter * 2)
        })
        return {data} // 任何在模板中使用的属性或方法,都需要return
    }
}
</script>
<template>
  <div>
    <p>reactive</p>
    <p>{{data.counter}}</p>
    <p>{{data.doubleCounter}}</p>
  </div>
</template>

reactive中的对象不可解构,会失去响应式的能力.如果需要解构要使用toRefs,使得对象中的字段变为响应式

<script>
import { computed, onMounted, onUnmounted, reactive, ref, toRefs } from 'vue'
export default {
    setup() {
        const data = reactive({
            counter: 1,
            doubleCounter: computed(() => data.counter * 2)
        })
        let timer
        onMounted(() => {
            timer = setInterval(() => {
                data.counter++
            }, 1000);
        })
        onUnmounted(() => {
            clearInterval(timer)

        })
        const {counter, doubleCounter} = toRefs(data)
        return {counter, doubleCounter}
    }
}
</script>

<template>
  <div>
    <p>reactive</p>
    <p>{{counter}}</p>
    <p>{{doubleCounter}}</p>
  </div>
</template>

ref

对于值类型可以使用ref,如果要修改这个ref的值,要通过.value修改,那么为什么模板直接通过变量名就可以显示呢?这个是因为Vue帮助我们把ref类型的值显示出来了

import { ref } from 'vue'
export default {
    setup() {
        const number = ref(1)
        const increase = () => {
            number.value++
        }
        return {
            number,
            increase
        }
    }
}
</script>

<template>
  <p>{{ number }}</p>
  <p @click="increase">plus</p>
</template>

setup

我们可以看到,我们上面大量用到了setup,那么setup是什么?setup是一个组合式API,组合式API是帮助我们将之前data,method这些分散在各个位置的逻辑可以统一在一起,随着复杂性越来越大,我们更倾向于这种将逻辑统一去管理的写法形式

setup接受两个参数

  • props 传入的属性
  • context 提供了三个常用属性attrs, slot, emit
// 一段伪代码
setup(props, context) {
    // goodsList
    data...
    methods...
    
    // user
    data...
    methods...
    
    // notice
    data...
    methods...

}

<script setup>

Vue3.2中,还增加了组合式API的语法糖,通过在script标签写setup标识,方便我们更好的去书写setup,不需要return,只要声明了变量和方法,就可以直接使用,甚至引入组件也不需要注册,简直不能太方便

<script setup lang="ts">
import { ref } from 'vue'
import HelloWorld from './components/HelloWorld.vue'

const number = ref(1)

const increase = () => {
  number.value++
}
</script>

<template>
  <p>{{ number }}</p>
  <p @click="increase">plus</p>
  <HelloWorld msg="Hello Vue 3 + TypeScript + Vite" />
</template>

computed

通过引入computed来实现

<script setup lang="ts">
import { computed, ref } from 'vue'
import HelloWorld from './components/HelloWorld.vue'

const number = ref(1)

const increase = () => {
  number.value++
}
const double = computed(() => number.value * 2)
</script>

<template>
  <p>{{ number }}</p>
  <p>{{ double }}</p>
  <p @click="increase">plus</p>
  <HelloWorld msg="Hello Vue 3 + TypeScript + Vite1" />
</template>

<style></style>

经过上面的例子,我们发现,Vue3.0的响应式和以前写法上有些许不同.Vue2.0是通过Object.defineProperty实现响应式的,Vue3.0则是通过Proxy.那么新的方案解决了什么问题? 在Vue2.0中,如果我们声明后的对象添加属性,则不会响应式,通过索引添加的数组也不会有响应式,而Proxy就是解决了这个问题

watch

依旧是引入一个方法watch,监听响应式数据变化

<script setup lang="ts">
import { computed, ref, watch } from 'vue'

const number = ref(1)

const increase = () => {
  number.value++
}

watch(number, (newVal, oldVal) => {
  document.title = number.value.toString()
})
</script>

watch的第一个参数也可以是一个数组,监听多个响应式数据,而newValoldVal也将以数组形式返回.这里面的第一个参数是一个值类型,如果我们想监听一个通过reactive的对象中某一个值,则需要一个函数

<script setup lang="ts">
import { ref, watch, reactive } from 'vue'

const number = ref(1)

const increase = () => {
  number.value++
}
const double = computed(() => number.value * 2)

const data = reactive({
  name: 'Kun',
  score: 1,
  increase: () => {
    data.score++
  }
})

watch([number, () => data.score], () => { // 通过回调函数监听
  document.title = number.value.toString() + data.score.toString()
})
</script>

或者通过toRef将某个属性转化为响应式

watch([number, toRef(data, 'score')], () => {
  document.title = number.value.toString() + data.score.toString()
})

父子组件传值

父 -> 子

// 父组件

<template>
  <Children name="Kun" />
</template>

<script setup lang="ts">
import Children from './components/Children.vue'
</script>

// 子组件
<template>
  <h1>{{ props.name }}</h1>
  <h1>{{ name }}</h1>
</template>

<script setup lang="ts">
const props = defineProps({
  name: {
    type: String,
    default: ''
  }
})
</script>

TIPS: defineProps默认在setup模式下引入,但是又由于eslint的提示,我们需要配置一下.eslintrc.js

// .eslintrc.js

...
globals: {
    defineProps: true
}
...

子 -> 父

// 子组件 Increase.vue

<template>
  <h1>count: {{ props.account }}</h1>
  <button @click="changeAccount">click +1</button>
</template>

<script setup lang="ts">
const props = defineProps({
  account: {
    type: Number,
    default: 0
  }
})

const emit = defineEmits(['updateAccount']) // 同理,自动引入,可配置`eslint globals`

const changeAccount = () => {
  emit('updateAccount')
}
</script>
// 父组件

<script setup>
import Increase from './components/Increase.vue'

const account = ref(0)
const updateAccount = () => {
  account.value++
}
</script>

<template>
    <Increase :account="account" @updateAccount="updateAccount" />
</template>

结构

Vue3.0template不需要强制要求有一个根节点

<template>
  <h1>{{ count }}</h1>
  <h1>{{ count }}</h1>
</template>

v-model

自定义组件的v-modelVue3.0中可这样定义:

// 父组件

<script setup>
import Model from './components/Model.vue'

const num = ref(0)
</script>

<template>
  <Model v-model="num" />
</template>
// 子组件

<script setup lang="ts">
const props = defineProps({
  modelValue: Number
})

const emits = defineEmits(['update:modelValue'])

const changeValue = () => {
  emits('update:modelValue', props.modelValue + 1)
}
</script>

<template>
  <h1>{{ modelValue }}</h1>
  <button @click="changeValue">click +1</button>
</template>

可以看到子组件propsmodelValue,modelValue是一个默认值,它相当于:

<Model v-model:modelValue="num" />

也就是说如果省略了,那么默认的props就是modelValue

emitprops是相对应的,正因为有这样的对应关系,我们可以有多个v-model

// 父组件

<script setup>
import Model from './components/Model.vue'

const num = ref(0)
const name = ref('')
</script>

<template>
  <Model v-model="num" v-model:name="name" />
</template>
// 子组件

<script setup lang="ts">
const props = defineProps({
  modelValue: Number,
  name: String
})

const emits = defineEmits(['update:modelValue', 'update:name'])

const changeValue = () => {
  emits('update:modelValue', props.modelValue + 1)
}

const changeName = () => {
  emits('update:name', `${props.name}!`)
}
</script>

<template>
  <h1>{{ modelValue }}</h1>
  <h1>{{ name }}</h1>
  <button @click="changeValue">click +1</button>
  <button @click="changeName">change name</button>
</template>

CSS v-bind

<script setup lang="ts">
const color = ref('red')
</script>

<style scoped>
h1 {
  color: v-bind('color');
}
</style>

生命周期

生命周期的区别

2.03.0
beforeCreatesetup
createdsetup
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestroyonBeforeUnmount
destroyedonUnmounted
activatedonActivated
deactivatedonDeactivated
errorCapturedonErrorCaptured

Teleport

Teleport是一个传送门,它可以将我们的一段html传送到其他地方,主要场景就是我在项目中用到的弹窗组件Dialog,Dialog我们经常要嵌套在其他Vue文件中,这样就造成了css的污染,而传送门就可以很好的解决这个问题

// App.vue

<template>
  <Modal />
  <p>App</p>
</template>

to指的是将teleport挂载到哪个节点上,与index.html#modal对应

// Modal.vue

<template>
  <teleport to="#modal">
    <div>
      <h2>modal</h2>
    </div>
  </teleport>
</template>

<script lang="ts"></script>
// index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <div id="modal"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

image.png

Suspense

Suspense是一个异步组件的解决方案,通过具名插槽来实现,异步组件执行完毕前,显示fallback内容,而异步执行完毕,显示default的内容.但是这个组件要返回一个PromiseVue 3.2中,可以看到,没有async,但是await可以正常使用,是因为<script setup>会被编译成async setup()作为顶层async

// App.vue

<Suspense>
    <template #default>
      <AsyncComp />
    </template>
    <template #fallback> Suspense loading... </template>
</Suspense>
// AsyncComp.vue

<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)
const data = await new Promise((resolve) => {
  setTimeout(() => {
    resolve(123)
  }, 2000)
})
count.value = data
</script>

<template>
  <h1>{{ count }}</h1>
</template>

总结

Vue3.0相比于老版本,真的是越来越像React了,虽然一开始用的时候,可能有那么一点奇怪,但是真正使用了就真香系列了😄