Vue 3.0 新特性与使用 四

简述

最近了解到 Vue3 的 setup 写法已经定稿了, 现在还是实验性阶段,但已经确定了这种写法,虽然可能还会有一些问题,不过相信在接下来的版本中会进行修复,并将会在不久后正式发布这个 setup 语法糖,下面来展示几个官方文档中所描述的 setup 的用法和一些目前的小问题。

setup 语法糖的时候,配合 eslint 的话,vue 也出了一个插件 eslint-plugin-vue

示例

基本案例

<script setup>
  import Foo from './components/Foo.vue';
  
  const msg = 'Hello!'
</script>

<template>
  <div>{{ msg }}</div>
</template>
复制代码

Compiled Output:

<script>
import Foo from './components/Foo.vue';

export default {
  compontents: {
    Foo
  },
  setup() {
    const msg = 'Hello!'

    return function render() {
      // has access to everything inside setup() scope
      return h('div', msg)
    }
  }
}
<script>
复制代码

props 和 emits

App.vue

<template>
  <Foo :count="count" @click="inc" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import Foo from './components/Foo.vue';

const count = ref(0);
const inc = () => {
  count.value ++;
}
</script>
复制代码

Foo.vue – export default

<template>
  <div class="container">
    count: {{count}}
    <button @click="inc">增加</button>
  </div>
</template>

<script lang="ts">
export default {
  props:  {
    count: Number
  },
  emits: ['inc'],
  setup(props, {attrs, slots, emit}) {
    const inc = () => {
        emit('inc')
    }
    return { inc }
  }
}
</script>
复制代码

Foo.vue – setup

<template>
  <div class="container">
    count: {{count}}
    <button @click="inc">增加</button>
  </div>
</template>

<script setup lang="ts">
  import { defineProps, defineEmits } from 'vue';
  const props = defineProps({
    count: Number
  });
  const emit = defineEmits(['inc']);
  const inc = () => {
    emit('inc')
  }
</script>
复制代码

注意到,在之前的 setup 语法糖的模式下,获取 attrslotsemit 可以通过 useContext 获取, 但是现在这个 Api 已经被抛弃了,目前使用的话会出现横向提示已被抛弃!

setup – defineProps

// 方法一
const props = defineProps('count');

// 方法二
const props = defineProps({
  count: Number
});

// 方法三 - ts
type Props = {
  count: number,
}
const props = defineProps<Props>();

复制代码

setup – defineProps 如何设置默认值

import { defineProps, withDefaults } from 'vue';

type Props = {
  count: number,
}
const props = withDefaults(defineProps<Props>(), {
  count: 1
});
复制代码

使用 components

<script setup>
  import Foo from './Foo.vue'
  import MyComponent from './MyComponent.vue'
</script>

<template>
  <Foo />
  <!-- kebab-case also works -->
  <my-component />
</template>
复制代码

Compiled Output:

import Foo from './Foo.vue'
import MyComponent from './MyComponent.vue'

export default {
  setup() {
    return function render() {
      return [h(Foo), h(MyComponent)]
    }
  }
}
复制代码

使用 slots 和 attrs

<script setup>
  import { useSlots, useAttrs } from 'vue'

  const slots = useSlots();
  const attrs = useAttrs();
</script>
复制代码

在 template 上也可以直接使用 $slots$attrs~

使用动态组件

<template>
  <component :is="HelloWorld" msg="Welcome to Your Vue.js + TypeScript App"></component>
  <component :is="Math.random() > 0.5 ? Foo : Boo" ></component>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import HelloWorld from './components/HelloWorld.vue';
import Foo from './components/Foo.vue';
import Boo from './components/Boo.vue';
</script>
复制代码

使用指令

vFocus.ts

export default {
  mounted(el: Window): void {
    console.log(el);
    el.focus();
  },
}
复制代码

App.vue

<template>
  <div class="container">
    <input type="text" v-focus/>
  </div>
</template>

<script setup lang="ts">
import vFocus from '../vFocus';
</script>
复制代码

这里有一点注意的是 Vue3 对于指令的话,必须要有一个 v 前缀,需要 v 前缀的原因是因为全局注册的指令(例如v-focus)很可能与同名的本地声明变量发生冲突, 如果不使用 v 前缀的话,会报错,比如:
App.vue

<template>
  <div class="container">
    <input type="text" v-focus/>
  </div>
</template>

<script setup lang="ts">
- import vFocus from '../vFocus';
+ import Focus from '../vFocus';
</script>
复制代码

使用 await

setup 语法糖上,顶层就支持 async/await 的语法了

<script setup lang="ts">
  const delay = () => new Promise(reslove => {
    setTimeout(() => {
      reslove('ok')
    }, 1000)
  })
  
  console.time('Await');
  await delay();
  console.timeEnd('Await');
</script>

// Output: Await: 1004.998779296875 ms
复制代码

关于 await 这里的使用,会有一个场景,比如:

该场景只针对于:export default { async setup() {} }

import { defineComponent, getCurrentInstance } from 'vue';

export default defineComponent({
  async setup() {
    const delay = () => new Promise(reslove => {
      setTimeout(() => {
        reslove('ok')
      }, 1000)
    })
    console.log('Vue 实例【await 前】:', getCurrentInstance());
    const res = await delay();
    console.log('Vue 实例【await 后】:', getCurrentInstance(), res);
  }
});
</script>

// Output:
// Vue 实例【await 前】: {uid: 4, vnode: {…}, type: {…}, parent: {…}, appContext: {…}, …}
// Vue 实例【await 后】: null ok
复制代码

至于为什么会这样,这里放出一小段源码:

/**
 * File: runtime-core.esm-bundler.js
 * Func: setupStatefulComponent
 */
function setupStatefulComponent(instance, isSSR) {
  ...
  if (setup) {
        const setupContext = (instance.setupContext =
            setup.length > 1 ? createSetupContext(instance) : null);
        currentInstance = instance;
        pauseTracking();
        const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [(process.env.NODE_ENV !== 'production') ? shallowReadonly(instance.props) : instance.props, setupContext]);
        resetTracking();
        currentInstance = null;
        ...
    }
  ...
}
复制代码

可以看到当存在 setup 的时候,会先进行 currentInstance = instance; 设置当前实例;
然后调用 callWithErrorHandling(setup, instance, 0, ...);
发现再调用 setup 的时候并没有去进行等待;
之后再进行了 currentInstance = null;

也就是在执行 setup 异步等待的时候,就已经把 currentInstance 设置为 null 了,可以看一下 getCurrentInstance() 获取的是什么:

/**
 * File: runtime-core.esm-bundler.js
 * Func: getCurrentInstance
 */
 const getCurrentInstance = () => currentInstance || currentRenderingInstance;
复制代码

获取的就是 currentInstance

解决办法:
讨论: github.com/vuejs/rfcs/…

import { defineComponent, getCurrentInstance } from 'vue';

export default defineComponent({
  async setup() {
    const delay = () => new Promise(reslove => {
      setTimeout(() => {
        reslove('ok')
      }, 1000)
    })
    console.log('Vue 实例【await 前】:', getCurrentInstance());
-   const res = await delay();
+   const res = await withAsyncContext(delay());
    console.log('Vue 实例【await 后】:', getCurrentInstance(), res);
  }
});
</script>
复制代码

向父级暴露组件的公共接口

默认情况下 <script setup> 将阻止向外暴露接口
简单说就是,Vue3 现在在父级不能够通过 ref 随意的访问子组件的变量和方法了,事实上,大多数时候我们都在公共接口方面过度暴露。

Foo.vue

<template>
  <div class="container"> Foo </div>
</template>

<script setup lang="ts">
  import { ref } from 'vue';

  const count = ref(0);
  const inc = () => {
    emit('inc')
  }
</script>
复制代码

App.vue

<template>
  <Foo ref="foo" />
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import Foo from './components/Foo.vue';

onMounted(() => {
  console.log(foo.value);
});
</script>

// Output:
//  Proxy {__v_skip: true}
//  [[Handler]]: Object
//  [[Target]]: Proxy
//  [[Handler]]: Object
//  [[Target]]: Object
//    __v_skip: true
//    __proto__: Object
//  [[IsRevoked]]: false
//  [[IsRevoked]]: false
复制代码

这样是拿不到 Foo 组件的内部变量和方法了,需要使用 defineExpose :

Foo.vue

<template>
  <div class="container"> Foo </div>
</template>

<script setup lang="ts">
  import { ref } from 'vue';

  const count = ref(0);
  const inc = () => {
    emit('inc')
  }
+ defineExpose({
+   count: props.count,
+   inc
+ })
</script>

// Output:
//  Proxy {__v_skip: true}
//  [[Handler]]: Object
//  [[Target]]: Proxy
//  [[Handler]]: Object
//  [[Target]]: Object
//    count: 0
//    inc: ƒ inc()
//    __v_skip: true
//    __proto__: Object
//  [[IsRevoked]]: false
//  [[IsRevoked]]: false
复制代码

这里有一个问题的是,上面的操作 defineExpose 操作如果加入了 await 会出现问题:
Foo.vue

<template>
  <div class="container"> Foo </div>
</template>

<script setup lang="ts">
  import { ref } from 'vue';

  const count = ref(0);
  const inc = () => {
    emit('inc')
  }
  
+ const res = await withAsyncContext(delay());

  defineExpose({
    count: props.count,
    inc
  })
</script>

// Output:
//  Proxy {__v_skip: true}
//  [[Handler]]: Object
//  [[Target]]: Proxy
//  [[Handler]]: Object
//  [[Target]]: Object
//    Vue 实例
//    __proto__: Object
//  [[IsRevoked]]: false
//  [[IsRevoked]]: false
复制代码

增加了 await 后拿到的 foo 结果不是 Proxy 而是一个 Vue 实例了,这应该是一个 Bug ~

火星人想法,有没有考虑过这样的操作:

import Foo, { getName } from './components/Foo.vue';
复制代码

Foo 中增加属性或者得到 getName,可以这样做:

Foo.vue

<template>
  <div class="container"> Foo </div>
</template>

+ <script lang="ts">
+   export default {
+     name: 'Benson',
+     age: 20
+   }
+   export const getName = (): void => console.log('Benson');
+ </script>


<script setup lang="ts">
  import { ref } from 'vue';

  const count = ref(0);
  const inc = () => {
    emit('inc')
  }
  
  defineExpose({
    count: props.count,
    inc
  })
</script>
复制代码

App.vue

import Foo, { getName } from './components/Foo.vue';

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import Foo, { getName } from './components/Foo.vue';

const foo = ref(null);

onMounted(() => {
  console.log(foo.value);
  console.log(Foo);
  getName();
});

// Output
// Foo 组件的 Proxy 包含内部变量和函数
// Foo 组件实例:{name: "Benson", age: 20, props: {…}, emits: Array(1), setup: ƒ, …}
// Benson
</script>
复制代码

这里要注意的是上面的 <script lang="ts"><script setup lang="ts"> 是有顺序依赖的,也就是 <script lang="ts"> 不能放到 <script setup lang="ts"> 的后面,否者会报错。

以上就是一些关于 setup 语法糖相关的使用和说明了。

参考文献

系列

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享