linalu
前端开发工程师
linalu 2022-06-29
一、Composition API
先举个 Vue 2 中的简单例子,一个累加器,并且还有一个计算属性显示累加器乘以 2 的结果。
<div id="app">
<h1 @click="add">{{count}} * 2 = {{double}}</h1>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
let App = {
data(){
return {
count:1
}
},
methods:{
add(){
this.count++
}
},
computed:{
double(){
return this.count*2
}
}
}
Vue.createApp(App).mount('#app')
</script>
vue3中使用Composition API的写法:
<div id="app">
<h1 @click="add">{{state.count}} * 2 = {{double}}</h1>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const {reactive,computed} = Vue
let App = {
setup(){
const state = reactive({
count:1
})
function add(){
state.count++
}
const double = computed(()=>state.count*2)
return {state,add,double}
}
}
Vue.createApp(App).mount('#app')
</script>
使用data、computed、methods、watch的组件选项来组织逻辑通常是很有效的。然而,当我们的组件开始变大,逻辑关注点的列表就会增长,导致代码难以阅读和理解。
如图所示,逻辑关注点按颜色进行分组,这种碎片化使得理解和维护组件变得困难,选项分离掩盖了潜在的逻辑问题。而使用Composition API则能够将同一逻辑关注点相关的代码收集到一起。
Options API存在的问题:
1、由于所有数据都挂载在 this 之上,因而 Options API 的写法对 TypeScript 的类型推导很不友好,并且这样也不好做 Tree-shaking 清理代码。
2、新增功能基本都得修改 data、method 等配置,并且代码上 300 行之后,会经常上下反复横跳,开发很痛苦。
3、代码不好复用,Vue 2 的组件很难抽离通用逻辑,只能使用 mixin,还会带来命名冲突的问题。
Composition API的好处:
1、所有 API 都是 import 引入的。用到的功能都 import 进来,对 Tree-shaking 很友好,我的例子里没用到功能,打包的时候会被清理掉 ,减小包的大小。
2、不再上下反复横跳,我们可以把一个功能模块的 methods、data 都放在一起书写,维护更轻松。
3、代码方便复用,可以把一个功能所有的 methods、data 封装在一个独立的函数里,复用代码非常容易。
4、Composotion API 新增的 return 等语句,在实际项目中使用
二、setup函数
setup() 函数是 vue3 中,专门为组件提供的新属性。它为我们使用 vue3 的 Composition API 新特性提供了统一的入口,setup 函数会在 beforeCreate 之前执行。因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。
支持<srcipt setup>
写法,它是在单文件组件中使用组合式API的编译时语法糖,本质上以更精简的方式来书写Composition API,我们定义的变量、函数和引入的组件,都不需要额外的生命周期,可以直接在模版中使用。
使用setup时,接收两个参数:
1、props:组件传入的属性,props是响应式的,当传入新的props时,它将会被更新。
2、context:非响应式,用来定义上下文(包含attrs、slot、emit)。上下文对象包含一些有用的属性,这些属性在vue2中需要通过this才能访问到,在setup函数中无法访问到this。
三、自定义Hooks
本质而言,hook就是一个函数,只不过hook是把 setup组合函数之中的 Composition API(ref、reactive、computed、watch、生命周期函数)等进行了封装抽离代码(公共代码,公共组件等),这样使得代码更加简洁。它类似于vue2中的mixin。我们约定这些「自定义 Hook」以 use 作为前缀,和普通的函数加以区分。
四、响应式机制
响应式一直都是 Vue 的特色功能之一。Vue 中用过三种响应式解决方案,分别是 defineProperty、Proxy 和 value setter。
首先,我们来以例子来了解下Vue2的defineProperty API。
let obj = {}
let count = 1
let double = getDouble(count)
Object.defineProperty(obj,'count',{
get(){
return count
},
set(val){
count = val
double = getDouble(val)
}
})
console.log(double) // 打印2
obj.count = 2
console.log(double) // 打印4 有种自动变化的感觉
这样就实现简易的响应式功能。但Vue2 实现响应式原理的语法中存在缺陷。如删除obj.count属性,set函数是不会执行的,double还是之前的值,这也是为什么在Vue2中,需要$delete一个专门的函数删除数据。
delete obj.count
console.log(double) // doube还是4
Vue 3 的响应式机制是基于 Proxy 实现的。先看看Proxy的例子。
let proxy = new Proxy(obj,{
get : function (target,prop) {
return target[prop]
},
set : function (target,prop,value) {
target[prop] = value;
if(prop==='count'){
double = getDouble(value)
}
},
deleteProperty(target,prop){
delete target[prop]
if(prop==='count'){
double = NaN
}
}
})
console.log(obj.count,double)
proxy.count = 2
console.log(obj.count,double)
delete proxy.count
// 删除属性后,我们打印log时,输出的结果就会是 undefined NaN
console.log(obj.count,double)
可以看出Proxy 实现的功能和 Vue 2 的 definePropery 类似,它们都能够在用户修改数据的时候触发 set 函数,从而实现自动更新 double 的功能。而且 Proxy 还完善了几个 definePropery 的缺陷,比如说可以监听到属性的删除。
Proxy 是针对对象来监听,而不是针对某个具体属性,所以不仅可以代理那些定义时不存在的属性,还可以代理更丰富的数据结构,比如 Map、Set 等,并且我们也能通过 deleteProperty 实现对删除操作的代理。
除了Proxy,Vue3中还有另一个响应式实现的逻辑,就是利用对象的 get 和 set 函数来进行监听,这种响应式的实现方式,只能拦截某一个属性的修改,这也是 Vue 3 中 ref 这个 API 的实现。在下面的代码中,我们拦截了 count 的 value 属性,并且拦截了 set 操作,也能实现类似的功能。
let getDouble = n => n * 2
let _value = 1
double = getDouble(_value)
let count = {
get value() {
return _value
},
set value(val) {
_value = val
double = getDouble(_value)
}
}
console.log(count.value,double)
count.value = 2
console.log(count.value,double)
三种实现原理的对比表格如下,更好的理解三种响应式的区别。
五、响应性API
理解了Vue响应式原理后,我们来看看几个响应性API的使用。
1、reactive:响应式转换是“深层”的——它影响所有嵌套property。
const obj = reactive({ count: 0 })
2、ref:接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。如果将对象分配为ref值,则它将被reactive函数处理为深层的响应式对象。
const count = ref(0)
console.log(count.value) // 0
3、toRefs:将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的ref。
在开发中,setup接收的参数props具有响应式,使用ES6解构则会消除它的响应式,想要使用解构后的数据则可以使用toRefs。
4、computed:接受一个getter函数,并根据getter的返回值返回一个不可变的响应式ref对象。
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误
5、watch:需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在侦听源发生变化时被调用。
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
// 侦听多个源
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
6、watchEffect:立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
对比watch和watchEffect:
watchEffect不需要手动传入依赖,会先执行一次用来自动收集依赖, 无法获取到变化前的值, 只能获取变化后的值。watch惰性地执行副作用,更具体地说明应触发侦听器重新运行的状态,访问被侦听状态的先前值和当前值。
相同点:watch 与 watchEffect在手动停止侦听、清除副作用 (将 onInvalidate 作为第三个参数传递给回调)、刷新时机和调试方面有相同的行为。
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0
setTimeout(() => {
count.value++
// -> logs 1
}, 100)
六、生命周期
新版的生命周期函数,可以按需导入到组件中,且只能在 setup() 函数中使用。
<script lang="ts">
import { defineComponent, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onErrorCaptured, onMounted, onUnmounted, onUpdated } from 'vue';
export default defineComponent({
setup(props, context) {
onBeforeMount(()=> {
console.log('beformounted!')
})
onMounted(() => {
console.log('mounted!')
})
onBeforeUpdate(()=> {
console.log('beforupdated!')
})
onUpdated(() => {
console.log('updated!')
})
onBeforeUnmount(()=> {
console.log('beforunmounted!')
})
onUnmounted(() => {
console.log('unmounted!')
})
onErrorCaptured(()=> {
console.log('errorCaptured!')
})
return {}
}
});
</script>
生命周期图示:
七、全部模块使用 TypeScript 重构
JavaScript 是弱类型的语言,Vue 2 是使用 Flow.js 来做类型校验。但现在 Flow.js 已经停止维护了,整个社区都在全面使用 TypeScript 来构建基础库。Vue 3 选择了 TypeScript,TypeScript 官方也对使用 TypeScript 开发 Vue 3 项目的团队也更加友好。 TypeScript 类型系统带来了更方便的提示,并且让我们的代码能够更健壮。
八、新组件
Vue 3 还内置了 Fragment、Teleport 和 Suspense 三个新组件。
Fragment: Vue 3 组件不再要求有一个唯一的根节点,清除了很多无用的占位 div。
// Vue2
<template>
<div>
<span></span>
<span></span>
</div>
</template>
// Vue3
<template>
<span></span>
<span></span>
</template>
Teleport: 允许组件渲染在别的元素内,主要开发弹窗组件的时候特别有用。
使用弹窗组件时,我们即希望继续在组件内部使用Dialog, 又希望渲染的 DOM 结构不嵌套在组件的 DOM 中。 此时就需要 Teleport 上场,我们可以用包裹Dialog, 此时就建立了一个传送门,可以将Dialog渲染的内容传送到任何指定的地方。
const app = Vue.createApp({
template: `
<h1>Root instance</h1>
<parent-component />
`
})
app.component('parent-component', {
template: `
<h2>This is a parent component</h2>
<teleport to="#endofbody">
<child-component name="John" />
</teleport>
`
})
app.component('child-component', {
props: ['name'],
template: `
<div>Hello, {{ name }}</div>
`
})
Suspense: 异步组件,更方便开发有异步请求的组件。该组件有两个插槽,它们都只接收一个直接子节点。default 插槽里的节点会尽可能展示出来。如果不能,则展示 fallback 插槽里的节点。
总结:
<script setup>
的功能,我们不需要额外的生命周期,就可以直接在模板中使用。