一、组件
1、自定义组件
1、全局注册
- 全局注册通过
javascript
import MyComponent from './App.vue'
app.component('MyComponent', MyComponent)
- 全局注册使用方便,但有两个缺点:如果未使用此组件,仍然会进行打包,无法自动移除(tree-shaking);依赖关系不明确,影响长期维护
2、局部注册
- 局部注册通过父组件引用子组件,
<script setup>
的单文件组件中,导入的组件可以直接在模板中使用,无需注册;其他需要用components
选项来显式注册。
xml
<script setup>
import ComponentA from './ComponentA.vue'
</script>
<template>
<ComponentA />
</template>
javascript
import ComponentA from './ComponentA.js'
export default {
components: {
ComponentA
},
setup() {
// ...
}
}
3、命名
命名使用PascalCase,模板中可以使用PascalCase或kebab-case
2、组件间传值方式?
1、父传子props
-
父传子,使用props,单向数据流 ,修改父组件的内容,子组件会同步。如果子组件需要修改父组件传过来的值,可以将props的值先赋值给其他变量,再进行修改;如果需要进一步转换prop,可以使用computed计算属性来更改
-
在
<script setup>
中使用defineProps定义prop,其他使用props,setup(props)
传入prop -
推荐prop写法:kebab-case写法,为了和 HTML attribute 对齐,使用驼峰命名法没有太大优势
-
prop中类型声明为Boolean时,可以将值省略,例如
xml
defineProps({
disabled: Boolean
})
<!-- 等同于传入 :disabled="true" -->
<MyComponent disabled />
<!-- 等同于传入 :disabled="false" -->
<MyComponent />
2、子传父 $emits
- 监听事件,子传父,使用$emits,子组件向父组件传递信息,组件触发的事件没有冒泡机制
- 在
<script setup>
中使用defineEmits定义emit,其他使用emits,setup(props,ctx)
第二个参数为ctx.emit('事件名')或者setup(props,{emit})
解构赋值出emit,添加事件参数,$emit('事件名', 参数1, 参数2)
js
<!-- 父组件 -->
<template>
<div>订单信息:{{text}}</div>
<div class="app">
<v-text
v-bind:text="text"
v-on:onChangeText="onChangeText"
/>
</div>
</template>
<script setup >
import { ref } from 'vue';
import VText from './text.vue'
const text = ref('环城东路888号');
const onChangeText = (newText) => {
text.value = newText;
}
</script>
js
<!-- 子组件 -->
<template>
<div class="v-text">
<span>地址:</span>
<input :value="props.text" @input="onInput" />
</div>
</template>
<script setup>
const props = defineProps({
text: String,
});
const emits = defineEmits(['onChangeText'])
const onInput = (e) => {
emits('onChangeText', e.target.value)
}
</script>
3、父组件获取子组件内容$children
、ref
- 一般不用$children,因为不能保证顺序,需要使用下标值来取值,要是改变需求时就需要经常改动,不方便
- 组件上的ref,可以在父组件中调用子组件的内容,在
<script setup>
中访问是私有的,默认访问不到子组件的内容,子组件需要使用defineExpose
暴露出来;其他情况是可以直接访问
4、子组件获取父组件内容$parent
、$root
一般不用$parent
,因为在开发中,一个子组件可能有好几个父组件,使用$parent
耦合性太高,所以一般不使用;可以使用$root
来访问根组件的实例
5、兄弟,多个组件依赖注入provide
、inject
1. 依赖注入是什么?
当有多个深层组件时,祖先组件-父组件-子组件,想从子组件中获取到祖先组件的数据,这时候可以使用props来一层一层传递,在这里边父组件可能就是充当一个传递的功能,并不会用到要传递的数据,这就形成了'prop透传问题',可以使用依赖注入进行传递
2. 依赖注入用法?
-
provide有两个参数,第一个是名称 ,可以是字符串或者
Symbol
;第二个是数值 ,可以是任何类型,包括响应式。在大型项目或者编写插件提供给其他使用者时,一般使用Symbol
。provide可以有局部,也可以有全局(应用层) -
inject注入参数,获取到祖先组件中的数据,inject第一个参数是provide提供的参数名;如果不用provide提供值,可以只在inject中写参数名,但是需要第二个参数加上默认值,否则会报错
-
第二个参数是响应式时,尽量将响应式数据放在provide提供方,这样方便后续管理;如果inject注入方需要修改数据,可以在provide提供方编写一个修改数据的方法;如果不想inject注入方修改传入的值,可以传入只读属性的provide
祖先组件:
xml
<script setup>
import { ref, provide } from 'vue'
import Child from './Child.vue'
const message = ref('hello')
provide('message', message)
</script>
<template>
<input v-model="message">
<Child />
</template>
父组件Child.vue:
xml
<script setup>
import GrandChild from './GrandChild.vue'
</script>
<template>
<GrandChild />
</template>
子组件GrandChild.vue:
xml
<script setup>
import { inject } from 'vue'
const message = inject('message')
</script>
<template>
<p>
Message to grand child: {{ message }}
</p>
</template>
6、使用第三方库pinia
使用Vue3中基于Proxy实现的公共状态管理工具Pinia,读数据使用内置的getter和state属性;写数据使用action方法 。使用Pinia需要在挂载DOM实例前引入
javascript
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './app.vue';
const app = createApp(App);
const pinia = createPinia();
// 加载pinia插件
app.use(pinia);
app.mount('#app');
例如实现:
Pinia 定义的独立 store 文件:
javascript
import { defineStore } from 'pinia';
export const useMyStore = defineStore('my-store', {
state: () => ({
text: '环城东路888号',
list: [
{ name: '苹果', price: 20, count: 0 },
{ name: '香蕉', price: 12, count: 0 },
{ name: '梨子', price: 15, count: 0 },
]
}),
getters: {
totalPrice(state) {
let total = 0;
state.list.forEach((item) => {
total += item.price * item.count;
});
return total;
},
},
actions: {
updateText(text) {
this.text = text;
},
increase(index) {
this.list[index].count += 1;
},
decrease(index) {
if (this.list[index].count > 0) {
this.list[index].count -= 1;
}
}
}
})
父组件(最外层组件):
xml
<template>
<div class="app">
<v-info />
<v-text />
<v-list/>
</div>
</template>
<script setup >
import { reactive } from 'vue';
import VInfo from './components/info.vue'
import VText from './components/text.vue'
import VList from './components/list.vue';
</script>
子组件(订单显示信息):
xml
<template>
<div class="v-info">
<div>订单信息:</div>
<div>收货地址:{{myStore.text}}</div>
<div>总金额:<span class="v-info-value">{{myStore.totalPrice}}</span></div>
</div>
</template>
<script setup>
import { useMyStore } from '../store';
const myStore = useMyStore();
</script>
子组件(文本编辑器):
xml
<template>
<div class="v-text">
<span>地址:</span>
<input :value="myStore.text" v-on:input="onInput" />
</div>
</template>
<script setup >
import { useMyStore } from '../store';
const myStore = useMyStore();
const onInput = (e) => {
myStore.updateText(e.target.value);
}
</script>
子组件(商品规格选择器):
xml
<template>
<div class="v-list">
<div class="v-list-item" v-for="(item, index) in myStore.list">
<span class="text">{{item.name}}</span>
<span class="text">单价: {{item.price}}</span>
<button class="btn" v-on:click="myStore.decrease(index)">-</button>
<span class="count"> {{item.count}}</span>
<button class="btn" v-on:click="myStore.increase(index)">+</button>
</div>
</div>
</template>
<script setup>
import { useMyStore } from '../store';
const myStore = useMyStore();
</script>
7、pinia与vuex区别?
pinia与vuex的区别:
-
包含内容:pinia没有mutation,只有getter,state,action来读写数据;vuex需要通过getter,state,mutation,action来读写数据,其中还需要dispatch来分发
-
写法:pinia的写法简单;vuex需要dispatch;pinia没有modules配置,每个独立仓库都是defineStore出来的
-
存储方式:pinia默认存于内存中,如果要本地存储,写法比vuex复杂
-
关于TS:与在 Vuex 中添加 TypeScript 相比,添加 TypeScript 更容易
-
优缺点:pinia不能用于时间旅行和编辑功能;vuex可以
-
用法:pinia体积小,适合中小型项目,管理简单;vuex适用于大型项目
3、透传attributes
1、什么是透传?
-
子组件是单根节点,父组件中除了props,emits外的所有属性,例如class,style,v-on等属性可以向下透传到子组件的根节点上
-
如果是多根节点,不会自动透传,
$attrs
没有被显式绑定,将会抛出一个运行时警告
2、透传的写法?
- 会自动透传到根节点中,比如 父组件:
ini
<MyButton class="large" />
子组件:
xml
<!-- <MyButton> 的模板 -->
<button>click me</button>
DOM渲染结果:
arduino
<button class="large">click me</button>
- 如果不想透传到根节点中,先禁用透传
inheritAttrs: false
,再使用$attrs
显示绑定到想要的元素上,例如透传到button上
父组件:
ini
<MyButton class="large" />
子组件:
xml
<div class="btn-wrapper">
<button class="btn" v-bind="$attrs">click me</button>
</div>
<script setup>
defineOptions({
inheritAttrs: false
})
// ...setup 逻辑
</script>
没有参数的 v-bind
会将一个对象的所有属性都作为 attribute 应用到目标元素上。
DOM渲染结果:
arduino
<button class="btn large">click me</button>
- 使用js写法:
xml
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
javascript
export default {
setup(props, ctx) {
// 透传 attribute 被暴露为 ctx.attrs
console.log(ctx.attrs)
}
}
4、插槽slot
1、slot是什么?
slot插槽是在子组件中预留位置,可以让父组件向子组件传递数据,数据可以是字符串内容,可以是HTML元素等
2、slot用法?
-
父组件传入的内容会渲染到子组件中,子组件的
<slot></slot>
是出口 -
渲染作用域:因为是在父组件中填写内容,所以作用域是父组件,默认情况下,不能获取到子组件的内容
-
默认插槽 :子组件
slot
中,默认写的内容,如果父组件不传递内容,会默认展出子组件内容 -
具名插槽 :子组件中有多个插槽的时候,父组件向子组件传递,不知道需要传递到那个里边,这时候就需要有个命名的插槽,name具名插槽,父组件传入
v-slot:header(子组件slot的name)
,简写#header
。当子组件中默认插槽与具名插槽一起使用时,父组件传入默认插槽内容使用#default
xml
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<template #default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<!-- 或者隐式的默认插槽 -->
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template #footer>
<p>Here's some contact info</p>
</template>
</BaseLayout>
- 动态插槽名:动态插槽
xml
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>
- 作用域插槽:上边说了,默认情况下的作用域是父组件,如果想用子组件的数据的话,也可以实现,使用作用域插槽,获取子组件的数据
父组件:
xml
<script setup>
import FancyList from './FancyList.vue'
</script>
<template>
<FancyList :api-url="url" :per-page="10">
<template #item="{ body, username, likes }">
<div class="item">
<p>{{ body }}</p>
<p class="meta">by {{ username }} | {{ likes }} likes</p>
</div>
</template>
</FancyList>
</template>
子组件:
xml
<script setup>
import { ref } from 'vue'
const props = defineProps(['api-url', 'per-page'])
const items = ref([])
// mock remote data fetching
setTimeout(() => {
items.value = [
{ body: 'Scoped Slots Guide', username: 'Evan You', likes: 20 },
{ body: 'Vue Tutorial', username: 'Natalia Tepluhina', likes: 10 }
]
}, 1000)
</script>
<template>
<ul>
<li v-if="!items.length">
Loading...
</li>
<li v-for="item in items">
<slot name="item" v-bind="item"/>
</li>
</ul>
</template>
3、slot原理?
子组件实例化时,获取到父组件内容存放到$slot
中,等遇到slot
,将$slot
替换到slot
中;也可以理解为,调用一个传参的函数,返回slot中内容
javascript
MyComponent({
// 类比默认插槽,将其想成一个函数
default: (slotProps) => {
return `${slotProps.text} ${slotProps.count}`
}
})
function MyComponent(slots) {
const greetingMessage = 'hello'
return `<div>${
// 在插槽函数调用时传入 props
slots.default({ text: greetingMessage, count: 1 })
}</div>`
}
5、动态组件
1、动态组件是什么?
在项目中经常会遇到,切换tab键,这个时候可以获取动态组件,根据切换值渲染不同的组件,被切换时,组件会被卸载,想保持存活状态,可以使用keepAlive
2、动态组件用法?
通过is属性
xml
<!-- currentTab 改变时组件也改变 -->
<component :is="tabs[currentTab]"></component>
6、异步组件
1、异步组件是什么?
- 当需要从服务器加载组件时,就需要使用异步组件,使用
defineAsyncComponent
来注册组件,返回一个Promise函数,满足需求,使用resolve引入组件,错误的话也可以使用错误的组件;
javascript
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve('./components/MyComponent.vue')
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
ES动态模块
导入也是返回Promise函数,所以就可以直接使用ES
引入
javascript
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
二、组合式函数
1、组合式函数
1、是什么?
我们经常会封装一个组件,用来复用这个组件,这时候,可以将封装的组件内容,写为一个组合式函数;组合式函数主要是返回有状态逻辑的函数(有状态逻辑是时间改变会改变状态,比如移动鼠标值改变);组合式函数重点在于逻辑,封装的复用组件会包含样式的内容
2、组合式函数写法?
- 一般定义一个js或者ts文件
- 导出一个函数,函数命名以use开头,使用驼峰命名
javascript
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'
// 按照惯例,组合式函数名以"use"开头
export function useMouse() {
// 被组合式函数封装和管理的状态
const x = ref(0)
const y = ref(0)
// 组合式函数可以随时更改其状态。
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// 一个组合式函数也可以挂靠在所属组件的生命周期上
// 来启动和卸载副作用
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 通过返回值暴露所管理的状态
return { x, y }
}
3、异步状态
当请求异步数据时,需要处理异步数据,经常有加载中,加载完成,加载失败的状态,每次都处理会有点繁琐,这时候就可以使用组合式函数
javascript
// fetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
fetch(url)
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
return { data, error }
}
xml
<script setup>
import { useFetch } from './fetch.js'
const { data, error } = useFetch('...')
</script>
参数也可以传入响应式参数,或者getter函数,这样的话就可以使用watchEffect、toValue来处理响应式参数与getter函数
toValue是将响应式,getter进行规范化值,是响应式的话,会返回响应式值,getter函数的话会调用函数,返回函数的返回值。注意 toValue(url)
是在 watchEffect
回调函数的 内部调用的。这确保了在 toValue()
规范化期间访问的任何响应式依赖项都会被侦听器跟踪。
javascript
// fetch.js
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const fetchData = () => {
// reset state before fetching..
data.value = null
error.value = null
fetch(toValue(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}
watchEffect(() => {
fetchData()
})
return { data, error }
}
4、使用限制
只能在<script setup>
或者setup()
中使用,也必须是同步调用
5、抽取为组合式函数的好处?
- 可以复用代码,直接调用文件就可以
- 代码结构方面,拆分为更小的函数,方便管理理解
6、与mixin对比?
- mixin是vue2中的函数,在vue3中虽然还保留,但是不推荐使用,只是为了兼容vue2
- mixin的数据来源不清晰,组合式函数只要ref+解构就可以;命名可能会有冲突,组合式函数可以通过解构变量重新命名;隐式的耦合在一起,组合式函数可以将结果作为另一个组合式函数的参数传递,跟普通函数一样
7、与无渲染组件对比?
- 无渲染组件:额外的组件会有实例开销,进而影响性能开销;复用逻辑+视图布局使用无渲染组件
- 组合式函数:则不会有额外实例开销;只包含逻辑,视图布局给消费组件
2、组合式API与其他对比?
1、是什么?
组合式API是api的合集,包含响应式:ref,reactive,计算属性,侦听器;生命周期:onMounted等;
2、为什么要用组合式API?
- 更好的复用逻辑:可以使用组合式函数,复用各个逻辑功能,通过逻辑组合到一起,避免了vue2中mixin的问题
- 更好的代码结构:使用选项式API或者vue2时,一个功能的代码会分散到各个钩子中,经常需要上下滚动来查看逻辑代码,很不方便;使用组合式,一个功能的代码会放到同一区域,在大型项目中,如果重构的话,可以直接提取功能代码,不用再重新组织结构,降低了重构成本,在维护代码方面很重要
- 打包为更小的体积:组合式API一般与
<script setup>
一起使用,<script setup>
中可以压缩变量名,如果是选项式的话,需要通过this访问下上文对象属性,但是对象属性名不能被压缩 - 更好的类型推导:与ts使用方便,组合式API使用ts与js差别不大
3、与选项式API对比?
- 选项式API规定了代码放置位置,组合式API更灵活,并且更适用于js的写法
- 组合式API是否覆盖了所有场景:能够覆盖所有状态逻辑方面的内容
- 同一个组件可以使用两种API吗:可以在选项式API中通过
setup()
使用组合式API,但是不提倡,除非是原旧项目想添加组合式内容 - 选项式API会被废弃吗:不会,还有一部分人喜欢使用选项式API
4、与React Hooks对比?
-
组合式函数:灵感来自React Hooks,逻辑方面也相似。
<script setup>
只会执行一次,与函数一样,不用担心闭包问题,并且可以有条件的调用- 计算属性,侦听器可以直接使用,vue组合式函数会满足,无需手动声明依赖
- 不用手动缓存回调函数来避免不必要的组件更新,大多数情况下,vue响应式系统仅执行必要的更新
-
React Hooks:
- 只能按顺序执行,不能写在条件分支中
- 变量会被一个钩子函数闭包捕获,如果传入了错误的依赖数组,会"过期"。这样得非常依赖Eslint,但是边缘问题会经常产生错误内容
- 昂贵计算的话,需要useMemo,需要传入正确依赖数组
- 传递给子组件事件处理函数会导致子组件进行不必要的更新,这也需要传入正确的依赖数组
三、指令
1、自定义指令
1、为什么要自定义指令?
重用代码的方式有:组件
和 组合式函数
。组件 主要是为了构建模块;组合式函数 是为了复用有状态的逻辑内容。自定义指令 是为了复用 只能通过操作DOM来实现的 普通元素 ,比如,autofocus
在vue动态插入元素后不会触发,而自定义的自动聚焦 vFocus 就可以触发
2、自定义指令是什么样的?局部指令,全局指令?
自定义指令是一个包含类似组件生命周期钩子的对象。在 vue 中以 v
开头的驼峰命名都是自定义指令。
- 自定义局部指令
xml
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
- 自定义全局指令
less
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', {
/* ... */
})
3、自定义指令的钩子函数?有哪些周期?
javascript
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
一个很常见的情况是仅仅需要在 mounted
和 updated
上实现相同的行为,除此之外并不需要其他钩子
css
<div v-color="color"></div>
app.directive('color', (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
el.style.color = binding.value
})
4、指令也可以接收任何合法的 JavaScript 表达式
less
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
app.directive('demo', (el, binding) => {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
})
5、不推荐在组件上使用自定义指令
四、插件
1、插件
1、为什么要自定义插件?组件和插件的区别?
插件是为vue添加全局功能的工具代码,与组件不同的是,组件需要每次引入,而插件一般情况引入一次就行;组件一般是依赖于项目的,而插件定义好后可以在不同项目中引入安装,开箱即用;自定义插件的过程其实是理解vue实例化的过程。
2、插件的使用范围?
插件没有明确的使用范围,一般可以使用3种方式进行注册:
app.component()
或者app.directive()
注册全局组件或指令app.provide()
与app.inject()
通过依赖注入到整个应用app.config.globalProperties
通过全局属性/方法,多个插件同时用全局属性方法时,很容易让应用变得难以理解和维护
3、自定义插件写法?
- 插件是可以包含
install()
方法的对象,也可以是安装函数本身
例如自定义一个国际化:
- 单独js文件
javascript
// plugins/i18n.js
export default {
install: (app, options) => {
// 注入一个全局可用的 $translate() 方法
app.config.globalProperties.$translate = (key) => {
// 获取 `options` 对象的深层属性
// 使用 `key` 作为索引
return key.split('.').reduce((o, i) => {
if (o) return o[i]
}, options)
}
}
}
- 引入文件
css
import i18nPlugin from './plugins/i18n'
app.use(i18nPlugin, {
greetings: {
hello: 'Bonjour!'
}
})
- 使用插件
bash
<h1>{{ $translate('greetings.hello') }}</h1>
4、vue.use()是什么,怎么用?注意点及场景?
vue.use() 是将vue进行实例化,