重学vue


全局配置

app暴露了一些可以全局使用的属性

app.config.errorHandler可以捕获子组件中未被捕获的异常(这在前端异常监控中很有用 就像原生的 window.addEventListener('error', callback, true) | window.addEventListener('unhandledRejection', callback)

1
app.config.errorHandler = (err) => { //处理错误 }

app.config.globalProperties可以显式配置一些全局可用的属性(比如附加在window上的自定义属性啥的 模板内的表达式不能访问没有显式声明的全局属性)

1
app.config.globalProperties.window.ownPro = 'own'

app.component可以注册一些全局可用的资源 比如使用其注册一个全局可用的组件 可以链接调用

1
app.component('globalComponent', globalComponent)

所有这些配置都要在应用挂载前配置好(before app.mount

多个应用实例

可以使用 多个createVue创建多个 vue 实例 这在我们只想要用 vue 去实现应用的部分功能时很有用比如说使用 vue 来增强服务端渲染SSR (我们可以将多个实例分别挂载在对应的部分上 而不是全部挂载在同一个实例上)

1
2
const app1 = createVue(...); app1.mount('route1') const app2 = createVue(...);
app2.mount('route2')

模板语法

如果使用 jsx 在编译阶段不会像 template一样被底层优化(vue 在编译阶段会将 template模板转换为 ast 在将 ast 转换为对应的render渲染函数)

v-html可以将渲染html内容而不是转换为字符串(不能使用 v-html来拼接组合模板 因为 vue 不是依靠字符串来解析模板的)

1
<span v-html="<h1>this is html</h1>"></span>

v-html具有很大的安全隐患 容易造成xss攻击 应该在确保绝对安全的情况下再使用

动态绑定多个值

可以通过 v-bind不带参数来动态绑定多个值

1
2
const data = { name: 'james', age: 23 }
<Person v-bind="data"></Person>

绑定的表达式中的方法会在组件每次更新时被调用 所以这个方法不应该有副作用(比如改变数据或者触发一异步操作)

1
<Compo :func="funcWithoutEffect(agrs)"></Compo>

动态参数

1
2
3
4
const propName = 'href'
<div :[proName]="href">
//等价于 :href = 'href'
</div>

动态参数的值只能是字符串或者 nullnull表示移除这个attribute

建议使用计算属性来返回动态参数

响应式

当响应式状态(reactive state)被更新时 DOM并不会立马做更新操作 而是会缓存起来 等到下一次更新时一起更新(比如期间改变一百次 最后只会执行一次更新操作)如果想在state改变后立马用改变后的值进行某些操作 可以使用 nextTick()

1
2
3
4
5
6
7
8
<script setup>
import { reactive, nextTick } from "vue";
const data = reactive({ name: "james" });
const change = () => {
data.name = "curry";
nextTick(() => {});
};
</script>

响应式默认是深度响应(如果想要创建只有根部具有响应性的浅度响应对象 可以使用 shalldowReactive

响应式代理

响应式代理的本质是返回该对象的proxy(vue2 使用definePropertype vue3 使用proxy)这意味着如果对一个对象使用reactive() 则返回其本身的代理 对该对象进行任何改变都没有响应式 对代理对象才有 对一个已经被reactive()的对象再次进行 reactive() 返回该对象本身

1
2
3
4
5
6
7
8
9
//同一个对象使用reactive会返回相同对象
reactive(raw) === proxy; //true
//对proxy进行reactive返回本身
reactive(proxy) === proxy; //true
//对深层次的对象属性进行reactive依然会是poxy
const proxy = reactive({});
const raw = {};
proxy.raw = raw;
paoxy.raw === raw; // false

我们必须保持对响应性对象的相同引用才能够保持其响应性(这就是为啥解构会使其失去响应性)

1
2
3
4
const data = reactive({
name: "james",
});
const { name } = data; //失去了响应性

可以使用 toRefs()来使其保持响应性

ref()可以在被传递/解构时保持响应性 这使得他经常用于组合函数中(提高代码逻辑的复用性)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const name = ref("james");
const change = (name) => {
name.value = "curry";
};
change(name); //name = curry 如果传递一个普通字符串 则做不到
//写一个返回坐标的组合函数
export const useLocation = () => {
const x = ref(0);
const y = ref(0);
onMounted(() => {
computedLocation(x, y);
});
return { x, y };
};
//使用
const { x, y } = useLocation();

当一个ref被当成对象的属性时 会被自动解包

1
2
3
4
5
6
const conunt = ref(0);
const data = reactive({
count,
});
data.count = 1;
console.log(conunt.value); //1

将一个新的 ref 赋值给对象的属性 则旧的 ref 会被替换

当 ref 作为数组或者 map 的元素时 不会自动解包(map.get(key).value)

计算属性

vue 的计算属性会自动追踪响应式依赖(计算属性会基于其以来进行缓存 直到依赖发生改变时再重新计算)

  • computedgetter不应该有副作用的操作(异步请求数据或者改变 dom)
  • 永远不要去改变 computed 返回的值 把它当成一个可读不可写的数据 如果需要更改该数据 应该去改变其依赖的响应式数据

类与 style 绑定

可以给类绑定一个计算属性 这很有用

1
2
3
4
5
6
7
8
const isActive = ref(true);
const error = ref(null);
const classObject = computed(() => ({
active: isActive.value && !error.value,
'text-danger': error.value && error.value.type === 'fatal'
}))

<div :class="classObject"> </div>

在组件上使用

对于只有一个根元素的组件 当你是用了 class时 这些class会被添加到根元素上 并且与该元素上已有的class合并

1
2
3
4
5
6
7
8
9
10
11
12
//MyComponent.vue
<p class="child">
Child
</p>
//parent
<MyComponent class="parent"> //子组件将被渲染成 class="parent child"
</MyComponent>
//如果组件有多个根元素 那么需要指定接收的子组件根元素 使用$attrs接收
<MyComponent class="baz"></MyComponent>
<!-- MyComponent 模板使用 $attrs 时 -->
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>

自动前缀

当在 :style中是用了需要浏览器前缀的 CSS 属性时 vue绘自动加上对应的前缀(polyfill

如果给一个样式属性提供多个不同的前缀值 则只会渲染浏览器支持的最后一个值(在支持不需要特别前缀的浏览器中都会渲染为 display:flex

条件渲染

v-ifv-show

  • v-if在切换时 组件会进行重新创建和销毁
  • v-if是惰性的 初次渲染是false时不会做任何事 等到被改为true才会被渲染 v-show则无论初始条件是啥都会被渲染 只有 display属性会被切换
  • 如果需要频繁切换 则使用 v-show 否则使用 v-if

v-if优先级高于 v-for

可以在外层新包装一层 <template>再在上面使用 v-for来解决这个问题

1
2
3
4
5
<template v-for="todo in todos" :key="todo.id">
<li v-if="!todo.isComplete">
{{ todo.name }}
</li>
</template>

列表渲染

vue 可以监听响应式数组的变更方法并在这些方法被调用的时候更新示图

push | pop | shift | unshift | splice | sort | reverse

当你需要展示过滤/排序后的结果时又不想改变原始数据时 可以创建返回已经处理好的数组的计算属性

1
2
3
4
5
6
7
8
9
const numbers = ref([1, 2, 3, 4, 5]) const evenNumbers = computed(() => { return
numbers.value.filter((n) => n % 2 === 0) })
<li v-for="n in evenNumbers">{{ n }}</li>
//当计算属性不行时 可以使用嵌套的v-for来解决 const sets = ref([ [1, 2, 3, 4, 5],
[6, 7, 8, 9, 10] ]) function even(numbers) { return numbers.filter((number) =>
number % 2 === 0) }
<ul v-for="numbers in sets">
<li v-for="n in even(numbers)">{{ n }}</li>
</ul>

在计算属性中使用会改变原数组的方法(比如 reverse | sort)时 应该创建一个副本进行操作

1
2
3
- return numbers.reverse()
+ return [...numbers].reverse()

事件处理

在内联事件处理器中访问事件参数

应该传入 $event或者使用内联箭头函数

1
2
3
4
5
6
7
8
9
10
11
<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>

<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
Submit
</button>

const warn = (message, event) => { //.... }

事件修饰符

.(stop | prevent | self | capture | once | passive)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- 单击事件将停止传递 -->
<a @click.stop="doThis"></a>

<!-- 提交事件将不再重新加载页面 -->
<form @submit.prevent="onSubmit"></form>

<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>

<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>

<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>
<!-- 添加事件监听器时,使用 `capture` 捕获模式 -->
<!-- 例如:指向内部元素的事件,在被内部元素处理前,先被外部处理 -->
<div @click.capture="doThis">...</div>

<!-- 点击事件最多被触发一次 -->
<a @click.once="doThis"></a>

<!-- 滚动事件的默认行为 (scrolling) 将立即发生而非等待 `onScroll` 完成 -->
<!-- 以防其中包含 `event.preventDefault()` -->
<div @scroll.passive="onScroll">...</div>

.passive一般用于触摸事件的监听器 可以用来改善移动端的滚屏性能

不要同时使用 .passive.prevent 因为 .passive表示不阻止事件的默认行为 而 .prevent表示阻止事件的默认行为

.exact允许控制触发一个事件所需的确定组合的系统按键修饰符

表单输入绑定

修饰符

默认情况下 v-model会在每次 input事件后更新数据 可以添加 .lazy修饰符改为每次 change事件后更新数据

生命周期

watch

watch的第一个参数可以是 ref(包括计算属性)、响应式对象、getter、多个数组源的数组

不可以直接监听响应式对象的属性值 应该监听其getter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const obj = reactive({
count: 0,
});
//不要直接监听属性
watch(obj.count, (count) => {
//...
});
//应该直接监听getter
watch(
() => obj.count,
() => {
//...
}
);

watch监听响应式对象时会隐式创建一个深层监听器 如果监听的是getter 则只有在返回不同的对象时才会触发回调(可以使用 {deep: true}强制深层监听)

watchEffect()

watchEffect会立即执行一遍回调函数 如果这时函数产生了副作用 则 Vue 会自动追踪副作用的依赖关系并自动分析出响应源(watch是惰性的 只有在依赖源发生改变时才会触发)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//比如我们需要立即获取数据后并更具url改变来重新获取数据
const url = ref("https://...");
const data = ref(null);

async function fetchData() {
const response = await fetch(url.value);
data.value = await response.json();
}

// 立即获取
fetchData();
// ...再侦听 url 变化
watch(url, fetchData);
//可以直接使用watchEffect
watchEffect(async () => {
const response = await fetch(url.value);
data.value = await response.json();
});
//回调会立即执行并且自动最终到url.value为依赖

watchEffect尽在同步执行期间才追踪依赖 在异步回调时 只有在第一个await正常工作前访问到的属性才会被追踪

watch vs watchEffect

  • watch值追踪明确侦听的数据源 watch避免在发生副作用期间追踪依赖 可以更加精确地控制回调函数的触发时机
  • watchEffect会在副作用发生期间追踪依赖 会在同步执行过程中自动追踪到所有能访问到的响应式依赖 但是依赖关系不明确

执行时机

监听器的回调执行会在vue更新之前被调用 所以意味着我们在回到中访问到的DOMvue更新之前的状态

如果想要在访问更新之后的 DOM 可以指明 flush: 'post' 或者使用 watchPostEffect

1
2
3
4
5
6
7
8
9
10
11
12
watch(source, callback, {
flush: "post",
});

watchEffect(callback, {
flush: "post",
});
import { watchPostEffect } from "vue";

watchPostEffect(() => {
/* 在 Vue 更新后执行 */
});

销毁监听器

正常通过同步语句创建的监听器会在宿主组件卸载时自动停止

如果是通过异步的方式创建(比如在 setTimeout中创建) 就必须手动停止防止内存泄漏

1
2
3
4
5
6
7
8
9
10
const unwatch = watch(source, callback);
//停止监听器
unwatch();
//一般不要异步创建监听器 如果需要等待一些异步数据 可以使用条件式监听器
const data = ref(null);
watchEffect(() => {
if (data.value) {
//数据加载后执行某些操作
}
});

模板引用

需要创建一个同名的ref来进行引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
import { ref, onMounted } from 'vue'

// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)

onMounted(() => {
input.value.focus()
})
</script>

<template>
<input ref="input" />
</template>

只有在组件挂载后才可以访问模板引用 这意味着如果想要侦听一个模板引用ref的变化 需要考虑null

1
2
3
4
5
6
7
watchEffect(() => {
if (input.value) {
input.value.focus();
} else {
// 此时还未挂载,或此元素已经被卸载(例如通过 v-if 控制)
}
});

v-for 中的模板引用

v-for中使用模板引用时 对应的ref是一个数组 将在元素被挂载后包含对应整个列表中的所有元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
import { ref, onMounted } from 'vue'

const list = ref([
/* ... */
])

const itemRefs = ref([])

onMounted(() => console.log(itemRefs.value))
</script>

<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>

深入组件

可以使用 app.component全局注册组件 (可以链式调用)

全局注册的缺点

  • 没有被使用到的组件无法在打包时被 three-shaking
  • 依赖关系不明确 不利于维护

props

需要使用 defineProps显式声明接收的props

1
2
3
4
defineProps<{
title?: string;
likes?: number;
}>();

可以使用没有参数的 v-bind一次性传递所有的props

v-bind="post" === :id="post.id" :title="post.title"

如果需要对传入的prop值做进一步的转换 建议使用计算属性

1
2
const props = defineProps(["size"]);
const normalizeSize = computed(() => porps.size.trim().toLowerCase());

除了props的类型时Boolean(默认是false) 其他都是 undefined

组件事件

触发和监听事件

可以通过在子组件使用 $emit定义一个事件并在父组件通过 v-on | @来触发

1
2
3
4
5
6
7
<button @click="$emit('someEvent')">
click me
</button>
//parent
<MyComponent @some-event.once="callback" />
//所有传入 $emit()的额外参数都会被直接传向监听器 $emit('foo', 1, 2,
3)触发后监听函数会接收到三个参数值

组件触发的事件没有 冒泡机制 所以你只能监听直接子组件 而不能监听后代组件或者兄弟组件 如果需要监听其他类型的组件 可以考虑使用 eventBus

声明要触发的事件

可以通过 defineEmits显式声明要触发的事件

1
2
3
4
const emits = defineEmits(['infocus']);
function handleClick = () => {
emits('infocus')
}

事件校验

通过返回一个布尔值来表明事件是否合法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup>
const emit = defineEmits({
// 没有校验
click: null,

// 校验 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>

如果一个原生事件的名字(比如click)被定义在emits选项中 那么监听器只会监听组件触发的事件而不会响应原生的事件

配合 v-model 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<input v-model="data" />
-->
<input :value="data" @input="data = $event.target.value" />
//编写组件
<Comp v-model="modalValue" />
//等价于
<Comp :modelValue="data" @update:modelValue="(newValue) => (data = newValue)" />
//Comp.vue
<!-- CustomInput.vue -->
<script setup>
defineProps(["modelValue"]);
defineEmits(["update:modelValue"]);
</script>

<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
//同时可以使用在get的时候返回modelValue并在set的时候触发相应的事件
<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>
//使用多个v-model
<UserName v-model:first-name="first" v-model:last-name="last" />
<script setup>
defineProps({
firstName: String,
lastName: String,
});

defineEmits(["update:firstName", "update:lastName"]);
</script>

<template>
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
/>
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
/>
</template>

参数修饰符

对于又有参数又有修饰符的v-model 生成的proparg + Modifiers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<MyComponent v-model.capitalize="myText" />
<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>

透传 Attributes

传递给一个组件 却没有被该组件声明为props或者emitsattribute或者 v-on事件监听器 将被自动添加到子元素的根元素上

v-on 监听器继承

1
2
3
4
<MyButton @click="onClick" />
<button @click="childClick" />
//点击button将会同时触发两个click
//如果原生的button元素自身也通过v-on绑定了一个事件监听器 那么这个监听器和从父组件继承的监听器都会被触发

深层组件继承

如果被defineProps | defineEmits声明过 则认为已经被该组件消费 否则将继续向下透传

禁用 Attributes 继承

如果不想要自动继承attributes 可以额外声明一个 script来声明 inheritAttrs: false

1
2
3
4
5
6
7
8
9
10
<script>
// 使用普通的 <script> 来声明选项
export default {
inheritAttrs: false,
};
</script>

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

如果 attribute 需要应用在根节点以外的其他元素上 通过设置 inheriAttrs: false 并且通过 $attrs即可访问到除了被声明的propsemits之外的所有其他attributes

1
2
3
4
5
6
7
//例如 想要父元素的所有属性都作为attribute应用到目标上 可以使用没有参数的v-bind
//Child.vue
<div class="btn-wrapper">
<button class="btn" v-bind="$attrs">click me</button>
</div>
//partent.vue
<Child .... />

多个根节点的透传

如果是多个根节点 则不会发生自动透传 需要显式指定 $attrs 否则会报错

可以使用 useAttrs()来访问一个组件的所有透传 attribute

Solts

渲染作用域

由于插槽是在父组件定义的 所以插槽能访问父组件的数据 但是无法访问子组件的数据

默认值

使用 <slot>defaultData<slot>给插槽提供默认值并在有填充值时被取代

具名插槽

当子组件有多个插槽位置时 就需要 指定对应的插槽名(默认为default

1
2
3
4
5
6
7
<BaseLayout>
<template v-slot:header> ||<template #header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
//插槽名也可以使用动态指令参数
v-slot: [dynamicSlotName] || #[dynamicSlotName]

作用域插槽

如果想要同时访问子组件和父组件的数据 则需要使用作用域插槽将子组件的对应数据暴露出去 然后父组件使用 slotProps接收

1
2
3
4
5
6
7
8
<slot class="class">

</slot>
<Comp>
<template v-slot="slotProps">
{{slotProps.class}}
</template>
</Comp>

同样也可以使用 具名作用域插槽 v-slot:name = "slotProps"

如果混用了具名插槽和默认插槽 则需要为默认插槽使用显式的 <template>标签

1
2
3
4
5
6
7
8
9
10
<!-- 该模板无法编译 -->
<template>
<MyComponent v-slot="{ message }">
<p>{{ message }}</p>
<template #footer>
<!-- message 属于默认插槽,此处不可用 -->
<p>{{ message }}</p>
</template>
</MyComponent>
</template>

为默认的插槽使用显式的 <template>标签可以更清晰地标注插槽数据的使用范围

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<MyComponent>
<!-- 使用显式的默认插槽 -->
<template #default="{ message }">
<p>{{ message }}</p>
</template>

<template #footer>
<p>Here's some contact info</p>
</template>
</MyComponent>
</template>

用途

可以用来做无渲染组件(只负责逻辑编写 然后将结果通过 prop 传出去 而样式以及数据的使用则交给父组件)

在需要同时服装逻辑 组合视图界面很有用(比如列表)

依赖注入(provide | inject)

应用层 provide

可以通过 app.provide(key, value)来提供全局可用的数据 这在编写插件时特别有用 因为插件一般不会使用组件形式来提供值

inject

如果provide的是一个ref则会传递整个ref而不是自动解包(这样可以保持响应性)

为了避免在用不到默认值的情况下进行不必要的计算或者产生副作用 可以使用工厂函数来创建默认值

1
const value = inject("key", () => new ExpensiveClass());

和响应式数据配合使用

为了数据的管理 我们应该避免在inject组件内对provide的响应式数据进行更改 如果有需要 应该在provide组件定义改变数据的方法并provide

对于一些不能在inject组件里更改的数据 可以使用readonly来保护 provide('read-only-count', readonly(count))

命名冲突

为了避免命名冲突 可以用一个单独的文件管理providekey 使用Symbol()修饰

export const myInjectionKey = Symbol()

异步组件

逻辑复用

组合式函数

可以将重复使用的逻辑封装复用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//mouse.js
import {ref, onMounted, onUnmounted} from 'vue'
export default 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}

}
//use
<script setup>
import { useMouse } from './mouse.js'

const { x, y } = useMouse()
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>
//我们还能将绑定和解绑的操作服装起来
// event.js
import { onMounted, onUnmounted } from 'vue'

export function useEventListener(target, event, callback) {
// 如果你想的话,
// 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback))
}
// mouse.js
import { ref } from 'vue'
import { useEventListener } from './event'

export function useMouse() {
const x = ref(0)
const y = ref(0)

useEventListener(window, 'mousemove', (event) => {
x.value = event.pageX
y.value = event.pageY
})

return { x, y }
}


每一个调用 useMouse()的组件示例会创建独立的状态 不会互相影响

异步组件获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//useFetch
import { ref, isRef, unref, watchEffect } from "vue";

export function useFetch(url) {
const data = ref(null);
const error = ref(null);

async function doFetch() {
// reset state before fetching..
data.value = null;
error.value = null;

// resolve the url value synchronously so it's tracked as a
// dependency by watchEffect()
const urlValue = unref(url);

try {
// artificial delay / random error
await timeout();
// unref() will return the ref value if it's a ref
// otherwise the value will be returned as-is
const res = await fetch(urlValue);
data.value = await res.json();
} catch (e) {
error.value = e;
}
}

if (isRef(url)) {
//如果输入ref 则在每次url改变时就重新请求
// setup reactive re-fetch if input URL is a ref
watchEffect(doFetch);
} else {
// otherwise, just fetch once //如果不是ref 则只请求一次
doFetch();
}

return { data, error, retry: doFetch };
}

// artificial delay
function timeout() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.3) {
resolve();
} else {
reject(new Error("Random Error"));
}
}, 300);
});
}
//use
<script setup>
import {(ref, computed)} from 'vue' import {useFetch} from './useFetch.js'
const baseUrl = 'https://jsonplaceholder.typicode.com/todos/' const id =
ref('1') const url = computed(() => baseUrl + id.value) const{" "}
{(data, error, retry)} = useFetch(url)
</script>;

约定和最佳实践

输入参数

如果你的参数是响应性并且可能被其他开发者使用 则应该使用 unref兼容ref而不是原始的值(unref会在是ref的时候返回..value 在不是的时候返回原样 )

1
2
3
4
5
import { unref } from "vue";

function useFeature(maybeRef) {
const value = unref(maybeRef);
}

如果组合式函数在接收ref为参数式会产生响应式effect 去额宝使用watch显式监听此effect或者在watchEffect中用unref进行正确的追踪

返回结果

建议使用多个ref构成的非响应式对象 如果非要使用响应式对象 建议用reactive在外面包一层 reactive(useMouse())

副作用

  • 如果用到了服务端渲染 确保在组件挂载后才调用的生命周期钩子中执行DOM相关的副作用
  • 确保在 onUnmounted()时清理副作用

组合式函数应该始终被同步调用

和无渲染组件的相比

组合式函数不会产生额外的组件实例开销 当在整个应用中使用时 由无渲染组件产生的额外组件实例会带来无法忽视的性能开销

在纯逻辑复用时使用组合式函数 在需要同时复用逻辑和视图布局时使用无渲染组件

自定义指令

自定义组件主要是为了重用涉及普通元素的底层DOM访问的逻辑

1
2
3
4
5
6
7
8
<script setup>
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>

当所需功能只能通过直接的 DOM 操作来实现时 才使用自定义指令 其他情况应该尽可能使用 v-bind 这样更高效 也对服务端渲染更好

不推荐在组件上使用自定义指令 因为指令默认会传递给组件的根节点上 且不能像attribute那样通过 $attrs来绑定

插件

插件是一个拥有 install()方法的对象 或者直接是一个install()函数本身

1
2
3
const myPlugin = {
install(app, options) {},
};

插件的应用场景

  • 通过 app.component() && app.directive()注册一到多个全局组件或者自定义指令
  • 通过 app.provide()使一注入全局资源
  • app.config.globalProperties中添加一些全局实例属性或方法
  • 一个可能上诉三种都包含的功能库(vue-router

编写一个插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default {
install: (app, options) => {
app.config.globalProperties.$translate = (key) => {
return key.split(".").reduce((o, i) => {
if (o) return o[i];
}, options);
};
},
};
//在app.use中注册
app.use(i18nPlugin, {
greetings: {
hello: "Bonjour!",
},
});

可以在插件中provide 这样在整个应用内部就都能使用了

Transition

<Transition>会在一个元素或组件进入和离开DOM时应用动画

1
2
3
4
5
6
<button @click="show = !show">Toggle</button>
<Transition>
<p v-if="show">hello</p>
</Transition>
/* 下面我们会解释这些 class 是做什么的 */ .v-enter-active, .v-leave-active {
transition: opacity 0.5s ease; } .v-enter-from, .v-leave-to { opacity: 0; }

<Transition>仅支持单个元素或组件作为其插槽内容 如果内容是一个组件 这个组件必须仅有一个根元素

当一个 <Transition>组件中的元素被插入或者移除时

  • Vue 会自动检测目标元素是否用了 CSS 过渡或动画 如果是 则一些 CSS 过度class会在适当时机被添加和移除
  • 如果没有探测到 CSS 过渡或动画、也没有提供 JavaScript 钩子,那么 DOM 的插入、删除操作将在浏览器的下一个动画帧后执行。

基于 CSS 的过度效果

CSS 过渡 class

image-20221108215022626

  1. v-enter-from:进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。
  2. v-enter-active:进入动画的生效状态。应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。
  3. v-enter-to:进入动画的结束状态。在元素插入完成后的下一帧被添加 (也就是 v-enter-from 被移除的同时),在过渡或动画完成之后移除。
  4. v-leave-from:离开动画的起始状态。在离开过渡效果被触发时立即添加,在一帧后被移除。
  5. v-leave-active:离开动画的生效状态。应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。
  6. v-leave-to:离开动画的结束状态。在一个离开动画被触发后的下一帧被添加 (也就是 v-leave-from 被移除的同时),在过渡或动画完成之后移除。

v-enter-activev-leave-active 给我们提供了为进入和离开动画指定不同速度曲线的能力

为过渡效果命名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<Transition name="fade">
</Transition>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
//<Transition>一般会搭配原生 CSS过渡一起使用
/*
进入和离开动画可以使用不同
持续时间和速度曲线。
*/
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}

.slide-fade-leave-active {
transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);
}

.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(20px);
opacity: 0;
}
//*-enter-from 不是在元素插入后立即移除 而是在一个animationend事件触发时被移除
.bounce-enter-active {
animation: bounce-in 0.5s;
}
.bounce-leave-active {
animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.25);
}
100% {
transform: scale(1);
}
}

性能考量

动画所用的一般都是 transfromopacity这些属性来制作动画 这些性能比较好

  • 他们在动画过程中不会影响到 DOM 结构 因此不会每一帧都触发昂贵的 CSS 布局重新计算
  • 大多数的现代浏览器都可以在 transfrom动画时利用 GPU 进行硬件加速

CSS-Triggers 可以查询哪些属性会在执行动画时触发 css 布局变动


文章作者: olddog
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 olddog !
  目录