【前端面试专题】【5】Vue3
Vue3 比 Vue2 有什么优势?
- 性能更好(后面详细讲)
- 体积更小
- 更好的 ts 支持
- 更好的代码组织
- 更好的逻辑抽离
- 更多新功能
Vue3 生命周期
Option API
- beforeDestroy 改为 beforeUnmount
- destroyed 改为 unmounted
- 其他沿用 Vue2 的生命周期
<!-- App.vue -->
<template>
<life-cycles v-if="flag" :msg="msg" />
<button @click="msg = Date.now()">change msg</button>
<button @click="flag = !flag">hide</button>
</template>
<script>
import LifeCycles from './components/LifeCycles.vue'
export default {
components: {
LifeCycles
},
data() {
return {
msg: 'vue3',
flag: true
}
}
}
</script>
<!-- LifeCycles.vue -->
<template>
<div>生命周期{{ msg }}</div>
</template>
<script>
export default {
name: 'LifeCycles',
props: ['msg'],
beforeCreate() {
console.log('beforeCreate')
},
created() {
console.log('created')
},
beforeMount() {
console.log('beforeMount')
},
mounted() {
console.log('mounted')
},
beforeUpdate() {
console.log('beforeUpdate')
},
updated() {
console.log('updated')
},
// beforeDestroy 改名
beforeUnmount() {
console.log('beforeUnmount')
},
// destroyed 改名
unmounted() {
console.log('unmounted')
}
}
</script>
Composition API
<!-- LifeCycles.vue -->
<template>
<div>生命周期{{ msg }}</div>
</template>
<script>
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'
export default {
name: 'LifeCycles',
props: ['msg'],
// 等于 beforeCreate 和 created
setup() {
console.log('setup')
onBeforeMount(() => {
console.log('onBeforeMount')
})
onMounted(() => {
console.log('onMounted')
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate')
})
onUpdated(() => {
console.log('onUpdated')
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount')
})
onUnmounted(() => {
console.log('onUnmounted')
})
}
}
</script>
Composition API 对比 Options API
Composition API 带来了什么
- 更好的代码组织
- 更好的逻辑复用
- 更好的类型推导
如何选择
- 不建议共用,会引起混乱
- 小型项目,业务逻辑简单,用 Options API
- 中大型项目,逻辑复杂,用 Composition API
理解 ref、toRef、toRefs
ref
- 生成值类型的响应式数据
- 可用于模板和 reactive
- 通过
.value
修改值
代码示例:
<template>
<div>
<h1>ref</h1>
<div>{{ ageRef }}</div>
<div>{{ details.name }}</div>
<p ref="eleRef">文字</p>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue'
export default {
name: 'ref',
setup() {
const ageRef = ref(22) // 值类型响应式
const nameRef = ref('Jae')
const details = reactive({
name: nameRef,
country: 'China',
province: 'XXX',
city: 'XXXX'
})
setTimeout(() => {
console.log('ageRef: ', ageRef.value) // .value 获取值
ageRef.value = 24 // .value 修改值
nameRef.value = 'Jack'
}, 1500)
const eleRef = ref(null)
onMounted(() => {
console.log(eleRef.value)
})
return {
ageRef,
details,
eleRef
}
}
}
</script>
toRef
- 针对一个响应式对象(reactive 封装)的 prop
- 创建一个 ref,具有响应式
- 两者保持引用关系
代码示例:
<template>
<div>
<h1>toRef</h1>
<div>{{ ageRef }}</div>
<div>{{ details.name }} - {{ details.age }}</div>
</div>
</template>
<script>
import { reactive, toRef } from 'vue'
export default {
name: 'toRef',
setup() {
const details = reactive({
age: 22,
name: 'Jae'
})
const ageRef = toRef(details, 'age')
setTimeout(() => {
details.age = 24 // ageRef 也会跟着变
}, 1500)
setTimeout(() => {
ageRef.value = 30 // details.age 也会跟着变
}, 2500)
return {
details,
ageRef
}
}
}
</script>
toRefs
- 将响应式对象(reactive 封装)转换为普通对象
- 对象的每个 prop 都是对应的 ref
- 两者保持引用关系
代码示例:
<template>
<div>
<h1>toRefs</h1>
<div>{{ age }}-{{ name }}</div>
</div>
</template>
<script>
import { reactive, toRefs } from 'vue'
export default {
name: 'toRefs',
setup() {
const details = reactive({
age: 22,
name: 'Jae'
})
const detailsRefs = toRefs(details) // 将响应式对象,变为普通对象
return detailsRefs
}
}
</script>
有同学可能会有疑问,不就是为了能在模板直接使用 {{ age }}
和 {{ name }}
吗,那直接 return { ...details }
解构出来不就好了,这里可以自己试一下,使用这种写法,然后设置一个定时器,改变 age 和 name 的值,可以发现页面中并没有发生变化。这是因为解构会丢失响应性
为什么解构属性会出现丢失响应式的问题呢?又该如何解决呢?可以参考一下这篇文章
简单来说就是 Vue3 使用 Proxy 来实现响应式,属性被解构出来后不再生效,解决方式就是通过 toRefs 来解决
最佳使用方式
- 用 reactive 做对象的响应式,用 ref 做值类型的响应式
- setup 中返回 toRefs(details),或者 toRef(details, ‘xxx’)
- 合成函数返回响应式对象时,使用 toRefs
示例:合成函数返回响应式对象
funciton useFunction() {
const details = reactive({
x: 1,
y: 2
})
//...
return toRefs(details) // 返回时转换为 ref
}
export default {
setup() {
const { x, y } = userFunction() // 可以在不失去响应式的情况下解构
return { x, y }
}
}
进阶 ref、toRef、toRefs
为什么需要 ref
- 返回值类型,会丢失响应式
- 在setup、computed、合成函数中,都有可能返回值类型
代码示例:
<template>
<div>
<h1>ref</h1>
<div>{{ age }}-{{ name }}</div>
</div>
</template>
<script>
import { reactive} from 'vue'
export default {
name: 'ref',
setup() {
let age = 22
setTimeout(() => {
age = 24 // 页面上不会有任何变化
}, 1500)
let details = reactive({
name: 'Jae'
})
setTimeout(() => {
details.name = 'Jack' // 页面上不会有任何变化
}, 1500)
return {
age, // 值类型不具有响应式
...details // 解构,相当于返回值类型,值类型不具有响应式
}
}
}
</script>
为什么需要 .value
- ref 是一个对象(为了不丢失响应式,而值类型不具有响应式),value 存储值,可以理解成 ref = { value: ‘xxx’ }
- 通过 .value 属性的 get 和 set 实现响应式
- 用于模板、reactive 时,不需要 .value(经过 Vue 编译),其他情况都需要
为什么需要 toRef 和 toRefs
- 初衷:在不丢失响应式的情况下,把对象数据分散/扩散(解构)
- 前提:针对的是响应式对象(reactive 封装的)非普通对象
- 注意:toRef 和 toRefs 并不是创造响应式,而是延续响应式
Vue3 升级了哪些重要的功能
- createApp
- emit 属性
- 生命周期
- 多事件
- Fragment
- 移除 .sync
- 异步组件的写法
- 移除 filter
- Teleport
- Suspense
- Composition API
createApp
// vue2.x
const app = new Vue({ ... })
Vue.use(...)
Vue.mixin(...)
Vue.component(...)
Vue.directive(...)
// vue3
const app = Vue.createApp({ ... })
app.use(...)
app.mixin(...)
app.component(...)
app.directive(...)
emits 属性
<!-- 父组件 -->
<HelloWorld :msg="msg" @onSay="say" />
// 子组件
export default {
name: 'HelloWorld',
props: { msg: String },
emits: ['onSay'], // 声明需要 emit 的事件名
setup(props, { emit }) {
emit('onSay', 'hello')
}
}
多事件处理
<!-- 在 methods 里定义 func1 和 func2 两个函数 -->
<button @click="func1($event), func2($event)">
提交
</button>
Fragment
template 中可以是多节点:
<!-- vue2.x -->
<template>
<div class="container">
<h1>title</h1>
<p>...</p>
</div>
</template>
<!-- vue3 -->
<template>
<h1>title</h1>
<p>...</p>
</template>
移除 .sync
<!-- vue2.x -->
<MyCom :title.sync="title" />
<!-- vue3 -->
<MyCom v-model:title="title" />
异步组件
<!-- vue2.x -->
new Vue({
components: {
'my-com': () => import('./myCom.vue')
}
})
<!-- vue3 -->
import { createApp, defineAsyncComponent } from 'vue'
createApp({
components: {
AsyncComponent: defineAsyncComponent(() => {
import('./myCom.vue')
})
}
})
移除 filter
<!-- vue2.x -->
<div>{{ message | formate }}</div>
<div :id="rowId | formaId"></div>
Teleport
<button @click="ifOpen = true">全屏</button>
<!-- teleport 弹窗,父元素是body -->
<teleport to="body">
<div v-if="ifOpen" class="model">
<div>teleport 弹窗</div>
<button @click="ifOpen = false">关闭</button>
</div>
</teleport>
Suspense
<Suspense>
<!-- 具有深层异步依赖的组件 -->
<template #default>
<my-com /> <!-- 异步组件 -->
</template>
<!-- 在 #fallback 插槽中显示 “loading” -->
<template #fallback>
loading...
</template>
</Suspense>
Composition API
- reactive
- ref 相关
- readonly
- watch、watchEffect
- setup
- 生命周期钩子函数
Composition API 实现逻辑复用
- 抽离逻辑代码到一个函数
- 函数命名约定为
useXxx
格式 - 在 setup 中引用 useXxx 函数
代码示例:
<!-- App.vue -->
<template>
<life-cycles v-if="flag" />
<button @click="flag = !flag">切换显示</button>
</template>
<script>
import HelloWord from './components/HelloWord.vue'
export default {
components: {
HelloWord
},
data() {
return {
flag: true
}
}
}
</script>
<!-- HelloWord.vue -->
<template>
<div>
<h1>mouse position {{ x }} {{ y }}</h1>
</div>
</template>
<script>
import useMousePosition from './useMousePosition'
export default {
name: 'MousePosition',
setup() {
const { x, y } = useMousePosition()
return {
x,
y
}
}
}
</script>
// useMousePosition.js
import { ref, onMounted, onUnmounted } from 'vue'
function useMousePosition() {
const x = ref(0)
const y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
console.log(x.value, y.value)
}
onMounted(() => {
console.log('useMousePosition mounted')
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
console.log('useMousePosition unMounted')
window.removeEventListener('mousemove', update)
})
return { x, y }
}
export default useMousePosition
Vue3 如何实现响应式
回顾 Object.defineProperty
在之前【前端面试专题】【4】Vue2 原理 有提到过 vue2 通过 Object.defineProperty 实现响应式的原理和缺陷,当时给出的总结是使用 Object.defineProperty 有这么几个缺点:
- 深度监听,需要递归到底,一次性计算量大
- 无法监听新增属性/删除属性
- 无法原生监听数组,需要特殊处理
那么 vue3 使用的 Proxy 是否解决了这些问题呢?
Proxy 基本使用
const data = {
name: 'Jae',
age: 22,
details: {
address: 'XXX'
}
}
const proxyData = new Proxy(data, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
console.log('get:', key)
return result // 返回结果
},
set(target, key, val, receiver) {
const result = Reflect.set(target, key, val, receiver)
console.log('set:', key, val)
console.log('result:', result)
return result // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property:', key)
console.log('result:', result)
return result // 是否删除成功
}
})
改一下测试用的 data,我们看下数组的表现如何:
const data = [1, 2, 'c']
我们可以看到当我们向数组中 push 一项时,触发了很多 get 与 set,这些触发真的都是有必要的吗?比如触发了 set: length 4
的更新,在触发 set: 3 1
的时候 length 已经是4了,此时再 set 一次没什么意义,因此可以进行一些优化:
get(target, key, receiver) {
// 只处理本身(非原型)的属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get:', key) // 监听
}
const result = Reflect.get(target, key, receiver)
return result // 返回结果
},
set(target, key, val, receiver) {
// 重复的数据不处理
const oldVal = target[key]
if (val === oldVal) return true
const result = Reflect.set(target, key, val, receiver)
console.log('set:', key, val)
return result // 是否设置成功
}
Reflect
const obj = {
name: 'Jae',
age: 22
}
Reflect.has(obj, 'a')
console.log(obj) // true,相当于 'a' in obj
Reflect.deleteProperty(obj, 'age')
console.log(obj) // { name: 'Jae' } 相当于 delete obj.age
Proxy 实现响应式
// 创建响应式
function reactive(target = {}) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组,返回
return target
}
// 代理配置
const proxyConf = {
get(target, key, receiver) {
// 只处理本身(非原型)的属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get:', key) // 监听
}
const result = Reflect.get(target, key, receiver)
return result // 返回结果
},
set(target, key, val, receiver) {
// 重复的数据不处理
const oldVal = target[key]
if (val === oldVal) return true
const result = Reflect.set(target, key, val, receiver)
console.log('set:', key, val)
return result // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property:', key)
console.log('result:', result)
return result // 是否删除成功
}
}
// 生成代理对象
const observed = new Proxy(target, proxyConf)
return observed
}
// 测试数据
const data = {
name: 'Jae',
age: 22,
details: {
country: 'China'
}
}
const proxyData = reactive(data)
测试一下:
输出结果都没什么问题,就是在获取 proxyData.details.country
的时候,只触发了 get: details
,并没有触发 get: country
,也就是深度监听还需要处理:
get(target, key, receiver) {
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get:', key)
}
const result = Reflect.get(target, key, receiver)
return reactive(result) // 再包一层,深度监听
}
对比一下 vue2 通过 Object.defineProperty 时候,创建响应式的时候一进入就开始递归每一层数据,而使用 Proxy,只是在需要的时候,也就是 get 中才进行递归,并没有一次性递归到底。
最后优化一下,判断是新增的属性还是旧的属性:
set(target, key, val, receiver) {
// 重复的数据不处理
const oldVal = target[key]
if (val === oldVal) return true
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('已有的key:', key)
} else {
console.log('新增的key:', key)
}
const result = Reflect.set(target, key, val, receiver)
console.log('set:', key, val)
return result // 是否设置成功
}
总结:
- 深度监听,性能更好
- 可监听 新增/删除 属性
- 可监听数组变化
- Proxy 无法兼容所有浏览器,无法 polyfill
v-model 参数的用法
先来回顾一下 Vue2 中 .sync
的用法,参考官方文档:
而在 Vue3 中去除了 .sync
的写法,改用 v-model 参数写法:
<myComponent v-model:title="bookTitle" />
<!-- 是以下的简写: -->
<myComponent :title="bookTitle" @update:title="bookTitle = $event" />
代码示例:
<!-- 父组件 -->
<template>
<div>
<p>{{ name }}-{{ age }}</p>
<user-info
v-model:name="name"
v-model:age="age"
/>
</div>
</template>
<script>
import { reactive, toRefs } from 'vue'
import UserInfo from './UserInfo.vue'
export default {
name: 'Vmodel',
components: {
UserInfo
},
setup() {
const details = reactive({
name: 'Jae',
age: 22
})
return toRefs(details)
}
}
</script>
<!-- 子组件 -->
<template>
<input type="text" :value="name" @input="$emit('update:name', $event.target.value)" />
<input type="text" :value="age" @input="$emit('update:name', $event.target.value)" />
</template>
<script>
export default {
name: 'UserInfo',
props: {
name: String,
age: Number
}
}
</script>
watch 和 watchEffect 的区别
- 两者都可以监听 data 属性变化
- watch 需要明确监听哪个属性
- watchEffect 会根据其中的属性,自动监听其变化
代码示例:
<template>
<div>
<p>watch and watchEffect</p>
<p>{{ numberRef }}</p>
<p>{{ name }}-{{ age }}</p>
</div>
</template>
<script>
import { reactive, ref, toRefs, watch, watchEffect } from 'vue'
export default {
name: 'Watch',
setup() {
const numberRef = ref(10)
const details = reactive({
name: 'Jae',
age: 22
})
watch(numberRef, (newNum, oldNum) => {
console.log('ref watch', oldNum, newNum)
}, {
immediate: true // 在初始化的时候就监听
})
// setTimeout(() => {
// numberRef.value = 200
// }, 1500)
watch(() => details.age, (newAge, oldAge) => {
console.log('details watch', newAge, oldAge)
})
setTimeout(() => {
details.age = 25
}, 1500)
return {
numberRef,
...toRefs(details)
}
}
}
</script>
// 初始化时会执行一次,收集要监听的数据
watchEffect(() => {
console.log('details.name', details.name)
})
setTimeout(() => {
details.name = 'Jack'
}, 1500)
setTimeout(() => {
details.age = 25
}, 1500)
setup 中如何获取组件实例
- 在 setup 和 其他 Composition API 中没有 this
- 可通过 getCurrentInstance 获取当前示例
- 若使用 Options API 可照常使用 this
代码示例:
<template>
<div>
<p>get instance</p>
<p>{{ name }}</p>
</div>
</template>
<script>
import { getCurrentInstance, onMounted } from 'vue'
export default {
name: 'GetInstance',
data() {
return {
name: 'Jae'
}
},
setup() {
console.log('this', this)
const instance = getCurrentInstance()
console.log('instance', instance)
console.log('data name', instance.data.name)
onMounted(() => {
console.log('onMounted name', instance.data.name)
})
}
}
</script>
Vue3 为什么比 Vue2 快
Proxy 响应式
前面已经提到过,对比 Vue2 使用的 Object.defineProperty 有何优势
PatchFlag(静态标记)
- 编译模板,动态节点做标记
- 标记,分为不同的类型,如 text props
- diff 算法时,可以区分静态节点,以及不同类型的动态节点
可以在这个网站可以看到 Vue3 模板编译的函数:
hoistStatic(静态提升)
- 将静态节点的定义,提升到父作用域,缓存起来
- 多个相邻的静态节点,会被合并起来
- 拿空间换时间的优化策略
把 hoistStatic 打开:
cacheHandler(事件监听缓存)
把 cacheHandler 打开:
缓存事件即:在遇到事件的时候如果没有该缓存函数则定义一个缓存函数,后面触发该事件的时候就不用再定义了
SSR 优化
- 静态节点直接输出,绕过了 vdom
- 动态节点还是需要动态渲染
把 SSR 打开:
tree-shaking
模板编译时,根据不同的情况,引入不同的 API
Vite
- 一个前端打包工具,Vue 作者发起的项目
- 借助 Vue 的影响力,发展较快,和 webpack 竞争
- 开发环境下无需打包,启动快
Vite 为什么启动快
- 开发环境下使用 ES6 Module,无需打包
- 生产环境使用 rollup,速度差别不是很大
Vue3 和 JSX
Vue3 中 JSX 的基本使用
使用之前需要安装依赖 npm i @vitejs/plugin-vue-jsx -D
,然后在 vite.config.js 中引入:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
plugins: [vue(), vueJsx()]
})
接下来就可以使用了:
<!-- template 版本 -->
<template>
<div>
<p>Demo {{ numRef }}</p>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'Demo',
setup() {
const numRef = ref(100)
return {
numRef
}
}
}
</script>
<!-- Vue3 JSX 版本 -->
<!-- 记得写上 lang -->
<script lang="jsx">
import { ref } from 'vue'
export default {
setup() {
const numberRef = ref(200)
return () => (
<p>Demo { numberRef.value }</p>
)
}
}
</script>
// JSX 版本
import { defineComponent, ref } from "vue"
import Child from './Child'
export default defineComponent({
setup() {
const numberRef = ref(300)
return () => (
<>
<p>demo1 { numberRef.value }</p>
<Child name="Jae" />
</>
)
}
})
// Child.jsx
import { defineComponent } from "vue"
export default defineComponent({
props: ['name'],
setup(props) {
return () => (
<p>Child { props.name }</p>
)
}
})
JSX 和 template 的区别
- 语法上有很大的区别
- 本质相同
- 具体示例:插值、自定义组件、属性和事件、条件和循环
语法区别:
- JSX 本质就是 js 代码,可以使用 js 的任何能力
- template 只能嵌入简单的 js 表达式,其他需要指令,如 v-if、v-for
- JSX 已经称为 ES 规范,template 还是 Vue 的自家规范
归纳:
- 在 tempalate 中,插值是用
{{ }}
,而在 JSX 中,插值是用{ }
- 在 JSX 引入自定义组件的时候,不能使用小写以及驼峰写法
- 在 template 中,属性可以写成动态类型的,比如
<Child :name="name" />
,而 JSX 中没有这种冒号的写法,需要动态的话也只能在括弧中写<Child age={age.value + 10} />
- 在 template 中,事件可以写成
<Child @click="handleClick" />
,而 JSX 中应该写成<Child onClick={handleClick} />
- 在 template 中,条件可以写成
<Child v-if="flag" />
,而 JSX 中应该写成{ flag.value && <Child /> }
- 在 template 中,循环 可以写成
<div v-for="item in list" :key="item.id">{{ item.name }}</div>
,而 JSX 中应该写成{list.map(item => <div>{item.name}</div>)}
JSX 和 slot
- slot 是 Vue 发明的概念,为了完善 template 的能力
- slot 本身的使用并不简单,特别是作用域 slot 较难以理解
实现普通插槽
代码示例:
<!-- index.vue -->
<template>
<tabs default-active-key="1" @change="onTabsChange">
<tab-panel key="1" title="title1">
<div>tab panel content 1</div>
</tab-panel>
<tab-panel key="2" title="title2">
<div>tab panel content 2</div>
</tab-panel>
<tab-panel key="3" title="title3">
<div>tab panel content 3</div>
</tab-panel>
</tabs>
</template>
<script>
import Tabs from './Tabs.jsx'
import TabPanel from './TabPanel.vue'
export default {
components: {
Tabs,
TabPanel
},
methods: {
onTabsChange(key) {
console.log('tab changed: ', key)
}
}
}
</script>
<!-- TabPanel.vue -->
<template>
<slot></slot>
</template>
<script>
export default {
name: 'TabPanel',
props: ['key', 'title']
}
</script>
// Tabs.jsx
import { ref } from 'vue'
export default {
name: 'Tabs',
props: ['defaultActiveKey'],
emits: ['change'],
setup(props, context) {
const children = context.slots.default()
const titles = children.map(panel => {
const { key, title } = panel.props || {}
return {
key, title
}
})
// 当前 actKey
const actKey = ref(props.defaultActiveKey)
function changeActKey(key) {
actKey.value = key
context.emit('change', key)
}
// jsx
const render = () => <>
<div>
{ /* 渲染 buttons */ }
{ titles.map(titleInfo => {
const { key, title } = titleInfo
return <button
key={key}
style={{ color: actKey.value === key ? 'blue' : '#333' }}
onClick={() => changeActKey(key)}
>{title}</button>
})}
</div>
<div>
{ /* 渲染内容 */ }
{children.filter(panel => {
const { key } = panel.props || {}
if (actKey.value === key) return true
return false
})}
</div>
</>
return render
}
}
实现作用域插槽
回顾一下 Vue 中的作用域插槽:常用于子组件将 data 中的值传到 slot 中,供父组件使用:
<!-- index.vue -->
<template>
<Child>
<template v-slot="slotProps">
<p>父组件传值</p>
<p>获取子组件 data:{{ slotProps }}</p>
</template>
</Child>
</template>
<script>
import Child from './Child.vue'
export default {
components: {
Child
}
}
</script>
<template>
<p>Child</p>
<slot :msg="msg"></slot>
</template>
<script>
export default {
data() {
return {
msg: '我是Child组件中data的msg'
}
}
}
</script>
这对于 Vue 的初学者或是很久没使用过作用域插槽的人来说是非常不友好的,因为这是 Vue 中自定的语法规范,很容易遗忘。接下来让我们看看使用 JSX 如何完成作用域插槽的功能:
// index.jsx
import { defineComponent } from "vue"
import Child from './Child.jsx'
export default defineComponent({
setup() {
function render(msg) {
return <p>msg: { msg }</p>
}
return () => (
<>
<p>父组件</p>
<Child render={render}></Child>
</>
)
}
})
// Child.jsx
import { defineComponent, ref } from "vue"
export default defineComponent({
props: ['render'],
setup(props) {
const msgRef = ref('作用域插槽Child')
return () => (
<p>{props.render(msgRef.value)}</p>
)
}
})
script setup
基本使用
- 顶级变量、自定义组件,可以直接用于模板
- 可以正常使用 ref reactive computed 等能力
- 和其他
<script>
同时使用
代码示例,Vue 版本需要大于等于 3.2.0:
<!-- index.vue -->
<template>
<div @click="addCount">{{ countRef }}</div>
<div>{{ details.name }}</div>
<Child />
</template>
<script lang='ts' setup>
import { ref, reactive } from 'vue'
import Child from './Child.vue'
const countRef = ref(100)
function addCount() {
countRef.value ++
}
const details = reactive({
name: 'Jae'
})
</script>
<!-- Child.vue -->
<template>
<div>Child</div>
</template>
<script lang='ts' setup>
</script>
属性和事件
- defineProps
- defineEmits
代码示例:
<!-- index.vue -->
<template>
<Child name="Jae" :age="25" @change="onChange" @delete="onDelete" />
</template>
<script lang='ts' setup>
import Child from './Child.vue'
function onChange(val) {
console.log(val)
}
function onDelete(val) {
console.log(val)
}
</script>
<template>
<div>Child - name: {{ props.name }}, age: {{ props.age }}</div>
<button @click="$emit('change', 'changeeeee')">change</button>
<button @click="handleClickDelete">delete</button>
</template>
<script lang='ts' setup>
// 定义属性
const props = defineProps({
name: {
type: String,
default: 'Jack'
},
age: {
type: Number,
default: 22
}
})
// 定义事件
const emit = defineEmits(['change', 'delete'])
function handleClickDelete() {
emit('delete', 'deleteeeee')
}
</script>
defineExpose
- 暴露数据给父组件
代码示例:
<!--index.vue-->
<template>
<Child ref="childRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
onMounted(() => {
// 拿到 Child 组件中的一些数据
console.log(childRef.value.num1) // 100
console.log(childRef.value.num2) // 200
})
</script>
<template>
<div>Child</div>
</template>
<script setup>
import { ref } from 'vue'
const num1 = ref(100)
const num2 = ref(200)
defineExpose({
num1, num2
})
</script>