简述
最近了解到 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 语法糖的模式下,获取 attr
、slots
、emit
可以通过 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 语法糖相关的使用和说明了。