【前端面试专题】【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> 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大杯美式不加糖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值