Vue Basic Notes
Directives
Control Flow Directives
<template>
<p v-if="isShow">Show</p>
<p v-if="isEnabled">Enabled</p>
<p v-else>Disabled</p>
<p v-if="inventory > 10">In Stock</p>
<p v-else-if="inventory <= 10 && inventory > 0">Almost Sold Out</p>
<p v-else>Out of Stock</p>
<ul>
<!-- list -->
<li v-for="item in items" :key="item.id">{{ item.message }}</li>
<li v-for="(item, index) in items">{{ item.message }} {{ index }}</li>
<!-- destructed list -->
<li v-for="{ message } in items">{{ message }}</li>
<li v-for="({ message }, index) in items">{{ message }} {{ index }}</li>
<!-- nested list -->
<li v-for="item in items">
<span v-for="childItem in item.children">
{{ item.message }} {{ childItem }}
</span>
</li>
<!-- iterator list -->
<li v-for="item of items">{{ item.message }}</li>
<!-- object list -->
<li v-for="(value, key, index) in myObject">
{{ index }}. {{ key }}: {{ value }}
</li>
<!-- range list -->
<li v-for="n in 10">{{ n }}</li>
</ul>
</template>
Prefer v-show
if you need to toggle something very often (display: none
),
and prefer v-if
if the condition is unlikely to change at runtime (lifecycle called).
- 不要把
v-if
和v-for
同时用在同一个元素上, 会带来性能方面 的浪费, 且有语法歧义: Vue 2.x 中v-for
优先级高于v-if
, Vue 3.x 中v-if
优先级高于v-for
. - 外层嵌套 template (页面渲染不生成 DOM 节点),
在这一层进行
v-if
判断, 然后在内部进行v-for
循环.
// Vue 2.x: compiler/codegen/index.js
export function genElement(el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre;
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state);
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state);
} else if (el.for && !el.forProcessed) {
return genFor(el, state);
} else if (el.if && !el.ifProcessed) {
return genIf(el, state);
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0';
} else if (el.tag === 'slot') {
return genSlot(el, state);
} else {
// component or element
}
}
<template v-if="isShow"><p v-for="item in items"></p></template>
Attributes Binding Directives
<template>
<a :href="url">Dynamic Link</a>
<img :src="link" :alt="description" />
<button :disabled="item.length === 0">Save Item</button>
</template>
Class and Style Binding Directives
- Static class.
- Array binding.
- Object binding.
<script setup lang="ts">
const isActive = ref(true);
const hasError = ref(false);
const activeClass = ref('active');
const errorClass = ref('text-danger');
const classObject = reactive({
active: true,
'text-danger': false,
});
const classObject = computed(() => ({
active: isActive.value && !error.value,
'text-danger': hashError.value,
}));
const activeColor = ref('red');
const fontSize = ref(30);
const styleObject = reactive({
color: 'red',
fontSize: '13px',
});
</script>
<template>
<div class="static"></div>
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<div :class="[isActive ? activeClass : '', errorClass]"></div>
<div :class="[{ active: isActive }, errorClass]"></div>
<div :class="classObject"></div>
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<div :style="[baseStyles, overridingStyles]"></div>
<div :style="styleObject"></div>
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
</template>
Event Handlers Directives
Event Handlers and Modifiers
<div id="handler">
<button @click="warn('Warn message.', $event)">Submit</button>
<button @click="one($event), two($event)">Submit</button>
<!-- 阻止单击事件继续传播 -->
<a @click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form @submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a @click.stop.prevent="doThat"></a>
<!-- 只有修饰符 -->
<form @submit.prevent></form>
<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理, 然后才交由内部元素进行处理 -->
<div @click.capture="doThis">...</div>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div @click.self="doThat">...</div>
<!-- 点击事件将只会触发一次 -->
<a @click.once="doThis"></a>
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成 -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div @scroll.passive="onScroll">...</div>
<input @keyup.enter="submit" />
<input @keyup.tab="submit" />
<input @keyup.delete="submit" />
<input @keyup.esc="submit" />
<input @keyup.space="submit" />
<input @keyup.up="submit" />
<input @keyup.down="submit" />
<input @keyup.left="submit" />
<input @keyup.right="submit" />
<input @keyup.page-down="onPageDown" />
<input @keyup.ctrl.enter="clear" />
<input @keyup.alt.space="clear" />
<input @keyup.shift.up="clear" />
<input @keyup.meta.right="clear" />
<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>
<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button>
<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>
<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button>
<button @click.left="onClick">Left click</button>
<button @click.right="onClick">Right click</button>
<button @click.middle="onClick">Middle click</button>
</div>
Vue.createApp({
methods: {
warn(message, event) {
if (event) event.preventDefault();
alert(message);
},
one(event) {
if (event) event.preventDefault();
console.log('one');
},
two(event) {
if (event) event.preventDefault();
console.log('two');
},
},
}).mount('#inline-handler');
Custom Events
Form events:
app.component('CustomForm', {
emits: {
// 没有验证
click: null,
// 验证 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true;
} else {
console.warn('Invalid submit event payload!');
return false;
}
},
},
methods: {
customEvent() {
this.$emit('custom-event');
},
submitForm(email, password) {
this.$emit('submit', { email, password });
},
},
});
<custom-form
@click="handleClick"
@submit="handleSubmit"
@custom-event="handleEvent"
></custom-form>
Drag and Drop events:
<!-- Drag.vue -->
<template>
<div
draggable="true"
@dragenter.prevent
@dragover.prevent
@dragstart.self="onDrag"
>
<slot />
</div>
</template>
<script>
export default {
props: {
transferData: {
type: Object,
required: true,
},
},
methods: {
onDrag(e) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.dropEffect = 'move';
e.dataTransfer.setData('payload', JSON.stringify(this.transferData));
},
},
};
</script>
<!-- Drop.vue -->
<template>
<div @dragenter.prevent @dragover.prevent @drop.stop="onDrop">
<slot />
</div>
</template>
<script>
export default {
methods: {
onDrop(e) {
const transferData = JSON.parse(e.dataTransfer.getData('payload'));
this.$emit('drop', transferData);
},
},
};
</script>
Model Directives
本质为语法糖 (v-model = v-bind + v-on
):
<input v-model="searchText" />
<input :value="searchText" @input="searchText = $event.target.value" />
checkbox
/radio
:checked
property and@change
event.- Multiple
checkbox
: value array. select
:value
property and@change
event.text
/textarea
:value
property and@input
event.- Child component:
- Default:
value
property and@input
event. - Use
options.model
on Child component to change defaultv-bind
andv-on
.
- Default:
<input v-model="message" placeholder="edit me" />
<textarea v-model="message" placeholder="add multiple lines"></textarea>
<input type="radio" id="one" value="One" v-model="picked" />
<input type="radio" id="two" value="Two" v-model="picked" />
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
<input type="checkbox" id="john" value="John" v-model="checkedNames" />
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
<select v-model="selected">
<option disabled value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<select v-model="selected" multiple>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<!-- Debounce -->
<input v-model.lazy="msg" />
<!-- 自动将用户的输入值转为数值类型 -->
<input v-model.number="age" type="number" />
<!-- 自动过滤用户输入的首尾空白字符 -->
<input v-model.trim="msg" /
Component v-model
directive:
<CustomInput v-model="searchText" />
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue']);
defineEmits(['update:modelValue']);
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<!-- CustomInput.vue -->
<script setup>
import { computed } from 'vue';
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
const value = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
},
});
</script>
<template>
<input v-model="value" />
</template>
Custom component v-model
name:
<!-- MyComponent.vue -->
<script setup>
defineProps(['title']);
defineEmits(['update:title']);
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
Custom component v-model
modifier:
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) },
});
const emit = defineEmits(['update:modelValue']);
function emitValue(e) {
let value = e.target.value;
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1);
}
emit('update:modelValue', value);
}
</script>
<template>
<input type="text" :value="modelValue" @input="emitValue" />
</template>
Custom Directives
Custom build-in directives:
<script setup lang="ts">
import { ref, vModelText } from 'vue';
vModelText.updated = (el, { value, modifiers: { capitalize } }) => {
if (capitalize && Object.hasOwn(value, 0)) {
el.value = value[0].toUpperCase() + value.slice(1);
}
};
const value = ref('');
</script>
<template>
<input v-model.capitalize="value" type="text" />
</template>
Custom new directives:
const vDirective = {
// 在绑定元素的 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) {},
};
Components
Computed Properties
<div id="computed-basics">
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</div>
Vue.createApp({
data() {
return {
author: {
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery',
],
},
};
},
computed: {
// 计算属性的 getter
publishedBooksMessage() {
// `this` 指向 vm 实例
return this.author.books.length > 0 ? 'Yes' : 'No';
},
fullName: {
// getter
get() {
return `${this.firstName} ${this.lastName}`;
},
// setter
set(newValue) {
const names = newValue.split(' ');
this.firstName = names[0];
this.lastName = names[names.length - 1];
},
},
},
}).mount('#computed-basics');
Slots
- Web Slot
name
attribute.fallback
content.- 插槽基本目的为自定义组件渲染细节: e.g 高级列表组件.
- Normal Slots: 在父组件编译和渲染阶段生成 Slots VNodes, 数据作用域为父组件实例 (使用插槽的组件), 即父组件同时提供 View 与 Data.
- Scoped Slots:
在父组件编译和渲染阶段为
vnode.data
添加scopedSlots
对象, 在子组件编译和渲染阶段生成 Slots VNodes, 数据作用域为子组件实例 (定义插槽的组件), 即父组件提供 View, 子组件提供 Data.
Fallback Slots
<!-- SubmitButton -->
<button type="submit">
<slot>Submit</slot>
</button>
<SubmitButton></SubmitButton>
render to
<button type="submit">Submit</button>
<SubmitButton>Save</SubmitButton>
render to
<button type="submit">Save</button>
Named Slots
#
:v-slot
directive shorthand.
<!-- Layout -->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<Layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<template v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</Layout>
Named slot directive shorthand:
<Layout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</Layout>
Scoped Slots
Pass data from child to parent
(like Render Props
in React):
app.component('TodoList', {
data() {
return {
items: ['Feed a cat', 'Buy milk'],
};
},
template: `
<ul>
<li v-for="( item, index ) in items">
<slot :item="item"></slot>
</li>
</ul>
`,
});
<TodoList>
<!-- `default` can be other named slots -->
<template v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</template>
</TodoList>
Slot props shorthand
(default
can be other named slots):
<TodoList v-slot="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</TodoList>
<TodoList v-slot="{ item }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</TodoList>
<TodoList v-slot="{ item: todo }">
<i class="fas fa-check"></i>
<span class="green">{{ todo }}</span>
</TodoList>
<TodoList v-slot="{ item = 'Placeholder' }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</TodoList>
<TodoList #default="{ item }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</TodoList>
Provide and Inject
Root provide
context value:
import { provide, ref } from 'vue';
const count = ref(0);
provide('key', count);
Child inject
context value:
import { inject } from 'vue';
const message = inject('message', defaultValue);
Composition API
Can't access to this
inside of setup
,
we cannot directly access this.$emit
or this.$route
anymore.
Setup Method
- Executes before
Components
,Props
,Data
,Methods
,Computed
properties,Lifecycle
methods. - Can't access
this
. - Can access
props
andcontext
:props
.context.attrs
.context.slots
.context.emit
.context.expose
.context.parent
.context.root
.
import { ref, toRefs } from 'vue';
// eslint-disable-next-line import/no-anonymous-default-export
export default {
setup(props, { attrs, slots, emit, expose }) {
const { title } = toRefs(props);
const count = ref(0);
const increment = () => ++count.value;
console.log(title.value);
return { title, increment };
},
};
Composition LifeCycle Hooks
onBeforeMount
.onMounted
.onBeforeUpdate
.onUpdated
.onBeforeUnmount
.onUnmounted
.onErrorCaptured
.onRenderTracked
.onRenderTriggered
.onActivated
.onDeactivated
.
beforeCreate
-> setup
-> created
:
No need for onBeforeCreate
and onCreated
hooks,
just put code in setup
methods.
Reactivity
Reactive Value
import { reactive, toRefs } from 'vue';
const book = reactive({
author: 'Vue Team',
year: '2020',
title: 'Vue 3 Guide',
description: 'You are reading this book right now ;)',
price: 'free',
});
const { author, title } = toRefs(book);
title.value = 'Vue 3 Detailed Guide';
console.log(book.title); // 'Vue 3 Detailed Guide'
Ref Value
ref
API:
import type { Ref } from 'vue';
import { isRef, reactive, ref, toRef, unref } from 'vue';
const count = ref(10);
const state = reactive({
foo: 1,
bar: 2,
});
const fooRef = toRef(state, 'foo');
console.log(isRef(count));
console.log(unref(count) === 10);
fooRef.value++;
console.log(state.foo === 2);
state.foo++;
console.log(fooRef.value === 3);
toRef
/toRefs
:
function toRef(reactive, key) {
const wrapper = {
get value() {
return reactive[key];
},
set value(val) {
reactive[key] = val;
},
};
Object.defineProperty(wrapper, '__v_isRef', {
value: true,
});
return wrapper;
}
function toRefs(reactive) {
const refs = {};
for (const key in reactive) {
refs[key] = toRef(reactive, key);
}
return refs;
}
proxyRefs
(auto unref):
function proxyRefs(target) {
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
return result.__v_isRef ? result.value : result;
},
set(target, key, value, receiver) {
const result = target[key];
if (result.__v_isRef) {
result.value = value;
return true;
}
return Reflect.set(target, key, value, receiver);
},
});
}
当一个 ref
被嵌套在 一个响应式对象中作为属性被访问或更改时,
会自动解包 (无需使用 .value
):
const count = ref(0);
const state = reactive({
count,
});
console.log(state.count); // 0
state.count = 1;
console.log(count.value); // 1
当 ref
作为响应式数组或 Map
原生集合类型的元素被访问时,
不会进行解包 (需要使用 .value
):
const books = reactive([ref('Vue 3 Guide')]);
console.log(books[0].value);
const map = reactive(new Map([['count', ref(0)]]));
console.log(map.get('count').value);
Computed Value
计算属性的计算函数应只做计算而没有任何其他 的副作用, 不要在计算函数中做异步请求或者更改 DOM:
const count = ref(1);
const plusOne = computed(() => count.value + 1);
console.log(plusOne.value); // 2
plusOne.value++; // error