生命周期钩子
我们可以直接看生命周期图来认识都有哪些生命周期钩子(图片来自公众号《程序员成长指北》):
全部生命周期钩子如图所示:
我们可以看到beforeCreate
和created
被setup
替换了。其次,钩子命名都增加了on
; Vue3.x还新增用于调试的钩子函数onRenderTriggered
和onRenderTricked
下面我们简单使用几个钩子, 方便大家学习如何使用,Vue3.x中的钩子是需要从vue中导入的:
<template>
<div>{{num}}</div>
</template>
<script>
import {
ref,
defineComponent,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onErrorCaptured,
onRenderTracked,
onRenderTriggered
} from "vue";
export default defineComponent({
// beforeCreate和created是vue2的
beforeCreate() {
console.log("------beforeCreate-----");
},
created() {
console.log("------created-----");
},
setup() {
console.log("------setup-----");
// vue3.x生命周期写在setup中
onBeforeMount(() => {
console.log("------onBeforeMount-----");
});
onMounted(() => {
console.log("------onMounted-----");
});
onUpdated(() => {
console.log('updated!')
})
onUnmounted(() => {
console.log('unmounted!')
})
const num = ref(0)
setInterval(() => {
num.value++;
}, 1000)
// 调试哪些数据发生了变化
onRenderTriggered((event) => {
console.log("------onRenderTriggered-----", event);
})
return {
num
}
},
});
</script>
复制代码
我们通过setInterval来改变数据,可以看到onUpdated函数和onRenderTriggered函数都被触发了!
setup
执行顺序
export default defineComponent ({
beforeCreate() {
console.log("----beforeCreate----");
},
created() {
console.log("----created----");
},
setup() {
console.log("----setup----");
},
})
结果是:
setup
beforeCreate
created
复制代码
warning 由于在执行setup
时尚未创建组件实例,因此在 setup
选项中没有 this
。
setup 参数
setup
接受两个参数:
- props: 组件传入的属性/参数
- context
setup中接受的props
是响应式的,由于是响应式的, 所以不可以使用ES6解构,解构会消除它的响应式。
错误代码示例, 这段代码会让props不再支持响应式:
export default defineComponent ({
setup(props, context) {
const { name } = props
console.log(name)
},
})
复制代码
如果要使用结构,则需要使用官方的toRefs
,这个我们后续介绍。
setup
第二个参数context
,setup
中不能访问Vue2中最常用的this
对象,所以context
中就提供了this
中最常用的三个属性:attrs
、slot
和emit
,分别对应Vue2.x中的 $attr
属性、slot
插槽 和$emit
发射事件,并且这几个属性都是自动同步最新的值,所以我们每次使用拿到的都是最新值。
reactive、ref与toRefs
在vue2.x中, 定义双向绑定的数据都是在data
中, 但是Vue3 要使用reactive
和ref
来进行双向绑定数据定义。
那么ref
和reactive
他们有什么区别呢?
<template>
<div>{{obj.name}}-{{obj.count}}</div>
<div>{{basetype}}</div>
<div>{{baseTypeReactive}}</div>
<div>{{objreactive.count}}</div>
</template>
<script>
import {
reactive,
ref
} from 'vue';
export default {
setup() {
const obj = ref({
count: 1,
name: "张三"
})
const basetype = ref(2)
setTimeout(() => {
obj.value.count = obj.value.count + 1
obj.value.name = "李四"
basetype.value += 1
}, 1000)
const baseTypeReactive = reactive(6)
const objreactive = reactive({
count: 10
})
return {
obj,
basetype,
baseTypeReactive,
objreactive
}
},
}
</script>
复制代码
reactive
reactive
是 Vue3 中提供的实现响应式数据的方法。- 在 Vue2 中响应式数据是通过 defineProperty 来实现的,在 Vue3 中响应式数据是通过 ES6 的
Proxy
来实现的。 - reactive 参数必须是对象 (json / arr),不能代理基本类型,例如字符串、数字、boolean等。
- 本质: 就是将传入的数据包装成一个Proxy对象
- 如果给 reactive 传递了其它对象(如Date对象)
- 默认情况下,修改对象无法实现界面的数据绑定更新。
- 如果需要更新,需要进行重新赋值。(即不允许直接操作数据,需要放个新的数据来替代原数据)
在 reactive
使用基本类型参数
基本类型(数字、字符串、布尔值)在 reactive
中无法被创建成 proxy
对象,也就无法实现监听,无法实现响应式。
<template>
<div>
<p>{{msg}}</p>
<button @click="c">button</button>
</div>
</template>
<script>
import {
reactive
} from 'vue'
export default {
name: 'App',
setup() {
let msg = reactive(0)
function c() {
console.log(msg);
msg++;
}
return {
msg,
c
};
}
}
</script>
复制代码
点击 button ,我们期望的结果是数字从 0 变成 1,然而实际上界面上的数字并没有发生任何改变。
查看控制台,它的输出是这样的(我点了 3 次)
出现提示
value cannot be made reactive: 0
而输出的值确实发生了变化,只不过这种变化并没有反馈到界面上,也就是说并没有实现双向数据绑定。当然,如果是 ref
的话,就不存在这样的问题。而如果要使用 reactive
,我们需要将参数从 基本类型 转化为 对象。
<template>
<div>
<p>{{msg.num}}</p>
<button @click="c">button</button>
</div>
</template>
<script>
import {
reactive
} from 'vue'
export default {
name: 'App',
setup() {
let msg = reactive({
num: 0
})
function c() {
console.log(msg);
msg.num++;
}
return {
msg,
c
};
}
}
</script>
复制代码
将参数替换成了对象 {num: 0}
,此时,点击按钮界面就会产生改变(我点了 3 次)。
在控制台打印消息
可以看到,msg
成功被创建成了 proxy
对象,他通过劫持对象的 get
和 set
方法实现了对象的双向数据绑定。
深层的、对象内部的变化也能被察觉到(注意下面代码中的 inner
)
<template>
<div>
<p>{{msg.num.inner}}</p>
<button @click="c">button</button>
</div>
</template>
<script>
import { reactive } from 'vue'
export default {
name: 'App',
setup() {
let msg = reactive({
num: {
inner: 0
}
})
function c() {
console.log(msg);
msg.num.inner ++;
}
return {
msg,
c
};
}
}
</script>
复制代码
数组变化当然也可以监听
<template>
<div>
<p>{{msg}}</p>
<button @click="c">button</button>
</div>
</template>
<script>
import {
reactive
} from 'vue'
export default {
name: 'App',
setup() {
let msg = reactive([1, 2, 3])
function c() {
console.log(msg);
msg[0] += 1;
msg[1] = 5;
}
return {
msg,
c
};
}
}
</script>
复制代码
对象数组也可
<template>
<div>
<p>{{msg}}</p>
<button @click="push">push</button>
<button @click="change">change</button>
</div>
</template>
<script>
import {
reactive
} from 'vue'
export default {
name: 'App',
setup() {
let msg = reactive([{
name: 'lilei',
age: 12
}])
function change() {
console.log(msg);
msg[0].age += 1;
}
function push() {
console.log(msg);
msg.push({
name: 'zhaodafa',
age: 22
})
}
return {
msg,
change,
push
};
}
}
</script>
复制代码
特殊情况:reactive
中监听 Date
日期格式数据
如果参数不是数组、对象,而是稍微奇怪一点的数据类型,例如说 Date
,那么麻烦又来了。
<template>
<div>
<p>{{msg}}</p>
<button @click="c">button</button>
</div>
</template>
<script>
import {
reactive
} from 'vue'
export default {
name: 'App',
setup() {
let msg = reactive(new Date())
function c() {
console.log(msg);
msg.setDate(msg.getDate() + 1);
console.log(msg);
}
return {
msg,
c
};
}
}
</script>
复制代码
这里我先打印了 msg
两次,可以看到,点击一次 button ,msg
的数据是存在变化的,但界面并未发生变化,同时我们发现在控制台里,msg
并未被识别成 proxy
。
就算我们把 Date
放在对象里,如下:
<template>
<div>
<p>{{msg.date}}</p>
<button @click="c">button</button>
</div>
</template>
<script>
import {
reactive
} from 'vue'
export default {
name: 'App',
setup() {
let msg = reactive({
date: new Date()
});
function c() {
console.log(msg);
msg.date.setDate(msg.date.getDate() + 1);
console.log(msg);
}
return {
msg,
c
};
}
}
</script>
复制代码
也仍然不起效果。
显然,对于这种数据类型,我们需要做特殊处理。
处理方式就是重新赋值(而不是直接修改原来的值)。
<template>
<div>
<p>{{msg.date}}</p>
<button @click="c">button</button>
</div>
</template>
<script>
import {
reactive
} from 'vue'
export default {
name: 'App',
setup() {
let msg = reactive({
date: new Date()
});
function c() {
console.log(msg);
msg.date.setDate((msg.date.getDate() + 1));
msg.date = new Date(msg.date);
console.log(msg);
}
return {
msg,
c
};
}
}
</script>
复制代码
这里我采用了拷贝的方案重新赋值了 msg.date
,界面成功发生了变化(日期 + 1)。
ref
ref可以监听复杂对象也可以监听基础数据类型,如下:
<template>
<!-- 直接取值,无需xxx.value -->
<div>{{obj.name}}-{{obj.count}}</div>
<div>{{basetype}}</div>
<div>{{date}}</div>
</template>
<script>
import {
ref
} from 'vue';
export default {
setup() {
const obj = ref({
count: 1,
name: "张三"
})
const basetype = ref(2)
const date = ref(new Date())
setTimeout(() => {
obj.value.count = obj.value.count + 1
obj.value.name = "李四"
basetype.value += 1
date.value.setDate((date.value.getDate() + 1)); // 此处也可直接修改Date类型,不需要重新赋值
// date.setDate((date.value.getDate() + 1));
// date = new Date(date);
}, 1000)
return {
obj,
basetype,
date
}
},
}
</script>
复制代码
ref 监听Date类型也可直接修改Date类型,不需要重新拷贝赋值
但是要注意ref监听的对象在setup方法中需要使用xxx.value
来赋值和取值;在页面上可以直接取值
解构方法:toRefs
页面是通过user.name
,user.age
写感觉很繁琐,我们能不能直接将user
中的属性解构出来使用呢?答案是不能直接对user
进行结构, 这样会消除它的响应式, 上面我们已经说过了。上面我们说props
不能使用ES6直接解构
解决办法就是使用toRefs
。
toRefs用于将一个reactive对象转化为属性全部为ref对象的普通对象。具体使用方式如下:
<template>
<div class="homePage">
<p>第 {{ year }} 年</p>
<p>姓名: {{ nickname }}</p>
<p>年龄: {{ age }}</p>
</div>
</template>
<script>
import {
defineComponent,
reactive,
ref,
toRefs
} from "vue";
export default defineComponent({
setup() {
const year = ref(0);
const user = reactive({
nickname: "xiaofan",
age: 26,
gender: "女"
});
setInterval(() => {
year.value++
user.age++
}, 1000)
return {
year,
// 使用reRefs
...toRefs(user)
}
},
});
</script>
复制代码
watch 与 watchEffect
watch 函数用来侦听特定的数据源,并在回调函数中执行副作用。默认情况是惰性的,也就是说仅在侦听的源数据变更时才执行回调。
watch(source, callback, [options])
复制代码
参数说明:
- source:可以支持string,Object,Function,Array; 用于指定要侦听的响应式变量
- callback: 执行的回调函数
- options:支持deep、immediate 和 flush 选项。
侦听reactive定义的数据
<template>
<div>{{nickname}}</div>
</template>
<script>
import {
defineComponent,
ref,
reactive,
toRefs,
watch
} from "vue";
export default defineComponent({
setup() {
const state = reactive({
nickname: "xiaofan",
age: 20
});
setTimeout(() => {
state.age++
}, 1000)
// 修改age值时会触发 watch的回调
watch(
() => state.age,
(curAge, preAge) => {
console.log("新值:", curAge, "老值:", preAge);
}
);
return {
...toRefs(state)
}
},
});
</script>
复制代码
侦听ref定义的数据
const year = ref(0)
setTimeout(() =>{
year.value ++
},1000)
watch(year, (newVal, oldVal) =>{
console.log("新值:", newVal, "老值:", oldVal);
})
复制代码
侦听多个数据
上面两个例子中,我们分别使用了两个watch, 当我们需要侦听多个数据源时, 可以进行合并, 同时侦听多个数据:
watch([() => state.age, year], ([curAge, preAge], [newVal, oldVal]) => {
console.log("新值:", curAge, "老值:", preAge);
console.log("新值:", newVal, "老值:", oldVal);
});
复制代码
侦听复杂的嵌套对象
我们实际开发中,复杂数据随处可见, 比如:
const state = reactive({
room: {
id: 100,
attrs: {
size: "140平方米",
type:"三室两厅"
},
},
});
watch(() => state.room, (newType, oldType) => {
console.log("新值:", newType, "老值:", oldType);
}, {deep:true});
复制代码
在复杂数据访问中,如果不使用第三个参数deep:true
, 是无法监听到数据变化的。
前面我们提到,默认情况下,watch是惰性的, 那什么情况下不是惰性的, 可以立即执行回调函数呢?其实使用也很简单, 给第三个参数中设置immediate: true
即可。
stop 停止监听
我们在组件中创建的watch
监听,会在组件被销毁时自动停止。如果在组件销毁之前我们想要停止掉某个监听, 可以调用watch()
函数的返回值,操作如下:
const stopWatchRoom = watch(() => state.room, (newType, oldType) => {
console.log("新值:", newType, "老值:", oldType);
}, {deep:true});
setTimeout(()=>{
// 停止监听
stopWatchRoom()
}, 3000)
复制代码
还有一个监听函数watchEffect
,在我看来watch
已经能满足监听的需求,为什么还要有watchEffect
呢?虽然我没有get到它的必要性,但是还是要介绍一下watchEffect
,首先看看它的使用和watch
究竟有何不同。
import { defineComponent, ref, reactive, toRefs, watchEffect } from "vue";
export default defineComponent({
setup() {
const state = reactive({ nickname: "xiaofan", age: 20 });
let year = ref(0)
setInterval(() =>{
state.age++
year.value++
},1000)
watchEffect(() => {
console.log(state);
console.log(year);
}
);
return {
...toRefs(state)
}
},
});
复制代码
执行结果首先打印一次state
和year
值;然后每隔一秒,打印state
和year
值。
从上面的代码可以看出, 并没有像watch
一样需要先传入依赖,watchEffect
会自动收集依赖, 只要指定一个回调函数。在组件初始化时, 会先执行一次来收集依赖, 然后当收集到的依赖中数据发生变化时, 就会再次执行回调函数。
所以总结对比如下:
- watchEffect 不需要手动传入依赖
- watchEffect 会先执行一次用来自动收集依赖
- watchEffect 无法获取到变化前的值, 只能获取变化后的值
Tips: 如果定义一个非响应式的值, watch和watchEffect是无法监听到值的变化的!!!
自定义 Hooks
在vue2 中可以抽出mixin 来实现共有逻辑功能,(但是其弊端在此处就不赘述了), vue3中可以将其封装成一个hook, 我们约定这些「自定义 Hook」以 use 作为前缀,和普通的函数加以区分。
useCount.js
实现:
import {
ref,
computed
} from "vue";
export default function useCount(initValue = 1) {
const count = ref(initValue);
const increase = (delta) => {
if (typeof delta !== "undefined") {
count.value += delta;
} else {
count.value += 1;
}
};
const multiple = computed(() => count.value * 2)
const decrease = (delta) => {
if (typeof delta !== "undefined") {
count.value -= delta;
} else {
count.value -= 1;
}
};
return {
count,
multiple,
increase,
decrease,
};
}
复制代码
接下来看一下在组件中使用useCount
这个 hook
:
<template>
<div>
<p>count: {{ count }}</p>
<p>倍数: {{ multiple }}</p>
<div>
<button @click="increase()">加1</button>
<button @click="decrease()">减一</button>
</div>
</div>
</template>
<script>
import {
defineComponent
} from 'vue';
import useCount from "../hooks/useCount";
export default defineComponent({
setup() {
const {
count,
multiple,
increase,
decrease
} = useCount(10);
return {
count,
multiple,
increase,
decrease,
};
},
});
</script>
复制代码
props父子组件传参
父子组建通过props传递参数,如 <test :name="basetype"></test>
中的name
// 父组件
<template>
<div>
父组件 basetype:{{basetype}}
<button @click="addNum">addNum</button>
</div>
<test :name="basetype"></test>
</template>
<script>
import {
ref, defineComponent, getCurrentInstance, readonly
} from 'vue';
import test from './test.vue'
export default defineComponent({
components: { test },
setup() {
const basetype = ref(2)
function addNum() {
basetype.value += 1
}
return {
addNum,
basetype,
}
},
})
</script>
复制代码
// 子组件
<template>
<div style="border: 1px solid red;">
<h2>子组件 name from basetype:{{name}}</h2>
</div>
</template>
<script>
import { defineComponent, watch } from 'vue'
export default defineComponent({
props: {
name: {
type: Number
}
},
setup(props) {
watch(props, (val, oldVal) => {
console.log('子组件watch: ' + JSON.stringify(val))
})
return {
// props传递过来的值直接在template中使用,不需要return
// name: props.name // 此处不可以写;写了之后就会认为是return { name: 2 };不会是响应式的了
}
},
})
</script>
复制代码
Teleport
Teleport 就是将组件的位置传送绑定到定义的Dom位置,像是哆啦A梦中的「任意门」,之前Element UI的dialog和select展开之后的组件默认不是绑定在父组件的dom元素上,而是在body下的全局元素。
我们的Teleport其实就是类似于这样的功能,但是它是支持想绑在哪里就绑在哪里的,先看一个小例子:
<template>
<div>父组件</div>
<button @click="show = !show">changeShow</button>
<modal v-if="show">
<div>子组件 slot</div>
<div slot="footer">子组件 footer slot</div>
</modal>
</template>
<script setup>
import { ref, defineComponent } from 'vue';
import modal from './test.vue'
let show = ref(false)
</script>
复制代码
// teleport 组件
<template>
<teleport to="body"> // 这里确定了绑定元素到body里
<div :class="$style.modal">
Modal
<div class="modal_content">
<slot></slot>
</div>
<div class="modal_footer">
<slot name="footer"></slot>
</div>
</div>
</teleport>
</template>
<style lang='scss' module>
.modal {
border: 1px solid red;
}
</style>
复制代码
我们再换一个地方绑定,将上面的body修改为<teleport to="#modaldom">
, 下图可看到绑定的位置发生了变化
Suspense
Suspense是Vue 3新增的内置标签,针对异步组件采取的降级渲染效果,小的层面可以处理请求数据未返回时候组件的渲染Loading,个人觉得在骨架屏方面也可以使用。
异步组件一般有以下的情况:
- 页面加载之前显示加载动画
- 显示占位符内容
- 处理延迟加载的图像和文件
- 骨架屏
以前在Vue 2中,我们必须使用v-if 加上一个loading标识位来检查我们的数据是否已加载并显示内容,但是现在,可以使用随Vue3的Suspense了,下面看代码:
<template>
<Suspense>
<template #default>
<asyncCom></asyncCom>
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue';
const asyncCom = defineAsyncComponent(() => import("./test.vue"));
</script>
复制代码
<template>
<div>
<ul>
<li v-for="item in jsonData" :key="item.name">{{ item.name }} - {{ item.age }}</li>
</ul>
</div>
</template>
<script setup>
import { ref } from "vue";
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{
name: "张三",
age: 15,
},
{
name: "李四",
age: 17,
},
]);
}, 1500);
});
}
ref: jsonData = await fetchData();
</script>
复制代码
效果如下:
异步组建加载前:
异步组件加载后:
Vue从上到下执行子组件的setup里的全部语句,执行完同步语句(包括await语句)之后,父组件就认为子组件加载完成,在这之前,子组件setup状态始终未pending,所以父组件显示降级内容(Loading…),等子组件setup的状态变成resolved或者rejected,父组件就显示默认内容。
拓展阅读:
Object.defineProperty vs Proxy
Vue2.x的时候就经常遇到一个问题,数据更新了啊,为何页面不更新呢?什么时候用$set
更新,什么时候用$forceUpdate
强制更新,你是否也一度陷入困境。后来的学习过程中开始接触源码,才知道一切的根源都是 Object.defineProperty
。
这里简单对比一下Object.defineProperty
与Proxy
Object.defineProperty
只能劫持对象的属性, 而Proxy是直接代理对象由于Object.defineProperty
只能劫持对象属性,需要遍历对象的每一个属性,如果属性值也是对象,就需要递归进行深度遍历。但是Proxy直接代理对象, 不需要遍历操作Object.defineProperty
对新增属性需要手动进行Observe
,因为Object.defineProperty
劫持的是对象的属性,所以新增属性时,需要重新遍历对象, 对其新增属性再次使用Object.defineProperty
进行劫持。也就是Vue2.x中给数组和对象新增属性时,需要使用$set
才能保证新增的属性也是响应式的,$set
内部也是通过调用Object.defineProperty
去处理的。
参考博文:
[1] vue3中reactive注意点(系列四) www.cnblogs.com/fsg6/p/1448…
[2]Vue 3新引入的Suspense组件介绍 www.jianshu.com/p/4bc2dfba1…