跳到主要内容

组合式API

setup

setup 钩子函数是 vue3 在使用组合式 api 的入口。

setup 中的 ref 和 props

<script>
import { ref, toRefs, toRef } from 'vue';
export default {
props: {
times: Number
},
setup(props) {
// 访问 Props => 此时 times 不具有响应式
// let times = props.times
// 使用 toRefs() 和 toRef() 这两个工具函数持响应性:
// let { times } = toRefs(props) 或者 =>
let times = toRef(props.times);
const count = ref(0);
// 返回值会暴露给模板和其他的选项式 API 钩子
return {
props,
count
};
}
};
</script>

<template>
<p>{{ times }}</p>
<button @click="count++">Son 页面的 count:{{ count }}</button>
</template>

setup 上下文

export default {
setup(props, context) {
// 透传 Attributes(非响应式的对象,等价于 $attrs)
console.log(context.attrs);

// 插槽(非响应式的对象,等价于 $slots)
console.log(context.slots);

// 触发事件(函数,等价于 $emit)
console.log(context.emit);

// 暴露公共属性(函数)
console.log(context.expose);
}
};

暴露公共属性:expose 函数用于显式地限制该组件暴露出的属性,当父组件通过模板引用访问该组件的实例时,将仅能访问 expose 函数暴露出的内容:

export default {
setup(props, { expose }) {
// 让组件实例处于 “关闭状态”
// 即不向父组件暴露任何东西
expose();
const publicCount = ref(0);
const privateCount = ref(0); // 有选择地暴露局部状态
expose({
count: publicCount
});
}
};

setup与渲染函数一起使用

import { h, ref } from 'vue';
export default {
setup(props, { expose }) {
const count = ref(0);
const increment = () => ++count.value;
expose({ increment });
return () => h('div', count.value);
}
};

<script setup>

<script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。当同时使用 SFC 与组合式 API 时该语法是默认推荐。相比于普通的 <script> 语法,它具有更多优势:

  • 更少的样板内容,更简洁的代码。
  • 能够使用纯 TypeScript 声明 props 和自定义事件。
  • 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。
  • 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。
<script setup>
import { capitalize } from './helpers';
import { ref } from 'vue';
// 组件
import MyComponent from './MyComponent.vue';

const count = ref(0);
const name = 'setup';
</script>

<template>
<div>{{ capitalize('hello') }}</div>
<button @click="count++">{{ count }}</button>
<MyComponent />
</template>
  • 顶层的绑定会被暴露给模板
  • import 导入的内容也会以同样的方式暴露。这意味着我们可以在模板表达式中直接使用导入的 helper 函数,而不需要通过 methods 选项来暴露它。
  • 响应式状态需要明确使用响应式 API 来创建。

defineProps() 和 defineEmits()

<script setup>
const props = defineProps({
foo: String
});

const emit = defineEmits(['change', 'delete']);
// setup 代码
</script>

defineComponent

<script>
import { defineComponent, ref } from 'vue';

export default defineComponent({
props: ['msg'],
setup() {
const count = ref(0);
console.log('count ~', typeof count.value);
return {
count,
plus: () => count.value++
};
}
});
</script>

<template>
<main>
<h1>prop-msg: {{ msg }}</h1>
<button @click="plus">count 点击事件 {{ count }}</button>
</main>
</template>
  • setup
    • <script setup> 中的代码会在每次组件实例被创建的时候执行
    • <script setup> 顶层的绑定会被暴露给模板
    • setup() 返回对象的属性和方法,会在模板中直接使用
  • ref()
    • 创建一个包含响应式数据的引用reference对象,通过 xxx.value 访问,操作时不需要 xxx.value
    • 创建基本类型的响应式数据

响应式核心

reactive

<script setup>
import { reactive } from 'vue';

const user = reactive({
name: 'houfei',
age: 20,
wife: {
name: 'zjj'
},
eat: function () {
console.log('apple');
}
});
let eat = () => {
user.age = 22;
user.eat();
};
</script>

<template>
<main>
<p>{{ user }}</p>
<button @click="eat">吃苹果</button>
</main>
</template>
<script>
import { defineComponent, reactive } from 'vue';

export default defineComponent({
props: ['msg'],
setup() {
const user = reactive({
name: 'houfei',
age: 20,
wife: {
name: 'zjj'
},
eat: function () {
console.log('apple');
}
});
return {
user,
eat: () => {
user.age = 22;
user.eat();
}
};
}
});
</script>

<template>
<main>
<h1>prop-msg: {{ msg }}</h1>
<p>{{ user }}</p>
<button @click="eat">吃苹果</button>
</main>
</template>
  • reactive()
    • 接受一个普通对象,返回一个对象的响应式代理
    • 必须始终保持对该响应式对象的相同引用
<script setup>
import { ref, reactive } from 'vue';

const count = ref(0);
const obj = {
countValue: count.value, // ref 解包
count // count 和 该属性存在响应式
};
let objR = reactive(obj);
console.log(obj === objR); // false 代理对象和原始对象不是全等的
const plus = function () {
count.value++;
};
</script>

<template>
<main>
<button @click="plus">count 点击事件 {{ count }}</button>
<p>{{ objR }}</p>
</main>
</template>

setup 的执行时机

  • setupbeforeCreate之前执行,所以在setup不存在this,其实在所有componsition api中都没有this
  • setup的返回值是一个对象,供模板使用,相当于data()methods的内容。

computed 和 watch

const computed1 = computed(() => {
return "ref | reactive 变量";
});
const computed2 = computed({
get: () => {
return "ref | reactive 变量";
},
set: (val) => {
console.log(val);
},
});

watch("ref | reactive 变量", callback, {
immediate,
deep,
});
// 第一次就会执行
watchEffect(() => {
"ref | reactive 变量1" = "ref | reactive 变量2"
});
watch([() => "reactive变量.attr1" () => "reactive变量.attr2"], callback)

hook

  • 使用 Vue3 的组合 API 封装的可复用的功能函数
  • 自定义 hook 的作用类似于 Vue2 的 mixins 技术
  • 自定义 Hook 的优势:很清楚复用功能代码的来源、更清楚易懂

收集鼠标点击的坐标

// useMousePosition.js
import { ref, onMounted, onBeforeUnmount } from 'vue';

export default function () {
const x = ref(-1),
y = ref(-1);
function clickHandler(event) {
console.log(event);
x.value = event.pageX;
y.value = event.pageY;
}
onMounted(() => {
window.addEventListener('click', clickHandler);
});
onBeforeUnmount(() => {
window.removeEventListener('click');
});
return {
x,
y
};
}

使用:

<script>
import { defineComponent } from 'vue';
import useMousePosition from './hooks/useMousePosition';
export default defineComponent({
setup() {
const { x, y } = useMousePosition();
return {
x,
y
};
}
});
</script>

<template>
<h1>x:{{ x }}, y: {{ y }}</h1>
</template>

toRefs

将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。

<script setup>
import { reactive, toRefs } from 'vue';

const state = reactive({
foo: 1,
bar: 2
});

const stateAsRefs = toRefs(state);
/*
stateAsRefs 的类型:{
foo: Ref<number>,
bar: Ref<number>
}
*/

// 这个 ref 和源属性已经“链接上了”
state.foo++;
console.log(stateAsRefs.foo.value); // 2

stateAsRefs.foo.value++;
console.log(state.foo); // 3
</script>

<template>
<main>
<p>state: {{ state }}</p>
<span>stateAsRefs: {{ stateAsRefs }}</span>
</main>
</template>

ref 获取元素

  • ref 自动获取焦点
<script setup>
import { onMounted, ref } from 'vue';

const inputRef = ref(null);

onMounted(() => {
inputRef.value && inputRef.value.focus();
});
</script>

<template>
<input
type="text"
v-model="kk"
ref="inputRef"
/>
</template>

响应式 API:进阶

shallowRef()

  • ref() 的浅层作用形式。

ref() 不同,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。只有对 .value 的访问是响应式的。

shallowRef() 常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成。

const state = shallowRef({ count: 1 });

// 不会触发更改
state.value.count = 2;

// 会触发更改
state.value = { count: 2 };

triggerRef()

强制触发依赖于一个浅层 ref 的副作用,这通常在对浅引用的内部值进行深度变更后使用。

const shallow = shallowRef({
greet: 'Hello, world'
});

// 触发该副作用第一次应该会打印 "Hello, world"
watchEffect(() => {
console.log(shallow.value.greet);
});

// 这次变更不应触发副作用,因为这个 ref 是浅层的
shallow.value.greet = 'Hello, universe';

// 打印 "Hello, universe"
triggerRef(shallow);

customRef()

创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。

customRef() 预期接收一个工厂函数作为参数,这个工厂函数接受 tracktrigger 两个函数作为参数,并返回一个带有 getset 方法的对象。

一般来说,track() 应该在 get() 方法中调用,而 trigger() 应该在 set() 中调用。然而事实上,你对何时调用、是否应该调用他们有完全的控制权。

创建一个防抖 ref,即只在最近一次 set 调用后的一段固定间隔后再调用:

js

import { customRef } from 'vue';

export function useDebouncedRef(value, delay = 200) {
let timeout;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
trigger();
}, delay);
}
};
});
}

在组件中使用:

vue

<script setup>
import { useDebouncedRef } from './debouncedRef';
const text = useDebouncedRef('hello');
</script>

<template>
<input v-model="text" />
</template>

shallowReactive()

reactive() 的浅层作用形式。

  • reactive() 不同,这里没有深层级的转换:一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。
const state = shallowReactive({
foo: 1,
nested: {
bar: 2
}
})

// 更改状态自身的属性是响应式的
state.foo++

// ...但下层嵌套对象不会被转为响应式
isReactive(state.nested) // false

// 不是响应式的
state.nested.bar++

shallowReadonly()

readonly() 的浅层作用形式

  • readonly() 不同,这里没有深层级的转换:只有根层级的属性变为了只读。属性的值都会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。
const state = shallowReadonly({
foo: 1,
nested: {
bar: 2
}
})

// 更改状态自身的属性会失败
state.foo++

// ...但可以更改下层嵌套对象
isReadonly(state.nested) // false

// 这是可以通过的
state.nested.bar++

toRaw()

根据一个 Vue 创建的代理返回其原始对象。

  • toRaw() 可以返回由 reactive()readonly()shallowReactive() 或者 shallowReadonly() 创建的代理对应的原始对象。

    这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。

    const foo = {}
    const reactiveFoo = reactive(foo)

    console.log(toRaw(reactiveFoo) === foo) // true

markRaw()

将一个对象标记为不可被转为代理。返回该对象本身。

const foo = markRaw({});
console.log(isReactive(reactive(foo))); // false

// 也适用于嵌套在其他响应性对象
const bar = reactive({ foo });
console.log(isReactive(bar.foo)); // false

effectScope()

创建一个 effect 作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理。

const scope = effectScope()

scope.run(() => {
const doubled = computed(() => counter.value * 2)

watch(doubled, () => console.log(doubled.value))

watchEffect(() => console.log('Count: ', doubled.value))
})

// 处理掉当前作用域内的所有 effect
scope.stop()

getCurrentScope()

如果有的话,返回当前活跃的 effect 作用域

function getCurrentScope(): EffectScope | undefined;

onScopeDispose()

在当前活跃的 effect 作用域上注册一个处理回调函数。当相关的 effect 作用域停止时会调用这个回调函数。

这个方法可以作为可复用的组合式函数中 onUnmounted 的替代品,它并不与组件耦合,因为每一个 Vue 组件的 setup() 函数也是在一个 effect 作用域中调用的。

依赖注入

provide

  • 详细信息

provide() 接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值。

与注册生命周期钩子的 API 类似,provide() 必须在组件的 setup() 阶段同步调用。

<script setup>
import {(ref, provide)} from 'vue' import {fooSymbol} from './injectionSymbols' // 提供静态值
provide('foo', 'bar') // 提供响应式的值 const count = ref(0) provide('count', count) // 提供时将
Symbol 作为 key provide(fooSymbol, count)
</script>

inject

注入一个由祖先组件或整个应用 (通过 app.provide()) 提供的值。

  • 详细信息

    第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject() 将返回 undefined,除非提供了一个默认值。

    第二个参数是可选的,即在没有匹配到 key 时使用的默认值。它也可以是一个工厂函数,用来返回某些创建起来比较复杂的值。如果默认值本身就是一个函数,那么你必须将 false 作为第三个参数传入,表明这个函数就是默认值,而不是一个工厂函数。

    与注册生命周期钩子的 API 类似,inject() 必须在组件的 setup() 阶段同步调用。

<script setup>
import {inject} from 'vue' import {fooSymbol} from './injectionSymbols' // 注入值的默认方式 const
foo = inject('foo') // 注入响应式的值 const count = inject('count') // 通过 Symbol 类型的 key 注入
const foo2 = inject(fooSymbol) // 注入一个值,若为空则使用提供的默认值 const bar = inject('foo',
'default value') // 注入一个值,若为空则使用提供的工厂函数 const baz = inject('foo', () => new
Map()) // 注入时为了表明提供的默认值是个函数,需要传入第三个参数 const fn = inject('function', ()
=> {}, false)
</script>

内置组件

Teleport

<Teleport> 是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。

<Teleport><Transition> 结合使用来创建一个带动画的模态框。示例

Suspence

它允许我们的应用程序在等待异步组件时渲染一些后备内容,可以让我们创建一个平滑的用户体验。

<Suspense> 是一个内置组件,用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。

<Suspense>
└─ <Dashboard>
├─ <Profile>
│ └─ <FriendStatus>(组件有异步的 setup())
└─ <Content>
├─ <ActivityFeed> (异步组件)
└─ <Stats>(异步组件)

在这个组件树中有多个嵌套组件,要渲染出它们,首先得解析一些异步资源。如果没有 <Suspense>,则它们每个都需要处理自己的加载、报错和完成状态。在最坏的情况下,我们可能会在页面上看到三个旋转的加载态,在不同的时间显示出内容。

有了 <Suspense> 组件后,我们就可以在等待整个多层级组件树中的各个异步依赖获取结果时,在顶层展示出加载中或加载失败的状态。

<Suspense> 可以等待的异步依赖有两种:

  1. 带有异步 setup() 钩子的组件。这也包含了使用 <script setup> 时有顶层 await 表达式的组件。
  2. 异步组件

async setup()

组合式 API 中组件的 setup() 钩子可以是异步的:

export default {
async setup() {
const res = await fetch(...)
const posts = await res.json()
return {
posts
}
}
}

如果使用 <script setup>,那么顶层 await 表达式会自动让该组件成为一个异步依赖:

<script setup>
const res = await fetch(...)
const posts = await res.json()
</script>

异步组件

异步组件默认就是**“suspensible”**的。这意味着如果组件关系链上有一个 <Suspense>,那么这个异步组件就会被当作这个 <Suspense> 的一个异步依赖。在这种情况下,加载状态是由 <Suspense> 控制,而该组件自己的加载、报错、延时和超时等选项都将被忽略。

异步组件也可以通过在选项中指定 suspensible: false 表明不用 Suspense 控制,并让组件始终自己控制其加载状态。

加载中状态

<Suspense> 组件有两个插槽:#default#fallback。两个插槽都只允许一个直接子节点。在可能的时候都将显示默认槽中的节点。否则将显示后备槽中的节点。

<Suspense>
<!-- 具有深层异步依赖的组件 -->
<Dashboard />

<!-- 在 #fallback 插槽中显示 “正在加载中” -->
<template #fallback>
Loading...
</template>
</Suspense>

在初始渲染时,<Suspense> 将在内存中渲染其默认的插槽内容。如果在这个过程中遇到任何异步依赖,则会进入挂起状态。在挂起状态期间,展示的是后备内容。当所有遇到的异步依赖都完成后,<Suspense> 会进入完成状态,并将展示出默认插槽的内容。

如果在初次渲染时没有遇到异步依赖,<Suspense> 会直接进入完成状态。

进入完成状态后,只有当默认插槽的根节点被替换时,<Suspense> 才会回到挂起状态。组件树中新的更深层次的异步依赖不会造成 <Suspense> 回退到挂起状态。

发生回退时,后备内容不会立即展示出来。相反,<Suspense> 在等待新内容和异步依赖完成时,会展示之前 #default 插槽的内容。这个行为可以通过一个 timeout prop 进行配置:在等待渲染新内容耗时超过 timeout 之后,<Suspense> 将会切换为展示后备内容。若 timeout 值为 0 将导致在替换默认内容时立即显示后备内容。

  • Demo1:
<script setup>
import { defineAsyncComponent } from 'vue';
const AsyncCompontent = defineAsyncComponent(() => import('./AsyncCompontent.vue'));
</script>

<template>
<main>
<Suspense>
<template #default>
<AsyncCompontent />
</template>
<template #fallback>正在加载...</template>
</Suspense>
</main>
</template>
<script setup>
import { ref } from 'vue';
const open = ref(false);
</script>

<template>
<button @click="open = !open">Open Modal</button>
<Teleport to="body">
<div
v-if="open"
class="modal"
>
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</Teleport>
</template>

<style scoped>
.modal {
position: fixed;
z-index: 999;
top: 20%;
left: 50%;
width: 300px;
margin-left: -150px;
}
</style>
  • Demo2
<template>
<main>
<Suspense>
<template #default>
<AsyncCompontent />
</template>
<template #fallback>正在加载...</template>
</Suspense>
</main>
</template>

<script>
import { defineComponent } from 'vue';
import AsyncCompontent from './AsyncCompontent.vue';

export default defineComponent({
name: 'App',
components: {
AsyncCompontent
}
});
</script>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
name: 'AsyncCompontent',
setup() {
return new Promise(resolve => {
setTimeout(() => {
resolve({
msg: 'setup 异步数据'
});
}, 3000);
});
}
});
</script>

<template>
<main>
{{ msg }}
</main>
</template>