Vue3 setup语法糖常用语法 - 使用指南

1,538 阅读3分钟

前言

在2021年,vue3发布了正式版本,经过一年的维护已经越来越稳定,在对TS的支持、Composition API清晰的语法结构、利用Proxy API对数据代理实现等特点的加持下,vue3的语法也逐渐被越来越多的开发者所接受。本篇文章就是介绍vue3 setup语法糖在具体使用过程中所涉及的api语法。

一、Vue setup 定义变量的写法

1、使用ref定义变量

<script lang="ts" setup>
import { ref, Ref } from 'vue'
// 可以用来定义简单数据类型
const strValue = ref('')
// 赋值
strValue.value = '张三'

// 使用ref定义复杂数据类型
// 定义对象类型
let activeMenu: Ref<any> = ref({
  menuColor: '#ffcc80',
  icon: 'icon-home',
  path: '/index',
})
// 赋值
activeMenu.value = {
    menuColor: '#ce93d8',
    icon: 'icon-wode',
    path: '/verify',
 }
 
// 定义any[]类型
let menuList: Ref<any[]> = ref([
  {
    menuColor: '#ffcc80',
    icon: 'icon-home',
    path: '/index',
  },
  {
    menuColor: '#81d4fa',
    icon: 'icon-xiaoxi1',
    path: '/verify',
  },
])
// 赋值
menuList.value = [
    {
      menuColor: '#ffcc80',
      icon: 'icon-home',
      path: '/index',
    },
    {
      menuColor: '#81d4fa',
      icon: 'icon-xiaoxi1',
      path: '/verify',
    },
 ]
// ts 声明变量类型
interface routerItme {
  name: string // 路由名称
  router: string // 路由跳转路径
  icon: string // 导航栏图标
  menuStatus: boolean // 该路由是否在导航栏显示
  parentId: any // 该路由父级id
  sort: string // 该路由的排序
  sign: string // 路由标识
  componentPath: string // 组件在文件中的路径
  children: routerItme[] // 子路由列表
  id: string // 路由ID
}
// 使用ts变量创建相关类型
const routeList: Ref<routerItme[]> = ref([])
</script>

2、使用reactive 定义变量

<template>
  <div>{{ pageObj.pageNum }} {{ pageObj.pageStr }}</div>
</template>
<script setup lang="ts">
import { onMounted, reactive } from 'vue'

let pageObj = reactive({
  pageNum: 0,
  pageStr: '默认',
})

onMounted(() => {
  // (1)错误写法
  // pageObj = {
  //   pageNum: 1,
  //   pageStr: '修改',
  // }
  // 改变了pageObj属性结构使其失去响应式
  // console.log(pageObj) // {pageNum: 1, pageStr: '修改'} 
  
  // (2)错误写法
  // pageObj = reactive({
  //   pageNum: 1,
  //   pageStr: '修改',
  // })
  // pageObj重新赋值改变数据的dom的绑定关系,其失去响应式
  // console.log(pageObj)   // Proxy {pageNum: 1, pageStr: '修改'} 
  
  // (3)正确写法
  pageObj.pageNum = 1
  pageObj.pageStr = '修改'
  console.log(pageObj)
})
</script>

二、使用计算属性

<template>
     {{screenWidth}}
</template>
<script lang="ts" setup>
import { computed } from 'vue'
// 当页面宽度改变时触发
const screenWidth = computed(() => {
  return document.body.clientWidth
})
</script>

三、vue侦听器

watchEffectwatch 的区别

1、 watchEffect 侦听器

<template></template>
<script setup lang="ts">
import { onMounted, reactive, ref, watchEffect, watchPostEffect} from 'vue'
onMounted(() => {
  count.value = 1
  state.count = 3
  console.log(state.count)
})
const count = ref(0)
const state = reactive({ count: 2 })
// reactive 定义的变量,普通的watchEffect无法监听
watchEffect(() => {
  // 先打印出0 后打印出3
  // -> logs 0
  // -> logs 1
  console.log(count.value)
  // 报错 
  // console.log(state.count)
})

// ref 和 reactive 中定义的变量都可以监听
watchPostEffect(() => {
  // 先打印出0 后打印出3
  // -> logs 0
  // -> logs 1
  console.log(count.value, 'count.value')
  // 先打印出2 后打印出3
  // -> logs 2
  // -> logs 3
  console.log(state.count, 'state.count')
})

</script>

2、 watch 侦听器

(1) 侦听单一数据源

<template></template>
<script setup lang="ts">
import { onMounted, reactive, ref, watch } from 'vue'
onMounted(() => {
  state.count = 1
  count.value = 3
})
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /*
     * count 修改后的数据
     * prevCount 修改前的数据
     */
    console.log(count, prevCount) // -> 1 0
  }
)

// 直接侦听一个 ref
const count = ref(2)
watch(count, (count, prevCount) => {
  /*
   * count 修改后的数据
   * prevCount 修改前的数据
   */
  console.log(count, prevCount) // -> 3 2
})
</script>

(2)侦听多个数据源

<template></template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
const firstName = ref('')
const lastName = ref('')
const firstIndex = ref(0)
const lastIndex = ref(2)
onMounted(() => {
  firstName.value = 'John'
  lastName.value = 'Smith'
})
// 如果你在同一个函数里同时改变这些被侦听的来源,侦听器仍只会执行一次, 分别改变时,则会执行多次
watch([firstName, lastName], (newValues, prevValues) => {
  // -> ['John', 'Smith'] ['', '']
  console.log(newValues, prevValues)
})

watch([firstIndex, lastIndex], (newValues, prevValues) => {
  // -> [1, 2] [0, 2]
  // -> [1, 3] [1, 2]
  console.log(newValues, prevValues)
})
firstIndex.value = 1
lastIndex.value = 3
</script>

(3) 侦听响应式对象

<template></template>
<script setup lang="ts">
import { onMounted, reactive, ref, watch } from 'vue'
const numbers = reactive([1, 2, 3, 4])
watch(
  () => [...numbers],
  (numbers, prevNumbers) => {
    //  -> [1,2,3,4,5] [1,2,3,4]
    console.log(numbers, prevNumbers)
  }
)
numbers.push(5)
</script>

(4) deep 深度监听

<template></template>
<script setup lang="ts">
import { reactive, watch } from 'vue'
const state = reactive({
  attributes: {
    name: '',
  },
})
watch(
  () => state,
  (state, prevState) => {
  // 监听到了name的修改 ,却无法获取正确的prevState值
  // -> 'deep' 'Alex' 'Alex'
    console.log('deep', state.attributes.name, prevState.attributes.name)
  },
  { deep: true }
)

state.attributes.name = 'Alex' 
</script>

(5)使用lodash深拷贝监听

<template></template>
<script setup lang="ts">
import { reactive, watch } from 'vue'
import _ from 'lodash'

const state = reactive({
  attributes: {
    name: 'name',
  },
})
watch(
  () => _.cloneDeep(state),
  (state, prevState) => {
    // -> 'lodash' 'Alex' 'name'
    console.log('lodash', state.attributes.name, prevState.attributes.name)
  }
)
state.attributes.name = 'Alex'
</script>

为了完全侦听深度嵌套的对象和数组,可能需要对值进行深拷贝。这可以通过诸如 lodash.cloneDeep 这样的实用工具来实现。

四、生命周期

选项式 APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
选项式 APIHook inside setup
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated
----------------------------------------
<script lang="ts" setup>
import { onMounted } from 'vue'
// 使用生命周期
onMounted(() => {})
</script>

五、定义以及使用组件的方法

1、使用vue setup语法糖 -- 推荐使用

// setup 语法糖
<template>
    <ElCard>
      <ElButton>{{ buttonFont }}</ElButton>
    </ElCard>
</template>
<script setup lang="ts">
import { ElButton, ElCard } from 'element-plus'
import { ref } from 'vue'
// 控制当前的状态 
let buttonFont = ref('buttonFont')
</script>

2、使用jsx返回HTML模板

1、下载npm i @vitejs/plugin-vue-jsx插件

2、在vite.config.ts 中配置

import { defineConfig, UserConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ElementPlus from 'unplugin-element-plus/vite'
import vueJsx from '@vitejs/plugin-vue-jsx' // plugin-vue-jsx 的插件
export default defineConfig(({ mode }: UserConfig): UserConfig => {
  return {
    plugins: [
      vue(),
      ElementPlus(),
      vueJsx({
        // options are passed on to @vue/babel-plugin-jsx
      }), // 挂载插件
    ]
  }
})

3、创建一个demo.jsx文件

import { defineComponent } from 'vue'
import { ElButton, ElCard } from 'element-plus'
export default defineComponent({
  render() {
    return (
      <ElCard>
        <ElButton>demo</ElButton>
      </ElCard>
    )
  },
})

3、使用setup返回HTML模板--不推荐

<template></template>
<script lang="ts">
import { h, ref } from 'vue'
export default {
  setup() {
    return () => h('div', 'demo')  // <div>demo</div>
  },
}
</script>

六、组件之间的通信

1、父组件向子组件传值 - 通过 defineProps 传值

父组件

<template>
     <myheader :collapsed="collapsed"></myheader>
</template>

<script lang="ts" setup>
import { ref } from 'vue'
import myheader from './header.vue'

// 控制当前菜单的状态
const collapsed = ref(false)
</script>

子组件

<template>
<!-- 在HTML中获取父组件的传值 -->
    {{collapsed}}
</template>

<script lang="ts" setup>
import {  defineProps } from 'vue'
  
const props = defineProps({
  collapsed: {
    type: Boolean,
    default: false,
  },
})
// 在js中获取父组件的传值
console.log(props.collapsed) // false
</script>

2、子组件向父组件传值 - 通过 defineEmits 传值

子组件

<script lang="ts" setup>
import { defineEmits} from 'vue'
const emit = defineEmits(['changeCollapsed'])
const changeCollapsed = () => {
  emit('changeCollapsed', true)
}
</script>

父组件

<template>
     <myheader  @change-collapsed="changeCollapsed"></myheader>
</template>

<script lang="ts" setup>
import { ref } from 'vue'
import myheader from './header.vue'

// 控制当前的状态
let collapsed = ref(false)

const changeCollapsed = (status: boolean) => {
  // 获取子件的传值
  console.log(status); // true
  collapsed.value = status
}
</script>

3、dom查找:父组件获取子组件上的属性 - 通过 defineExpose 导出 ref 获取

子组件

<template></template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const childNum = ref(2)
/**
 * 不使用defineExpose 将无法通过模板ref或者$parent获取 childNum 的值
 */
defineExpose({
  childNum,
})
</script>

父组件

<template>
  <child ref="childRef" />
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import child from '@/components/child.vue'
const childRef = ref()
onMounted(() => {
  /**
   * 没有使用defineExpose 暴露时值为undefined
   * 使用之后获取子组件上的属性和方法正常 为 2
   */
  console.log(childRef.value.childNum) // 2
})
</script>

4、dom查找:子组件获取上父组件的属性 - 通过 defineExpose 导出 getCurrentInstance 获取

getCurrentInstance 支持访问内部组件实例, vue 官方强烈反对在应用的代码中使用 getCurrentInstance,本文仅是做演示使用。

子组件

<template></template>
<script setup lang="ts">
import { onMounted, getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
onMounted(() => {
  console.log(instance?.parent?.exposeProxy?.parentNum) // 1
})
</script>

父组件

<template>
  <child />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import child from './child.vue'
const parentNum = ref(1)
/**
 * 不使用defineExpose 将无法通过模板ref或者$parent获取 childNum 的值
 */
defineExpose({
  parentNum,
})
</script>

5、 通过provide和inject组合通信

祖父级组件

<template>
    <father />
</template>

<script setup lang="ts">
import father from './father.vue'
import { provide } from 'vue'
provide('name', 'grandfather')
</script>

父级组件

<template>
    <child />
</template>

<script setup lang="ts">
import child from './child.vue'
</script>

子级组件

<template>
  <div>我是子组件</div>
</template>

<script setup lang="ts">
import { inject } from 'vue'
const name = inject('name')
console.log(name) // -> grandfather
</script>

6、使用VueX进行状态管理

展示效果的页面

<template>
  <div class="text_box">
    <!-- // 从state 中 读取属性 -->
    <div>{{ store.state.stateText }}</div>

    <!-- // 从getters 中 读取属性 -->
    <div>{{ store.getters.doneTodos }}</div>

    <!-- // 同步修改 store 中的属性 -->
    <input @input="inputMutationsText" v-model="mutationsText" />
    <div>{{ store.state.mutationsText }}</div>

    <!-- // 异步修改 store 中的属性 -->
    <input @input="inputActionsText" v-model="actionsText" />
    <div>{{ store.state.actionsText }}</div>
  </div>
</template>

<script setup lang="ts">
import { onMounted, provide, ref, reactive } from 'vue'
import { useStore } from 'vuex'
const store = useStore()

//1、 从state 中 读取属性
console.log(store.state.stateText) // -> '001'

//2、 Getter 相当于 store 的计算属性
console.log(JSON.stringify(store.getters.doneTodos)) 
// -> [{"id":1,"text":"...","done":true}]

/**
 * mutations与actions区别
 * mutations与actions其本质是没有任何区别的,
 * mutations中的函数可以写成异步,
 * actions中的函数也可以直接去改变state中的值 -- 不建议这么做
 * 在mutations 中写同步函数 在actions 中写异步函数,是VueX在写法上的规定
 */
//3、 同步修改 store 中的属性 -- mutations
let mutationsText = ''
const inputMutationsText = (event: any) => {
  // console.log(event.target.value)
  store.commit('setMutationsText', event.target.value)
  console.log(store.state.mutationsText) // -> 你输入的值
}

//4、 异步修改 store 中的属性 -- actions
let actionsText = ''
const inputActionsText = (event: any) => {
  store.dispatch('getActionsText', event.target.value) // -> 你输入的值
}

// 5、 Vuex 允许我们将 store 分割成模块(module) -- Module
console.log(store.state.module.stateText) // -> modulesText

store.commit('module/setMutationsText', 'moduleMutations')
console.log(store.state.module.mutationsText) // -> moduleMutations

store.dispatch('module/getActionsText', 'moduleActions')
console.log(store.state.module.actionsText) // -> moduleActions
</script>
<style lang="scss" scoped>
.text_box {
  margin: 30px;
}
</style>

store主文件

import { createStore } from 'vuex'
import module from './modules/module'
export default createStore({
  modules: {
    module,
  },
  state: {
    stateText: '001',
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false },
    ],
    mutationsText: 'mutationsText',
    actionsText: 'actionsText',
  },
  getters: {
    doneTodos: (state) => {
      return state.todos.filter((todo) => todo.done)
    },
  },
  mutations: {
    setMutationsText(state: any, text) {
      state.mutationsText = text
    },
    setActionsText(state: any, text) {
      state.actionsText = text
    },
  },
  actions: {
    getActionsText({ commit, state }: any, actionsText) {
      commit('setActionsText', actionsText)
    },
  },
})

module模块

const module = {
  namespaced: true,
  state: {
    stateText: 'modulesText',
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false },
    ],
    mutationsText: 'mutationsText',
    actionsText: 'actionsText',
  },
  getters: {
    doneTodos: (state: any) => {
      return state.todos.filter((todo: any) => todo.done)
    },
  },
  mutations: {
    setMutationsText(state: any, text: string) {
      state.mutationsText = text
    },
    setActionsText(state: any, text: string) {
      state.actionsText = text
    },
  },
  actions: {
    getActionsText({ commit, state }: any, actionsText: string) {
      commit('setActionsText', actionsText)
    },
  },
}

export default module

7、通过pinia进行状态管理

(1)、下载pinia yarn add pinia or npm install pinia (2)在main文件中挂载pinia

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')

具体使用方法

七、在vue3 setup中使用ref的方法

1、基础的ref 获取dom实例方法

<template>
  <div ref="page"></div>
</template>
<style lang="scss" scoped></style>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
// 定义一个ref声明名称相同和变量名
const page = ref()
onMounted(() => {
  // 获取到ref为page的实例
  console.log(page.value, 'page.value')
})
</script>

2、在v-for循环中获取ref dom实例的方法

<template>
  <div>
    <div
      :ref="
        (el) => {
          if (el) itemArray[i] = el
        }
      "
      v-for="(item, i) in 6"
      :key="i"
    ></div>
  </div>
</template>
<style lang="scss" scoped></style>
<script setup lang="ts">
import { onMounted, Ref, ref } from 'vue'
// 申明存储ref实例的数组
let itemArray: Ref<any[]> = ref([])
onMounted(() => {
  console.log(itemArray.value, 'itemArray.value')
})
</script>

1641195033(1).png

3、通过ref 获取子组件上的属性或方法 defineExpose -- setup语法糖

使用 <script setup> 的组件是默认关闭的,也即通过模板 ref 或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定。

为了在 <script setup> 组件中明确要暴露出去的属性,使用 defineExpose 编译器宏:

子组件

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

const childNum = ref(2)
/**
 * 不使用defineExpose 将无法通过模板ref或者$parent获取 childNum 的值
*/
defineExpose({
  childNum,
})
</script>

父组件

<template>
  <child ref="childRef" />
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import child from '@/components/child.vue'

const childRef = ref()
onMounted(() => {
  /**
   * 没有使用defineExpose 暴露时值为undefined
   * 使用之后获取子组件上的属性和方法正常 为 2
   */
  console.log(childRef.value.childNum)
})
</script>

八、单文件组件样式特性

1、深度选择器

<style scoped>中父组件可以通过使用 :deep()改变子组件中的样式

<style scoped>
:deep(.red_box) {
    color: red;
}
</style>

2、<style module>

<style module> 标签会被编译为 CSS Modules 并且将生成的 CSS 类作为 $style 对象的键暴露给组件:

(1) 未命名样式注入

<template>
  <p :class="$style.red">
    This should be red
  </p>
</template>

<style module>
.red {
  color: red;
}
</style>

(2) 自定义注入名称

<template>
  <p :class="classes.red">red</p>
</template>

<style module="classes">
.red {
  color: red;
}
</style>

(3) 与组合式 API 一同使用

注入的类可以通过 useCssModule API 在 setup() 和 <script setup> 中使用。对于使用了自定义注入名称的 <style module> 模块,useCssModule 接收一个对应的 module attribute 值作为第一个参数。

// 默认, 返回 <style module> 中的类
useCssModule()

// 命名, 返回 <style module="classes"> 中的类
useCssModule('classes')

3、状态驱动的动态 CSS

<template>
  <p>hello</p>
</template>
<script setup>
const theme = {
  color: 'red'
}
</script>
<style scoped>
p {
  color: v-bind('theme.color');
}
</style>

九、Vue Router

(1)、下载Vue Router npm install vue-router@4

(2)、创建一个可以被 Vue 应用程序使用的路由实例

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'

let routes: RouteRecordRaw[] = [
  {
    path: '/login',
    component: () => import('@/view/login/login.vue'),
  },
]

const router = createRouter({
  history: createWebHistory(),
  routes: routes,
})


export default router

(3)、在main文件中挂载VueRouter

import { createApp } from 'vue'
import App from './App.vue'
import router from '@/router/index'
const app = createApp(App)
app.use(router)
app.mount('#app')