# <script setup lang="ts">

Vue3的单文件组件<script setup>的一些ts写法。可能不是特别全面, 但是应该是可用的一版。

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

  • 更少的样板内容,更简洁的代码。
  • 能够使用纯 Typescript 声明 props 和抛出事件。
  • 更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)。
  • 更好的 IDE 类型推断性能 (减少语言服务器从代码中抽离类型的工作)。

文档sfc-script-setup (opens new window)

官方文档给出的是不带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 开始,watchPostEffectwatchSyncEffect 别名也可以用来让代码意图更加明显。

# 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 共享的行为

watchwatchEffect (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中使用

文档在这里 (opens new window)

获取一个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"

上次更新: 1/22/2025, 9:39:13 AM