一些概念
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71// 逻辑功能(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 的处理
让我们来关注逻辑,抽离逻辑,先看主体的代码结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29import 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
开头进行命名,有那味了~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38// 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 弹框组件,我们希望的弹框是这样子的,绝对居中,层级最高,如:
组件的结构是这样子的
1
2
3<Home> <Modal /> </Home>
但是如果在父组件 Home 有类似这样的样式,如 transform
:
就会影响到 Modal 的位置,即使 Modal 用了 position:fixed
来定位,如:
这就是为什么我们需要用 Teleport 组件来帮助我们 “跳出” 容器,避免受到父组件的一些约束控制,把组件的 DOM 元素挂载到 body 下,如:
1
2
3
4
5<Teleport to="body"> <div v-if="show"> ...Modal 组件的 DOM 结构... </div> </Teleport>
注意:即使 Modal 跳出了容器,也保持 “父子组件关系”,只是 DOM 元素的位置被移动了而已 。
异步组件(defineAsyncComponent)
我们都知道在 Vue2 也有异步组件的概念,但整体上来说不算完整~,Vue3 提供了 defineAsyncComponent
方法与 Suspense
内置组件,我们可以用它们来做一个优雅的异步组件加载方案。
直接看代码:
HOCLazy/index.tsx
1
2
3
4
5
6
7
8
9
10
11
12import { 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48<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
1
2
3
4
5
6const slow = (comp: any, delay: number = 1000): Promise<any> => { return new Promise(resolve => { setTimeout(() => resolve(comp), delay); }); }; export default slow;
调用(组件级)
1
2
3
4
5
6
7
8
9
10
11
12<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
属性的对象。
1
2
3
4
5
6
7
8setup(props, context) { const count = ref<number>(1); // 赋值 count.value = 2; // 读取 console.log('count.value :>> ', count.value); return { count }; }
在 template 中 ref 包装对象会被自动展开(Ref Unwrapping),也就是我们在模板里不用再 .value
1
2
3<template> {{count}} </template>
reactive
与 Vue2 中的 Vue.observable()
是一个概念。
用来返回一个响应式对象,如:
1
2
3
4
5const obj = reactive({ count: 0 }) // 改变 obj.count++
注意:它用来返回一个响应式对象,本身就是对象,所以不需要包装。我们使用它的属性,不需要加 .value
来获取。
toRefs
官网:因为 props 是响应式的,你不能使用 ES6 解构,因为它会消除 prop 的响应性。
让我们关注 setup
方法的 props 的相关操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<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 ,如:
1
2
3
4// es6 syntax const { name } = props; // to es5 syntax var name = props.name;
假设父组件更改了 props.name 值,当我们再点击了 button 输出的 name 就还是之前的值,不会跟着变化,这其实是一个基础的 js 的知识点。
为了方便我们对它进行包装,toRefs
可以理解成批量包装 props 对象,如:
1
2
3
4
5const { name } = toRefs(props); const handleClick = () => { // 因为是包装对象,所以读取的时候要用.value console.log('name :>> ', name.value); };
可以理解这一切都是因为我们要用解构,toRefs
所采取的解决方案。
toRef
toRef 的用法,就是多了一个参数,允许我们针对一个 key 进行包装,如:
1
2const 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 方法简单结构
1
2
3
4
5
6
7
8watchEffect(onInvalidate => { // 执行副作用 // do something... onInvalidate(() => { // 执行/清理失效回调 // do something... }) })
执行失效回调,有两个时机
- 副作用即将重新执行时,也就是监听的数据发生改变时
- 组件卸载时
一个例子:我们要通过 id 发起请求获取「水果」的详情,我们监听 id,当 id 切换过于频繁(还没等上个异步数据返回成功)。可能会导致最后 id=1
的数据覆盖了id=2
的数据,这并不是我们希望的。
我们来模拟并解决这个场景:
模拟接口 getFruitsById
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28interface 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 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17setup() { 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 ,如:
子组件
1
2
3
4
5
6<template> <div class="fragment"> <div>div1</div> <div>div2</div> </div> </template>
父组件调用,新增了一个 class
1<MyFragment class="extend-class" />
子组件会被渲染成
1
2
3
4<div class="fragment extend-class"> <div> div1 </div> <div> div2 </div> </div>
如果我们使用了 片段 ,就需要显式的去指定绑定 attrs ,如子组件:
1
2
3
4<template> <div v-bind="$attrs">div1</div> <div>div2</div> </template>
emits
在 Vue2 我们会对 props 里的数据进行规定类型,默认值,非空等一些验证,可以理解 emits 做了类似的事情,把 emit 规范起来,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// 也可以直接用数组,不做验证 // 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 语法糖,举个例子:
先从父组件看
1
2
3<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
1
2
3
4
5
6
7
8
9
10
11
12
13// ... 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
1
2
3
4
5
6
7
8
9<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 ,如:
1
2
3
4
5
6
7
8
9
10
11
12<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 我们对于一些全局的配置可能是这样子的,例如我们使用了一个插件
1
2
3
4
5Vue.use({ /* ... */ }); const app1 = new Vue({ el: '#app-1' }); const app2 = new Vue({ el: '#app-2' });
但是这样子这会影响两个根实例,也就是说,会变得不可控。
在 Vue3 引入一个新的 API createApp
方法,返回一个实例:
1
2import { createApp } from 'vue'; const app = createApp({ /* ... */ });
然后我们就可以在这个实例上挂载全局相关方法,并只对当前实例生效,如:
1
2
3
4
5
6app .component(/* ... */) .directive(/* ... */ ) .mixin(/* ... */ ) .use(/* ... */ ) .mount('#app');
需要注意的是,在 Vue2 我们用了 Vue.prototype.$http=()=>{}
这样的写法,来对 “根Vue” 的 prototype 进行挂载方法,使得我们在子组件,可以通过原型链的方式找到 $http
方法,即 this.$http
。
而在 Vue3 我们类似这样的挂载需要用一个新的属性 globalProperties
:
1app.config.globalProperties.$http = () => {}
在 setup 内部使用 $http
:
1
2
3
4
5setup() { const { ctx: { $http } } = getCurrentInstance(); }
2. 底层优化
Proxy 代理
Vue2 响应式的基本原理,就是通过 Object.defineProperty
,但这个方式存在缺陷。使得 Vue 不得不通过一些手段来 hack,如:
- Vue.$set() 动态添加新的响应式属性
- 无法监听数组变化,Vue 底层需要对数组的一些操作方法,进行再封装。如
push
,pop
等方法。
而在 Vue3 中优先使用了 Proxy 来处理,它代理的是整个对象而不是对象的属性,可对于整个对象进行操作。不仅提升了性能,也没有上面所说的缺陷。
简单举两个例子:
- 动态添加响应式属性
1
2
3
4
5
6
7
8
9
10
11
12
13const 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
更加方便,否则我们需要如:
1
2
3
4get: function (target, propKey, receiver) { console.log(`getting ${propKey}!`); return target[propKey]; },
- 对数组的操作进行拦截
1
2
3
4
5
6
7
8
9
10const 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
1
2
3
4<div class="div"> <div>content</div> <div>{{message}}</div> </div>
Compiler 后,没有静态提升
1
2
3
4
5
6function 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 后,有静态提升
1
2
3
4
5
6
7
8
9const _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 推导
1
2
3
4
5
6
7
8
9
10import { 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 来进行强转声明,如:
1
2
3
4
5
6
7
8
9interface IObj { id: number; name: string; } obj: { type: Object as PropType<IObj>, default: (): IObj => ({ id: 1, name: '张三' }) },
或 联合类型
1
2
3
4type: { 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
来代替。
我们把它们封装成一个对象,并且暴露出去,如:
1
2
3
4
5
6// utils const obj = { method1() {}, method2() {} }; export default obj;
1
2
3// 调用 import util from '@/utils'; util.method1();
经过webpack打包压缩之后为:
1a={method1:function(){},method2:function(){}};a.method1();
我们不用对象的形式,而用函数的形式来看看:
1
2
3// utils export function method1() {} export function method2() {}
1
2
3// 调用 import { method1 } from '@/utils'; method1();
经过webpack打包压缩之后为:
1function a(){}a();
用这个例子我们就可以了解 Vue3 为什么能更好的 tree-sharking ,因为它用的是基于函数形式的API,如:
1
2
3
4
5
6
7
8
9
10import { defineComponent, reactive, ref, watchEffect, watch, onMounted, toRefs, toRef } from 'vue';
5. options api 与 composition api 取舍
我们上面的代码都是在 setup 内部实现,但是目前 Vue3 还保留了 Vue2 的 options api 写法,就是可以“并存”,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// ... 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 的抽象层级,「组件级 + 函数级」,可以让我们处理逻辑时分的更细,更好维护。
感谢您的阅读,如果对您有帮助,欢迎关注"CRMEB"。码云上有我们开源的商城项目,知识付费项目,JAVA版全开源商城系统,学习研究欢迎使用,老铁顺手点个star呗,老板奖励五毛,分你两毛五,????????关注我们保持联系!
最后
以上就是内向画笔最近收集整理的关于Vue3丨从 5 个维度来讲 Vue3 变化一些概念Vue 和 React 的逻辑复用手段5个维度来讲 Vue3总结的全部内容,更多相关Vue3丨从内容请搜索靠谱客的其他文章。
发表评论 取消回复