# <script setup lang="ts">
Vue3的单文件组件<script setup>
的一些ts写法。可能不是特别全面, 但是应该是可用的一版。
<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。相比于普通的 <script>
语法,它具有更多优势:
- 更少的样板内容,更简洁的代码。
- 能够使用纯 Typescript 声明 props 和抛出事件。
- 更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)。
- 更好的 IDE 类型推断性能 (减少语言服务器从代码中抽离类型的工作)。
官方文档给出的是不带ts版本的, 所以我们自己摸索试试ts版本怎么去写这个setup组件。
首先如果你有一些比如设定组件name、inheritAttrs或者一些自定义属性的需求时, 可以借助一些第三方的工具,比如这个unplugin-vue-define-options (opens new window). 如果不想借助工具, 可以自己写一个不带setup属性的script。比如
<script lang="ts">
export default {
name: 'CompoentName',
inheritAttrs: false,
// $options.customProp 访问
customProps: 'my prop'
};
</script>
# reactive、readonly
<template>
<div>
<div>{{msg}}</div>
<button @click="count++">click me to add,count: {{count}}</button>
<div>{{p.name}}</div>
<div>{{p.age}}, <button @click="handleAdd">add age</button></div>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
const count = ref<number>(0);
console.log(count.value)
interface Person {
name: string;
age: number;
sex: 1 | 0;
}
const p = reactive<Person>({
name: 'vue3',
age: 2,
sex: 1
});
function handleAdd() {
p.age++;
}
/*
const copyP: {
readonly name: string;
readonly age: number;
readonly sex: 1 | 0;
}
*/
const copyP = readonly<Person>(p);
watchEffect(() => {
console.log(copyP.age);
});
</script>
# toRaw
const pRaw: Person = {
name: 'row',
age: 0,
sex: 0
};
const pReactive = reactive(pRaw);
// 这里其实都有类型推断, 不写也是没事的
const pRawCopy = toRaw<Person>(pReactive);
console.log(pRawCopy === pRaw);
# markRaw
标记一个对象,使其永远不会转换为 proxy。返回对象本身。
WARNING
- 有些值不应该是响应式的,例如复杂的第三方类实例或 Vue 组件对象。
- 当渲染具有不可变数据源的大列表时,跳过 proxy 转换可以提高性能。
# shallowReactive
const state = shallowReactive({
foo: 1,
nested: {
bar: 2
}
})
// 改变 state 本身的性质是响应式的
state.foo++
// ...但是不转换嵌套对象
isReactive(state.nested) // false
state.nested.bar++ // 非响应式
# ref
const foo = ref<string | number>('foo');
foo.value = 123;
# unref、toRef、toRefs、isRef、customRef
# computed
interface ListItem {
name: string;
code: string;
}
const list = reactive<ListItem[]>([]);
list.push({
name: 'A',
code: '1111'
});
list.push({
name: 'B',
code: '2222'
});
interface ComputedListItem extends ListItem {
id: number;
}
const listComputed = computed<ComputedListItem[]>(() =>
list.map((item) => ({
...item,
id: 1
}))
);
调试模式
import { computed, DebuggerEvent, ref } from 'vue';
const count = ref(1);
const doubleCount = computed(() => count.value * 2, {
onTrack(e: DebuggerEvent) {
// 当 count.value 作为依赖被追踪时触发
console.log('onTrack', e);
},
onTrigger(e: DebuggerEvent) {
// 当 count.value 被修改时触发
console.log('onTrigger', e);
}
});
// 访问 doubleCount,应该触发 onTrack
console.log(doubleCount.value);
// 修改 count.value,应该触发 onTrigger
count.value = 2;
# watchEffect
为了根据响应式状态自动应用和重新应用副作用,我们可以使用 watchEffect
函数。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。 如果依赖更新,副作用会在组件更新前执行。
const count = ref(1);
watchEffect(() => {
// 会立即执行 打印出 1
console.log('watchEffect', count.value);
});
停止watch
const stopWatchCount = watchEffect(() => {
console.log('watchEffect', count.value);
});
setTimeout(() => {
count.value = 5;
stopWatchCount();
}, 1000);
清除副作用
传入一个onInvalidate
参数, 可以用来注册失效回调, 注册的回调会在一下两种情况触发
- 副作用即将重新执行时
- 侦听器被停止 (如果在
setup()
或生命周期钩子函数中使用了watchEffect
,则在组件卸载时)
注意:注册回调行为最好发生在异步发起之前
const stopWatchCount = watchEffect((onInvalidate) => {
onInvalidate(() => {
console.log('clear');
});
console.log('watchEffect', count.value);
});
setTimeout(() => {
count.value = 5;
// stopWatchCount();
}, 1000);
TIP
如果需要在组件更新后重新运行侦听器副作用,我们可以传递带有 flush
选项的附加 options
对象, (默认为 'pre'
)
const str = ref('A');
// 在组件更新后触发,这样你就可以访问更新的 DOM。
// 注意:这也将推迟副作用的初始运行,直到组件的首次渲染完成。
watchEffect(
() => {
console.debug(str.value);
console.log(document.querySelector('.str'));
},
{
flush: 'post'
}
);
setTimeout(() => {
console.log((str.value = 'B'));
});
flush
选项还接受 sync
,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。
从 Vue 3.2.0 开始,watchPostEffect
和 watchSyncEffect
别名也可以用来让代码意图更加明显。
# watch
watch
API 完全等同于组件侦听器 (opens new window) property, 就是类似于options api里面的watch选项。watch
需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调。
- 与 watchEffect (opens new window) 比较,
watch
允许我们:- 懒执行副作用;
- 更具体地说明什么状态应该触发侦听器重新运行;
- 访问侦听状态变化前后的值。
import { reactive, watch, ref } from 'vue';
const p = reactive<{ name: string }>({
name: 'Cloud'
});
watch(p, (newP, oldP) => {
// 感觉直接watch一个Proxy, 没法区分新旧值, 这里拿到的都是新值
console.log(newP.name, oldP.name, '=======');
});
// 使用侦听器来比较一个数组或对象的值,这些值是响应式的,要求它有一个由值构成的副本
watch(
() => ({ ...p }),
(newP, oldP) => {
// 如此就能正确的拿到值
console.log(newP.name, oldP.name, '-------');
}
);
// watch 一个数组
const numbers = reactive<number[]>([1, 2, 3, 4, 5]);
watch(
() => [...numbers],
(newNumbers, oldNumbers) => {
console.log(numbers === newNumbers, numbers === oldNumbers);
}
);
numbers.push(6);
// 尝试检查深度嵌套对象或数组中的 property 变化时,仍然需要 deep 选项设置为 true。
watch(
() => p,
(newP, oldP) => {
console.log(newP.name, oldP.name, '=======');
},
{ deep: true }
);
// watch 一个getter
watch(
() => p.name,
(name, prevName) => {
console.debug(name, prevName);
}
);
// 侦听一个ref
const count = ref(0);
watch(count, (count, oldCount) => {
console.debug(count, oldCount);
});
setTimeout(() => {
count.value++;
p.name = 'new Cloud';
}, 1000);
# 与 watchEffect
共享的行为
watch
与 watchEffect
(opens new window)共享停止侦听 (opens new window),清除副作用 (opens new window) (相应地 onInvalidate
会作为回调的第三个参数传入)、副作用刷新时机 (opens new window)和侦听器调试 (opens new window)行为。
# provide / inject
// Parent.vue
import {InjectionKey, provide} from 'vue'
export const provideKey: InjectionKey<string> = Symbol('test');
provide(provideKey, 'hello, world!');
// Child.vue
import { inject } from 'vue';
import { provideKey } from 'Parent.vue';
const provideValue = inject(provideKey);
provide也可以添加响应性, 使用ref或者reactive
const provideValue = ref('hello')
provide(provideKey, provideValue);
# 自定义指令
<template>
<div>
<div v-color-directive>asdasdd</div>
</div>
</template>
<script lang="ts" setup>
const vColorDirective = {
beforeMount: (el: HTMLElement) => {
el.style.color = 'red';
}
};
</script>
# defineProps 声明props
文档在这里, 仅限 TypeScript 的功能 (opens new window)
<script lang="ts" setup>
import { onMounted, toRefs, watch } from 'vue';
// 声明props的key和类型
interface Props {
foo: string;
bar?: number;
}
const props = defineProps<Props>();
// watch props 的变化
const propsRefs = toRefs<Props>(props);
watch(propsRefs.foo, (v) => {
console.log(v);
});
// 也可以直接整个watch
watch(props, (v) => {
console.log(v);
});
onMounted(() => {
console.log(props.bar);
console.log(propsRefs.foo.value);
});
</script>
设定props的默认值, 借助 withDefaults
函数
interface Props {
foo: string;
bar?: number;
}
const props = withDefaults(defineProps<Props>(), {
foo: 'default foo'
});
withDefaults 、 defineProps这些函数都不需要单独引入
# defineEmits
<template>
<div>
<label>
{{props.label}}
<input :type="props.type"
:value="props.modelValue"
class="border-4 border-blue-500 border-solid p-2"
@input="onInput($event)">
</label>
<button class="bg-red-500 hover:bg-red-700"
@click="handleSubmit">submit</button>
</div>
</template>
<script lang="ts">
export default {
name: 'MyInput'
};
</script>
<script lang="ts" setup>
interface Props {
label?: string;
type?: string;
modelValue: string;
}
const props = withDefaults(defineProps<Props>(), {
label: 'label',
type: 'text',
modelValue: ''
});
interface Emits {
(e: 'update:modelValue', value: string): void;
(e: 'submit'): void;
}
const emits = defineEmits<Emits>();
const onInput = (e: Event) => {
emits('update:modelValue', (e.target as HTMLInputElement).value);
};
const handleSubmit = () => {
emits('submit');
};
</script>
<style scoped>
</style>
# 模板引用,$refs
就是获取DOM或者获取组件实例, 以前可能用this.$refs
,现在需要在组合式API中使用
获取一个DOM元素
<template>
<div>
<p ref="pElRef">refs</p>
</div>
</template>
<script lang="ts">
export default {
name: 'Refs'
};
</script>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
const pElRef = ref(null);
onMounted(() => {
console.log(pElRef.value);
const p: HTMLParagraphElement | null = pElRef.value;
p!.textContent = 'mounted';
});
</script>
获取一个组件实例
// Parent.vue
<template>
<Refs ref="ChildRef"></Refs>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import Refs from './components/Refs.vue';
const ChildRef = ref<{ a?: number; log?(): void }>({});
onMounted(() => {
const refsIns = ChildRef.value;
console.log(refsIns.a);
refsIns.log && refsIns.log();
});
</script>
// Refs.vue
<template>
<div>
<p ref="pElRef">refs</p>
</div>
</template>
<script lang="ts">
export default {
name: 'Refs'
};
</script>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
const pElRef = ref(null);
onMounted(() => {
console.log(pElRef.value);
const p: HTMLParagraphElement | null = pElRef.value;
p!.textContent = 'mounted';
});
defineExpose({
a: 1,
log() {
console.log('log');
}
});
</script>
<style scoped>
</style>
这里也可以看到defineExpose
的使用,使用 <script setup>
的组件是默认关闭的,也即通过模板 ref 或者 $parent
链获取到的组件的公开实例,不会暴露任何在 <script setup>
中声明的绑定。
为了在 <script setup>
组件中明确要暴露出去的属性,使用 defineExpose
编译器宏:
# useAttrs, useSlots
<script lang="ts" setup>
import { useAttrs } from 'vue';
const attrs = useAttrs();
/*
type Record<K extends keyof any, T> = {
[P in K]: T;
};
type Data = Record<string, unknown>;
*/
console.log(attrs);
</script>
attrs的类型是Data, 这块确实不好定义
TIP
可以通过v-bind="$attrs"
将传入的所有attr赋值给指定的元素, 或者自定义的对象也可以v-bind="obj"