学习 Vue3 基础


学习 Vue3 基础

Vue3 基本概述

介绍

  • 2020 年 9 月 18 日,Vue 发布了 3.0 版本,代号:One Piece(海贼王),周边生态原因,当时大多数开发者还处于观望状态。

  • 现在主流组件库都已经发布了支持 Vue3.0 的版本,例如 Element PlusVantVue Use,其他生态也在不断地完善中,所以 Vue3 是趋势。

  • 2022 年 2 月 7 日开始,Vue3 也将成为新的默认版本

优点

  • Composition API,能够更好的组织、封装、复用代码、RFCs。

  • 性能:打包大小减少 41% 、初次渲染快 55% 、更新渲染快 133%、内存减少 54%,主要原因在于 Proxy,VNode,Tree Shaking support

  • Better TS support,源码

  • 新特性:Fragment、Teleport、Suspense。

  • 趋势:未来肯定会有越来越多的企业使用 Vue3.0 + TS 进行大型项目的开发。

  • 对于个人来说:适应市场需求,学习流行的技术提升竞争力,加薪!

Vite 基本使用

Vite是什么?

  • 它是下一代前端开发与构建工具,热更新、打包构建速度更快,但目前周边生态还不如 Webpack 成熟,所以实际开发中还是建议使用 Webpack。
  • 但目前就学习 Vue3 语法来说,我们可以使用更轻量的 Vite,例如要构建一个 Vite + Vue 项目,如下。
npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev
  • Webpack:将所有的模块提前编译、打包进 bundle 中,不管这个模块是否被用到,随着项目越来越大,打包启动的速度自然越来越慢。

  • Vite:瞬间开启一个服务,并不会先编译所有文件,当浏览器用到某个文件时,Vite 服务会收到请求然后编译后相应到客户端。

创建 Vue3 应用

步骤

1.在 main.js 中按需导入 createApp 函数

2.定义App.vue 根组件,导入到 main.js

3.使用 createApp 函数基于 App.vue 根组件创建应用实例。

4.挂载至 index.html 的 #app 容器

main.js

// 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')

App.vue

<template>
    <div class="container">我是根组件</div>
</template>
<script>
    export default {
        name: 'App'
    }
</script>

选项/组合 API

目标

理解什么是 Options API 写法,什么是 Composition 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>

Vue3 实现

  • 优点:可以把同一功能的数据业务逻辑组织到一起,方便复用和维护。

  • 缺点:需要有良好的代码组织和拆分能力,相对没有 Vue2 容易上手。

  • 注意:为了能较好的过渡到 Vue3.0 版本,目前也是支持 Vue2.x 选项 API 的写法。

  • 链接:why-composition-apicomposition-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>

setup 入口函数

介绍

  • 是什么: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')
        },
    }
</script>

reactive 包装函数

介绍

reactive 是一个函数,用来将普通对象/数组包装成响应式式数据使用(基于 Proxy),无法直接处理基本数据类型!

Vue3 生命周期

介绍

  • 组合 API生命周期写法,其实 选项 API 的写法在 Vue3 中也是支持。
  • Vue3(组合 API)常用的生命周期钩子有 7 个,可以多次使用同一个钩子,执行顺序和书写顺序相同。
  • setup、onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount、onUnmounted。

toRef

介绍

toRef 函数的作用:转换响应式对象中某个属性为单独响应式数据,并且转换后的值和之前是关联的(ref 函数也可以转换,但值非关联,后面详讲 ref 函数)。

toRefs

介绍

toRefs 函数的作用:转换响应式对象中所有属性为单独响应式数据,并且转换后的值和之前是关联的。

ref 函数

介绍

ref 函数,常用于把简单数据类型包裹为响应式数据,注意 JS 中操作值的时候,需要加 .value 属性,模板中正常使用即可。

  • 注意:ref 其实也可以包裹复杂数据类型为响应式数据,一般对于数据类型未确定的情况下推荐使用 ref。
  • 当你明确知道需要包裹的是一个对象,那么推荐使用 reactive,其他情况使用 ref 即可。
  • ref 处理基本数据类型用的是 Object.defineProperty 进行数据劫持,处理复杂数据类型用的是 Proxy(内部借助了 reactive 函数)。

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>

配合 v-for 循环可以获取一组 DOM 或者组件。

<template>
    <ul>
        <!-- #4 -->
        <li v-for="i in 4" :key="i" :ref="setDom">{{ i }} li</li>
    </ul>
</template>
<script>
    import { onMounted } from 'vue'
    export default {
        name: 'App',
        setup() {
            // #1
            const domList = []
            // #2
            const setDom = (el) => {
                domList.push(el)
            }
            onMounted(() => {
                // #5
                console.log(domList)
            })
            // #3
            return { setDom }
        },
    }
</script>

computed

作用

computed 函数用来定义计算属性

<template>
    <p>firstName: {{ person.firstName }}</p>
    <p>lastName: {{ person.lastName }}</p>
    <p>fullName: {{ person.fullName }}</p>
</template>
<script>
    import { computed, reactive } from 'vue'
    export default {
        name: 'App',
        setup() {
            const person = reactive({
                firstName: '朱',
                lastName: '逸之',
            })
            person.fullName = computed(() => {
                return person.firstName + ' ' + person.lastName
            })
            // 也可以传入对象,目前和上面等价
            /* person.fullName = computed({
                get() {
                    return person.firstName + ' ' + person.lastName
                },
            }) */
            return {
                person,
            }
        },
    }
</script>

高级

<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>
  • 给 computed 传入函数,返回值就是计算属性的值。

  • 给 computed 传入对象,get 获取计算属性的值,set 监听计算属性改变。

watch

监听一个 ref 数据

<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 数据

<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 对象里面的数据并不会触发监听,说明 ref 并不是默认开启 deep 的。

<template>
    <p>{{ obj.hobby.eat }}</p>
    <button @click="obj.hobby.eat = '面条'">修改 obj.hobby.eat</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)
            })

            return { obj }
        },
    }
</script>
  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>
  1. 解决:开启深度监听 ref 数据。
watch(
    obj,
    (newValue, oldValue) => {
        console.log(newValue, oldValue)
        console.log(newValue === oldValue)
    },
    {
        deep: true,
    }
)

监听 reactive 数据

基本操作

注意:监听 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>

知识补充

  • 想让 ref 内部数据的修改被观测到,除了前面学习的开启深度监听,还可以通过监听 ref.value 来实现同样的效果。
  • 因为 ref.value 是一个 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>

监听普通数据

  1. 监听响应式对象中的某一个普通属性值,要通过函数返回的方式进行(如果返回的是对象/响应式对象,修改内部的数据需要开启深度监听)。
<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: '西瓜',
                },
            })
            // 不叫普通属性值,是一个 reactive
            /* watch(obj.hobby, (newValue, oldValue) => {
                console.log(newValue, oldValue)
                console.log(newValue === oldValue)
            }) */
            // 叫普通属性值
            watch(
                () => obj.hobby.eat,
                (newValue, oldValue) => {
                    console.log(newValue, oldValue)
                    console.log(newValue === oldValue)
                }
            )

            return { obj }
        },
    }
</script>
  1. 监听 ref 数据的另一种写法。
<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);
            }); */
            // 另一种写法,函数返回一个普通值
            watch(
                () => age.value,
                (newValue, oldValue) => {
                    console.log(newValue, oldValue)
                }
            )

            return { age }
        },
    }
</script>

watchEffect

<template>
    <p>{{ obj.hobby.eat }}</p>
    <button @click="obj.hobby.eat = '面条'">修改 obj</button>
</template>

<script>
    import { reactive, watchEffect } from 'vue'
    export default {
        name: 'App',
        setup() {
            const obj = reactive({
                hobby: {
                    eat: '西瓜',
                },
            })
            // 叫普通属性值
            /* watch(obj, (newValue, oldValue) => {
                console.log(newValue, oldValue)
                console.log(newValue === oldValue)
            }) */

            watchEffect(() => {
                // 1. 不指定监视哪一个,这里面用到了谁就监听谁
                // 2. 第一次的时候肯定会执行
                // 例如对 obj.hobby.eat 的修改,由于这里用到了 obj.hobby.eat,则会执行
                // !注意如果这里用的是 obj 则不会被执行
                console.log(obj.hobby.eat)
            })

            return { obj }
        },
    }
</script>

provide/inject

把 App.vue 中的数据传递给孙组件,Child.vue。

App.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>

Parent.vue

<template>
    <div>
        Parent
        <hr />
        <Child />
    </div>
</template>

<script>
    import Child from './Child.vue'
    export default {
        components: {
            Child,
        },
    }
</script>

Child.vue

<template>
    <div>
        Child
        <p>{{ money }}</p>
        <button @click="changeMoney(1)">1 块钱</button>
    </div>
</template>

<script>
    import { inject } from 'vue'
    export default {
        setup() {
            const money = inject('money')
            const changeMoney = inject('changeMoney')
            return { money, changeMoney }
        },
    }
</script>

响应式数据的判断

  • isRef: 检查一个值是否为 ref 对象。
  • isReactive: 检查一个对象是否是由 reactive 创建的响应式代理。
  • isReadonly: 检查一个对象是否是由 readonly 创建的只读代理。
  • isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理。
<template>
    <div>name: {{ name }}</div>
    <div>age: {{ age }}</div>
</template>

<script>
    import { reactive, readonly, ref, toRefs, isRef, isReactive, isReadonly, isProxy } from 'vue'
    export default {
        setup() {
            const person = reactive({ name: 'xxx', age: 18 })
            const num = ref(0)
            const readonlyPerson = readonly(person)
            console.log(isRef(num))
            console.log(isReactive(person))
            console.log(isReadonly(readonlyPerson))
            console.log(isProxy(person)) // true
            console.log(isProxy(readonlyPerson)) // true
            return {
                ...toRefs(person),
            }
        },
    }
</script>

setup 函数参数

setup 中参数的使用。

父传子

App.vue

<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>

Son.vue

<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>

子传父

App.vue

<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>

Son.vue

<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>

v-model

基本操作

在 Vue2 中 v-mode 语法糖简写的代码。

<Son :value="msg" @input="msg=$event" />

在 Vue3 中 v-model 语法糖有所调整。

<Son :modelValue="msg" @update:modelValue="msg=$event" />

App.vue

<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>

Son.vue

<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>

传递多个

App.vue

<template>
    <h2>count: {{ count }} age: {{ age }}</h2>
    <hr />
    <Son v-model="count" v-model:age="age" />
</template>
<script>
    import { ref } from 'vue'
    import Son from './Son.vue'
    export default {
        name: 'App',
        components: {
            Son,
        },
        setup() {
            const count = ref(10)
            const age = ref(18)
            return { count, age }
        },
    }
</script>

Son.vue

<template>
    <h2>子组件 {{ modelValue }} {{ age }}</h2>
    <button @click="$emit('update:modelValue', 100)">改变 count</button>
    <button @click="$emit('update:age', 19)">改变 age</button>
</template>
<script>
    export default {
        name: 'Son',
        props: {
            modelValue: {
                type: Number,
                default: 0,
            },
            age: {
                type: Number,
                default: 18,
            },
        },
    }
</script>

Fragment

  • Vue2 中组件必须有一个跟标签。
  • Vue3 中组件可以没有根标签,其内部会将多个标签包含在一个 Fragment 虚拟元素中。
  • 好处:减少标签层级和内存占用。

Teleport

作用

传送,能将特定的 HTML 结构(一般是嵌套很深的)移动到指定的位置,解决 HTML 结构嵌套过深造成的样式影响或不好控制的问题。

案例

在 Child 组件点击按钮进行弹框。

<template>
    <div class="child">
        <dialog v-if="bBar" />
        <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>

解决

<template>
    <div class="child">
        <teleport to="body">
            <dialog v-if="bBar" />
        </teleport>
        <button @click="handleDialog">显示弹框</button>
    </div>
</template>

Suspense

异步组件加载期间,可以使用此组件渲染一些额外的内容,增强用户体验。

异步组件

<template>
    <div class="app">
        App
        <Test />
    </div>
</template>

<script>
    // 静态引入 => 等待所有子组件加载完再统一渲染
    // import Test from './Test.vue'
    // 动态/异步引入
    import { defineAsyncComponent } from 'vue'
    const Test = defineAsyncComponent(() => import('./Test.vue'))
    export default {
        name: 'App',
        components: {
            Test,
        },
    }
</script>

优化代码

<template>
    <div class="app">
        App
        <Suspense>
            <template v-slot:default>
                <Test />
            </template>
            <template v-slot:fallback>
                <div>loading...</div>
            </template>
        </Suspense>
    </div>
</template>

<script>
    // 静态引入 => 等待所有子组件加载完再统一渲染
    // import Test from './Test.vue'
    // 动态/异步引入
    import { defineAsyncComponent } from 'vue'
    const Test = defineAsyncComponent(() => import('./Test.vue'))
    export default {
        name: 'App',
        components: {
            Test,
        },
    }
</script>

一个细节

setup 也可以返回一个 Promise 实例,但要异步引入此组件并配合 Suspense 使用。

<template>
    <div class="test">Test</div>
</template>

<script>
    import { ref } from 'vue'
    export default {
        name: 'Test',
        /* setup() {
            const count = ref(0)
            // 也可以返回 Promise 实例,但要异步引入此组件并配合 Suspense 使用
            return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve({ count })
            }, 3000)
            })
        }, */
        async setup() {
            const count = ref(0)
            return await new Promise((resolve, reject) => {
                setTimeout(() => {
                    resolve({ count })
                }, 3000)
            })
        },
    }
</script>

script setup

文档

data

<script setup>
    import { reactive, toRefs } from 'vue'
    const state = reactive({
        name: 'ifer',
        age: 18,
    })
    const { name, age } = toRefs(state)
</script>
<template>
    <h1>name: {{ name }} age: {{ age }}</h1>
</template>

method

<script setup>
    import { reactive, toRefs } from 'vue'
    const state = reactive({
        name: 'ifer',
        age: 18,
    })
    const { name, age } = toRefs(state)

    // 修改名字
    const changeName = () => {
        name.value = 'xxx'
    }
</script>
<template>
    <h1>name: {{ name }} age: {{ age }}</h1>
    <button @click="changeName">修改名字</button>
</template>

computed

<script setup>
    import { reactive, computed, isRef } from 'vue'
    const state = reactive({
        firstName: '热',
        lastName: '巴',
    })
    const fullName = computed(() => state.firstName + state.lastName)
    console.log(isRef(fullName)) // true
</script>
<template>
    <h1>fullName: {{ fullName }}</h1>
</template>

watch

<script setup>
    import { ref, watch } from 'vue'
    const count = ref(0)
    watch(count, (newValue, oldValue) => {
        console.log(newValue, oldValue)
    })
</script>
<template>
    <h1>count: {{ count }}</h1>
    <button @click="count++">+1</button>
</template>

props

父组件

<script setup>
    import { reactive } from 'vue'
    import Hello from './Hello.vue'
    const person = reactive({
        name: 'ifer',
        age: 18,
    })
</script>
<template>
    <Hello v-bind="person" />
</template>

子组件

<script setup>
    // defineProps 无需引用,可以在 script setup 中直接使用
    const props = defineProps({
        name: String,
        age: Number,
    })
</script>
<template>
    <div>name: {{ props.name }} age: {{ age }}</div>
</template>

emit

父组件

<script setup>
    import { reactive } from 'vue'
    import Hello from './Hello.vue'
    const person = reactive({
        name: 'ifer',
        age: 18,
    })
    const updateAge = () => {
        person.age++
    }
</script>
<template>
    <Hello v-bind="person" @updateAge="updateAge" />
</template>

子组件

<script setup>
    // defineProps 无需引用,可以在 script setup 中直接使用
    const props = defineProps({
        name: String,
        age: Number,
    })
    const emit = defineEmits(['updateAge'])

    const updateAge = () => {
        emit('updateAge')
    }
</script>
<template>
    <div>name: {{ props.name }} age: {{ age }}</div>
    <button @click="emit('updateAge')">update name</button>
    <button @click="$emit('updateAge')">update name</button>
    <button @click="updateAge">update name</button>
</template>

v-model

父组件

<script setup>
    import { reactive } from 'vue'
    import Hello from './Hello.vue'
    const person = reactive({
        name: 'ifer',
        age: 18,
    })
</script>
<template>
    <Hello v-model="person.name" v-model:age="person.age" />
</template>

子组件

<script setup>
    // defineProps 无需引用,可以在 script setup 中直接使用
    const props = defineProps({
        modelValue: String,
        age: Number,
    })
    const emit = defineEmits(['update:modelValue', 'update:age'])

    const updateName = () => {
        emit('update:modelValue', 'xxx')
    }
    const updateAge = () => {
        emit('update:age', 20)
    }
</script>
<template>
    <div>name: {{ props.modelValue }} age: {{ age }}</div>
    <button @click="updateName">update name</button>
    <button @click="updateAge">update age</button>
</template>

defineExpose

  • 标准组件写法中,父组件通过 ref 拿到子组件实例,并可以直接访问子组件中的 data 和 method。
  • script-setup 模式下,data 和 method 默认只能给当前组件的 template 使用,外界通过 ref 无法访问到。
  • 解决:需要手动的通过 defineExpose 进行暴露。

父组件

<script setup>
    import { ref, nextTick } from 'vue'
    import Hello from './Hello.vue'
    const childRef = ref(null)
    nextTick(() => {
        childRef.value.updatePerson('xxx', 20)
    })
</script>
<template>
    <Hello ref="childRef" />
</template>

子组件

<script setup>
    import { reactive } from 'vue'
    const person = reactive({
        name: 'ifer',
        age: 18,
    })
    const updatePerson = (name, age) => {
        person.name = name
        person.age = age
    }
    // 注意是 defineExpose,不要打成 defineProps 了
    defineExpose({
        updatePerson,
    })
</script>
<template>
    <h2>name: {{ person.name }} age: {{ person.age }}</h2>
</template>

slot

父组件

<script setup>
    import Hello from './Hello.vue'
</script>
<template>
    <Hello>
        <!-- 默认插槽 -->
        <h2>默认插槽</h2>
        <!-- 具名插槽 -->
        <template #title>
            <h2>具名插槽</h2>
        </template>
        <!-- 作用域插槽 -->
        <template #footer="{ person }">
            <h2>通过作用域插槽获取到的数据:{{ person.name }}</h2>
        </template>
    </Hello>
</template>

子组件

<script setup>
    import { reactive, useSlots } from 'vue'
    const slots = useSlots()
    const person = reactive({
        name: 'ifer',
        age: 18,
    })
    // 可以拿到插槽相关的信息
    console.log(slots)
</script>
<template>
    <slot />
    <slot name="title" />
    <slot name="footer" :person="person" />
</template>

CSS 变量注入

<script setup>
    import { reactive } from 'vue'
    const state = reactive({
        color: 'pink',
    })
</script>
<template>
    <h2>Hello Vue3</h2>
</template>
<style scoped>
    h2 {
        /* 可以使用 v-bind 绑定变量 */
        color: v-bind('state.color');
    }
</style>

原型绑定与组件使用

main.js

import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.config.globalProperties.year = '再见 2021,你好 2022~~'
app.mount('#app')

App.vue

<script setup>
    import { getCurrentInstance } from 'vue'
    const { proxy } = getCurrentInstance()
</script>
<template>
    <h1>{{ proxy.year }}</h1>
</template>

对 await 支持

<script setup>
    const r = await fetch('https://autumnfish.cn/api/joke')
    const d = await r.text()
    console.log(d)
</script>
<template>
    <h1>{{ proxy.year }}</h1>
</template>

定义组件的 name

<template>
    <div>Hello</div>
</template>
<script setup></script>

<script>
    export default {
        name: 'HelloCmp',
    }
</script>

mixins

混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能,一个混入对象可以包含任意组件选项,当组件使用混入对象时,所有混入对象的选项将被“混合”进该组件本身。

// Vue2 写法
Vue.mixin({})
// Vue3 写法
app.mixin({})

follow.js

export const follow = {
    data() {
        return {
            loading: false,
        }
    },
    methods: {
        followFn() {
            this.loading = true
            // 模拟请求
            setTimeout(() => {
                // 省略请求代码
                this.loading = false
            }, 2000)
        },
    },
}

App.vue

<template>
    <a href="javascript:;" @click="followFn">{{ loading ? '请求中...' : '关注' }}</a>
    <Son />
</template>
<script>
    import Son from './Son.vue'
    import { follow } from './follow'
    export default {
        name: 'App',
        components: {
            Son,
        },
        mixins: [follow],
    }
</script>

Son.vue

<template>
    <a href="javascript:;" @click="followFn">{{ loading ? '请求中...' : '关注' }}</a>
</template>
<script>
    import { follow } from './follow'
    export default {
        name: 'Son',
        mixins: [follow],
    }
</script>

其他变更

参考 Vue3 迁移指南

  1. 全局 API 的变更,链接
  2. data 只能是函数,链接
  3. 自定义指令 API 和组件保持一致,链接
  4. keyCode 作为 v-on 修饰符被移除、移除 v-on.native 修饰符、filters 被移除,链接
  5. $on、$off、$once 被移除,链接
  6. 过渡类名的更改,链接

文章作者: feico
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 feico !
评论
  目录