props在VSCode中默认类型推导出错

问题描述

调用 props.xxx 的时候会报错。props 的类型推导不正确。
image.png
image.png

问题排查

一开始不知道 Vue3 中对 props 有进行默认的类型推导,所以我给 props 写了一个 Interface ,并在使用的时候将 props 的类型定义为我自己写的 Interface 。 如下:

  1. interface IBadgeProps {
  2. count: number
  3. dot: boolean
  4. overflowCount: number | string
  5. className: string
  6. showZero: boolean
  7. text: string
  8. status: string
  9. type: string
  10. offset: number[]
  11. color: string
  12. }
  13. ...
  14. setup(props: IBadgeProps) {
  15. }

这样就不会在使用 props.xxx 的时候报错了。
后来经过提醒,才知道 Vue3 会对 props 默认进行类型推导的,并不需要自己写一个 Interface 。
那么是什么导致类型推导出错呢?
于是将 props 传值逐个筛选,最终发现是 validator 导致的。
我的写法是:

  1. status: {
  2. validator(value: string) {
  3. return oneOf(value, ['success', 'processing', 'default', 'error', 'warning'])
  4. }
  5. },

vue-nextissue 2474 中有提到这个问题。将 validator 换成箭头函数的写法即可解决这一问题。

解决方案

将 validator 写成箭头函数

  1. status: {
  2. validator: (value: string): boolean => {
  3. return oneOf(value, ['success', 'processing', 'default', 'error', 'warning'])
  4. }
  5. },

这个时候再看 props 的类型推导也正确了
image.png
补充:
https://v3.cn.vuejs.org/guide/typescript-support.html#%E6%B3%A8%E8%A7%A3-props
根据 vue3 官方文档中说:
image.png

  1. import { defineComponent, PropType } from 'vue'
  2. interface Book {
  3. title: string
  4. year?: number
  5. }
  6. const Component = defineComponent({
  7. props: {
  8. bookA: {
  9. type: Object as PropType<Book>,
  10. // 请务必使用箭头函数
  11. default: () => ({
  12. title: 'Arrow Function Expression'
  13. }),
  14. validator: (book: Book) => !!book.title
  15. },
  16. bookB: {
  17. type: Object as PropType<Book>,
  18. // 或者提供一个明确的 this 参数
  19. default(this: void) {
  20. return {
  21. title: 'Function Expression'
  22. }
  23. },
  24. validator(this: void, book: Book) {
  25. return !!book.title
  26. }
  27. }
  28. }
  29. })

使用这两种方式都可以。

Write operation failed: computed value is readonly

问题描述

在开发的时候,发现浏览器报了这个警告
image.png

问题排查

vue3 的文档中说道:

computed 接受一个 getter 函数,并为从 getter 返回的值返回一个不变的响应式 ref 对象。 或者,它也可以使用具有 get 和 set 函数的对象来创建可写的 ref 对象。

所以我怀疑是我写的代码中修改 computed 的返回值但是没有用 set 函数。于是重新看了一遍代码,发现并没有修改 computed 的值。
最终排查到是 模板引用 ref 与 其中一个 computed 重名了,修改其中一个名字就不会出现这个错误。
这时我本来的写法:

  1. <template>
  2. <span ref="badge">
  3. <sup v-show="badge"></sup>
  4. </span>
  5. </template>
  6. <script lang="ts">
  7. import { computed, defineComponent } from 'vue'
  8. export default defineComponent({
  9. name: 'Badge',
  10. setup() {
  11. const badge = computed(() => {
  12. return true
  13. })
  14. return {
  15. badge
  16. }
  17. }
  18. })
  19. </script>

可以看到 ref 中的 badge 与组合式 api 里的 computed 里的 badge 重名了。

看了文档:在组合式 API 中使用 template refs 。在使用组合式 api 时,响应式引用和模板引用的概念是统一的。为了获得对模板元素或组件实例的引用,我们可以像往常一样声明 ref 并从 setup() 返回。

解决方案

将 computed 的 badge 改名字,同时声明 ref 作为组件实例的引用并从 setup 返回。

  1. <template>
  2. <span ref="badge">
  3. <sup v-show="badgeShow"></sup>
  4. </span>
  5. </template>
  6. <script lang="ts">
  7. import { ref, computed, defineComponent } from 'vue'
  8. export default defineComponent({
  9. name: 'Badge',
  10. setup() {
  11. const badge = ref(null);
  12. const badgeShow = computed(() => {
  13. return true
  14. })
  15. return {
  16. badge,
  17. badgeShow
  18. }
  19. }
  20. })
  21. </script>

总结

在 Vue2 中 ref 与 computed 中的方法名重名也不会造成冲突,但是在 Vue3 中使用组合式 api 时,响应式引用和模板引用的概念是统一的,所以当我们的模板引用 ref=”xxx” 与响应式引用 let xxx = ref(1); 中的 xxx 重名时,会发生错误,编译器会把模板引用与响应式引用当成同一个值,造成意想不到的错误。可以看下面一个简单的例子:

  1. <template>
  2. <div class="hello">
  3. <div ref="test">
  4. <span>{{test}}</span>
  5. </div>
  6. </div>
  7. </template>
  8. <script>
  9. import { ref } from "vue"
  10. export default {
  11. name: 'HelloWorld',
  12. props: {
  13. msg: String
  14. },
  15. setup() {
  16. let test = ref(1)
  17. return {test}
  18. },
  19. methods: {
  20. }
  21. }
  22. </script>

最终 test 变成 "[object HTMLDivElement]" 。不是我们想要的结果。
因此要注意:不要让模板引用的名字与响应式引用的名字重名了。

Vue3全局配置

Vue 的 2.x 版本有很多的全局 API 和配置,它们会在全局范围内改变 Vue 的行为。
比如常见的全局 API 有:Vue.component / Vue.mixin / Vue.extend / Vue.nextTick;
常见的全局配置有:Vue.config.slient / Vue.config.devtools / Vue.config.productionTip

比如,如果你想创建一个全局的组件:

  1. Vue.component('trump-sucks', {
  2. data: () => ({ position: 'America president' }),
  3. template: `<h1>Trump is the worst ${position}</h1>`;
  4. });

或者声明一个全局指令:

  1. Vue.directive('focus', {
  2. inserted: el => {
  3. console.log('聚焦!');
  4. el.focus();
  5. },
  6. });

这样确实比较方便,但是会造成一些问题。由同一个 Vue 构造函数创建的 Vue 实例都会共享来自构造函数的全局配置。

为了规避这些问题,Vue3 引入了应用实例的概念。
调用 createApp 会返回一个应用实例。

  1. import { createApp } from 'vue';
  2. const app = createApp();

应用实例会暴露一个当前全局 API 的子集。在这个重构工作中, Vue 团队秉承的经验法则是:任何会在全局范围内影响 Vue 行为的 API 都会被迁移至应用实例中去。

2.x的全局API 3.x的应用实例API
Vue.config app.config
Vue.config.productionTip 移除
Vue.config.ignoredElements app.config.isCustomElement
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use

其他不会在全局影响 Vue 行为的 api 都已改造为具名导出的构建方式,为了支持 TreeShaking 。

在使用 createApp(VueInstance) 得到一个应用实例后,这个应用实例就可以用来把整个 Vue 跟实例挂载到页面上了:

  1. import { createApp } from 'vue';
  2. import MyApp from './MyApp.vue';
  3. const app = createApp(MyApp);
  4. app.mount('#app');

上面提到的全局组件和全局指令在 Vue3 用下面的写法:

  1. app.component('trump-sucks', {
  2. data: () => ({ position: 'America president', }),
  3. template: `<h1>Trump is the worst ${position}</h1>`;
  4. });
  5. app.directive('focus', {
  6. inserted: el => {
  7. console.log('聚焦!');
  8. el.focus();
  9. },
  10. });
  11. // 至此,所有在 app 所包含的组件树内创建的 Vue 实例才会共享 trump-sucks 这个组件和 focus 这个指令,而 Vue 构造函数并没有被污染。

参考链接:https://segmentfault.com/a/1190000023462887

this

在 Vue2 中,我们访问 data 或 props 中的变量,都是通过类似 this.number 这样的形式去获取的,但要特别注意的是,在 setup 中, this 指向的是 undefined ,也就是说不能再向 Vue2 一样通过 this 去获取变量了。

那么到底该如何获取到 props 中的数据呢?

其实 setup 函数还有两个参数,分别是 props 、 context ,前者存储着定义当前组件允许外界传递过来的参数名称以及对应的值;后者是一个上下文对象,能从中访问到 attr 、emit 、slots 。其中 emit 就是我们熟悉的 Vue2 中与父组件通信的方法,可以直接拿来调用。

事件API:$on, $off, $once 弃用

Vue3 中,从实例中移除了 $on , $off 和 $once 方法, $emit 仍然是现有 API 的一部分,因为它用于触发由父组件以声明方式附加的事件处理程序。
可用通过使用 mitt 来替换。
使用方法:http://bbs.itying.com/topic/5fbb23c92afeb47d24964422

  1. 安装 mitt
    1. yarn add mitt
    在 utlis 中新建 event.ts
    1. import mitt from 'mitt'
    2. const emitter = mitt()
    3. export default emitter
    在 App.vue 中进行监听
    1. import emitter from '../utils/event.ts'
    2. export default {
    3. // ...
    4. setup() {
    5. mitter.on('test', () => {
    6. console.log('mitt')
    7. })
    8. }
    9. }
    在 HelloWorld.vue 中进行事件发布
    1. import emitter from '../utils/event.ts'
    2. export default {
    3. // ...
    4. setup () {
    5. mitter.emit('test', 'a')
    6. }
    7. }

    $el 的替代方法

    Vue3文档 中提到:推荐使用模板引用来直接获取 DOM 元素。

    It is recommended to use template refs for direct access to DOM elements instead of relying on $el.

因此,如果我们在 setup 中需要使用到 $el 。我们可以在根结点中使用一个模板引用来获取到根节点的 dom 树。可以参考下面的代码:

  1. <template>
  2. <div ref="root">
  3. <h1 @click="test">Hello World</h1>
  4. </div>
  5. </template>
  6. <script>
  7. import { ref } from 'vue'
  8. export default {
  9. setup () {
  10. const root = ref(null);
  11. const test = () => {
  12. console.log(root.value); // 与 $el 一致
  13. }
  14. return {
  15. root,
  16. test
  17. }
  18. }
  19. }
  20. </script>

$emit 的用法改变

在组合式 api 中想要使用 this.$emits 向父组件传递事件。
在 Vue2.x 中的用法:

  1. // 父组件
  2. <template>
  3. <HelloWorld @on-change="change" />
  4. </template>
  5. <script>
  6. export default {
  7. methods: {
  8. change() {
  9. console.log('change');
  10. }
  11. }
  12. }
  13. </script>
  1. // 子组件
  2. <template>
  3. <h1 @click="change">hello world</h1>
  4. </template>
  5. <script>
  6. export default {
  7. methods: {
  8. change() {
  9. this.$emit('on-change')
  10. }
  11. }
  12. }
  13. </script>

在 Vue3.x 中则改为:

  1. // 父组件
  2. <template>
  3. <HelloWorld @on-change="change" />
  4. </template>
  5. <script>
  6. import { defineComponent } from 'vue'
  7. export default defineComponent ({
  8. setup() {
  9. const change = () => {
  10. console.log('change');
  11. }
  12. return {
  13. change
  14. }
  15. }
  16. })
  17. </script>
  1. // 子组件
  2. <template>
  3. <h1 @click="change">hello world</h1>
  4. </template>
  5. <script>
  6. import { defineComponent } from 'vue'
  7. export default defineComponent ({
  8. emits: ['on-change'],
  9. setup(props, { emit }) {
  10. const change = () => {
  11. emit('on-change')
  12. }
  13. return {
  14. change
  15. }
  16. }
  17. })
  18. </script>

详细的原因可参考文档

v-for 导致 $slots 的结果不一致

首先我们了解一下关于 slots Vue3 做了什么更新。
当我们获取 this.$slots.default 时返回的是一个函数。

待补坑。。。

computed与watchEffect

担心有人没耐心看完下面一连串的例子,所以先把研究结论写在前头。
主要分为下面三种情况:

  1. **computed****template** 中被调用computed 函数会在组件初始化时被调用一次,修改 computed 里的响应式变量时也会重新计算 computed ,vue2 和 vue3 表现一致。
  2. **computed****script** 中被调用:在 vue2 中,computed 函数会在组件初始化时被调用一次,修改 computed 里的响应式变量时也会重新计算 computed 。而在 vue3 中都不会。
  3. **computed****template****script** 中都没有被调用computed 在初始化时不会被执行,修改 computed 里的响应式变量时不会重新计算 computed,vue2 和 vue3 都不会。

因此如果我想要在 vue3 中根据多个响应式变量来进行计算,但是又不需要在 template 中调用时,可以使用 vue3 中提供的新特性 watchEffect 来实现。

接下来通过举例说明。

情况一:computedtemplate 中被调用

  1. // vue2
  2. <template>
  3. <div>
  4. <h1 @click="changeValue">{{result}}</h1>
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. name: 'HelloWorld',
  10. data () {
  11. return {
  12. value: 1
  13. }
  14. },
  15. computed: {
  16. result() {
  17. console.log('查看computed是否有执行');
  18. return this.value + 1
  19. }
  20. },
  21. methods: {
  22. changeValue() {
  23. this.value = 2
  24. }
  25. }
  26. }
  27. </script>

结果是初始化时输出了一次 查看computed是否有执行 ,点击 <h1> 的时候改变了 this.value 的值,所以又触发了一次 result 的执行,又输出了一次 查看computed是否有执行

vue3 的示例代码如下:

  1. // vue3
  2. <template>
  3. <div>
  4. <h1 @click="changeValue">{{result}}</h1>
  5. </div>
  6. </template>
  7. <script>
  8. import { ref, computed } from "vue"
  9. export default {
  10. name: 'HelloWorld',
  11. setup() {
  12. let value = ref(1)
  13. const result = computed(() => {
  14. console.log('查看computed是否有执行');
  15. return value.value + 1
  16. })
  17. const changeValue = (() => {
  18. value.value = 2
  19. })
  20. return {result, changeValue}
  21. },
  22. }
  23. </script>

表现结果与上述的 vue2 一致。

情况二:**computed****script** 中被调用

  1. // vue2
  2. <template>
  3. <div>
  4. <h1 @click="changeValue">点我呀</h1>
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. name: 'HelloWorld',
  10. data () {
  11. return {
  12. value: 1
  13. }
  14. },
  15. watch: {
  16. result(value) {
  17. console.log(value);
  18. }
  19. },
  20. computed: {
  21. result() {
  22. console.log('查看computed是否有执行');
  23. return this.value + 1
  24. }
  25. },
  26. methods: {
  27. changeValue() {
  28. this.value = 2
  29. }
  30. }
  31. }
  32. </script>

结果是初始化时输出了一次 查看computed是否有执行 ,点击 <h1> 的时候改变了 this.value 的值,所以又触发了一次 result 的执行,又输出了一次 查看computed是否有执行

在 vue3 中的情况:

  1. // vue3
  2. <template>
  3. <div>
  4. <h1 @click="changeValue">点我呀</h1>
  5. </div>
  6. </template>
  7. <script>
  8. import { ref, computed, watch } from "vue"
  9. export default {
  10. name: 'HelloWorld',
  11. setup() {
  12. let value = ref(1)
  13. watch(() => result, value => console.log(value))
  14. const result = computed(() => {
  15. console.log('查看computed是否有执行');
  16. return value.value + 1
  17. })
  18. const changeValue = (() => {
  19. value.value = 2
  20. })
  21. return {result, changeValue}
  22. },
  23. }
  24. </script>

初始化时不会执行 result ,点击 <h1> 的时候也不会执行 result 。因此也不会触发 watch 的变化。

这种情况下,vue3 提供了一种解决方案:watchEffect

  1. // vue3
  2. <template>
  3. <div>
  4. <h1 @click="changeValue">点我呀</h1>
  5. </div>
  6. </template>
  7. <script>
  8. import { ref, watchEffect, watch } from "vue"
  9. export default {
  10. name: 'HelloWorld',
  11. setup() {
  12. let value = ref(1)
  13. let result = ref(0)
  14. watch(() => result.value, value => console.log(value))
  15. watchEffect(() => {
  16. console.log('查看computed是否有执行');
  17. result.value = value.value + 1
  18. })
  19. const changeValue = (() => {
  20. value.value = 2
  21. })
  22. return {result, changeValue}
  23. },
  24. }
  25. </script>

使用了 watchEffect 就可以达到跟上面 vue2 一样的效果了。具体 watchEffect 的用法可以查看官方文档

情况三:computedtemplatescript 中都没有被调用

在 vue2 中:

  1. // vue2
  2. <template>
  3. <div>
  4. <h1 @click="changeValue">点我呀</h1>
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. name: 'HelloWorld',
  10. data () {
  11. return {
  12. value: 1
  13. }
  14. },
  15. computed: {
  16. result() {
  17. console.log('查看computed是否有执行');
  18. return this.value + 1
  19. }
  20. },
  21. methods: {
  22. changeValue() {
  23. this.value = 2
  24. }
  25. }
  26. }
  27. </script>

在 vue3 中:

  1. // vue3
  2. <template>
  3. <div>
  4. <h1 @click="changeValue">点我呀</h1>
  5. </div>
  6. </template>
  7. <script>
  8. import { ref, computed } from "vue"
  9. export default {
  10. name: 'HelloWorld',
  11. setup() {
  12. let value = ref(1)
  13. const result = computed(() => {
  14. console.log('查看computed是否有执行');
  15. return value.value + 1
  16. })
  17. const changeValue = (() => {
  18. value.value = 2
  19. })
  20. return {result, changeValue}
  21. },
  22. }
  23. </script>

上面两段代码中,在初始化和点击 <h1> 的时候都不会触发 result 的执行。