Vue3丨从 5 个维度来讲 Vue3 变化
一些概念
Vue Composition API(VCA) 在实现上也其实只是把 Vue 本身就有的响应式系统更显式地暴露出来而已。这不是函数式,只是 API 暴露为函数。3.0 Template 编译出来的性能会比手写 jsx 快好几倍。——尤雨溪
Vue2 传统的 data,computed,watch,methods 写法,我们称之为「选项式api(Options API )」 Vue3 使用 Composition API (VCA)可以根据逻辑功能来组织代码,一个功能相关的 api 会放在一起。
Vue 和 React 的逻辑复用手段
到目前为止,
Vue:Mixins(混入)、HOC(高阶组件)、作用域插槽、Vue Composition API(VCA/组合式API)。
React:Mixins、HOC、Render Props、Hook。
我们可以看到都是一段越来越好的成长史,这里就不再举例赘述,本文重心在 VCA,VCA 更偏向于「组合」的概念。
5个维度来讲 Vue3
1. 框架
一个例子先来了解 VCA
在 Vue 中,有了抽象封装组件的概念,解决了在页面上模块越多,越显臃肿的问题。但即使进行组件封装,在应用越来越大的时候,会发现页面的逻辑功能点越来越多,
data/computed/watch/methods
中会被不断塞入逻辑功能,所以要将逻辑再进行抽离组合、复用,这就是 VCA。
举个简单的例子:
我们要实现 3 个逻辑
- 根据 id 获取表格的数据
- 可对表格数据进行搜索过滤
- 弹框新增数据到表格中
Vue2 options api 的处理
为了阅读质量,省略了部分代码,但不影响我们了解 VCA
// 逻辑功能(1)
const getTableDataApi = id => {
const mockData = {
1: [
{ id: 11, name: '张三1' },
{ id: 12, name: '李四1' },
{ id: 13, name: '王五1' }
],
2: [
{ id: 21, name: '张三2' },
{ id: 22, name: '李四2' },
{ id: 23, name: '王五2' }
]
};
return new Promise(resolve => {
setTimeout(() => {
resolve(mockData[id] || []);
}, 1000);
});
};
export default {
name: 'VCADemo',
components: { Modal },
data() {
return {
// 逻辑功能(1)
id: 1,
table: [],
// 逻辑功能(2)
search: '',
// 逻辑功能(3)
modalShow: false,
form: {
id: '',
name: ''
}
};
},
computed: {
// 逻辑功能(2)
getTableDataBySearch() {
return this.table.filter(item => item.name.indexOf(this.search) !== -1);
}
},
watch: {
// 逻辑功能(1)
id: 'getTableData'
},
mounted() {
// 逻辑功能(1)
this.getTableData();
},
methods: {
// 逻辑功能(1)
async getTableData() {
const res = await getTableDataApi(this.id);
this.table = res;
},
// 逻辑功能(3)
handleAdd() {
this.modalShow = true;
},
// 逻辑功能(3)
handlePost() {
const { id, name } = this.form;
this.table.push({ id, name });
this.modalShow = false;
}
}
};
复制代码
这里只是举例简单的逻辑。如果项目复杂了,逻辑增多了。涉及到一个逻辑的改动,我们就可能需要修改分布在不同位置的相同功能点,提升了维护成本。
Vue3 composion api 的处理
让我们来关注逻辑,抽离逻辑,先看主体的代码结构
import useTable from './composables/useTable';
import useSearch from './composables/useSearch';
import useAdd from './composables/useAdd';
export default defineComponent({
name: 'VCADemo',
components: { Modal },
setup() {
// 逻辑功能(1)
const { id, table, getTable } = useTable(id);
// 逻辑功能(2)
const { search, getTableBySearch } = useSearch(table);
// 逻辑功能(3)
const { modalShow, form, handleAdd, handlePost } = useAdd(table);
return {
id,
table,
getTable,
search,
getTableBySearch,
modalShow,
form,
handleAdd,
handlePost
};
}
});
复制代码
setup 接收两个参数:props,context。可以返回一个对象,对象的各个属性都是被 proxy
的,进行监听追踪,将在模板上进行响应式渲染。
我们来关注其中一个逻辑,useTable
,一般来说我们会用 use
开头进行命名,有那味了~
// VCADemo/composables/useTable.ts
// 逻辑功能(1)相关
import { ref, onMounted, watch, Ref } from 'vue';
import { ITable } from '../index.type';
const getTableApi = (id: number): Promise<ITable[]> => {
const mockData: { [key: number]: ITable[] } = {
1: [
{ id: '11', name: '张三1' },
{ id: '12', name: '李四1' },
{ id: '13', name: '王五1' }
],
2: [
{ id: '21', name: '张三2' },
{ id: '22', name: '李四2' },
{ id: '23', name: '王五2' }
]
};
return new Promise(resolve => {
setTimeout(() => {
resolve(mockData[id] || []);
}, 1000);
});
};
export default function useTable() {
const id = ref<number>(1);
const table = ref<ITable[]>([]);
const getTable = async () => {
table.value = await getTableApi(id.value);
};
onMounted(getTable);
watch(id, getTable);
return {
id,
table,
getTable
};
}
复制代码
我们把相关逻辑独立抽离,并「组合」在一起了,可以看到在 vue 包暴露很多独立函数提供我们使用,已经不再 OO 了,嗅到了一股 FP 的气息~
上面这个例子先说明了 VCA 的带来的好处,Vue3 的核心当然是 VCA,Vue3 不仅仅是 VCA,让我们带着好奇往下看~
生命周期,Vue2 vs Vue3
选项式 API(Vue2) | Hook inside setup(Vue3) |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
Hook inside setup,顾名思义,VCA 建议在 setup
这个大方法里面写我们的各种逻辑功能点。
Teleport 组件
传送,将组件的 DOM 元素挂载在任意指定的一个 DOM 元素,与 React Portals 的概念是一致的。
一个典型的例子,我们在组件调用了 Modal 弹框组件,我们希望的弹框是这样子的,绝对居中,层级最高,如:
组件的结构是这样子的
<span class="hljs-tag"><<span class="hljs-name">Home</span>></span>
<span class="hljs-tag"><<span class="hljs-name">Modal</span> /></span>
<span class="hljs-tag"></<span class="hljs-name">Home</span>></span>
<span class="copy-code-btn">复制代码</span>
但是如果在父组件 Home 有类似这样的样式,如 transform
:
就会影响到 Modal 的位置,即使 Modal 用了 position:fixed
来定位,如:
这就是为什么我们需要用 Teleport 组件来帮助我们 “跳出” 容器,避免受到父组件的一些约束控制,把组件的 DOM 元素挂载到 body 下,如:
<Teleport to="body">
<div v-if="show">
...Modal 组件的 DOM 结构...
</div>
</Teleport>
复制代码
注意:即使 Modal 跳出了容器,也保持 “父子组件关系”,只是 DOM 元素的位置被移动了而已 。
异步组件(defineAsyncComponent)
我们都知道在 Vue2 也有异步组件的概念,但整体上来说不算完整~,Vue3 提供了 defineAsyncComponent
方法与 Suspense
内置组件,我们可以用它们来做一个优雅的异步组件加载方案。
直接看代码:
HOCLazy/index.tsx
import { defineAsyncComponent, defineComponent } from 'vue';
import MySuspense from './MySuspense.vue';
export default function HOCLazy(chunk: any, isComponent: boolean = false) {
const wrappedComponent = defineAsyncComponent(chunk);
return defineComponent({
name: 'HOCLazy',
setup() {
const props = { isComponent, wrappedComponent };
return () => <MySuspense {...props} />;
}
});
}
复制代码
解释:HOCLazy 接收了两个参数,chunk
就是我们经常采用的组件异步加载方式如:chunk=()=>import(xxx.vue)
,isComponent
表示当前的“组件”是一个 组件级 or 页面级,通过判断 isComponent
来分别对应不同的 “loading” 操作。
HOCLazy/MySuspense.vue
<template>
<Suspense>
<template #default>
<component :is="wrappedComponent"
v-bind="$attrs" />
</template>
<template #fallback>
<div>
<Teleport to="body"
:disabled="isComponent">
<div v-if="delayShow"
class="loading"
:class="{component:isComponent}">
<!-- 组件和页面有两种不一样的loading方式,这里不再详细封装 -->
<div> {{isComponent?'组件级':'页面级'}}Loading ...</div>
</div>
</Teleport>
</div>
</template>
</Suspense>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent, ref, onMounted } from 'vue';
export default defineComponent({
name: 'HOCLazy',
props: ['isComponent', 'wrappedComponent'],
setup(props) {
const delayShow = ref<boolean>(false);
onMounted(() => {
setTimeout(() => {
delayShow.value = true;
// delay 自己拿捏,也可以以 props 的方式传入
}, 300);
});
return { ...props, delayShow };
}
});
</script>
<style lang="less" scoped>
.loading {
// 组件级样式
&.component {
}
// 页面级样式
}
</style>
复制代码
解释:
- Suspense 组件有两个插槽,具名插槽
fallback
我们这里可以理解成一个 loading 的占位符,在异步组件还没显示之前的后备内容。 - 这里还用了 Vue 的动态组件 component 来灵活的传入一个异步组件,
v-bind="$attrs"
来保证我们传递给目标组件的 props 不会消失。 - fallback 中我们利用了判断 isComponent 来展示不同的 loading ,因为我们希望页面级的 loading 是“全局”的,组件级是在原来的文档流,这里用了
Teleport :disabled="isComponent"
来控制是否跳出。 - 细心的小伙伴会发现这里做了一个延迟显示
delayShow
,如果我们没有这个延迟,在网络环境良好的情况下,loading 每次都会一闪而过,会有一种“反优化”的感觉。
调用 HOCLazy:为了更好的看出效果,我们封装了 slow 方法来延迟组件加载:
utils/slow.ts
const slow = (comp: any, delay: number = 1000): Promise<any> => {
return new Promise(resolve => {
setTimeout(() => resolve(comp), delay);
});
};
export default slow;
复制代码
调用(组件级)
<template>
<LazyComp1 str="hello~" />
</template>
const LazyComp1 = HOCLazy(
() => slow(import('@/components/LazyComp1.vue'), 1000),
true
);
// ...
components: {
LazyComp1
},
// ...
复制代码
看个效果:
其实这与 React 中的
React.lazy + React.Suspense
的概念是一致的,之前写过的一篇文章 《React丨用户体验丨hook版 lazy loading》,小伙伴可以看看做下对比~
ref,reactive,toRef,toRefs 的区别使用
ref(reference)
ref 和 reactive 的存在都是了追踪值变化(响应式),ref 有个「包装」的概念,它用来包装原始值类型,如 string 和 number ,我们都知道不是引用类型是无法追踪后续的变化的。ref 返回的是一个包含 .value
属性的对象。
setup(props, context) {
const count = ref<number>(1);
// 赋值
count.value = 2;
// 读取
console.log('count.value :>> ', count.value);
return { count };
}
复制代码
在 template 中 ref 包装对象会被自动展开(Ref Unwrapping),也就是我们在模板里不用再 .value
<span class="hljs-tag"><<span class="hljs-name">template</span>></span>
{{count}}
<span class="hljs-tag"></<span class="hljs-name">template</span>></span>
<span class="copy-code-btn">复制代码</span>
reactive
与 Vue2 中的 Vue.observable()
是一个概念。用来返回一个响应式对象,如:
const obj = reactive({
count: 0
})
// 改变
obj.count++
复制代码
注意:它用来返回一个响应式对象,本身就是对象,所以不需要包装。我们使用它的属性,不需要加 .value
来获取。
toRefs
官网:因为 props 是响应式的,你不能使用 ES6 解构,因为它会消除 prop 的响应性。
让我们关注 setup
方法的 props 的相关操作:
<template>
{{name}}
<button @click="handleClick">点我</button>
</template>
// ...
props: {
name: { type: String, default: ' ' }
},
setup(props) {
const { name } = props;
const handleClick = () => {
console.log('name :>> ', name);
};
return { handleClick };
}
// ...
复制代码
注意:props 无需通过 setup 函数 return,也可以在 template 进行绑定对应的值
我们都知道解构是 es6 一种便捷的手段,编译成 es5 ,如:
// es6 syntax
const { name } = props;
// to es5 syntax
var name = props.name;
复制代码
假设父组件更改了 props.name 值,当我们再点击了 button 输出的 name 就还是之前的值,不会跟着变化,这其实是一个基础的 js 的知识点。
为了方便我们对它进行包装,toRefs
可以理解成批量包装 props 对象,如:
const { name } = toRefs(props);
const handleClick = () => {
// 因为是包装对象,所以读取的时候要用.value
console.log('name :>> ', name.value);
};
<span class="copy-code-btn">复制代码</span>
可以理解这一切都是因为我们要用解构,toRefs
所采取的解决方案。
toRef
toRef 的用法,就是多了一个参数,允许我们针对一个 key 进行包装,如:
const name = toRef(props,'name');
console.log('name :>> ', name.value);
复制代码
watchEffect vs watch
Vue3 的 watch 方法与 Vue2 的概念类似,watchEffect 会让我们有些疑惑。其实 watchEffect 与 watch 大体类似,区别在于:
watch 可以做到的
- 懒执行副作用
- 更具体地说明什么状态应该触发侦听器重新运行
- 访问侦听状态变化前后的值
对于 Vue2 的 watch 方法,Vue3 的 "watch" 多了一个「清除副作用」 的概念,我们着重关注这点。
这里拿 watchEffect
来举例:
watchEffect:它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
watchEffect 方法简单结构
watchEffect(onInvalidate => {
// 执行副作用
// do something...
onInvalidate(() => {
// 执行/清理失效回调
// do something...
})
})
复制代码
执行失效回调,有两个时机
- 副作用即将重新执行时,也就是监听的数据发生改变时
- 组件卸载时
一个例子:我们要通过 id 发起请求获取「水果」的详情,我们监听 id,当 id 切换过于频繁(还没等上个异步数据返回成功)。可能会导致最后 id=1
的数据覆盖了id=2
的数据,这并不是我们希望的。
我们来模拟并解决这个场景:
模拟接口 getFruitsById
interface IFruit {
id: number;
name: string;
imgs: string;
}
const list: { [key: number]: IFruit } = {
1: { id: 1, name: '苹果', imgs: 'https://xxx.apple.jpg' },
2: { id: 2, name: '香蕉', imgs: 'https://xxx.banana.jpg' }
};
const getFruitsById = (
id: number,
delay: number = 3000
): [Promise<IFruit>, () => void] => {
let _reject: (reason?: any) => void;
const _promise: Promise<IFruit> = new Promise((resolve, reject) => {
_reject = reject;
setTimeout(() => {
resolve(list[id]);
}, delay);
});
return [
_promise,
() =>
_reject({
message: 'abort~'
})
];
};
复制代码
这里封装了“取消请求”的方法,利用 reject 来完成这一动作。
在 setup 方法
setup() {
const id = ref<number>(1);
const detail = ref<IFruit | {}>({});
watchEffect(async onInvalidate => {
onInvalidate(() => {
cancel && cancel();
});
// 模拟id=2的时候请求时间 1s,id=1的时候请求时间 2s
const [p, cancel] = getFruitsById(id.value, id.value === 2 ? 1000 : 2000);
const res = await p;
detail.value = res;
});
// 模拟频繁切换id,获取香蕉的时候,获取苹果的结果还没有回来,取消苹果的请求,保证数据不会被覆盖
id.value = 2;
// 最后 detail 值为 { "id": 2, "name": "香蕉", "imgs": "https://xxx.banana.jpg" }
}
复制代码
如果没有执行 cancel()
,那么 detail 的数据将会是 { "id": 1, "name": "苹果", "imgs": "https://xxx.apple.jpg" }
,因为 id=1 数据比较“晚接收到”。
这就是在异步场景下常见的例子,清理失效的回调,保证当前副作用有效,不会被覆盖。感兴趣的小伙伴可以继续深究。
fragment(片段)
我们都知道在封装组件的时候,只能有一个 root 。在 Vue3 允许我们有多个 root ,也就是片段,但是在一些操作值得我们注意。
当 inheritAttrs=true[默认]
时,组件会自动在 root 继承合并 class ,如:
子组件
<template>
<div class="fragment">
<div>div1</div>
<div>div2</div>
</div>
</template>
复制代码
父组件调用,新增了一个 class
<MyFragment class="extend-class" />
复制代码
子组件会被渲染成
<div class="fragment extend-class">
<div> div1 </div>
<div> div2 </div>
</div>
复制代码
如果我们使用了 片段 ,就需要显式的去指定绑定 attrs ,如子组件:
<template>
<div v-bind="$attrs">div1</div>
<div>div2</div>
</template>
复制代码
emits
在 Vue2 我们会对 props 里的数据进行规定类型,默认值,非空等一些验证,可以理解 emits 做了类似的事情,把 emit 规范起来,如:
// 也可以直接用数组,不做验证
// emits: ['on-update', 'on-other'],
emits: {
// 赋值 null 不验证
'on-other': null,
// 验证
'on-update'(val: number) {
if (val === 1) {
return true;
}
// 自定义报错
console.error('val must be 1');
return false;
}
},
setup(props, ctx) {
const handleEmitUpdate = () => {
// 验证 val 不为 1,控制台报错
ctx.emit('on-update', 2);
};
const handleEmitOther = () => {
ctx.emit('on-other');
};
return { handleEmitUpdate, handleEmitOther };
}
复制代码
在 setup 中,emit 已经不再用 this.$emit
了,而是 setup 的第二个参数 context
上下文来获取 emit 。
v-model
个人还是挺喜欢 v-model 的更新的,可以提升封装组件的体验感~
在Vue2,假设我需要封装一个弹框组件 Modal,用
show
变量来控制弹框的显示隐藏,这肯定是一个父子组件都要维护的值。因为单向数据流,所以需要在 Modal 组件 emit 一个事件,父组件监听事件接收并修改这个show
值。为了方便我们会有一些语法糖,如 v-model,但是在 Vue2 一个组件上只能有一个 v-model ,因为语法糖的背后是value
和@input
的组成, 如果还有多个类似这样的 “双向修改数据”,我们就需要用语法糖.sync
同步修饰符。
Vue3 把这两个语法糖统一了,所以我们现在可以在一个组件上使用 多个 v-model 语法糖,举个例子:
先从父组件看
<VModel v-model="show"
v-model:model1="check"
v-model:model2.hello="textVal" />
复制代码
hello为自定义修饰符
我们在一个组件上用了 3 个 v-model 语法糖,分别是
v-model 语法糖 | 对应的 prop | 对应的 event | 自定义修饰符对应的 prop |
---|---|---|---|
v-model(default) | modelValue | update:modelValue | 无 |
v-model:model1 | model1 | update:model1 | 无 |
v-model:model2 | model2 | update:model2 | model2Modifiers |
这样子我们就更清晰的在子组件我们要进行一些什么封装了,如:
VModel.vue
// ...
props: {
modelValue: { type: Boolean, default: false },
model1: { type: Boolean, default: false },
model2: { type: String, default: '' },
model2Modifiers: {
type: Object,
default: () => ({})
}
},
emits: ['update:modelValue', 'update:model1', 'update:model2'],
// ...
复制代码
key attribute
<template>
<input type="text"
placeholder="请输入账号"
v-if="show" />
<input type="text"
placeholder="请输入邮箱"
v-else />
<button @click="show=!show">Toggle</button>
</template>
复制代码
类似这样的 v-if/v-else,在 Vue2 中,会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染,所以当我们在第一个 input 中输入,然后切换第二个 input 。第一个 input 的值将会被保留复用。
有些场景下我们不要复用它们,需要添加一个唯一的 key ,如:
<template>
<input type="text"
placeholder="请输入账号"
v-if="show"
key="account" />
<input type="text"
placeholder="请输入邮箱"
v-else
key="email" />
<button @click="show=!show">Toggle</button>
</template>
复制代码
但是在 Vue3 我们不用显式的去添加 key ,这两个 input 元素也是完全独立的,因为 Vue3 会对 v-if/v-else 自动生成唯一的 key。
全局 API
在 Vue2 我们对于一些全局的配置可能是这样子的,例如我们使用了一个插件
Vue.use({
/* ... */
});
const app1 = new Vue({ el: '#app-1' });
const app2 = new Vue({ el: '#app-2' });
复制代码
但是这样子这会影响两个根实例,也就是说,会变得不可控。
在 Vue3 引入一个新的 API createApp
方法,返回一个实例:
import { createApp } from 'vue';
const app = createApp({ /* ... */ });
复制代码
然后我们就可以在这个实例上挂载全局相关方法,并只对当前实例生效,如:
app
.component(/* ... */)
.directive(/* ... */ )
.mixin(/* ... */ )
.use(/* ... */ )
.mount('#app');
复制代码
需要注意的是,在 Vue2 我们用了 Vue.prototype.$http=()=>{}
这样的写法,来对 “根Vue” 的 prototype 进行挂载方法,使得我们在子组件,可以通过原型链的方式找到 $http
方法,即 this.$http
。
而在 Vue3 我们类似这样的挂载需要用一个新的属性 globalProperties
:
app.config.globalProperties.$http = () => {}
复制代码
在 setup 内部使用 $http
:
setup() {
const {
appContext: {
config: {
globalProperties: { $http }
}
}
} = getCurrentInstance()
}
复制代码
2. 底层优化
Proxy 代理
Vue2 响应式的基本原理,就是通过 Object.defineProperty
,但这个方式存在缺陷。使得 Vue 不得不通过一些手段来 hack,如:
- Vue.$set() 动态添加新的响应式属性
- 无法监听数组变化,Vue 底层需要对数组的一些操作方法,进行再封装。如
push
,pop
等方法。
而在 Vue3 中优先使用了 Proxy 来处理,它代理的是整个对象而不是对象的属性,可对于整个对象进行操作。不仅提升了性能,也没有上面所说的缺陷。
简单举两个例子:
- 动态添加响应式属性
const targetObj = { id: '1', name: 'zhagnsan' };
const proxyObj = new Proxy(targetObj, {
get: function (target, propKey, receiver) {
console.log(`getting key:${propKey}`);
return Reflect.get(...arguments);
},
set: function (target, propKey, value, receiver) {
console.log(`setting key:${propKey},value:${value}`);
return Reflect.set(...arguments);
}
});
proxyObj.age = 18;
// setting key:age,value:18
复制代码
如上,用 Proxy
我们对 proxyObj
对象动态添加的属性也会被拦截到。
Reflect
对象是ES6 为了操作对象而提供的新 API。它有几个内置的方法,就如上面的 get
/ set
,这里可以理解成我们用 Reflect
更加方便,否则我们需要如:
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return target[propKey];
},
复制代码
- 对数组的操作进行拦截
const targetArr = [1, 2];
const proxyArr = new Proxy(targetArr, {
set: function (target, propKey, value, receiver) {
console.log(`setting key:${propKey},value:${value}`);
return Reflect.set(...arguments);
}
});
proxyArr.push('3');
// setting key:2,value:3
// setting key:length,value:3
复制代码
静态提升(hoistStatic) vdom
我们都知道 Vue 有虚拟dom的概念,它能为我们在数据改变时高效的渲染页面。
Vue3 优化了 vdom 的更新性能,简单举个例子
Template
<div class="div">
<div>content</div>
<div>{{message}}</div>
</div>
复制代码
Compiler 后,没有静态提升
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", { class: "div" }, [
_createVNode("div", null, "content"),
_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
]))
}
复制代码
Compiler 后,有静态提升
const _hoisted_1 = { class: "div" }
const _hoisted_2 = /*#__PURE__*/_createVNode("div", null, "content", -1 /* HOISTED */)
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", _hoisted_1, [
_hoisted_2,
_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
]))
}
复制代码
静态提升包含「静态节点」和「静态属性」的提升,也就是说,我们把一些静态的不会变的节点用变量缓存起来,提供下次 re-render 直接调用。如果没有做这个动作,当 render
重新执行时,即使标签是静态的,也会被重新创建,这就会产生性能消耗。
3. 与 TS
3.0 的一个主要设计目标是增强对 TypeScript 的支持。原本我们期望通过 Class API 来达成这个目标,但是经过讨论和原型开发,我们认为 Class 并不是解决这个问题的正确路线,基于 Class 的 API 依然存在类型问题。——尤雨溪
基于函数的 API 天然 与 TS 完美结合。
defineComponent
在 TS 下,我们需要用 Vue 暴露的方法 defineComponent,它单纯为了类型推导而存在的。
props 推导
import { defineComponent } from 'vue';
export default defineComponent({
props: {
val1: String,
val2: { type: String, default: '' },
},
setup(props, context) {
props.val1;
}
})
复制代码
当我们在 setup 方法访问 props 时候,我们可以看到被推导后的类型,
- val1 我们没有设置默认值,所以它为
string | undefined
- 而 val2 的值有值,所以是
string
,如图:
PropType
我们关注一下 props 定义的类型,如果是一个复杂对象,我们就要用 PropType 来进行强转声明,如:
interface IObj {
id: number;
name: string;
}
obj: {
type: Object as PropType<IObj>,
default: (): IObj => ({ id: 1, name: '张三' })
},
复制代码
或 联合类型
type: {
type: String as PropType<'success' | 'error' | 'warning'>,
default: 'warning'
},
复制代码
4. build丨更好的 tree-sharking(摇树优化)
tree-sharking 即在构建工具构建后消除程序中无用的代码,来减少包的体积。
基于函数的 API 每一个函数都可以用 import { method1,method2 } from "xxx";
,这就对 tree-sharking 非常友好,而且函数名同变量名都可以被压缩,对象去不可以。举个例子,我们封装了一个工具,工具提供了两个方法,用 method1
,method2
来代替。
我们把它们封装成一个对象,并且暴露出去,如:
// utils
const obj = {
method1() {},
method2() {}
};
export default obj;
复制代码
// 调用
import util from '@/utils';
util.method1();
复制代码
经过webpack打包压缩之后为:
a={method1:function(){},method2:function(){}};a.method1();
复制代码
我们不用对象的形式,而用函数的形式来看看:
// utils
export function method1() {}
export function method2() {}
复制代码
// 调用
import { method1 } from '@/utils';
method1();
复制代码
经过webpack打包压缩之后为:
function a(){}a();
复制代码
用这个例子我们就可以了解 Vue3 为什么能更好的 tree-sharking ,因为它用的是基于函数形式的API,如:
import {
defineComponent,
reactive,
ref,
watchEffect,
watch,
onMounted,
toRefs,
toRef
} from 'vue';
复制代码
5. options api 与 composition api 取舍
我们上面的代码都是在 setup 内部实现,但是目前 Vue3 还保留了 Vue2 的 options api 写法,就是可以“并存”,如:
// ...
setup() {
const val = ref<string>('');
const fn = () => {};
return {
val,
fn
};
},
mounted() {
// 在 mounted 生命周期可以访问到 setup return 出来的对象
console.log(this.val);
this.fn();
},
// ...
复制代码
结合 react ,我们知道 “函数式”,hook 是未来的一个趋势。
所以个人建议还是采用都在 setup
内部写逻辑的方式,因为 Vue3 可以完全提供 Vue2 的全部能力。
总结
个人觉得不管是 React Hook 还是 Vue3 的 VCA,我们都可以看到现在的前端框架趋势,“更函数式”,让逻辑复用更灵活。hook 的模式新增了 React / Vue 的抽象层级,「组件级 + 函数级」,可以让我们处理逻辑时分的更细,更好维护。