vue3教程

273 阅读9分钟

​一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情

目的

本篇教程励志用简单的例子来教会你理解并使用常用vue3的新特性,适合于从vue2转vue3的码友,本文请按顺序阅读,否则可能会出现需要查找解释的情况。

响应性api

在vue3.x中如果想让数据在改变时,视图层也相应发生变化就需要使用到响应性api。

想创建一个响应式数据,归根结底就两种方法 refreactive

ref

接受一个内部值并返回一个响应式且可变的 ref 对象,该对象仅有一个value属性指向内部值, 可以用来读取和更改。
    const num = ref(0);
    console.log(num.value) // 0
    num.value++; // 1

这样就可以创建一个具有响应性的数据,当然ref也可以接收数组、对象、其他基础类型的值,但我们通常不建议在ref中传递对象,因为响应式转换是深层的,它会影响所有它嵌套的属性,而我们应该尽量避免依赖原始对象。

reactive

返回一个对象的响应式副本。
    const introduce = reactive({
        name: '喵十一',
        age: 19,
        hobby: ['编程', '游戏', '运动']
    })
    console.log(introduce.name) // 喵十一
    introduce.name = "十一"
    console.log(introduce.name) // 十一

这样会返回一个具有相应式的对象副本,但我们不会在reactive中传递基础类型数据,原因请见下方。

ref 与 reactive的对比

可传递类型

ref

可传入基本类型和引用数据类型, 但不推荐传入对象

reactive

可传入对象/数组/json, 但不可以传入基本类型数据, 如果传入则不会返回proxy对象。
由于 reactive是基于proxy实现的响应式,所以如果返回的不是一个proxy对象则该数据不会具有响应式。
即修改数据也不会使得视图层发生变化。

修改方式

ref

.value的方式进行修改

reactive

直接修改

reactive注意事项

如果reactive传入的对象中某一属性是用ref创建的响应式数据,那它将被自动解包。
本条极其重要。
    const introduce = reactive({
        name: '喵十一',
        age: ref(19),
        hobby: ['编程', '游戏', '运动']
    })
    console.log(introduce.age) // 19
    console.log(introduce.age.value) // undefined
    console.log(introduce.age.value = 1) // error
个人对解包的理解是变为了一个普通的属性,然后由于reactive的缘故又使整个对象具有了响应性

ref 和 reative的总结

通常情况下ref用于为基本类型创建响应性,reactive为引用类型创建响应性。

组合式api

在解释这个名词之前,让我们先来了解一下vue2的选项式api的缺点

  1. 业务逻辑分散在各个选项中(methods、watch、data等),要修改某一功能时可能需要连续去好几个选项中修改,不便于维护与管理
  2. 代码冗长时不方便快速定位某一业务
  3. 会使得我们不断地在选项之间来回“跳转”
组合式api,顾名思义,就是将不同的业务逻辑拆分成一块块的像搭积木般构建起我们的业务。

这样就近乎完美的避开了vue2中选项式api的缺点

例子

我们使用一个极其简单的数量框的例子来介绍组合式api

 <template>
    <div class="combinationApi">
        <div class="num">
            <button @click="changeNum('-')">-</button>
            <button>{{num}}</button>
            <button @click="changeNum('+')">+</button>
        </div>
        <button @click="resetNum">重置</button>
    </div>
</template>
<script>
import changeButton from "../composables/changeButton";
import resetButton from "../composables/resetButton";
import {ref} from "vue";
export default {
    name: "combinationApi",
    props: {
        name: {
            default: "123",
            type: String
        }
    },
    setup(props) {
        // 创建一个响应式的数据
        let num = ref(0);
        // 使用组合式api
        let { changeNum } = changeButton(num);
        let { resetNum } = resetButton(num);
        // 返回需要的内容
        return {
            num,
            changeNum,
            resetNum
        }
    }
}
</script>

可以看到我们在script中引入了两个js文件,changeButtonresetButton并在setup中使用了它们。

changeButton的功能是将数量更改。

resetButton的功能是将数量重置。

我们先来看一下这两个文件

changeButton

    import { ref, reactive } from "vue";
    export default function changeButton(num) {
        // 改变数量事件
        let changeNum = (type)=>{
            type == "+" ? num.value++ : type == "-" && num.value > 0 ? num.value-- : '';
        }
        return {
            changeNum
        }        
    }

resetButton

    export default function changeButton(num) {
        // 重置按钮数量
        let resetNum = ()=>{
            num.value = 0;
        }
        return {
            resetNum
        }        
    }

功能非常的简单,他们都接收了一个参数 num,都返回了一个对 num 进行操作的方法。 这就是一个简易的组合式api,整个页面的逻辑就像搭积木一样一块一块的构建了起来,方便以后修改逻辑,找起来也不会麻烦。

setup

setup函数会接受两个参数,propscontext

props

props是具有响应性的,当有新的prop传入时,它将会被更新。 正因为它具有响应性所以请不要使用结构的方式使用它,这样会失去响应性。

context

context是一个普通的js对象,这里面接收了剩余我们可能在setup中使用的值,因为是一个普通的js对象,咱们是可以放心使用结构的方式使用它

setup中可访问property

  1. props
  2. attrs
  3. slots
  4. emit
  5. expose

setup中可返回的类型

对象

    <template>
        <div class="setup">
            {{num}}
        </div>
    </template>
    <script>
        export default {
            props: {
                name: {
                    default: "123",
                    type: String
                }
            },
            setup(props, context) {
                let num = ref(0); // 该数据将在暴漏出去时自动解包
                // 暴漏给template
                return {
                    num                
                }
            }
        }
    </script>
该返回的对象中的property 和 props中的property,都可在模板中直接使用,

其中的refs将会被自动解包, 所以在模板中访问时不需要在使用.value了

渲染函数

    <script>
        // 解构出创建渲染函数的h方法
        import {h} from "vue";
        export default {
            props: {

            },
            setup(props, context) {
                // 一个小姐姐的图片链接
                let img = "https://img1.baidu.com/it/u=3149018235,1913956841&fm=253&fmt=auto&app=138&f=JPEG?w=231&h=500"            
                // 暴漏给template 一个能创建img的渲染函数
                return ()=>h('img', {src: img});
            }
        }
    </script>

本照片为百度网络资源,仅作展示:

image.png

这里返回的渲染函数将自动渲染在页面上。

查看更多渲染函数相关知识请点击这里

setup中的this

由于setup会在其他组件选项之前解析,这使得setup中的this和其他组件选择中的this完全不同,

因此在setup中使用this,因为不仅访问不到其他选项式api,还会造成和它们之间的混肴。

setup语法糖

想使用setup语法糖非常简单,只需要在script标签上加上setup

    <script setup>
    ....
    </script>

接下来我们来讲一下setup的语法糖的好处

  • 更少的样板内容,更简洁的代码。
  • 能够使用纯 Typescript 声明 props 和抛出事件。
  • 更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)。
  • 更好的 IDE 类型推断性能 (减少语言服务器从代码中抽离类型的工作)。

顶层的绑定会被暴露给模板

不需要再将数据抛出, 在此语法糖中创建的变量可以直接在模板中访问, ref创建的数据依然会被解包。

    <template>
        <div>{{name}}</div>
    </template>
    <script setup>
        import {ref} from vue;
        let name = ref('喵十一')
    </script>

使用组件

引入的组件可以直接使用,不需要再次注册, 且引入的名称可以直接当自定义标签来使用。

    <template>
        <button></button>
    </template>
    <script setup>
        import button from '@/component/button';
    </script>

当然从单文件件中引入多组件

<template>
  <Form.Input>
    <Form.Label>label</Form.Label>
  </Form.Input>
</template>
<script setup>
import * as Form from './form-components'
</script>

自定义指令

全局自定义指令可以直接使用,但如果在本地自定义指令则需要使用vNameDirective的方式来命名。

例如:

    <template>
      <div v-msy-directive>This is a Heading</div>
    </template>
    <script setup>
    const vMsyDirective = {
        // 代码
    }
    </script>

defineProps 和 defineEmits

如果在setup语法糖中想使用像普通的 script 标签中的 props 和 emits, 则需要使用 defineProps 和 defineEmits来声明

    <template>
      
    </template>
    <script setup>
        let props = defineProps({
            name: {
                default: "喵十一",
                type: String
            }
        });
        let emits = defineEmits(['change']);
    </script>

definePropsdefineEmits 仅在 setup 语法中使用且不需导入

defineExpose

用来在 setup 语法糖中确认要暴漏出去的属性,和前两个方法一样不需要导入。

useSlots 和 useAttrs

在 <script setup> 使用 slots 和 attrs 的情况应该是很罕见的,因为可以在模板中通过 $slots 和 $attrs 来访问它们。在你的确需要使用它们的罕见场景中,可以分别用 useSlots 和 useAttrs 两个辅助函数:

<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()
</script>

useSlots 和 useAttrs 是真实的运行时函数,它会返回与 setupContext.slots 和 setupContext.attrs 等价的值,同样也能在普通的组合式 API 中使用。

顶层await

需要和Suspense组合使用,但Suspense还在测试阶段,因为本文不做更多介绍,想要了解更多的可以去查询一下

仅 ts 的功能

仅限类型的 props/emit 声明

props 和 emits 都可以使用传递字面量类型的纯类型语法做为参数给 defineProps 和 defineEmits 来声明:

const props = defineProps<{
  foo: string
  bar?: number
}>()

const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
  • defineProps 或 defineEmits 只能是要么使用运行时声明,要么使用类型声明。同时使用两种声明方式会导致编译报错。

  • 使用类型声明的时候,静态分析会自动生成等效的运行时声明,以消除双重声明的需要并仍然确保正确的运行时行为。

    • 在开发环境下,编译器会试着从类型来推断对应的运行时验证。例如这里从 foo: string 类型中推断出 foo: String。如果类型是对导入类型的引用,这里的推断结果会是 foo: null (与 any 类型相等),因为编译器没有外部文件的信息。
    • 在生产模式下,编译器会生成数组格式的声明来减少打包体积 (这里的 props 会被编译成 ['foo', 'bar'])。
    • 生成的代码仍然是有着类型的 Typescript 代码,它会在后续的流程中被其它工具处理。
  • 截至目前,类型声明参数必须是以下内容之一,以确保正确的静态分析:

    • 类型字面量
    • 在同一文件中的接口或类型字面量的引用

    现在还不支持复杂的类型和从其它文件进行类型导入。理论上来说,将来是可能实现类型导入的。

使用类型声明时的默认 props 值

仅限类型的 defineProps 声明的不足之处在于,它没有可以给 props 提供默认值的方式。为了解决这个问题,提供了 withDefaults 编译器宏:

interface Props {
  msg?: string
  labels?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  labels: () => ['one', 'two']
})

上面代码会被编译为等价的运行时 props 的 default 选项。此外,withDefaults 辅助函数提供了对默认值的类型检查,并确保返回的 props 的类型删除了已声明默认值的属性的可选标志。

限制

由于 <script setup> 依赖于单文件上下文,所以当将其移动到 .js.ts 将会失去作用。

Teleport

一种将模板内容移动到 DOM Vue App 之外的方法。

可以用它来指定将内容渲染到 DOM 的哪个父元素下。

它可以做很多事情,且可以很好的解决我们的问题,比如 常见的全屏弹框组件 ,如果我们想全凭 css 去挂在到全屏下通常时较困难的,因为我们可能会被引入组件的页面所影响,但我们如果使用 teleport 将组件渲染至 body 下则会全然不受印象。

全屏弹框组件:

<template>
    <teleport to='body'>
        <div class="mask">
            <div class="container" :class="`${type}`">
                <slot></slot>
            </div>
        </div>
    </teleport>
</template>
<script setup>
    let props = defineProps({
        type: {
            default: "top"
        }
    })
</script>
<style lang="less" scoped>
    .mask {
        position: absolute;
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;
        background-color: rgba(0, 0, 0, 0.6);
        .container {
            position: absolute;
            left: 50%;
            transform: translateX(-50%);
            &.top {
                top: 0;
            }
            &.bottom {
                bottom: 0;
            }
            &.center {
                top: 50%;
                transform: translate(-50%, -50%);
            }
        }
    }
</style>

这里我们可以看到指定了组件渲染的位置 body

<teleport to='body'>

关键词 to

用来指定渲染位置,类型 string,一定要是一个有效的查询选择器或 HTMLElement,请务必记住teleport是将内容渲染到 Vue App外,所以在 Vue App 内的查询器 或 HTMLElement将是无效的

效果图

企业微信截图_16511632709225.png

v-model的变更

v-model的变化总结起来如下:

变更:

  1. prop 和 默认事件名已更改

    value -> modelValue;

    input -> updata:modelValue;

新增:

  1. 可以在同一组件上使用多个 v-model
  2. v-model 可以使用自定义修饰符了

移除:

  1. v-bind 的 .sync 修饰符和组件的 model 选项已移除,可在 v-model 上加一个参数代来代替

v-model参数

vue3.x 中我们如果想要改变 mode 的名称,可以为 v-model 传入一个参数

<EsyInput v-model:content="content"></EsyInput>

这样就为 v-model 传递了一个参数,也是下方的简写

 <EsyInput 
    v-model:content="content" 
    @updata:content="content = $event"
></EsyInput>

使用多个 v-model

<EsyInput v-model:content="content" v-model:text="text"></EsyInput>

v-model 自定义修饰符

一般情况下我们使用 vue 内置的修饰符,如.trim.number 和 .lazy。就可以满足需求了,但在某些情况下我门需要自定义的修饰符。

比如想实现下面单词首字母大写的功能就可以使用自定义修饰符 image.png

创建自定义修饰符

在组建的 props 中声明一个 modelModifiers 即可

     export default {
        props: {
            // model 默认名
            modelValue:{
              type: String,
              default: ''
            },
            // 自定义修饰符
            modelModifiers: {
              default: () => ({})
            }
        }
    }

使用自定义修饰符

只需要在传入组件更新 v-model 数据时判断一下即可

    export default {
        props: {
            // model 默认名
            modelValue:{
              type: String,
              default: ''
            },
            // 自定义修饰符
            modelModifiers: {
              default: () => ({})
            }
        },
        methods: {
            emitValue = (e)=>{
                let value = e.target.value;
                // 如果存在自定义修饰符
                if(this.props.modelModifiers.capitalize) {
                  console.log(`转换前:` + value)
                  
                  // 以空格分开的每个单词或短句首字母大写逻辑
                  let arr = value.split(" ");
                  arr.forEach((item, index) => {
                    item ? arr[index] = item[0].toUpperCase() + item.slice(1) : '';
                  });
                  value = arr.join(" ");
                  
                  console.log(`转换后:` + value)
                }
                this.$emit('update:modelValue', value)
            }
        }
    }
 }
    // 使用自定义修饰符
    <input-components v-model.capitalize="text"></input-components>

由于在 props 中声明了 modelModifiers, 所以如果使用自定义修饰符的话,那么 modelModifiers.capitalize 一定为 true, 就可以根据结果判断是否使用了自定义修饰符。

带参的 v-model 自定义修饰符

对于带参数的 v-model 绑定,生成的 prop 名称将为 arg + Modifiers

定义

    export default {
        props: {
            content:{
              type: String,
              default: ''
            },
            // 自定义修饰符
            contentModifiers: { // 重点
              default: () => ({})
            }
        }
    }

使用

    // 使用自定义修饰符
    <input-components v-model:content.capitalize="content"></input-components>

v-bind

v-bind的合并行为

v-bind的绑定顺序会影响渲染的结果

在原来的vue2.x中如果在同一元素同时使用了 v-bind 和一个独立的 attribute,那么这个独立的 attribute 总是会覆盖 v-bind 中的 attribute

<div class="ied" :v-bind="{class: 'dei'}"></div>

以上的渲染结果在vue2.x中被渲染成如下

<div class="ied"></div>

但在 vue3.x 中 是以后者覆盖前者的方式进行渲染,因此绑定的顺序决定了渲染的是谁

例如:

<div class="ied" :v-bind="{class: 'dei'}"></div>

在 vue3.x 中会被渲染成

<div class="dei"></div>

当然将独立的 attribute 放在后方也会渲染独立的 attribute,这样的方式使得现在开发者能够对自己所希望的合并行为做更好的控制。

css变量 (v-bind)

经过尤大和他们团队的努力,我们终于可以在 style 中使用 v-bind 来动态绑定一个属性了

<template>
    <div class="vBind"></div>
</template>
<script>
    export default {
        setup(props, context) {
            let backgroundColor = `#40faff`;
            return {
                backgroundColor
            }
        }
    }
</script>
<style scoped>
    .vBind {
        width: 100px;
        height: 100px;
        background-color: v-bind(backgroundColor);
    }
</style>

这个语法也同样适用于 <script setup> 语法糖,且支持 javascript 表达式,但需要使用引号包裹起来

    <template>
    <div class="vBind"></div>
</template>
<script setup>
    let style = {
        backgroundColor: "red" 
    }
    let backgroundColor = "blue"
</script>
<style scoped>
    .vBind {
        width: 100px;
        height: 100px;
        background-color: v-bind(backgroundColor); /* setup 普通用法 */
        background-color: v-bind('style.backgroundColor'); /* javascript表达式用法 需要带引号 */
    }
</style>

这个语法的实现原理:

实际的值会被编译成 hash 的 CSS 自定义 property,CSS 本身仍然是静态的。自定义 property 会通过内联样式的方式应用到组件的根元素上,并且在源值变更的时候响应式更新。

演示图

Vizard_20220501_180123_.gif

下方是我个人对整个过程的理解图,如有不对之处请大佬们指正。

image.png

v-for 与 v-if 的优先级变换

vue2.x 中 同一元素上 v-for 的优先级总是高于 v-if。 但 vue3.x 中 同一元素上 v-if 的优先级总是高于 v-for

组件事件emits选项声明

当在 vue3.x 想使用自定义事件一定要在 emits 选项来定义组件可触发的事件。 这个是必须的因为 vue3.x 中移除了 .native修饰符这会导致,任何未在 emits 中声明的事件监听器都会被算入组件的 $attrs,并将默认绑定到组件的根节点上。

因此下方事件会执行两次,一次原生事件,一次自定义事件,所以一定要声明可出发的事件

<template>
    <button v-on:click="$emit('click', $event)">OK</button>
</template>
<script>
    export default {
      emits: [] // 不声明事件
    }
</script>

因此正确的做法应该是下方这样

<template>
    <button v-on:click="$emit('click', $event)">OK</button>
</template>
<script>
    export default {
      emits: ["click"] // 声明事件
    }
</script>

验证自定义事件(验证抛出的事件)

当使用 emits 的语法是对象不是数组时,则可以进行验证。

<script>
    export default {
        emits: {
            click: (num)=>{
                // 进行基本验证
                if(num > 0) {
                    return true
                } else {
                    console.warn('num must be greater than 1');
                    return false
                }
            }
        }
    }
</script>

当想要对一个事件进行验证时需要给他分配一个函数,该函数接收传递给 $emit 调用的参数,并返回一个布尔值以指示事件是否有效。

移除api(简介, 该部分来自官网介绍)

结语

天才无非是长久的忍耐,努力吧!

本文将持续更新,在不断摸索中提高本文质量。