script setup 语法糖

使用方式极其简单,仅需要在 script 标签加上 setup 关键字即可

<script setup></script>
1

组件

组件导入即可,无需在注册

<template>
  <Child />
</template>

<script setup>
import Child from './Child.vue'
</script>
1
2
3
4
5
6
7

props

通过defineProps指定当前 props 类型,获得上下文的props对象

<script setup>
import { defineProps } from 'vue'

const props = defineProps({
  title: String,
})
</script>
1
2
3
4
5
6
7

emits

使用defineEmit定义当前组件含有的事件,并通过返回的上下文去执行 emit

<script setup>
import { defineEmits } from 'vue'

const emit = defineEmits(['change', 'delete'])

const handleChange = () => {
  emits('change', { id: id })
}

const handleDel = () => {
  emits('delete', { id: id })
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

获取 slots 和 attrs

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

const attrs = useAttrs()
const slots = useSlots()
</script>
1
2
3
4
5
6

defineExpose API

传统的写法,我们可以在父组件中,通过 ref 实例的方式去访问子组件的内容,但在 script setup 中,该方法就不能用了,setup 相当于是一个闭包,除了内部的 template模板,谁都不能访问内部的数据和方法。 如果需要对外暴露 setup 中的数据和方法,需要使用 defineExpose API

<script setup>
import { defineExpose } from 'vue'
const a = 1
const b = 2
defineExpose({
  a,
})
</script>
1
2
3
4
5
6
7
8

属性和方法的使用

这可能是带来的较大便利之一,在vue3.0正常的写法中,定义数据和方法,都需要在结尾 return 出去,才能在模板中使用。在 script setup语法糖中,定义的属性和方法无需返回,可以直接使用即可。

<template>
  <div>
    <p>My name is {{ name }},I am {{ state.year }} old</p>
  </div>
  <button @click="change"></button>
</template>

<script setup>
import { reactive, ref } from 'vue'

const name = ref('Sam')
const state = reactive({ year: 26 })

const change = () => {
  name.value = 'Li'
  state.year = 18
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

自定义 hook

自定义hook(钩子函数)钩子函数的作用类似于vue2中的mixin,是一个可复用的功能函数,相对于mixin自定义hook的优势可以很清楚的知道复用功能代码的来源,简洁易懂。

例子:自定义一个获取鼠标点击的x,y轴的方法的hook

新建一个useMouseClick.ts文件

import { onBeforeUnmount, onMounted, ref } from 'vue'

export function useMouseClick() {
  // 初始化x,y轴坐标
  let x = ref(0)
  let y = ref(0)

  // 鼠标点击后的方法
  let mouseClick = (event: MouseEvent) => {
    x.value = event.pageX
    y.value = event.pageY
  }

  // 页面初始化的时候监听点击事件
  onMounted(() => {
    window.addEventListener('click', mouseClick)
  })

  // 页面销毁的时候移除监听点击事件
  onBeforeUnmount(() => {
    window.removeEventListener('click', mouseClick)
  })

  // 导出坐标
  return {
    x,
    y,
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

最后在需要的页面引入这个公共方法

<template>
  <div>
    <p>x: {{ x }}</p>
    <p>y: {{ y }}</p>
  </div>
</template>

<script lang="ts" setup>
import { useMouseClick } from '@/hooks/useMouseClick'

const { x, y } = useMouseClick()
</script>
1
2
3
4
5
6
7
8
9
10
11
12

ref 语法糖

由于 ref 语法糖目前还处于实验性的(Experimental)阶段,所以在 Vue3 中不会默认支持 ref 语法糖。

在使用 Vite + Vue3 项目开发时,是由 @vitejs/plugin-vue 插件来实现对 .vue 文件的代码转换(Transform)、热更新(HMR)等。所以,我们需要在 vite.config.js 中给@vitejs/plugin-vue 插件的选项(Options)传入 refTransform: true

这样一来 @vitejs/plugin-vue 插件内部会根据传入的选项中 refTransform 的值判断是否需要对 ref 语法糖进行特定的代码转换。由于,这里我们设置的是 true,显然它是会对 ref 语法糖执行特定的代码转换。

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    vue({
      refTransform: true,
    }),
  ],
})
1
2
3
4
5
6
7
8
9
10
11

接着,我们就可以在 .vue 文件中使用 ref 语法糖

<template>
  <div>{{ count }}</div>
  <button @click="add">click me</button>
</template>

<script setup>
let count = $ref(1)

function add() {
  count++
  //使用的时候无需加 .value
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

获取子组件 ref 变量和 defineExpose 暴露

子组件代码如下(示例):

<template>
  <p>{{ data }}</p>
</template>

<script setup>
import { reactive, toRefs } from 'vue'

/**
 * 数据部分
 * */
const data = reactive({
  modelVisible: false,
  historyVisible: false,
  reportVisible: false,
})
defineExpose({
  ...toRefs(data),
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

父组件代码如下(示例):

<template>
  <button @click="onClickSetUp">点击</button>
  <Content ref="content" />
</template>

<script setup>
import { ref } from 'vue'

// content组件ref
const content = ref('content')
// 点击设置
const onClickSetUp = ({ key }) => {
  content.value.modelVisible = true
}
</script>
<style scoped lang="less"></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Teleport

Teleport 形象的比喻是哆啦 A 梦中的「任意门」,任意门的作用就是可以将人瞬间传送到另一个地方。 例子: 在子组件中使用到Dialog组件,我们实际开发中经常会在类似的情形下使用到 Dialog ,此时Dialog就被渲染到一层层子组件内部,处理嵌套组件的定位、z-index和样式都变得困难。 Dialog从用户感知的层面,应该是一个独立的组件,从 dom 结构应该完全剥离 Vue 顶层组件挂载的 DOM;同时还可以使用到 Vue 组件内的状态(data或者props)的值。简单来说就是,即希望继续在组件内部使用Dialog, 又希望渲染的 DOM 结构不嵌套在组件的 DOM 中。 此时就需要 Teleport 上场,我们可以用<Teleport>包裹Dialog, 此时就建立了一个传送门,可以将Dialog渲染的内容传送到任何指定的地方。

Teleport 的使用

我们希望 Dialog 渲染的 dom 和顶层组件是兄弟节点关系, 在index.html文件中定义一个供挂载的元素:

<body>
  <div id="app"></div>
  <div id="dialog"></div>
</body>
1
2
3
4

定义一个Dialog组件Dialog.vue, 留意 to 属性, 与上面的id选择器一致:

<template>
  <teleport to="#dialog">
    <div class="dialog">
      <div class="dialog_wrapper">
        <div class="dialog_header" v-if="title">
          <slot name="header">
            <span>{{ title }}</span>
          </slot>
        </div>
      </div>
      <div class="dialog_content">
        <slot></slot>
      </div>
      <div class="dialog_footer">
        <slot name="footer"></slot>
      </div>
    </div>
  </teleport>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

最后在一个子组件Child.vue中使用Dialog组件。

<div class="child">
    <Dialog v-if="dialogVisible"></Dialog>
</div>
1
2
3

使用 teleport 组件,通过 to 属性,指定该组件渲染的位置与 <div id="app"></div> 同级,也就是在 body 下,但是 Dialog 的状态 dialogVisible 又是完全由内部 Vue 组件控制的。