Vue.js 小记
目录
Import maps
import { createApp } from 'vue'
导入映射表 (Import Maps)用于在浏览器中更好地管理 JavaScript 模块的导入路径。
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<div id="app">{{ message }}</div>
<script type="module">
import { createApp, ref } from 'vue'
createApp({
setup() {
const message = ref('Hello Vue!')
return {
message
}
}
}).mount('#app')
</script>
上面的代码中,导入的vue模块路径被映射到一个CDN地址。
全局错误捕获
应用实例会暴露一个 .config
对象允许我们配置一些应用级的选项,例如定义一个应用级的错误处理器,用来捕获所有子组件上的错误:
app.config.errorHandler = (err) => {
/* 处理错误 */
}
绑定
<div v-bind:id="id"></div>
<div :id="id"></div>
<div :id></div>
<div v-bind:id></div>
上面四种写法的效果是一样的。
动态绑定多个值:
const objectOfAttrs = {
id: 'container',
class: 'wrapper',
style: 'background-color:green'
}
注意:不带参数的 v-bind
<div v-bind="objectOfAttrs"></div>
受限的全局访问:
模板中的表达式将被沙盒化,仅能够访问到有限的全局对象列表。该列表中会暴露常用的内置全局对象,比如 Math
和 Date
。
const GLOBALS_ALLOWED =
'Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,' +
'decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,' +
'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol'
没有显式包含在列表中的全局对象将不能在模板内表达式中访问,例如用户附加在 window
上的属性。然而,你也可以自行在 app.config.globalProperties
上显式地添加它们,供所有的 Vue 表达式使用。
动态参数:
<a v-bind:[attributeName]="url"> ... </a>
<a v-on:[eventName]="doSomething"> ... </a>
attributeName
会作为一个 JavaScript 表达式被动态执行
动态参数中表达式的值应当是一个字符串,或者是 null
。特殊值 null
意为显式移除该绑定。其他非字符串的值会触发警告。
样式绑定
样式对象绑定:
const classObject = reactive({
active: true,
'text-danger': false
})
<div :class="classObject"></div>
或者绑定一个返回对象的计算属性
内联样式:
const activeColor = ref('red')
const fontSize = ref(30)
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
直接绑定一个样式对象
const styleObject = reactive({
color: 'red',
fontSize: '30px'
})
<div :style="styleObject"></div>
v-for中的key
Vue的DOM更新策略是就地更新,这意味着当通过V-for渲染元素后,当数据发生顺序变化,但是DOM不会发生变化,只会就地更新每个元素。
如果想让Vue跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key
属性
官网的一句话:默认模式是高效的,但只适用于列表渲染输出的结果不依赖子组件状态或者临时 DOM 状态 (例如表单输入值) 的情况。
"不依赖子组件状态" 的意思是:如果你列表中的每一个子组件实例都有自己独立的、内部维护的状态(比如表单输入值、组件内部的选中状态、展开/收起状态等),那么当列表顺序变化或有增删时,没有 key
会导致这些内部状态被错误地复用,从而出现 Bug。
"临时 DOM 状态" 的意思是:如果你列表中的元素是表单控件,并且用户可能直接在这些控件上进行输入或操作,那么当列表顺序变化或有增删时,没有 key
会导致 Vue 复用带有旧的临时 DOM 状态的元素,从而出现视图与数据不一致的问题。
Key的使用场景:渲染的列表项是动态的,即会发生增加,删除,重排等事件,并且这些列表项包括:子组件,表单元素等,就应该使用key
,并且确保 key
是每个列表项独一无二的标识。
推荐在任何可行的时候为 v-for
提供一个 key
attribute,除非所迭代的 DOM 内容非常简单 (例如:不包含组件或有状态的 DOM 元素),或者你想有意采用默认行为来提高性能。
事件
关于按钮修饰符可以直接使用 KeyboardEvent.key
暴露的按键名称作为修饰符,但需要转为 kebab-case 形式。
<input @keyup.page-down="onPageDown" />
侦听器
侦听器可以监听的数据源可以是:ref(包括计算属性),一个响应式对象、一个 getter 函数、或多个数据源组成的数组。
注意,你不能直接侦听响应式对象的属性值,需要写成getter函数:
// 提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`Count is: ${count}`)
}
)
深层侦听器:直接给 watch()
传入一个响应式对象,会隐式地创建一个深层侦听器,该回调函数在所有嵌套的变更时都会被触发:
相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时(比如整个对象被替换),才会触发回调,不过可以显式地加上 deep
选项,强制转成深层侦听器,deep
选项还可以是一个数字,表示最大遍历深度
在 setup()
或 <script setup>
中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。因此,在大多数情况下,你无需关心怎么停止一个侦听器。如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。如下方这个例子:
<script setup>
import { watchEffect } from 'vue'
// 它会自动停止
watchEffect(() => {})
// ...这个则不会!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
要手动停止一个侦听器,请调用 watch
或 watchEffect
返回的函数:
需要异步创建侦听器的情况很少,请尽可能选择同步创建。如果需要等待一些异步数据,你可以使用条件式的侦听逻辑:
// 需要异步请求得到的数据
const data = ref(null)
watchEffect(() => {
if (data.value) {
// 数据加载后执行某些操作...
}
})
ref
ref允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。
<script setup>
import { useTemplateRef, onMounted } from 'vue'
// 第一个参数必须与模板中的 ref 值匹配
const input = useTemplateRef('my-input')
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="my-input" />
</template>
组件
props
const props = defineProps({
title: {
rtype: String,
required: false,
default: 'Default Title'
}
})
// <demo title="my title" />
监听事件
子组件触发自定义事件,父组件监听对应事件
<demo @down="console.log('父组件监听到down事件')"/>
<button @click="$emit('down')">Click</button> <!-- 子组件点击触发 -->
用于<script setup>
中
const event = defineEmits(['down']);
const fun = () => {
event('down');
}
插槽
<demo>
<h2>Fly</h2>
</demo>
...
<slot>Default</slot>
...
动态组件
<!-- currentTab 改变时组件也改变 -->
<component :is="tabs[currentTab]"></component>
深入组件
全局注册的组件在生产打包的时候无法被 tree-shaking 优化掉。
在组件中只写key没有value(仅写上 prop 但不传值)会被隐式的转化为true
props遵循单向数据流,父组件向子组件传递数据时,子组件不能直接修改父组件的数据,如果你在子组件中去更改一个 prop,Vue 会在控制台上向你抛出警告:
如果父组件传入一个复合类型的 prop ,比如对象或者数组,那么子组件就可以修改并且不会触发警告,这是因为 JavaScript 的对象和数组是按引用传递,对 Vue 来说,阻止这种更改需要付出的代价异常昂贵,这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。
最佳实践是子组件抛出一个事件,父组件监听这个事件并在回调中修改数据。
$emit 校验:
<script setup>
const emit = defineEmits({
// 校验 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
})
function submitForm(email, password) {
emit('submit', { email, password })
}
</script>
为组件的 emits 标注类型(校验)https://cn.vuejs.org/guide/typescript/composition-api.html#typing-component-emits
组件 v-model
const model = defineModel()
// <Child v-model="countModel" />
defineModel
的主要应用场景是开发自定义表单控件或需要双向数据绑定的组件,功能上和原生的input
、select
等表单元素类似。
defineModel()
返回的值是一个 ref,它可以像其他 ref 一样被访问以及修改,不过它能起到在父组件和当前组件中变量之间的双向绑定的作用:
<script setup>
const model = defineModel()
</script>
<template>
<input v-model="model" />
</template>
<!-- <child v-model='count'/> -->
底层机制:
组件 v-model
本质上是 defineProps
和 defineEmits
的语法糖
<!-- Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
<!-- Parent.vue -->
<Child
:modelValue="foo"
@update:modelValue="$event => (foo = $event)"
/>
还可以通过给 defineModel
传递选项,来声明底层 prop 的选项:
const model = defineModel({ required: true })
const model = defineModel({ default: 0 })
参数:
<MyComponent v-model:title="bookTitle" />
将字符串作为第一个参数传递给 defineModel()
来支持相应的参数,额外的 prop 选项,应该在 model 名称之后传递,同样可以使用 defineModel 原理 的方式的写出。
const title = defineModel('title',{ required: true })
有了参数对应,就可以有多个v-model
绑定。
除了内除的修饰符,还可以在自定义组件的v-model
中自定义修饰符。
在子组件中通过解构defineModel
的返回值得到使用时的修饰符
const [model, modifiers] = defineModel()
可以给 defineModel()
传入 get
和 set
这两个选项,这两个选项在从模型引用中读取或设置值时会接收到当前的值
<script setup>
const [model, modifiers] = defineModel({
set(value) {
if (modifiers.capitalize) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
return value
},
get(val){
...
return val
}
})
</script>
<template>
<input type="text" v-model="model" />
</template>
上述的例子还能实现多v-model
,defineModel
的第一个参数是名称,第二个是处理选项
const [model, modifiers] = defineModel('...',{...})
prop的安全的默认值创建方式:default: () => ({ name: 'default' })
关于为什么默认值要使用工厂函数:
如果是一个原始类型可以直接写,但是如果是引用类型(对象,数组),需要用工厂函数返回对象,才能确保每个实例都会创建新对象,否则所有组件实例共享同一个对象引用。
默认值的工厂函数机制是为了保证数据实例的独立性,防止某些操作意外修改数据后影响了所有组件引用的数据(数据污染)。
透传 Attributes
Attributes 继承是指在组件系统中,当父组件将属性(attributes)传递给子组件时,如果子组件没有显式地声明或处理这些属性,那么这些属性就会被“继承”到子组件的根元素上。
如果一个子组件的根元素已经有了 class
或 style
attribute,它会和从父组件上继承的值合并。
<!-- <MyButton> 的模板 -->
<button>Click Me</button>
<MyButton class="large" />
最终渲染:
<button class="large">Click Me</button>
同样的规则也适用于 v-on
事件监听器。
如果一个组件在根组件上渲染另一个组件,那么它接收的 Attributes 会直接继续传给嵌套组件(组件内的组件)
注意:透传 Attributes 不会包含声明过的 props 或是针对 emits
声明事件的 v-on
侦听函数,换句话说,声明过的 props 和侦听函数被 <MyButton>
“消费”了。
如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false
,透传进来的 attribute 可以在模板的表达式中直接用 $attrs
访问到,除了被"消费"了的 attribute
<script setup>
defineOptions({
inheritAttrs: false
})
// ...setup 逻辑
</script>
更改透传属性的位置:
<div class="btn-wrapper">
<button class="btn" v-bind="$attrs">Click Me</button>
</div>
补充
无参数的 v-bind
会将一个对象的所有属性都作为 attribute 应用到目标元素上。
如果一个组件是多根节点,那么自动 attribute 透传行为是没有的,如果 $attrs
没有被显式绑定,将会抛出一个运行时警告。
如果需要在JavaScript中访问透传 attribute ,需要先从vue
中引入useAttrs
API。
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
Slots
当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template>
节点都被隐式地视为默认插槽的内容。
条件插槽:
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
动态插槽名:
<template v-slot:[dynamicSlotName]></template>
作用域插槽:
子组件将数据交给父组件,父组件进行结构设计后再传入子组件,也就是父组件在使用插槽的时候同时拥有当前组件数据和子组件数据
方法是像对组件传递 props 那样,向一个插槽的出口上传递 attributes:
<!-- <MyComponent> 的模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
通过子组件标签上的 v-slot
指令,直接接收到了一个插槽 props 对象:
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
具名作用域插槽:
<MyComponent>
<template #header="headerProps">
{{ headerProps }}
</template>
<template #default="defaultProps">
{{ defaultProps }}
</template>
<template #footer="footerProps">
{{ footerProps }}
</template>
</MyComponent>
向具名插槽中传入 props:
<slot name="header" message="hello"></slot>
注意:插槽上的 name
是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。
依赖注入
依赖注入::一个父组件向下“分发”数据,它的所有后代(无论隔几代)都可以“接收”这些数据。
一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
要为组件后代提供数据,需要使用到 provide()
函数:
<script setup>
import { provide } from 'vue'
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
provide()
函数接收两个参数。第一个参数被称为注入名,可以是一个字符串或是一个 Symbol
。后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide()
,使用不同的注入名,注入不同的依赖值。
第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref:
import { ref, provide } from 'vue'
const count = ref(0)
provide('key', count)
提供的响应式状态使后代组件可以由此和提供者建立响应式的联系。
应用层 Provide:除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖:
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
在应用级别提供的数据在该应用内的所有组件中都可以注入。
要注入上层组件提供的数据,需使用 inject()
函数:
<script setup>
import { inject } from 'vue'
const message = inject('message')
</script>
如果有多个父组件提供了相同键的数据,注入将解析为组件链上最近的父组件所注入的值。
如果注入名在祖先链上没有组件提供,可以声明一个默认值,否则会抛出一个运行时警告。
// 如果没有祖先组件提供 "message"
// `value` 会是 "默认值"
const value = inject('message', '默认值')
在一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用,我们可以使用工厂函数来创建默认值。第三个参数表示默认值应该被当作一个工厂函数。
const value = inject('key', () => new ExpensiveClass(), true)
当向后代提供数据时,建议尽可能将任何对响应式数据的变更都保持在供给方组件中,即传入数据的同时也传入改变数据的方法。
使用 Symbol 作注入名:在大型应用中会有很多的依赖提供者,建议最好使用 Symbol 来作为注入名以避免潜在的冲突。
推荐在一个单独的文件中导出这些注入名 Symbol:
// keys.js
export const myInjectionKey = Symbol()
// 在供给方组件中
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'
provide(myInjectionKey, {
/* 要提供的数据 */
})``
// 注入方组件
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'
const injected = inject(myInjectionKey)
异步组件
在大型项目中,为提高性能和优化加载速度,可以将应用拆分为更小的模块,并按需从服务器加载所需组件。Vue 提供了 defineAsyncComponent
方法,以实现组件的异步加载功能。。
并且在类似Vite这样的打包工具中,异步组件会被自动拆分成独立的代码块。
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
defineAsyncComponent
方法接收一个返回 Promise 的加载函数,而ESM 模块的 import()
函数正好符合这个要求:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
AsyncComp
是一个被包装过的组件,Vue 会在需要时自动加载它,它会将接收到的 props 和 插槽传递给内部的组件,所以可以使用异步组件去无替换原始组件,并且实现了懒加载。
组合式函数
组合式函数是一个用组合式API封装的可复用有状态逻辑的函数。
自定义指令
https://cn.vuejs.org/guide/reusability/custom-directives
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode) {}
}
钩子参数
el
:指令绑定到的元素。这可以用于直接操作 DOM。binding
:一个对象,包含以下属性。value
:传递给指令的值。例如在v-my-directive="1 + 1"
中,值是2
。oldValue
:之前的值,仅在beforeUpdate
和updated
中可用。无论值是否更改,它都可用。arg
:传递给指令的参数 (如果有的话)。例如在v-my-directive:foo
中,参数是"foo"
。modifiers
:一个包含修饰符的对象 (如果有的话)。例如在v-my-directive.foo.bar
中,修饰符对象是{ foo: true, bar: true }
。instance
:使用该指令的组件实例。dir
:指令的定义对象。
vnode
:代表绑定元素的底层 VNode。prevVnode
:代表之前的渲染中指令所绑定元素的 VNode。仅在beforeUpdate
和updated
钩子中可用。
插件
https://cn.vuejs.org/guide/reusability/plugins.html
Transition
在使用Transition组件时候如果没有指定name
属性,那么默认过渡Class类将以v
作为前缀。
触发条件:
- 由
v-if
所触发的切换 - 由
v-show
所触发的切换 - 由特殊元素
<component>
切换的动态组件 - 改变特殊的
key
属性
如果进入和离开的元素都是在同时开始动画的,那么不得不将它们设为 position: absolute
以避免二者同时存在时出现的布局问题。
然而,很多情况下这可能并不符合需求。我们可能想要先执行离开动画,然后在其完成之后再执行元素的进入动画。手动编排这样的动画是非常复杂的,好在我们可以通过向 <Transition>
传入一个 mode
prop 来实现这个行为。