一、Vue3.0项目搭建
1、使用脚手架Vite
npm init vite vue3.0-demo --template vue
2、通过脚手架vue-cli进行搭建
(1)首先查看电脑中vue-cli的版本,查看版本号:vue -V
(2)如果是旧版本的(1.x或2.x),需要先卸载旧的版本: npm uninstall vue-cli -g
(3)安装新的版本:npm install -g @vue/cli
(4)创建项目:vue create <文件名称>
(5)根据项目所需选择自己所需的配置
二、为什么升级Vue3.0
使用 Vue2.x 的小伙伴都熟悉,Vue2.x 中所有数据都是定义在data中,方法定义在methods中的,并且使用this来调用对应的数据和方法,当一个组件的代码超过几百行时,这时增加或者修改某个需求, 就要在 data、methods、computed 以及 mounted 中进行反复的跳转,甚至一个功能还有会依赖其他功能,全搅合在一起。如图所示:
当我们业务复杂了就会大量出现上面的情况, 随着复杂度上升,就会出现这样一张图, 每个颜色的方块表示一个功能:
Vue2.x 版本给出的解决方案就是 Mixin, 但是使用 Mixin 也会遇到让人苦恼的问题:
如果可以按照逻辑进行分割,将上面这张图变成下边这张图,是不是就清晰很多了呢, 这样的代码可读性和可维护性都会更高,所以,Vue3.x 就推出了Composition API主要就是为了解决上面的问题,将零散分布的逻辑组合在一起来维护,并且还可以将单独的功能逻辑拆分成单独的文件 。
setup函数是 Composition API(组合API)的入口
运行结果:
setup 执行时机是在 beforeCreate 之前执行的
1、使用方法:
使用setup的时候,需要接收两个参数:
(1)props:组件传入的属性
(2)context
setup 中接受的props是响应式的, 当传入新的 props 时,会及时被更新。由于是响应式的, 所以不可以使用 ES6 解构,解构会消除它的响应式。 错误代码示例, 这段代码会让 props 不再支持响应式:
export default defineComponent ({
setup(props, context) {
const { name } = props
console.log(name)
},
})
那在开发中我们想要使用解构,还能保持props的响应式,有没有办法解决呢?大家可以思考一下,在后面toRefs学习的地方再进行介绍。 接下来我们来说一下setup接受的第二个参数context,我们前面说了setup中不能访问 Vue2 中最常用的this对象,所以context中就提供了this中最常用的三个属性:attrs、slot 和emit,分别对应 Vue2.x 中的 a t t r 属 性 、 s l o t 插 槽 和 attr属性、slot插槽 和 attr属性、slot插槽和emit发射事件。并且这几个属性都是自动同步最新的值,所以我们每次使用拿到的都是最新值。
export default {
setup(props, context) {
// Attribute (非响应式对象,等同于 $attrs)
console.log(context.attrs)
// 插槽 (非响应式对象,等同于 $slots)
console.log(context.slots)
// 触发事件 (方法,等同于 $emit)
console.log(context.emit)
// 暴露公共 property (函数)
console.log(context.expose)
}
}
context 是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着可以安全地对 context 使用 ES6 解构。
export default {
setup(props, { attrs, slots, emit, expose }) {
...
}
}
attrs 和 slots 是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以 attrs.x 或 slots.x 的方式引用 property。请注意,与 props 不同,attrs 和 slots 的 property 是非响应式的。如果你打算根据 attrs 或 slots 的更改应用副作用,那么应该在 onBeforeUpdate 生命周期钩子中执行此操作。
四、响应式API
1、响应式基础API
(1)reactive、ref
在 vue2.x 中, 定义数据都是在data中, 但是 Vue3.x 可以使用reactive和ref来进行数据定义。
setup() {
let num1 = 10;
let num2 = ref(100);
let num3 = relative(100);
console.log(num1);
console.log(num2);
console.log(num3);
return {num1,num2,num3}
}
打印结果如图所示:
在控制台输出一个警告信息, 提示100这个值不能被reactive创建,官方也推荐在定义数据的时候,reactive定义复杂的数据类型的数据,ref推荐定义基本数据类型,所以如果要使用reactive定义基本数据类型的话,我们需要在reactive中将数据包装一下:
let num3 = relative({val:10});
ref定义的数据打印结果需要.value才能获取到结果,而reactive则不需要
ref和relative的区别:
(1)reactive 和 ref 都是用来定义响应式数据的 reactive更推荐去定义复杂的数据类型 ref 更推荐定义基本类型。
(2)ref 和 reactive 本质我们可以简单的理解为ref是对reactive的二次包装, ref定义的数据访问的时候要多一个.value
(3)使用ref定义基本数据类型,ref也可以定义数组和对象。
isRef:检查值是否为一个 ref 对象。
isReactive:检查对象是否是由 reactive 创建的响应式代理。
import { ref,reactive, isRef,isReactive } from 'vue'
export default {
setup() {
const state = reactive({
name: 'John'
})
const num = ref(10);
console.log(isRef(num)) //true
console.log(isReactive(state)) // -> true
}
}
<template>
<div class="homePage">
<p>第 {{ year }} 年</p>
<p>姓名: {{ user.nickname }}</p>
<p>年龄: {{ user.age }}</p>
</div>
</template>
<script>
import { defineComponent, reactive, ref} 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,
user
};
},
});
</script>
上面的代码中,我们绑定到页面是通过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>
(2)Readonly
const foo = {
name:'lzihi',
other:{
age:18,
sex:'男'
}
}
const readonlyFoo = readonly(foo)
readonlyFoo.name = 'lisi' // 报错 Set operation on key "name" failed: target is readonly.
readonlyFoo.other.age ++ // 报错 Set operation on key "age" failed: target is readonly.
isReadonly:检查对象是否是由 readonly 创建的只读代理。
2、Computed 与 Watch
(1)computed
接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象。
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误
或者,接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象。
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
(2)watch
watch 函数用来侦听特定的数据源,并在回调函数中执行副作用。默认情况是惰性的,也就是说仅在侦听的源数据变更时才执行回调。watch(source, callback, [options])
参数说明:
source: 可以支持 string,Object,Function,Array,用于指定要侦听的响应式变量
callback: 执行的回调函数
options:支持 deep、immediate 和 flush 选项。
侦听 reactive 定义的数据:
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),
};
},
}
侦听 ref 定义的数据:
const year = ref(0);
setTimeout(() => {
year.value++;
}, 1000);
watch(year, (newVal, oldVal) => {
console.log("新值:", newVal, "老值:", oldVal);
})
侦听多个数据:
上面两个例子中,我们分别使用了两个 watch, 当我们需要侦听多个数据源时, 可以进行合并, 同时侦听多个数据:
watch(
[() => state.age, year],
([curAge, newVal], [preAge, 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的使用:
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会自动收集依赖, 只要指定一个回调函数。在组件初始化时, 会先执行一次来收集依赖, 然后当收集到的依赖中数据发生变化时, 就会再次执行回调函数。所以总结对比如下: