props and emits
defineProps and defineEmits |
|
ref or $parent
defineExpose |
|
slots and attrs
useSlots and useAttrs() |
|
ref
接受一个内部值并返回一个响应式且可变的 ref 对象,在setup函数内部访问ref函数需要加.value,
如果要加类型,要用泛型,不加则会类型推论,也可以不给初始值,则是any,且是undefined:
const ref1 = ref(1);
const ref2 = ref<number>(2);
const ref3 = ref1.value;//不是响应式,相当于变量
const ref4 = ref();//refs.value是undefined
ref2.value = 5;//赋值 读取都要.value
一般来说,ref只会赋值基础数据类型和数组,也可以泛型联合类型
如果将对象分配为ref值,则它将被reactive函数处理为深层的响应式对象:
//不建议,内部也是reactive处理
const ref1 = ref({
a: 10,
});
//不确定类型
const ref3 = ref<string | number>();
ref3.value = 1;
ref3.value = "";
//数组对象,ts类型声明,用泛型
type Obj1 = { c: string };
type Obj2 = {
b: string;
c: Obj1[];
};
const ref2 = ref<Obj2[]>([
{
b: "",
c: [{ c: "" }],
},
]);
reactive
用来声明响应式的对象,类型通过泛型添加:
type Obj = {
a: number;
b: string;
};
let obj = reactive<Obj>({
a: 10,
b: "",
});
let state = reactive({
a: 10,
b: "",
});
reactive将会解包所有深层的 refs,同时维持ref的响应性,
将ref分配给reactive的property时,ref也会被自动解包。
简单说就是ref的值和reactive的值响应式了
const count = ref(1)
const obj = reactive({ count })
// ref 会被解包
console.log(obj.count === count.value) // true
// 它会更新 `obj.count`
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2
// 它也会更新 `count` ref
obj.count++
console.log(obj.count) // 3
console.log(count.value) // 3
//跟上面一样效果
const count = ref(1)
const obj = reactive({})
obj.count = count
console.log(obj.count) // 1
console.log(obj.count === count.value) // true
toRef
为源响应式对象上的某个property新创建一个ref,也就是reactive创建的才可以,
会保持对其源 property 的响应式连接:
const state = reactive({
foo: 1,
bar: 2
})
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3
toRefs
将响应式对象转换为普通对象,其中结果对象的每个property都是指向原始对象相应property的ref:
const state = reactive({
foo: 1,
bar: 2
})
const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型:
{
foo: Ref<number>,
bar: Ref<number>
}
*/
// ref 和原始 property 已经“链接”起来了
state.foo++
console.log(stateAsRefs.foo.value) // 2
stateAsRefs.foo.value++
console.log(state.foo) // 3
toRef和toRefs在对一些响应式对象结构展开会很有用,可以有用的时候看看。
watch
watch接收两个参数,第一个参数可以是有return的函数,也可以是一个ref,
第二个参数则跟vue2相同的函数,并且watch可以很多个:
//一般ref不用这种方式
let count = ref(0);
watch(
() => count.value,
(val, old) => {
console.log(old, val);
}
);
//单个ref推荐
watch(count, (val, old) => {
console.log(old, val);
});
//监听reactive对象
let state = reactive({ count: 0 });
//函数参数必须具体到某个值,如果是() => state无效,或者加上{ deep: true }
watch(
() => state.count,
(val, old) => {
console.log(old, val);
}
);
//加上{ deep: true }
watch(
() => state,
(val, old) => {
console.log(old, val);
},
{ deep: true }
);
//监听整个对象,新旧的值都相同,或者用lodash.cloneDeep进行深拷贝
//state改成() => _.cloneDeep(state)
watch(state, (val, old) => {
console.log(old.count, val.count);
});
也可以同时监听多个,两个参数分别用数组,个人还是建议单个:
const state = reactive({ count: 1 });
const count = ref(0);
// 监听一个数组
watch(
[() => state.count, count],
([newState, newCount], [oldState, oldCount]) => {
console.log("new:", newState, newCount);
console.log("old:", oldState, oldCount);
}
);
官网一直提到惰性,其实就是是否加immediate: true,加了初始化就会执行。
watchEffect
它"立即执行"传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数:
const state = reactive({ count: 1 });
const count = ref(0);
watchEffect(() => {
if (state.count > 3) {
count.value++;
}
});
watchEffect(() => console.log(count.value));
至于watch和watchEffect共享停止侦听,清除副作用 (相应地 onInvalidate 会作为回调的第三个参数传入)、
副作用刷新时机和侦听器调试行为后面要仔细研究一下。
computed
从vue2开始,就很多人分不清什么时候用computed什么时候用watch,computed主要是用来声明
有两个及以上的依赖的数据,也就是说一个变量是根据多个数据进行判断的,用computed,单个的用watch。
至于语法的不同就不多说了,vue3的computed常用语法是一个带有return的函数,也可以同时存在多个:
let count = ref(0);
let page = ref(0);
let pg = computed(() => {
return count.value + page.value;
});
要注意,computed声明的变量(pg)是不能直接去修改的(只读),跟vue2一样也有get、set函数(可读可写)。
defineProps、defineEmits
在单文件组件中必须使用defineProps和defineEmits API来声明props和emits,可以算是语法糖。
父组件还是跟原来一样传值,子组件接收:
//父组件
<template>
<div class="home">
<input v-model="msg" />
<HelloWorld :msg="msg" @change="change" />
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import HelloWorld from "@/components/HelloWorld.vue";
let msg = ref("is parent");
const change = (val: string) => {
msg.value = val;
};
</script>
//子组件
<template>
<div>{{ msg }}</div>
<button @click="change">emit</button>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits } from "vue";
const props = defineProps({
msg: String,
});
console.log(props.msg);
const emit = defineEmits(["change"]);
const change = () => {
emit("change", "is son");
};
</script>
//设置默认值
defineProps({
msg: {
type: Number,
default: 100,
},
});
template可以直接使用msg,要调用的话就得props.msg。
withDefaults
defineProps只能限制类型,没有提供默认值(这边应该是限定用ts的接口才没有默认值)。为了解决这个问题,提供了 withDefaults 编译器宏:
type Porps = {
msg: string;
};
const props = withDefaults(defineProps<Porps>(), {
msg: "default",
});
withDefaults接收两个参数,第一个参数是defineProps加props字段的泛型,第二个字段是默认值,也可以不设置。
也可以监听props:
watch(
() => props.msg,
(val) => {
console.log(val);
}
);
this.$refs
vue2很多情况还是需要用到这个api,vue3中比较特别,
为了获得对模板内元素或组件实例的引用,我们可以像往常一样声明ref,
在渲染上下文中暴露root,并通过ref="root",将其绑定到 div 作为其 ref。
在虚拟DOM补丁算法中,如果 VNode 的 ref 键对应于渲染上下文中的 ref,
则VNode的相应元素或组件实例将被分配给该ref的值。
这是在虚拟 DOM 挂载/打补丁过程中执行的,因此模板引用只会在初始渲染之后获得赋值。
<button ref="testRef">testRef</button>
let testRef = ref(null);
onMounted(() => {
// DOM 元素将在初始渲染后分配给 ref
console.log(testRef.value); // <button>testRef</button>
});
nextTick
跟vue2一样的使用:
nextTick(() => {
console.log(testRef.value);
});
也可以跟官网提供的一样用async的函数await:
let testRef = ref(null);
const nextTickFn = async () => {
await nextTick();
console.log(testRef.value);
};
nextTickFn();
defineExpose
vue2有时候会用this.$refs调用子组件的函数或者变量,
单文件组件是默认关闭的,所以单文件组件要用defineExpose编译器宏暴露出去:
//父组件
<template>
<div class="home">
<HelloWorld ref="sonRef" />
</div>
</template>
<script lang="ts" setup>
import { nextTick, ref } from "vue";
import HelloWorld from "@/components/HelloWorld.vue";
let sonRef = ref();
nextTick(() => {
sonRef.value.sonFn();
console.log(sonRef.value.sonRef);
});
</script>
//子组件
let sonRef = ref("is son");
const sonFn = () => {
console.log("is son fn");
};
defineExpose({ sonFn, sonRef });
vue2的this.$parent在单文件组件中,自己是实现了,但是官方没有直接给出代码:
//父组件
const parentRef = ref("is parent ref");
const parentFn = () => {
console.log("is parent fn");
};
defineExpose({
parentRef,
parentFn,
});
//子组件
let parent = getCurrentInstance();
console.log(parent?.parent?.exposed?.parentRef.value);
parent?.parent?.exposed?.parentFn();
新增组件
teleport
官网介绍了很多,这个组件最大的作用就是可以让组件脱离固定的组件位置,可以挂载在逻辑上最优的位置,
其它使用都跟组件一样,只有位置改变:
<teleport to="#teleportDiv">
<HelloWorld />
</teleport>
<teleport to="body">
<HelloWorld />
</teleport>
挂载的元素会从上向下解析,第一个to的元素,标签、class、id等。一般使用也不会随便,都会用id元素或者body。
Suspense
Suspense是一个试验性的新特性,官方也说生产环境请勿使用。
主要是允许将组件异步处理等待过程提升到组件树中处理。
单文件组件里面顶层await里面提到:
async setup() 必须与 Suspense 组合使用,Suspense 目前还是处于实验阶段的特性。
我们打算在将来的某个发布版本中开发完成并提供文档。
所以这边就不给出不是单文件组件的代码实现了。
生命周期
生命周期还是跟原来的一样,只是前面都加上了on,destroy都变成了unmount。
setup会在beforeCreate、created之前执行,取代了beforeCreate、created。
- beforeCreate --> beforeCreate
- created --> setup
- beforeMount --> onBeforeMount
- mounted --> onMounted
- beforeUpdate --> onBeforeUpdate
- updated --> onUpdated
- beforeDestroy --> onBeforeUnmount
- destroyed --> onUnmount
使用:
onMounted(() => {
console.log("mounted");
});
试了一下,居然可以写多个。
vue3新增或者删除了哪些API,其实没必要一下子全部去记住,当一些API使用的时候如果发现不生效了,
再去官网查一下,当然,基础的这些API就得记下来。
还是那句话,先学会基础,上手开发,然后慢慢去了解学习不常用的,甚至是源码。
(学习视频分享:vuejs教程、web前端)