草庐IT

Vue3 script setup 语法糖详解

暴富979 2023-04-10 原文

前言:

目前setup sugar已经进行了定稿,vue3 + setup sugar + TS的写法看起来很香,写本文时 Vue 版本是 "^3.2.6"

1.script setup 语法糖

新的 setup 选项是在组件创建之前, props 被解析之后执行,是组合式 API 的入口。

在 setup 中你应该避免使用 this,因为它不会找到组件实例。setup 的调用发生在 data property、computed property 或 methods 被解析之前,所以它们无法>在 setup 中被获取。

setup 选项是一个接收 props 和 context 的函数,我们将在之后进行讨论。此外,我们将 setup 返回的所有内容都暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。

它是 Vue3 的一个新语法糖,在 setup 函数中。所有 ES 模块导出都被认为是暴露给上下文的值,并包含在 setup() 返回对象中。相对于之前的写法,使用后,语法也变得更简单。

在添加了setup的script标签中,我们不必声明和方法,这种写法会自动将所有顶级变量、函数,均会自动暴露给模板(template)使用\
这里强调一句 “暴露给模板,跟暴露给外部不是一回事”

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

<script setup></script>

该setup功能是新的组件选项。它是组件内部暴露出所有的属性和方法的统一API。

使用后意味着,script标签内的内容相当于原本组件声明中setup()的函数体,不过也有一定的区别。

使用 script setup 语法糖,组件只需引入不用注册,属性和方法也不用返回,也不用写setup函数,也不用写export default ,甚至是自定义指令也可以在我们的template中自动获得。基本语法

2.调用时机 

创建组件实例,然后初始化 props ,紧接着就调用setup 函数。从生命周期钩子的视角来看,它会在 beforeCreate 钩子之前被调用.

3.模板中使用

如果 setup 返回一个对象,则对象的属性将会被合并到组件模板的渲染上下文

<template>
  <div>
 	{{ count }} {{ object.foo }}
  </div>
</template>

4.setup 参数

1.「props」 第一个参数接受一个响应式的props,这个props指向的是外部的props。如果你没有定义props选项,setup中的第一个参数将为undifined。props和vue2.x并无什么不同,仍然遵循以前的原则;

  • 不要在子组件中修改props;如果你尝试修改,将会给你警告甚至报错。
  • 不要结构props。结构的props会失去响应性。

2.「context」 第二个参数提供了一个上下文对象,从原来 2.x 中 this 选择性地暴露了一些 property。

    <script setup="props,context" lang="ts">
        context.attrs
        context.slots
        context.emit
    <script>

像这样,只要在setup处声明即可自动导入,同时也支持解构语法:

    <script setup="props, { emit }" lang="ts">

    <script>

 5.组件自动注册

  导入 component 或 directive 直接import即可,无需额外声明

import { MyButton } from "@/components"

import { directive as clickOutside } from 'v-click-outside'

与原先一样,模板中也支持使用kabab-case来创建组件,如<my-button />

在 script setup 中,引入的组件可以直接使用,无需再通过components进行注册,并且无法指定当前组件的名字,它会自动以文件名为主,也就是不用再写name属性了。示例:

<template>
 	<Firstchild />
</template>
 	 
<script setup>
 	import Firstchild from "./components/Firstchild.vue";  //此处使用 Vetur 插件会报红
</script>

如果需要定义类似 name 的属性,可以再加个平级的 script 标签,在里面实现即可。

 

组件核心 API 的使用

6.定义组件的 props

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

<script setup>
  import { defineProps } from 'vue'
   
  const props = defineProps({
  msg:{
     type:String,
     default:'',
   },
   age:{
     type:String,
     default:'',
   },
  })
</script>

  <!-- 或者 -->


<script setup lang="ts">
 	import { ref,defineProps } from 'vue';
 	 
 	type Props={
 	msg:string
 	}
 	defineProps<Props>();
</script>

7.定义 emit

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

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

  const emit = defineEmits(['change', 'delete'])
</script>

8.父子组件通信

defineProps 用来接收父组件传来的 props ; defineEmits 用来声明触发的事件。

//父组件
<template>
  <my-son foo="??????" @childClick="childClick" />
</template>

<script lang="ts" setup>
  import MySon from "./MySon.vue";

  let childClick = (e: any): void => {
    console.log('from son:', e); //??????
  };
</script>


//子组件
<template>
  <span @click="sonToFather">信息:{{ props.foo }}</span>
</template>

<script lang="ts" setup>
  import {defineEmits,defineProps} from "vue";

  const emit = defineEmits(["childClick"]); // 声明触发事件 childClick
  const props = defineProps({
    foo: String
  }); // 获取props

  const sonToFather = () => {
    emit('childClick', props.foo)
  }
</script>

子组件通过 defineProps 接收父组件传过来的数据,子组件通过 defineEmits 定义事件发送信息给父组件

增强的props类型定义:

const props = defineProps<{ foo: string bar?: number }>()

  const emit = defineEmit<(e: 'update' | 'delete' , id: number)=> void>()

不过注意,采用这种方法将无法使用props默认值。

9.定义响应变量、函数、监听、计算属性computed

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

  const count = ref(0); //不用 return ,直接在 templete 中使用

  const addCount = () => { //定义函数,使用同上
    count.value++;
  }

  //定义计算属性,使用同上
  const howCount = computed(() => "现在count值为:" + count.value);

  //定义监听,使用同上 //...some code else
  watchEffect(() => console.log(count.value));
</script>

10.watchEffect和watch区别

watchEffect用于有副作用的操作,会自动收集依赖。

watch无需区分deep,immediate,只要依赖的数据发生变化,就会调用

11.reactive

此时name只会在初次创建的时候进行赋值,如果中间想要改变name的值,那么需要借助composition api 中的reactive。

<script setup lang="ts">
  import { reactive , onUnmounted } from 'vue'

  const state = reactive({
    counter: 0
  })
  // 定时器 每秒都会更新数据
  const timer = setInterval(() => {
    state.counter++
  }, 1000);

  onUnmounted(() => {
    clearInterval(timer);
  })
</script>
<template>
  <div>{{state.counter}}</div>
</template>

使用ref也能达到我们预期的'counter',并且在模板中,vue进行了处理,我们可以直接使用counter而不用写counter.value.

ref和reactive的关系:

ref是一个{value:'xxxx'}的结构,value是一个reactive对象

12.ref 暴露变量到模板

曾经的提案中,如果需要暴露变量到模板,需要在变量前加入export声明:

export const count = ref(0)

不过在新版的提案中,无需export声明,编译器会自动寻找模板中使用的变量,只需像下面这样简单的声明,即可在模板中使用该变量

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

  const counter = ref(0); //不用 return ,直接在 templete 中使用

  const timer = setInterval(() => {
    counter.value++
  }, 1000)

  onUnmounted(() => {
    clearInterval(timer);
  })
</script>
<template>
  <div>{{counter}}</div>
</template>

13.生命周期方法

因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。

可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。官网:生命周期钩子

下表包含如何在 setup () 内部调用生命周期钩子:

选项式 APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated
<script setup lang="ts">
  import {
    onMounted
  } from 'vue';

  onMounted(() => {
    console.log('mounted!');
  });
</script>

 14.获取 slots 和 attrs

注:useContext API 被弃用,取而代之的是更加细分的 api。

可以通过useContext从上下文中获取 slots 和 attrs。不过提案在正式通过后,废除了这个语法,被拆分成了useAttrsuseSlots

  1. useAttrs:见名知意,这是用来获取 attrs 数据,但是这和 vue2 不同,里面包含了 class属性方法
    <template>
      <component v-bind='attrs'></component>
    </template>
    <srcipt setup lang='ts'>
      const attrs = useAttrs();
    <script>
     
  2. useSlots: 顾名思义,获取插槽数据。

 使用示例:

// 旧
<script setup>
  import {
    useContext
  } from 'vue'

  const {
    slots,
    attrs
  } = useContext()
</script>

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

  const attrs = useAttrs()
  const slots = useSlots()
</script>

15.其他 Hook Api

  1. useCSSModule:CSS Modules 是一种 CSS 的模块化和组合系统。vue-loader 集成 CSS Modules,可以作为模拟 scoped CSS。允许在单个文件组件的setup中访问CSS模块。此 api 本人用的比较少,不过多做介绍。
  2. useCssVars: 此 api 暂时资料比较少。介绍v-bind in styles时提到过。
  3. useTransitionState: 此 api 暂时资料比较少。
  4. useSSRContext: 此 api 暂时资料比较少。

16. defineExpose API

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

如果需要对外暴露 setup 中的数据和方法,需要使用 defineExpose API。示例:

const a = 1
const b = ref(2)
defineExpose({ a, b, })

注意:目前发现defineExpose暴露出去的属性以及方法都是 unknown 类型,如果有修正类型的方法,欢迎评论区补充。

//父组件
<template>
    <Daughter ref="daughter" />
</template>

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

    const daughter = ref(null)
    console.log('????~daughter',daughter)
</script>

//子组件

<template>
    <div>Hellow{{ msg }}</div>
</template>

<script lang="ts" setup>
    import { ref ,defineExpose} from "vue";
    const msg = ref('吕布')
        defineExpose({
    msg
    })
</script>

//属性和方法无需返回,直接使用!	 
//这可能是带来的较大便利之一,在以往的写法中,定义数据和方法,都需要在结尾 return 出去,
//才能在模板中使用。在 script setup 中,定义的属性和方法无需返回,可以直接使用!示例:
 	 
 <template>
   <div>
 	<p>My name is {{name}}</p>
   </div>
 </template>
 	 
 <script setup>
 	import { ref } from 'vue';
 	 
 	const name = ref('Sam')
 </script>

17.支持 async await 异步

注意在vue3的源代码中,setup执行完毕,函数 getCurrentInstance 内部的有个值会释放对 currentInstance 的引用,await 语句会导致后续代码进入异步执行的情况。所以上述例子中最后一个 getCurrentInstance() 会返回 null,建议使用变量保存第一个 getCurrentInstance() 返回的引用.

<script setup>
 	const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>

<script setup> 中可以使用顶层 await。结果代码会被编译成 async setup()

<script setup>
 	const post = await fetch(`/api/post/1`).then(r => r.json())
</script>

另外,await 的表达式会自动编译成在 await 之后保留当前组件实例上下文的格式。

注意
async setup() 必须与 Suspense 组合使用,Suspense 目前还是处于实验阶段的特性。我们打算在将来的某个发布版本中开发完成并提供文档 - 如果你现在感兴趣,可以参照 tests 看它是如何工作的。

18.定义组件其他配置

配置项的缺失,有时候我们需要更改组件选项,在setup中我们目前是无法做到的。我们需要在上方再引入一个 script,在上方写入对应的 export即可,需要单开一个 script。

<script setup> 可以和普通的 <script> 一起使用。普通的 <script> 在有这些需要的情况下或许会被使用到:

  • 无法在 <script setup> 声明的选项,例如 inheritAttrs 或通过插件启用的自定义的选项。
  • 声明命名导出。
  • 运行副作用或者创建只需要执行一次的对象。

在script setup 外使用export default,其内容会被处理后放入原组件声明字段。

<script>
  // 普通 `<script>`, 在模块范围下执行(只执行一次)
  runSideEffectOnce()

  // 声明额外的选项
  export default {
    name: "MyComponent",
    inheritAttrs: false,
    customOptions: {}
  }
</script>
<script setup>
  import HelloWorld from '../components/HelloWorld.vue'
  // 在 setup() 作用域中执行 (对每个实例皆如此)
  // your code
</script>
<template>
  <div>
    <HelloWorld msg="Vue3 + TypeScript + Vite" />
  </div>
</template>

注意:Vue 3 SFC 一般会自动从组件的文件名推断出组件的 name。在大多数情况下,不需要明确的 name 声明。唯一需要的情况是当你需要 <keep-alive> 包含或排除或直接检查组件的选项时,你需要这个名字。

关于 TS 与 ESLint 的不完美

  1. @typescript-eslint/no-unused-vars规则不兼容,此规则含义为定义了,未进行使用。该规则其实影响不大,关闭即可。

  2. 与导入的类型声明不兼容,当你通过解构的方式去导入类型,setup sugar 会进行自动导出。这时候,你就会收到 TS 的一条报错:此为类型,但被当作值使用。解决办法:类型导出使用export default导出或者引入时使用import * as xx来进行引入,也可以使用 import type { test } from "./test";解决。

 语法糖实现

//vue文件代码

<template>
  <div>{{ msg }}</div>
</template>
<script setup>
  const msg = 'Hello!'
</script>

//编译后的js代码:

export default {
    setup() {
    const msg = 'Hello!'

    return function render() {
// has access to everything inside setup() scope
// 在函数 setup 作用域,函数 render 能访问 setup 的一切,
    return h('div', msg)
    }
  }
}

注意到,即使普通变量也能作为模版被置入 template 中被编译,某些人认为这不合适,不够分离。

vscode配套插件

volar 是一个vscode插件,用来增强vue编写体验,使用volar插件可以获得script setup语法的最佳支持。

vetur相同,volar是一个针对vuevscode插件,不过与vetur不同的是,volar提供了更为强大的功能,让人直呼卧槽

安装的方式很简单,直接在vscode的插件市场搜索volar,然后点击安装就可以了。

vscode 中使用的时候,先禁用 Vetur,再下载使用Volar

使用习惯

options api切换到composition api最大的问题无异于最大的问题就是没有强制的代码分区,如果书写的人没有很好的代码习惯,那么后续的人将会看的十分难受。目前我是这么解决的:

  • 自我代码分区并且尽量抽离方法(写好注释),分区如下:

    1. 相关引入
    2. 响应式数据、props、emit 定义
    3. 生命周期以及 watch 书写
    4. 方法定义
    5. 方法、属性暴露
  • 组件抽离:将页面拆成两个文件夹,一个为 views,一个为 components。views 和 components 文件夹下有各自的文件。views 文件夹中为页面入口,掌管数据,而 components 则为页面中一些组件抽离。如果是公共组件,再抽离到 components 文件夹下其他位置。

  • hook 抽离:尽可能将逻辑抽离,并不一定要进行复用。

写作不易,希望可以获得你的一个「赞」。如果文章对你有用,可以选择「关注 + 收藏」。 如有文章有错误或建议,欢迎评论指正,谢谢你。❤️ 

有关Vue3 script setup 语法糖详解的更多相关文章

  1. ruby - 树顶语法无限循环 - 2

    我脑子里浮现出一些关于一种新编程语言的想法,所以我想我会尝试实现它。一位friend建议我尝试使用Treetop(Rubygem)来创建一个解析器。Treetop的文档很少,我以前从未做过这种事情。我的解析器表现得好像有一个无限循环,但没有堆栈跟踪;事实证明很难追踪到。有人可以指出入门级解析/AST指南的方向吗?我真的需要一些列出规则、常见用法等的东西来使用像Treetop这样的工具。我的语法分析器在GitHub上,以防有人希望帮助我改进它。class{initialize=lambda(name){receiver.name=name}greet=lambda{IO.puts("He

  2. ruby-on-rails - 使用 Sublime Text 3 突出显示 HTML 背景语法中的 ERB? - 2

    所以我在关注Railscast,我注意到在html.erb文件中,ruby代码有一个微弱的背景高亮效果,以区别于其他代码HTML文档。我知道Ryan使用TextMate。我正在使用SublimeText3。我怎样才能达到同样的效果?谢谢! 最佳答案 为SublimeText安装ERB包。假设您安装了SublimeText包管理器*,只需点击cmd+shift+P即可获得命令菜单,然后键入installpackage并选择PackageControl:InstallPackage获取包管理器菜单。在该菜单中,键入ERB并在看到包时选择

  3. ruby - 覆盖相似的方法,更短的语法 - 2

    在Ruby类中,我重写了三个方法,并且在每个方法中,我基本上做同样的事情:classExampleClassdefconfirmation_required?is_allowed&&superenddefpostpone_email_change?is_allowed&&superenddefreconfirmation_required?is_allowed&&superendend有更简洁的语法吗?如何缩短代码? 最佳答案 如何使用别名?classExampleClassdefconfirmation_required?is_a

  4. ruby 语法糖 : dealing with nils - 2

    可能已经问过了,但我找不到它。这里有2个常见的情况(对我来说,在编程Rails时......)用ruby​​编写是令人沮丧的:"astring".match(/abc(.+)abc/)[1]在这种情况下,我得到一个错误,因为字符串不匹配,因此在nil上调用[]运算符。我想找到的是比以下内容更好的替代方法:temp="astring".match(/abc(.+)abc/);temp.nil??nil:temp[1]简而言之,如果不匹配,则简单地返回nil而不会出错第二种情况是这样的:var=something.very.long.and.tedious.to.writevar=some

  5. ruby - Ruby 语法糖有 "rules"吗? - 2

    我正在学习Ruby的基础知识(刚刚开始),我遇到了Hash.[]method.它被引入a=["foo",1,"bar",2]=>["foo",1,"bar",2]Hash[*a]=>{"foo"=>1,"bar"=>2}稍加思索,我发现Hash[*a]等同于Hash.[](*a)或Hash.[]*一个。我的问题是为什么会这样。是什么让您将*a放在方括号内,是否有某种规则可以在何时何地使用“it”?编辑:我的措辞似乎造成了一些困惑。我不是在问数组扩展。我明白了。我的问题基本上是:如果[]是方法名称,为什么可以将参数放在括号内?这看起来几乎——但不完全是——就像说如果你有一个方法Foo.d

  6. 计算机毕业设计ssm+vue基本微信小程序的小学生兴趣延时班预约小程序 - 2

    项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU

  7. ruby - 如何让Ruby捕获线程中的语法错误 - 2

    我正在尝试使用ruby​​编写一个双线程客户端,一个线程从套接字读取数据并将其打印出来,另一个线程读取本地数据并将其发送到远程服务器。我发现的问题是Ruby似乎无法捕获线程内的错误,这是一个示例:#!/usr/bin/rubyThread.new{loop{$stdout.puts"hi"abc.putsefsleep1}}loop{sleep1}显然,如果我在线程外键入abc.putsef,代码将永远不会运行,因为Ruby将报告“undefinedvariableabc”。但是,如果它在一个线程内,则没有错误报告。我的问题是,如何让Ruby捕获这样的错误?或者至少,报告线程中的错误?

  8. ruby -::在 Ruby 语法中是什么意思? - 2

    这个问题在这里已经有了答案:WhatisRuby'sdouble-colon`::`?(12个答案)关闭8年前。什么是::?@song||=::TwelveDaysSong.new

  9. ruby - ruby 乘法语句中星号中断语法前的空格 - 2

    在添加一些空格以使代码更具可读性时(与上面的代码对齐),我遇到了这个:classCdefx42endendm=C.new现在这将给出“错误数量的参数”:m.x*m.x这将给出“语法错误,意外的tSTAR,期待$end”:2/m.x*m.x这里的解析器到底发生了什么?我使用Ruby1.9.2和2.1.5进行了测试。 最佳答案 *用于运算符(42*42)和参数解包(myfun*[42,42])。当你这样做时:m.x*m.x2/m.x*m.xRuby将此解释为参数解包,而不是*运算符(即乘法)。如果您不熟悉它,参数解包(有时也称为“spl

  10. 语法类似于 GitHub Flavored Markdown 的 Ruby markdown 解释器? - 2

    我使用Jekyll运行博客,并认为我会解决RedcarpetMarkdown解释器,因为它是developedandusedbyGitHub.好吧,我只是碰巧遇到了一个错误,去检查问题,然后foundthis.Maintainersays,"Asyouprobablyhavenoticed(harharharhar)Idon'thavetimetomaintainRedcarpetanymore.It'snotapriorityforme(IfindMarkdownthoroughlyboring)andit'snotapriorityforGitHub,becausewenolong

随机推荐