一个诡异的问题带来的启发

463 阅读6分钟

大家好,我是子辰老师。我们来看一个 Vue3 中一个非常奇怪的现象,通过这个问题可以帮你们梳理清楚很多的知识。

我们先来看两个 Vue 的组件:

FooScriptSetup.vue 的代码很简单,语法使用的是 script setup 语法糖:

除了 FooScriptSetup.vue 组件之外还有一个 FooSetup.vue 组件,代码是一模一样的功能,只不过不再使用 setup 语法糖,而是直接使用 setup 函数:

问题所在

那么问题是什么呢?就是在使用它们的时候居然是有差异的:

我们引入两个组件,并且都加了 ref,在 onMounted 的时候去打印组件实例里边的数据 msg,按照理想状态来说应该可以各自取到相应的数据,但是结果却不是这样的:

一个打印能获取到,一个却获取不到,这就奇怪了。

这个 script setup 不是一个语法糖吗?最终的转化结果不是应该和 setup 函数是一样的吗?可是为什么会出现差异呢?

那我们继续的去打印一下,这次不打印 msg 了,直接打印组件实例:

我们看一下打印出来的组件实例:

使用 setup 函数的组件打印出来的是正常的,这个组件里面有很多的东西,包括里边的 count、increase、msg 都打印出来了。

然后我们看一下使用 setup 语法糖的组件,打印出来的什么都没有,空空如也,这就是为什么我们刚才打印 msg 得到的是 undefined 的原因。

探究根本

那么问题就来了,这是为什么呢?这里我们就要探究它的原因了,探究的话需要我们安装一个插件 npm vite-plugin-inspect -D

这个插件的作用就是可以看到 vite 对单文件组件的编译结果,然后我们就可以通过编译出来的结果进行分析对比了,因为最终运行的是编译结果而不是直接运行组件的代码。

然后我们再次运行项目:

运行之后除了出现访问页面的地址之外,还会出现一个地址,这个地址打开之后就这这样的:

这里就编译了很多文件,通过这些文件就可以看到编译之后的结果。

首先我们看一下 FooSetup 组件,也就是使用了 setup 函数的组件,看看它编译出来的是什么:

这里就再次验证了,在真正运行的时候模板是压根不存在的,什么 v-for、v-if 语法,模板里的各种指令全都不存在,最终运行的是这个 render 函数,这是之前学过的知识。

然后我们再去看一下 FooScriptSetup 组件,也就是使用了 setup 语法糖的组件:

通过上图的分析我们知道 expose 是关键问题,那我们去官网查一下看看:

在官网找到 expose 的章节,官网对它有详细的阐述。

expose 有多种的使用方式:

export default {
  // 像选项 API 一样使用
  expose: ['publicMethod']
}

export default {
  // 也可以通过第二个参数解构出来
  setup (, { expose }) {
    // 然后调用
    expose()
  }
}

这个单词的含义就是暴露,顾名思义它是可以指定我们要暴露哪些东西到这个对象的实例上,一旦配置了 expose 或者调用了 expose,它就不能暴露其它任何的属性。

也就意味着 FooScriptSetup 组件编译后调用了 expose() 并且没有传递任何东西,那么 Vue 就认为你不想暴露任何东西到这个实例上,因此我们最终的结果里实例上就没有挂载任何东西,就是这么来的,而 FooSetup 组件之所以有数据,是因为它没有调用 expose()

如果我们在 FooSetup 组件里手动调用了 expose 那么结果就是一样的:

这就说明 setup 语法糖跟 setup 函数的区别就在于,setup 语法糖帮我们去调用了一次 expose,这样子我们就无法通过组件实例拿到任何东西。

那么说到这里结束了吗?

image.png

并没有结束,我们要学一个知识点那就要把它学的透彻。

我们要研究两个问题:

  1. setup 语法糖为什么要这么做。
  2. 那既然实例上没有这个东西的话,模板在渲染的时候数据哪来的?我们都知道模板的数据来自于组件实例,可是实例上没有。

这两个问题如果搞清楚了,你不仅能够理解 setup 语法糖,还能理解 expose 到底起什么作用,子辰讲知识就一定会将透彻,看文章的你一定要学的透彻,否则未来还要回过头来继续学懂。

一:为什么要这么做?

子辰先回答第一个问题,setup 语法糖帮我们调用一次 expose 是为了保证单项数据流,这个就要说到 ref 了。

很多同学都有一个习惯,喜欢去滥用 ref,通过 ref 确实可以拿到组件的实例,拿到实例之后就可以拿到组件里的任何东西,然后说蹭蹭不进去结果一忍不住就通过这个 ref 去改动了它的某一个数据 $ref.xxx.data = xxx,这样就打破了单项数据流。

父子组件的数据传递,正常的话应该是通过属性和事件来进行传递的,不过现在单向数据流一打破就会给将来留下非常非常多的隐患,一旦遇到问题就很难调试。

那么一个成熟的框架为了避免大家去做这种不规范的事情,他就做出了这么一个限制,就建议大家尽量不要在组件实例里去暴露内部的东西。

如果说一定要暴露的话,应该使用 expose 手动的去暴露,比如说 FooSetup 组件指定暴露的数据:

这样子就不会暴露其它数据了,避免了造成隐患。

同学们应该都用过一些 UI 组件库对吧,以前这个 Element UI 是最喜欢使用 ref 去操作的,其实是很不好的。而 Ant Design Vue 几乎见不到使用 ref 去操作的场景。

一旦用了 ref 去操作组件,就很难忍住去改动它里边的东西,一旦改动就打破单项数据流了。

如果说我们要在 FooScriptSetup 组件里也要暴露的话,那么我们可以使用一个宏,叫做 defineExpose:

可以看到效果是一样的,那我们看一下编译的结果:

可以看到 defineExpose 编译成了 expose, 也就是说 defineExpose 它压根是不存在的,它只是一个宏,所以说这就是它不需要导入的原因,因为在运行的时候它压根不存在,一旦手动指定暴露的数据,那么除了指定的数据,其他任何东西都不会出现在实例之上。

到这第一个问题就解决了,我们说第二个问题。

二:数据的来源

它既然没有暴露到组件实例上,那么模板里的东西哪来的呢?其实这个问题很简单。

我们看一下它的编译结果就完事了:

模板编译出来以后是一个 render 函数,那么这个 render 里边的数据是通过函数的参数 $setup 传递进来的,它会在内部把 setup 函数返回的数据作为参数传进来,然后我们就可以拿到 setup 函数返回的数据,所以这个数据不在组件实例,没有从实例里边拿,这就解释清楚了模板的数据来源。

总结

现在问题就解释清楚了,对我们开发的启发就是不要去随便使用 ref ,很容易打破单项数据流。

尽管我们看到 Element UI 这个组件库经常这么干,但是这不是一个好的办法。

如果说你将来一定要暴露一些东西到实例上,那么你可以使用 expose 的方式。

而且我们在开发当中尽量使用 setup 语法糖的方式来编写 Vue3 的项目,因为它会自动的帮我们调用 expose,这样就避免了外部的滥用,这就是一个强力的约束。

本文来源

本文来源自渡一公众号:Duing,欢迎关注,获取超新超深入的技术讲解

感谢你阅读本文,如果你有任何疑问或建议,请在评论区留言,如果你觉得这篇文章有用,请点赞收藏或分享给你的朋友!