年初团队开始引入vue3版本,为后期ts的全栈使用,从语言层面(Typescript)作准备,但是由于团队小伙伴新人较多,对ts的接受需要一定的时间来作准备,顾先在小型项目中使用js版本的vue3,等大家相对熟悉新的api之后再逐渐介入ts,下面开始正文:
生命周期钩子
钩子函数
我们可以直接看生命周期图来认识都有哪些生命周期钩子(图片来自公众号《程序员成长指北》):
全部生命周期钩子如图所示:
我们可以看到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与toRef
toRefs
-
toRefs可以看作toRef的语法糖,toRefs遍历传入对象的所有属性,使其都具备响应式。
-
页面是通过
user.name
,user.age
写很繁琐,但是又不能直接对user
进行Es6的解构, 这样会消除它的响应式, 上面我们已经说过了,解决办法就是使用toRefs
;与上面我们说props
不能使用ES6直接解构的情况是一致的。 -
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>
toRef
- 用于解构常规(非响应式)对象,解构之后的变量具备响应式,跟随原对象值改变(但是修改现变量的值,不会反向改变原基础对象的值)
const obj1 = {
count: 1,
name: '张三1'
};
let name1 = toRef(obj1, 'name'); //
<template>
<div style="color: white">
<div>obj1: {{ obj1 }}<br />name1:{{ name1 }}-----name1Cp:{{ name1Cp }}</div>
<br />
<div>
obj2: {{ obj2 }} <br />
name2:{{ name2 }}
</div>
</div>
</template>
<script>
import { ref, toRef, defineComponent } from 'vue';
export default defineComponent({
setup() {
const obj1 = {
count: 1,
name: '张三1'
};
let name1 = toRef(obj1, 'name'); // 解构之后具备响应式,跟随对象原值改变(但是修改name1,不会反向改变原对象)
let { name: name1Cp } = obj1; // es6的解构是没有响应式的,虽然他的值也发生了变化
const obj2 = ref({
count: 1,
name: '张三2'
});
let name2 = toRef(obj2, 'name'); // 不行的,只能解构基础对象,不能解构ref响应式对象
setInterval(() => {
obj1.count++;
obj1.name = '李四' + obj1.count; // name1也会随之修改
// name1 = '王五' + obj1.count;
// console.log(name1, obj1); // name1值修改成功,但是页面无法感知
// console.log(name1Cp); // 一直是:张三1
obj2.value.count++;
obj2.value.name = '李四' + obj2.value.count;
name2 = '王五2';
}, 1000);
return {
obj1,
obj2,
name1,
name2,
name1Cp
};
}
});
</script>
<style lang="less">
.demo {
color: @fontsize-color;
font-size: @fontsize-level1;
}
</style>
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是无法监听到值的变化的!!!
使用ref获取dom元素
先看下vue2中获取方式
<div ref="myRef"></div>
this.$refs.myRef
vue3用法: 获取单个dom
<template>
<div ref="myRef">获取单个DOM元素</div>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
setup() {
const myRef = ref(null);
onMounted(() => {
console.dir(myRef.value);
});
return {
myRef
};
}
};
</script>
vue3获取多个dom
<template>
<div :class="$style.demo">
<div>获取多个DOM元素</div>
<ul>
<li v-for="(item, index) in arr" :key="index" :ref="setRef">
{{ item }}
</li>
</ul>
</div>
</template>
<script>
import { ref, nextTick } from 'vue';
export default {
setup() {
const arr = ref([1, 2, 3]);
// 存储dom数组
const myRef = ref([]);
const setRef = el => {
myRef.value.push(el);
};
nextTick(() => {
console.dir(myRef.value);
});
return {
arr,
setRef
};
}
};
</script>
控制台打印如下:
Proxy
[[Handler]]: Object
[[Target]]: Array(3)
0: li
1: li
2: li
length: 3
自定义 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>
代替vuex的状态管理组件pina
Pinia 类似 Vuex, 是用于 Vue 的状态管理库,Pinia 支持 Vue2 和 Vue3
本文只讲 Pinia 在 Vue3 中的使用, 在 Vue2 中使用略有差异,参考 官方文档
它非常的轻量, 仅有 1 KB;采用模块化设计,按需引入,易于学习。
下面我们直接上代码;
main.js
import { setupStore } from '@/store';
import App from './App.vue';
const app = createApp(App);
setupStore(app); // 注册store
Src/store/index.js
import { createPinia } from 'pinia';
const store = createPinia();
export function setupStore(app) {
app.use(store);
}
export { store };
Src/store/modules/app.js
import { defineStore } from 'pinia';
import { store } from '@/store';
import ehvApi from '@/api/ehv.js';
export const useAppStore = defineStore({
id: 'app', // 唯一id
// 数据存储区
state: () => ({
currentStation: '',
stationList: [],
pageLoading: false
}),
getters: {
getCurrentStation() {
return this.currentStation;
},
getStationList() {
return this.stationList;
}
},
// 异步修改
actions: {
async queryStationList() {
this.stationList = await ehvApi.stationList();
return this.stationList;
},
setCurrentStation(val) {
this.currentStation = val;
}
}
});
App.vue
<script setup>
import { useAppStore } from '@/store/modules/app.js';
const appStore = useAppStore();
// 调取action的接口请求
appStore.queryStationList();
// 获取store中信息
let selectOptions = computed(() => appStore.getStationList);
</script>
事件总线mitt.js
Vue2.x 使用 EventBus 进行组件通信,通过 new 一个 Vue 实例的方式,让它来充当事件总线,管理事件派发响应。 Vue3.x 由于源码的改动不在支持原有的写法,官方推荐使用mitt.js。Vue3 从实例中完全删除了 $on
、$off
和 $once
方法。$emit
仍然是现有API的一部分,但是它目前用于触发由父组件以声明方式附加的事件。
比起 Vue 实例上的 EventBus,mitt.js 足够小,仅有200bytes,且支持全部事件的监听和批量移除,还可以跨框架使用,React 或者 Vue,甚至 jQuery 项目都能使用同一套库。
安装: npm install --save mitt
使用方式:
Utils/mitt.js
import mitt from 'mitt';
export default mitt();
A.vue
import emitter from '@/utils/mitt';
... ...
// 触发事件
emitter.emit('changeStation', value);
B.vue
import emitter from '@/utils/mitt';
// 接收事件
emitter.on('changeStation', val => {
// 接收的回调方法
});
其他用法,通过 on 方法添加事件,off 方法移除,clear 清空所有。
import mitt from 'mitt'
const emitter = mitt()
// listen to an event
emitter.on('foo', e => console.log('foo', e) )
// listen to all events
emitter.on('*', (type, e) => console.log(type, e) )
// fire an event
emitter.emit('foo', { a: 'b' })
// clearing all events
emitter.all.clear()
// working with handler references:
function onFoo() {}
emitter.on('foo', onFoo) // listen
emitter.off('foo', onFoo) // unlisten
拓展阅读:
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] 公众号程序员成长指北- Vue3.0 新特性以及使用变更总结(实际工作用到的)