什么是Composition API
在旧版本的VUE中,使用Options API,这样对一个数据属性的声明(data),初始化(mounted),衍生(computed),被分散到不同的方法中。在这种方法中,并没有通过逻辑进行分组。这样,当项目规模变大时,代码就变得难以维护。
Composition API和React Hooks的设计理念非常类似。通过Composition API,可以:
- 将逻辑集中到setup函数中
- 更加容易的创建可重用的逻辑/函数
Composition API并不是强制性的,而是一个可选的特性。通常在规模较大的项目中,推荐使用Composition API。
setup是所有Composition API表演的舞台。
export default {
setup() {
// data
// methods
// computed
// lifecycle hooks
}
}简单示例
<template>
<div>
<p>My id is {{ id }} and my name is {{ name }}
<button @click="register">Click me</button>
</div>
</template>
<script>
export default {
setup() {
let id = '9901';
let name = 'George';
const register = () => {
console.log('Registering...');
}
return { id, name, register };
}
}
</script> 需要注意,在上面setup中定义的id/name不是reactive的,也就说,当其值发生变化时,UI并不会自动更新。而在传统的data()方法中,所有属性值都是reactive的。
强烈建议不要把Vue 2中的data/methods/computed等配置和Vue 3 setup中的配置混用。
- 一旦混用,在Vue 2中可以读取Vue 3 setup中的属性,方法。但在Vue 3的setup中,无法读取Vue 2中的data/methods/computed等。
- 如果两者冲突,则以Vue 3为主
- 不能在setup前添加async修饰符
- setup会在beforeCreate之前运行,且只执行一次。同时当时的this为undefined。
关于setup
- setup发生于beforeCreate声明周期钩子之前
- 在setup中this的值为undefined
- setup只接受两个参数:props和context(包含attrs, emit, slots)
slot
在Vue3中,推荐这种方式:
<Test>
<template v-slot:slot1>
<div>test</div>
</template>
</Test> 在setup中返回渲染函数
如果在setup中返回一个渲染函数,则可以自定义渲染内容,模板中内容将被忽略。
<script>
import {h} from 'vue'
export default {
name: 'App',
setup() {
return () => h('h2', '逻思编程')
}
}
</script> setup中可以接受两个参数:props, context
使用ref
Vue中的ref和React中的useRef非常类似,通过ref,可以使得setup中的变量reactive。 其原理是通过ref创建了一个引用对象。
- 对于基本类型,其底层实现方式是通过Object.defineProperty()中的gettter/setter实现的。
- 对于对象类型,其底层实现是借助于Vue 3中的reactive函数。
<template>
<p>Name: {{ name }}</p>
<input type="text" v-model="name" />
<button @click="handleClick">Click me</button>
</template>
<script>
import { ref } from "vue";
export default {
name: "App",
setup() {
const name = ref("George");
const handleClick = () => {
console.log(name, name.value);
name.value = "Paul";
};
return { name, handleClick };
},
};
</script>需要注意的是,在返回ref variable之前,无法引用其值及对应的属性。这是因为尚未将其和对应的DOM元素绑定。另外,ref对应的变量链接到对应的DOM元素,访问其值的方式为:name.value。
对于上面文本框的绑定,也可以通过这种方式:
<input type="text" v-model="name" />注意:在对象上使用ref的时候,需要通过如下方式获取其值/赋值:
const student = ref({
id: "9901",
name: "Paul"
})
// ...
student.value.id = '9902';reactive
reactive用于对象类型的响应式数据。
ref vs reactive:
- 在ref中既可以使用基本类型,也可以使用对象类型。reactive中不能使用基本类型(primitive types)
- reactive通过Proxy实现响应式。但ref通过Object.defineProperty的getter/setter来实现数据劫持/响应式的。
建议对于复杂对象,使用reactive;一般对象则使用ref就足够了。
在使用reactive的时候,无需通过value属性来访问对应的值。
在Vue3中,通过使用Proxy来捕获对象中属性的变化,并使用Reflect对源对象的属性进行操作。
<template>
<p>Name: {{ student.name }}</p>
<input type="text" v-model="student.name" />
<button @click="handleClick">Click me</button>
</template>
<script>
import { reactive } from "vue";
export default {
name: "App",
setup() {
const student = reactive({ id: "9901", name: "George" });
const handleClick = () => {
console.log(student);
student.name = "Paul";
};
return { student, handleClick };
},
};
</script>toRefs
在上面代码中也可以返回id及name,但这样就会丧失reactivity:
setup() {
const student = reactive({ id: "9901", name: "George" });
// ...
return { name: student.name, handleClick };
}这样当更新name的值时,Vue并不会更新UI。 这时可以使用toRef/toRefs来达到这个目标:
import { reactive, toRef } from 'vue'
setup() {
const student = reactive({ id: "9901", name: "George" });
// ...
return {
name: toRef(student.name)
};
}或者使用toRefs来返回某个对象的所有属性:
return {
...toRefs(student)
}在Composition API中使用方法
<template>
<div>{{ count }}</div>
<button @click="increase">+</button>
</template>
<script>
import { ref } from 'vue';
export default {
// ...
setup() {
const count = ref(0)
function increase() {
count.value++;
}
return {
count, increase
}
}
}
</script>判断变量类型的几个函数
- isRef
- isReactive
- isReadonly
- isProxy
computed values
基本用法
import { computed } from 'vue';
export default {
setup() {
const fullName = computed(() => {
return `${lastName}, ${firstName}`;
})
return { fullName };
}
}使用计算属性过滤信息
再来看一个简单的例子:根据用户输入的关键字过滤学生数据,然后进行显示:
<template>
<input type="text" v-model="keyword" />
<div v-for="student in filteredStudents" :key="student.id">
{{ student.name }}
</div>
</template>
<script>
import { ref, computed } from "vue";
export default {
name: "App",
setup() {
const keyword = ref("");
const students = ref([
{ id: "9901", name: "George" },
{ id: "9902", name: "Paul" },
{ id: "9903", name: "Lucy" },
]);
const filteredStudents = computed(() => {
return students.value.filter((student) =>
student.name.includes(keyword.value)
);
});
return { keyword, students, filteredStudents };
},
};
</script>可读写的计算属性
setup() {
let fullName = computed({
get() {
return `${student.firstName} ${student.lastName}`
},
set(value) {
const names = value.split(' ')
student.firstName = names[0]
student.lastName = names[1]
}
})
}同时可以将计算属性绑定到reactive对象上:
student.fullName = computed(() => {
return `${student.firstName} ${student.lastName}`
})watch & watchEffect
一般用法
watch的作用就是监视某个变量值的变化,比如:
setup() {
const keyword = ref('');
const stopWatch = watch(keyword, (newValue, oldValue) => {
// ...
})
}监视两个变量
setup() {
const username = ref('');
const password = ref('');
watch([username, password], (newValues, oldValues) => {
console.log(newValues[0], oldValues[0])
})
}即刻生效的watcher
在前面的例子中,页面首次加载时watcher并不会生效,只有在值发生变化时,才会生效。
如果想要即刻生效的watcher,可以给watch函数添加第三个参数:
watch(keyword, (newValue, oldValue) => {
// ...
}, {immediate: true})结束watch
如果想要结束watch,只需要调用watch返回的回调函数stopWatch就可以:
stopWatch();和reactive的结合使用
import { reactive, toRefs } from 'vue'
export default {
name: 'WatcherDemo',
setup() {
const state = reactive({
id: '',
name: ''
});
watch(state, function(newValue, oldValue) {
console(oldValue.id, newValue.id);
console(oldValue.name, newValue.name);
})
// ...
return {
...toRefs(state)
}
}
}可以看到,在上面的例子中,oldValue.id 和 newValue.id的值是相等的。这是默认的行为。
如果想要实现真正的监测:
watch(() => {return {...state}}, function(newValue, oldValue) {
console(oldValue.id, newValue.id);
console(oldValue.name, newValue.name);
})只监测某个属性
watch(()=> state.id, function(newValue, oldValue) {
console(oldValue, newValue);
})使用deep watcher监测嵌套属性
需要注意,这里需要一个被监测对象的深拷贝:
import _ from 'lodash';
const state = reactive({
id: '',
name: '',
contact: {
address: '',
phone: '',
email: '',
}
});
watch(() => _.cloneDeep(state.contact), function(newValue, oldValue) {
// ...
},
{deep: true});watchEffect
watchEffect会自动检测在回调函数中使用到的变量并对其进行监测。watchEffect感觉更加智能化,其实它和computed函数有些类似,只不过computed需要通过返回值来体现最新的数据,而watchEffect则是强调当关联数据发生变化时,另一个过程将被调用。
setup() {
const keyword = ref('');
watchEffect(() => {
// 由于下面使用了keyword,因此才会对其进行监测
console.log(keyword.value);
})
}使用provide/inject
基本用法
In parent component:
import { provide } from 'vue';
export default {
setup() {
provide('c_authenticated', true);
}
}In child component:
import { inject } from 'vue';
export default {
setup() {
// 如果没有组件provide c_authenticated,则默认值为false
const authenticated = inject('c_authenticated', false);
return {
authenticated
}
}
}和ref/reactive的结合使用
父组件:
import { provide, ref, reactive, toRefs } from 'vue'
export default {
setup() {
const count = ref(0);
const state = reactive({
id: '9901',
name: 'Paul'
});
provide('c_count', count);
provide('c_student', state);
return {
count,
...toRefs(state)
}
}
}子组件:
import { inject, toRefs } from 'vue';
export default {
setup() {
const count = inject('c_count', 0);
const student = inject('c_student', {});
return {
count,
...toRefs(student)
}
}
}provide/inject操作数据的方法
父组件:
setup() {
function increase() {
count.value += 1;
}
provide('increase', increase);
return {
count,
increase
}
}子组件:
setup() {
const increase = inject('increase');
return {
//...
increase
}
}使用Template refs
import { ref, onMounted } from 'vue';
export default {
name 'Refs test',
setup() {
const nameRef = ref(null);
onMounted(() => {
nameRef.value.focus();
});
return {
nameRef,
};
}
}使用props从父组件向子组件传递信息
要想在Composition API中使用属性props,只需要在定义setup的时候传入props参数就可以:
setup(props) {
const snippet = computed(() => {
return props.product.summary.substring(0, 100) + '...';
})
}使用context从子组件向父组件传递信息
在Options API中,我们可以使用$this.emit,但在composition API中,就可以在子组件中接受context作为参数,然后访问其对应属性了:
子组件:
setup(props, context) {
function sendEvent() {
context.emit('registered', studentId);
}
return {
studentId,
sendEvent
}
}
emits: ['registered']上面的context中又包含emit, props, slots, attrs等属性。
父组件:
<template>
<Student @registered="registered" />
</template>
<script>
export default {
setup() {
function registered(studentId) {
console.log(studentId);
}
}
}
</script> 使用生命周期钩子
需要注意的是,在composition API中,对应的方法名称稍有改变:
| Options API | Composition API |
|---|---|
| beforeCreate | NOT NEEDED |
| created | NOT NEEDED |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
| errorCaptured | onErrorCaptured |
| renderTracked | onRenderTracked |
| renderTriggered | onRenderTriggered |
import {
onMounted,
onUnmounted,
onUpdated
} from 'vue'
export default {
name: 'Lifecycle Test',
setup(props) {
onMounted(() => {
console.log("onMounted");
})
onUnmounted(() => {
console.log("onUnmounted");
})
onUpdated(() => {
console.log("onUpdated");
})
}
}构建可重用组件Composable component
创建组件
首先创建一个目录composables,然后在其中添加可重用的组件,比如useStudentLoader.js。注意之类的命令规范:useXXX。
import { ref } from 'vue';
const useStudentLoader = () => {
const students = ref([]);
const error = ref(null);
const loadData = async () => {
try {
let result = await fetch(YOUR_URL);
if(!result.ok) {
throw Error('Error while loading data');
}
students.value = await result.json();
} catch (err) {
error.value = err.message;
console.log(error.value);
}
}
return { students, error, loadData };
}
export default useStudentLoader;在上面的定义中,也可以传给useStudentLoader参数:
const useStudentLoader = (initialStudentList) => {
const students = ref(initialStudentList);
// ...
} 在Vue组件中使用useStudentLoader组件
import useStudentLoader from '../composable/useStudentLoader';
export default {
setup() {
const { students, error, loadData } = useStudentLoader();
loadData();
return { students, error, loadData };
}
}