Vue3.0学习笔记

Vue3.0的学习正在一点点地推进,我会在这里记录一些学习的过程与重要的知识点

该笔记仅记录componsitionApi的方式

基础

~~

组件

组件是vue中最重要的内容之一,组件内部也有很多区别于基础的方法与属性

props

  1. 当我们需要将一个对象的所有属性全部当作prop传入的时候,可以使用v-bind的方式直接传入整个对象

    1
    2
    3
    4
    5
    // post对象中所有的属性都要单独传入
    const post = {
    id: 1,
    title: 'Hello Word'
    }

    该情况下下面的两种使用方式完全相同

    1
    2
    <BlogPost v-bind="post" />
    <BlogPost :id="post.id" :title="post.title" />
  2. ⚠️ 通过prop传入的对象/数组都是通过引用的形式,因此在自组件中修改对应的对象/数组内部的值并不会报错且会修改原值

  3. 所有的prop参数默认为可选的,如果需要必选则要声明require: true

  4. 由于对ts的支持,prop的type也可以是自定义的类或构造函数,可以通过instanceof来检查、断言

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Person {
    constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
    }
    }

    defineProps({
    author: Persion
    })

    如上会校验author的值是否是通过new Person的方式创建的

事件

  1. 组件内事件的声明可以通过const emit = defineEmits(['inFocus', 'submit'])的方式进行声明

  2. emits方法支持对象语法,可以用来对触发事件的参数进行验证

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <script setup>
    const emit = defineEmits({
    submit: ({account, password}) => {
    if (account && password) {
    return true;
    } else {
    console.warn('账号/密码未通过校验');
    return false;
    }
    }
    })
    </script>
  3. 虽是可选的但是官方推荐将所有要出发的emit事件都定义到emits中

属性透传

当我们在父元素中对子组件挂在了一个属性或者v-on监听事件,那么当dom渲染时会将这些属性透传到子组件的根元素中,并与根元素已有的属性活事件进行合并,最常见的就是classstyle的设定

如果我们不希望组件自动继承透传属性则需要在组件选项中设置inheritAttrs: false
如果使用的是<script setup>方式则需要增加一个<script>来书写这个声明

1
2
3
4
5
6
7
8
9
<script>
export default {
inheritAttrs: false
}
</script>

<script setup>
// ...setup 部分逻辑
</script>

如此设定之后可以通过v-bind=“$attrs”来决定透传进来的属性如何应用

在js中可以通过const attrs = useAttrs()的方式访问一个组件的所有透传attribute(非响应式)

插槽

插槽是组件只是中最重要的一个属性,也是自定义组件中最重要的一部分

⚠️插槽内容是无法访问子组件数据的

  • 任何父组件模板中的东西都只被编译到父组件的作用域中;而任何子组件模板中的东西都只被编译到子组件的作用域中。
  • 子组件的模板中的内容只能通过插槽来访问,不能通过子组件的data来访问

当我们在父组件中使用子组件中定义的具名插槽时,可以使用v-slot:name的指令,也可简写为#name

1
2
3
4
5
6
7
<BaseLayout>
<template v-slot:header></template>
</BaseLayout>
<!-- 此处两种方式作用相同 -->
<BaseLayout>
<template #header></template>
</BaseLayout>
  • 作用域插槽:在某些场景下我们需要我们可能需要同时使用父组件域内和子组件域内的数据(例如<el-table>组件中的自定义列),这样的情况就需要使用作用域插槽来实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <!-- 子组件 -->
    <div>
    <slot :text="message" :count="1"></slot>
    </div>

    <!-- 父组件1 -->
    <MyComponent v-slot="slotProps">
    {{ slotProps.text }} {{ slotProps.count }}
    </MyComponent>

    <!-- 父组件2 -->
    <MyComponent v-slot="{text, count}">
    {{ text }} {{ count }}
    </MyComponent>

    图1

依赖注入

背景:当我们需要从父组件向子组件传递数据时我们会使用props进行传递,但是在真实开发中存在组件层层嵌套,层级很深的情况,在层层嵌套传递数据的过程中,会有很多组件其实并不关心这个props但是迫于子组件需要又必须沿着链路传递下去,这一过程被称为“prop drilling”,这时候我们就需要使用依赖注入的方式来解决这个问题

解决这个问题我们可以使用provideinject来实现,provide用来提供依赖,inject用来注入依赖,无论层级多深都可以注入由父组件提供给这条链路的依赖

  • Provide:用来提供依赖,可以提供多个依赖,也可以提供一个依赖

    • 如果我们不使用<script setup>的方式,那么必须保证providesetup()可以同步调用
    1
    2
    3
    4
    5
    <script setup>
    import { provide } from 'vue'

    provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
    </script>
  • Inject:用来注入依赖,可以注入多个依赖,也可以注入一个依赖

    • 如果注入的值是一个ref,则不会进行解包,保持响应性链接
    • 如果我们不使用<script setup>的方式,那么必须保证injectsetup()可以同步调用
    • inject(key, value)如果该key值没有被任何处于组件链中的组件提供,则value为其默认值(形似props)
  • 在我们注入一个响应式的值的时候,我们应该尽量保证对响应式状态的修改都保持在provide内部,这样可以确保provide的状态和变更操作都位于同一个组件内部

  • 如果某些情况下我们需要在injector中更改数据,那么可以在provide中提供一个函数,这个函数会在injector中被调用,这样就可以在injector中修改数据

  • 如果想要保证通过provide传的数据无法被injector修改,那么可以使用readonly

    1
    2
    3
    4
    5
    <script setup>
    import { provide, inject, readonly } from 'vue'
    const count = ref(0);
    provide('count', readonly(count));
    </script>
  • 协同开发中推荐使用Symbol来作为注入名来避免命名冲突

异步组件

Vue中提供了一个defineAsyncComponent方法用于在需要时再从服务器加载相关组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { defineAsyncComponent } from 'vue';
const AsyncComp = defineAsyncComponent(() => {
return new Promise(resolve => {
/*从服务器加载组件*/
setTimeout(() => {
resolve({
/* 获取到的组件 */
render() {
return <div>Hello</div>
}
})
}, 1000)
})
});

defineAsyncComponent方法接收的是一个返回Promise的函数,这个函数会在组件被调用时执行,这个函数返回的Promise会在组件被渲染时被解析,模块动态导入也会返回一个Promise,所以可以和defineAsyncComponent配合使用

1
2
3
4
5
import { defineAsyncComponent } from 'vue';

const AsyncComp = defineAsyncComponent(() => {
return import(/* webpackChunkName: 'async-comp' */ './async-comp.vue');
});

上述程序的执行可以得到一个AsyncComp的包装器组件,这个包装器组件会在组件被渲染时被解析,并且会在组件被渲染时被解析,这样就可以在组件被渲染时加载相关的组件

当然异步操作一定会涉及到加载和错误状态的处理,因此defineAsyncComponent()也支持处理这些状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),

// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,

// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})

自定义指令

先看一个例子

1
2
3
4
5
6
7
8
9
10
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>

<template>
<input v-focus />
</template>

上述代码执行的效果就是会在页面加载时自动聚焦到input,而focue就是我们自定义的指令,这会比autofocus更有用,因为它可以在任何你需要的时候运行

所谓自定义指令的目的就是为了重用涉及到元素底层DOM访问的逻辑。

自定义指令在模版中使用前必须提前进行注册,即使用directives选项完成注册,在<script setup>中任何以v开头的驼峰式命名的变量都可以被用作自定义指令,即vFocus在模版中可以v-focus的方式使用

1
2
3
4
5
6
7
8
9
10
11
export default {
setup() {
/*...*/
},
directives: {
// 在模板中启用 v-focus
focus: {
/* ... */
}
}
}

也可以通过以下方法将自定义指令注册在全局

1
2
3
4
5
6
const app = createApp({})

// 使 v-focus 在所有组件中都可用
app.directive('focus', {
/* ... */
})

指令钩子

一个指令的定义对象可以有如下钩子可供选择:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节

},
// 在元素被插入到 DOM 前调用
beforeMount() {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted() {},
// 绑定元素的父组件更新前调用
beforeUpdate() {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated() {},
// 绑定元素的父组件卸载前调用
beforeUnmount() {},
// 绑定元素的父组件卸载后调用
unmounted() {}
}
}

钩子参数如下:

  • el:指令绑定到的元素。这可以用于直接操作 DOM
  • binding:一个对象,包含以下 property。
    value:传递给指令的值
    oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用
    arg:传递给指令的参数
    modifiers:一个包含修饰符的对象
    instance:使用该指令的组件实例
    dir:指令的定义对象
  • vnode:代表绑定元素的底层VNode
  • prevNode:之前的渲染中代表指令所绑定元素的 VNode。仅在 beforeUpdateupdated 钩子中可用。

简化形式

如果在自定义指令中需要在mountedupdated上实现相同的行为,且无需定义在其他钩子中的事件时,可以进行如下操作

1
2
3
4
5
6
7
8
9
<template>
<div v-color="color"></div>
</template>
<script setup>
app.directive('color', (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
el.style.color = binding.value
});
</script>

内置组件

Teleport·传送门

<Teleport> 是一个内置组件,使我们可以将一个组件的一部分模板“传送”到该组件的 DOM 层次结构之外的 DOM 节点中。

<Teleport> 最好的实例就是模态框的实现,<Teleport :to="body">可以将标签下的元素传送到body上,它只会改变渲染的DOM结构,并不会改变