使用Vue2.x的小伙伴都熟悉,Vue2.x中所有数据都是定义在data
中,方法定义在methods
中的,并且使用this
来调用对应的数据和方法。那Vue3.x中就可以不这么玩了, 具体怎么玩我们后续再说, 先说一下Vue2.x版本这么写有什么缺陷,所以才会进行升级变更的。
文档
https://vue3js.cn/vue-composition-api/
回顾 Vue2.x 实现加减
<template>
<div class="homePage">
<p>count: {{ count }}</p>
<p>倍数: {{ multiple }}</p>
<div>
<button style="margin-right:10px" @click="increase">加1</button>
<button @click="decrease">减一</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
count: 0,
};
},
computed: {
multiple() {
return 2 * this.count;
},
},
methods: {
increase() {
this.count++;
},
decrease() {
this.count++;
},
},
};
</script>
上面代码只是实现了对count
的加减以及显示倍数, 就需要分别在data、methods、computed中进行操作,当我们增加一个需求,就会出现下图的情况:
当我们业务复杂了就会大量出现上面的情况, 随着复杂度上升,就会出现这样一张图, 每个颜色的方块表示一个功能:
甚至一个功能还有会依赖其他功能,全搅合在一起。
当这个组件的代码超过几百行时,这时增加或者修改某个需求, 就要在data、methods、computed以及mounted中反复的跳转,这其中的的痛苦写过的都知道。
那我们就想啊, 如果可以按照逻辑进行分割,将上面这张图变成下边这张图,是不是就清晰很多了呢, 这样的代码可读性和可维护性都更高:
那么vue2.x版本给出的解决方案就是Mixin, 但是使用Mixin也会遇到让人苦恼的问题:
- 命名冲突问题
- 不清楚暴露出来的变量的作用
- 逻辑重用到其他 component 经常遇到问题
关于上面经常出现的问题我就不一一举例了,使用过的小伙伴多多少少都会遇到。文章的重点不是Mixin,如果确实想知道的就留言啦~
所以,我们Vue3.x就推出了Composition API
主要就是为了解决上面的问题,将零散分布的逻辑组合在一起来维护,并且还可以将单独的功能逻辑拆分成单独的文件。接下来我们就重点认识Composition API
。
Composition API
setup()
setup
函数是一个新的组件选项。作为在组件内使用**Composition API**
的入口点。从生命周期钩子的视角来看,它会在beforeCreate
钩子之前被调用,所有变量、方法都在setup
函数中定义,之后return
出去供外部使用。从setup
返回的所有内容都将暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。
export default defineComponent ({
beforeCreate() {
console.log("----beforeCreate----");
},
created() {
console.log("----created----");
},
setup() {
console.log("----setup----");
},
})
由于在执行setup
时尚未创建组件实例,因此在 setup
选项中没有 this
。
你只能访问以下 property:
- props
- attrs
- slots
- emit
换句话说,你将无法访问以下组件选项:
- data
- computed
- methods
该函数有2个参数:
<!-- 组件传值 -->
<com-setup p1="传值给 com-setup"/>
// 通过 setup 函数的第一个形参,接收 props 数据:
setup(props) {
console.log(props)
},
// 在 props 中定义当前组件允许外界传递过来的参数名称:
props: {
p1: String
}
setup中接受的props
是响应式的, 当传入新的props 时,会及时被更新。由于是响应式的, 所以不可以使用ES6解构,解构会消除它的响应式。
错误代码示例, 这段代码会让props不再支持响应式:
// demo.vue
export default defineComponent ({
setup(props, context) {
const { name } = props
console.log(name)
},
})
通过toRefs、toRef解构props
如果需要解构 prop,可以通过使用 setup 函数中的 toRefs 来完成此操作
import { toRefs } from 'vue'
setup(props) {
const { title } = toRefs(props)
console.log(title.value)
}
如果 title 是可选的 prop,则传入的 props 中可能没有 title 。在这种情况下,toRefs 将不会为 title 创建一个 ref 。你需要使用 toRef 替代它:
// MyBook.vue
import { toRef } from 'vue'
setup(props) {
const title = toRef(props, 'title')
console.log(title.value)
}
Context
setup 函数的第二个形参 context 是一个上下文对象,前面说了setup
中不能访问Vue2中最常用的this
对象,所以context
中提供属性(attrs
,slots
,emit
,parent
,root
),其对应于vue2中的this.$attrs
,this.$slots
,this.$emit
,this.$parent
,this.$root
。
在 vue 3.x 中,它们的访问方式如下:
setup(props, context) {
console.log(context)
// Attribute (非响应式对象)
console.log(context.attrs)
// 插槽 (非响应式对象)
console.log(context.slots)
// 触发事件 (方法)
console.log(context.emit)
console.log(this) // undefined
},
/*
attrs: Object
emit: ƒ ()
listeners: Object
parent: VueComponent
refs: Object
root: Vue
...
*/
解构context
export default {
setup (props, { emit }) {
const handleUpdate = () => {
emit('update', 'Hello World')
}
return { handleUpdate }
}
}
setup也用作在tsx中返回渲染函数:
setup(props, { attrs, slots }) {
return () => {
const propsData = { ...attrs, ...props } as any;
return <Modal {...propsData}>{extendSlots(slots)}</Modal>;
};
},
注意:this关键字在setup()函数内部不可用,在方法中访问setup中的变量时,直接访问变量名就可以使用。
为什么props没有被包含在上下文中?
- 组件使用props的场景更多,有时甚至只需要使用props
- 将props独立出来作为一个参数,可以让TypeScript对props单独做类型推导,不会和上下文中其他属性混淆。这也使得setup、render和其他使用了TSX的函数式组件的签名保持一致。
使用渲染函数
setup 还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态:
import { h, ref, reactive } from 'vue'
export default {
setup() {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })
// Please note that we need to explicitly expose ref value here
return () => h('div', [readersNumber.value, book.title])
}
}
reactive(), ref() 创建响应式数据
在 Vue 3.0 中,我们可以通过一个新的 ref 函数使任何响应式变量在任何地方起作用,作用等同于在vue2中的data
,不同的是他们使用了ES6
的Porxy API
解决了vue2 defineProperty
无法监听数组和对象新增属性的痛点,而且使任何响应式变量在任何地方起作用。
<template>
<div class="contain">
<el-button type="primary" @click="numadd">add</el-button>
<span>{{ `${state.str}-${num}` }}</span>
</div>
</template>
<script lang="ts">
import { reactive, ref } from 'vue';
interface State {
str: string;
list: string[];
}
export default {
setup() {
const state = reactive<State>({
str: 'test',
list: [],
});
//ref需要加上value才能获取
const num = ref(1);
console.log(num) // { value: 1 }
console.log(num.value) // 1
const numadd = () => {
num.value++;
};
return { state, numadd, num };
},
method:{
numAdd(){
this.num++ //setup return的就是value,所以这里不需要加value
}
}
};
</script>
reactive
函数可以代理一个对象, 但是不能代理基本类型,例如字符串、数字、boolean等。
上面的代码中,我们绑定到页面是通过user.name
,user.age
;这样写感觉很繁琐,我们能不能直接将user
中的属性解构出来使用呢?答案是不能直接对**user**
进行解构, 这样会消除它的响应式, 这里就和上面我们说**props**
不能使用ES6直接解构就呼应上了。那我们就想使用解构后的数据怎么办,解决办法就是使用**toRefs**
。
toRefs()
将传入的reactive对象里所有的属性都转化为响应式数据对象(
ref
)
使用reactive
return 出去的值每个都需要通过reactive
对象 .属性的方式访问非常麻烦,我们可以通过解构赋值的方式范围,但是直接解构的参数不具备响应式,此时可以使用到这个api(也可以对props
中的响应式数据做此处理)
将前面的例子作如下👇修改使用起来更加方便:
<template>
<div class="contain">
<el-button type="primary" @click="numadd">add</el-button>
- <span>{{ `${state.str}-${num}` }}</span>
+ <span>{{ `${str}-${num}` }}</span>
</div>
</template>
<script lang="ts">
import { reactive, ref, toRefs } from 'vue';
interface State {
str: string;
list: string[];
}
export default {
setup() {
const state = reactive<State>({
str: 'test',
list: [],
});
//ref需要加上value才能获取
const num = ref(1);
const numadd = () => {
num.value++;
};
- return { state, numadd, num };
+ return { ...toRefs(state), numadd, num };
},
};
</script>
具体使用方式如下:
toRef()
toRef
用来将引用数据类型
或者reavtive数据类型
中的某个属性转化为响应式数据
reactive 数据类型
/* reactive数据类型 */
let obj = reactive({ name: '小黄', sex: '1' });
let state = toRef(obj, 'name');
state.value = '小红';
console.log(obj.name); // 小红
console.log(state.value); // 小红
obj.name = '小黑';
console.log(obj.name); // 小黑
console.log(state.value); // 小黑
引用数据类型
<template>
<span>ref----------{{ state1 }}</span>
<el-button type="primary" @click="handleClick1">change</el-button>
<!-- 点击后变成小红 -->
<span>toRef----------{{ state2 }}</span>
<el-button type="primary" @click="handleClick2">change</el-button>
<!-- 点击后还是小黄 -->
</template>
<script>
import { ref, toRef, reactive } from 'vue';
export default {
setup() {
let obj = { name: '小黄' };
const state1 = ref(obj.name); // 通过ref转换
const state2 = toRef(obj, 'name'); // 通过toRef转换
const handleClick1 = () => {
state1.value = '小红';
console.log('obj:', obj); // obj:小黄
console.log('ref', state1); // ref:小红
};
const handleClick2 = () => {
state2.value = '小红';
console.log('obj:', obj); // obj:小红
console.log('toRef', state2); // toRef:小红
};
return { state1, state2, handleClick1, handleClick2 };
},
};
</script>
https://mp.weixin.qq.com/s/avfb-jJeW7f_tQVOtO93NQ
watch() 响应式更改
就像我们在组件中使用 watch 选项或者 $watch api 在 data property 上设置侦听器一样,我们也可以使用从 Vue 导入的 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, 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
即可。关于flush
配置,还在学习,后期会补充
stop 停止监听
我们在组件中创建的watch
监听,会在组件被销毁时自动停止。如果在组件销毁之前我们想要停止掉某个监听, 可以调用watch()
函数的返回值,操作如下:
const stopWatchRoom = watch(() => state.room, (newType, oldType) => {
console.log("新值:", newType, "老值:", oldType);
}, {deep:true});
setTimeout(()=>{
// 停止监听
stopWatchRoom()
}, 3000)
watchEffect()
computed()
与 ref 和 watch 类似,也可以使用从 Vue 导入的 computed 函数在 Vue 组件外部创建计算属性。
import { ref, computed } from 'vue'
const counter = ref(0)
const twiceTheCounter = computed(() => counter.value * 2)
counter.value++
console.log(counter.value) // 1
console.log(twiceTheCounter.value) // 2
vue3 生命周期钩子
可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。
下表包含如何在 setup () 内部调用生命周期钩子:
vue2选项式 API | vue3 Hook inside setup |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
- 可以看出来vue2的
beforeCreate
和created
变成了setup
- 绝大部分生命周期都是在原本vue2的生命周期上带上了
on
前缀使用
因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。
Vue3.x中的钩子是需要从vue中导入的:
这些函数接受一个回调函数,当钩子被组件调用时将会被执行
在setup中使用生命周期:
import { onMounted } from 'vue';
export default {
setup() {
onMounted(() => {
// 在挂载后请求数据
getList();
})
}
};
import { 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-----");
});
// 调试哪些数据发生了变化
onRenderTriggered((event) =>{
console.log("------onRenderTriggered-----",event);
})
},
});