上一篇文章主要引入 Vue3 中的一些
Big Changes
,这篇来深入探讨三个东西:
<script setup>
的真正优势- 对比 Vue 2 中的
Mixins
,Vue 3 中的Composable
好在哪里?- 你没见过的隐藏高级技能 -
defineModel
<script setup>
的真正优势
这个实际上在官方是有说明的:sfc-script-setup,但是却没有做更进一步的解释了。
<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。当同时使用 SFC 与组合式 API 时该语法是默认推荐。相比于普通的 <script>
语法,它具有更多优势:
- 更少的样板内容,更简洁的代码。
- 能够使用纯 TypeScript 声明 props 和自定义事件。
- 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。
- 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。
更简洁的代码
在 setup
刚出来那会儿,作为一个长时间写 React
的人对下面这种每一个变量和方法都要手动 return
出去的写法,真的觉得非常的不适应,这哪是高级语言该有的东西!心中有说不出的万马奔腾感:
好在后面出了 <script setup>
这个语法糖,减少了模板代码,做了简化,在上一篇文章中我已经着重解释过了,这里就不废话了,贴一张对比图,直观明了:
小技巧
:推荐大家一个提升协作效率的工具,VS Code 插件:CodeSnap,直接在编辑器中生成代码美化的图片。
你只需要要将需要生成的代码选中,然后右键选择 CodeSnap,点击上方的 Logo,即可生成图片,实在方便!
更好的运行性能
回过头来,就一直想知道一个问题:setup()
写法为什么要 return?
查了一些资料,结论是是因为 SFC 会将 <template>
和 <script>
中的内容编译成组件的渲染函数。而 <script>
和 <script setup>
在性能上的差异,就在于渲染函数从 <script>
中取得 <template>
所需变量的方式不同。
<script> + setup()
官方文档中有提到,在开发过程中 ,基于开发工具检查 和模板热重载 的原因,使用 <script setup>
开发的组件,还是会被编译成回传组件,而不是直接回传给渲染函数。
也就是说,想看到 <script setup>
和 <script>
差别,要看正式编译打包后的产物( npm run build
)。
直接编译出来的产物实在很难读,最好在
vite.config.ts
中加一个编译配置:产物中的函数名可以保持原来的命名,便于阅读和理解。
然后 pnpm run build
一下,来看看这种写法的编译结果:
可以看到,setup()
函数和渲染函数会分开声明,各自形成自己的闭包。所以,声明在 setup()
内的变量需要 return
出去,才能传给渲染函数,在渲染 template
的时候才能取到所有变量。
<script setup>
这种语法糖的写法不需要经过中间代理,运行时性能比较好。
组件的渲染函数会存在在 setup()
的 scope
内,也就是将渲染函数写在 setup
的闭包内,最后才回传出去。所以,渲染函数可以通过闭包拿到外层 setup
函数( <script setup>
) 内,所有 top-level
变量(包含:变量、函数和 imports
),也不用担心泄漏过多变量或逻辑。
看看这种写法的编译结果:
渲染函数可以通过必报直接拿到外层的变量和方法等等。你搞懂了吗!
与普通的 <script>
一起使用
来自:官方文档
<script setup>
可以和普通的 <script>
一起使用。普通的 <script>
在有这些需要的情况下或许会被使用到:
- 声明无法在
<script setup>
中声明的选项,例如inheritAttrs
或插件的自定义选项 (在 3.3+ 中可以通过defineOptions
替代)。 - 声明模块的具名导出 (
named exports
)。 - 运行只需要在模块作用域执行一次的副作用,或是创建单例对象。
在同一组件中将 <script setup>
与 <script>
结合使用的支持仅限于上述情况。具体来说:
- 不要 为已经可以用
<script setup>
定义的选项使用单独的<script>
部分,如props
和emits
。 - 在
<script setup>
中创建的变量不会作为属性添加到组件实例中,这使得它们无法从选项式 API 中访问。我们强烈反对以这种方式混合 API。
如果你发现自己处于以上任一不被支持的场景中,那么你应该考虑切换到一个显式的 setup()
函数,而不是使用 <script setup>
。
Composable
(组合式函数)
在 Vue 2 中组件之间可复用逻辑通常会用 Mixins。但现在有了 Composable(官方翻译为:组合式函数),可以以更简洁的方式实现代码的复用。
大概写过
React
的人看了 Vue 3 的Composition API
,难免直呼"内行"!这可不就是React Hooks
?Composable
不就是React Custom Hooks
?
这下好了,面试官又有的可问了:同学,我看你骨骼惊奇,居然 React 和 Vue 双修成仙,那你讲讲 Vue3 Composition API 和 React Hooks 的区别呗?
实际上,但它俩还是有区别的,一言两语根本讲不完,放这篇文章肯定是不合适的,后续专门写一篇文章来讲这个问题!
先来讲讲为什么会出现 Composable
。
Mixins 的缺点
一个新事物的出现必然是为了改善旧事物的不足的,过去在 Vue 中实现逻辑复用主要是用到 Mixins
。
先来看个例子,比如你接手了一个旧项目,然后你写了一段可复用的逻辑,将其封装成了一个 Mixins
,但是你没注意到之前的同学也封装了一个 Mixins
,里面有个同名的方法,这两个 Mixins
长这样:
然后你自信满满,信手拈来,引入到了组件中:
想想,调用 getData
方法会发生什么?答案是:数组中的第二个 mixin ( itemsMixin
) 会覆盖 userMixin
中定义的 getData
方法。
除了这种方法的互相覆盖,还有变量的覆盖,跟注入组件中的变量的冲突等等之类的问题。
来说说 Mixins
的一些缺点:
-
命名冲突 :当多个
Mixins
中具有同名属性或方法时,会造成命名冲突,导致出现不可预期的结果。当然也有一些办法来解决这个问题,比如在mixins
中使用特殊前缀或命名空间来避免冲突。 -
紧耦合,依赖关系难以追踪 :在
mixins
和组件之间遇到隐式依赖关系的情况并不少见。这使得在不破坏现有代码的情况下重构组件或mixins
变得极其困难。随着新需求的出现,mixins
还可以与其他mixins
紧密耦合。解决这个问题,可以使用更明确的依赖注入方式,例如provide/inject
。 -
难以理解和调试 :对于新开发人员来说,理解包含大量
mixins
的代码库非常困难。属性的来源并不明显,尤其是在存在全局注册的mixins
的情况下。随着应用程序的增长,隔离错误也成为一场噩梦。 -
复用逻辑难以维护 :使用
mixins
可以实现逻辑复用,但是在应用复杂逻辑时,如果多个mixins
互相依赖或冲突,会使得逻辑变得难以维护和理解。解决这个问题,可以采用更清晰和结构化的代码组织方式,例如使用高阶组件
或插件
。
Composable
在了解了 mixins
的缺点之后,我们用最新的 Composable
来改写一下上面的这个例子:
然后你可以在组件中这么使用:
这不仅可以控制对外暴露哪些数据,还可以你在使用的时候通过解构来重命名你的变量和方法,很好地解决了 Mixins
的那些问题。
很多初学者可能会觉得这个心智模型有点难,其实就很简单,比如封装一个常用的 API 请求的 Composable
:
怎么用呢?
你只需要记住:只要可复用的逻辑,都应该封装成公共方法或者
Composable
,如果这段逻辑中使用了Composition API(ref、reactive...)
,那你就封装成以"use"
开头的Composable
,否则,就将其封装成普通的公共方法。
你不能说它跟 React Hooks
一模一样(比如 ref
和 useState
,还是不一样的),只能说是基本相同,哈哈。
反正就是借鉴呗 ~
推荐两个高质量的第三方 composable 库
VueUse
类似 React 中的 ahooks,提供了一系列高质量、常用的自定义 Hooks。
VueUse
使用 TypeScript 编写,提供 200 多个可组合函数,适用于 Vue 2 和 3。
shell
npm i @vueuse/core
vue-composable
vue-composable
是一个库,提供即用型通用组合 API 函数。你可以使用 Yarn 或 npm 安装它:
shell
npm install @vue/composition-api vue-composable
它提供的部分功能类别包括事件、日期、格式、断点、存储、i18n 和 Web。
隐藏高级技能 - defineModel
在 VueJS 3.3 版本 中,推出了一个新的语法糖 - defineModel
,可以让我们以一种非常优雅的方法来支持 v-model
的双向绑定,但需要注意的是,当前这个宏还是默认未开启状态。
是的,截至目前(2023-09-17
),连官方网站都还没挂上去:
推荐阅读:pull/8018
下面,我们就来讲讲这是个什么东西!
在旧版本的 Vue
中,创建具有双向绑定功能的 Vue 元素需要两个步骤:
- 首先,组件必须声明一个属性以接受来自父组件的数据。
- 随后,它需要发出一个具有相应名称的更新事件,以便在数据更新时通知父组件。
这一过程不仅出现了模板代码,还增加了组件开发的复杂性,比起 React,真的是相当别扭。
不过,随着 VueJS 3.3 版本 的发布,现在可以利用 defineModel
宏。这个宏大大简化了双向绑定的过程。
需要注意的是:编译时,宏会声明一个具有相同名称的道具和一个相应的事件
update:propName
。这是一个实验性功能,引入了一个名为defineModel
的新编译器脚本选项,默认情况下是禁用的。 vuejs/rfcs#503
defineModel
宏会自动处理这些任务,而不是手动声明 props
和发出更新事件。让我们来看一个例子,看看以前的双向绑定是如何实现的,而 defineModel
宏又是如何轻松实现相同的功能。
旧实现
ParentComponent.vue
:
ChildComponent.vue
:
测试:
当父组件使用属性 :model-value="modelValue"
将 prop modelValue
传递给子组件,而子组件使用 emit("update:modelValue", value)
函数向父组件发送更新事件时,就建立了双向绑定。
新实现
需要注意的是,defineModel
目前还是默认关闭的,要使用它,需要在 vite.config.ts
中打开:
ParentComponent.vue
:
ChildComponent.vue
:
测试:
使用 defineModel
创建一个数值模型,该模型将在父组件和子组件之间共享。如你所见,新方法更加简洁明了。宏自动注册道具并返回一个可直接更改的引用,而不是显式声明 props 并发出事件,从而提供了一种连续的双向数据绑定体验。
使用 defineModel() 实现多个双向绑定
假设我们有一个父组件,需要使用多个双向绑定与子组件通信。我们将使用三个 props:count
、counter
和 name
。父组件将向子组件发送这些 props 的更新值,而子组件则可以修改并发回更新值。
ParentComponent.vue
:
ChildComponent.vue
:
测试:
通过利用响应式变量和 props
,我们实现了父组件(ParentComponent
)和子组件(ChildComponent
)之间的高效双向通信。这一强大的功能确保了组件之间无缝的数据同步,从而实现了连贯、同步的用户体验。让我们深入了解这种方法的机制,见证它的实际效果。