<template>
<life-cycles v-if="flag" :msg="msg" />
<button @click="msg = Date.now()">change msgbutton>
<button @click="flag = !flag">hidebutton>
template>
<script>
import LifeCycles from './components/LifeCycles.vue'
export default {
components: {
LifeCycles
},
data() {
return {
msg: 'vue3',
flag: true
}
}
}
script>
<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>
<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>
.value
修改值代码示例:
<template>
<div>
<h1>refh1>
<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>
代码示例:
<template>
<div>
<h1>toRefh1>
<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>
代码示例:
<template>
<div>
<h1>toRefsh1>
<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 来解决
示例:合成函数返回响应式对象
funciton useFunction() {
const details = reactive({
x: 1,
y: 2
})
//...
return toRefs(details) // 返回时转换为 ref
}
export default {
setup() {
const { x, y } = userFunction() // 可以在不失去响应式的情况下解构
return { x, y }
}
}
代码示例:
<template>
<div>
<h1>refh1>
<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>
// 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(...)
<HelloWorld :msg="msg" @onSay="say" />
// 子组件
export default {
name: 'HelloWorld',
props: { msg: String },
emits: ['onSay'], // 声明需要 emit 的事件名
setup(props, { emit }) {
emit('onSay', 'hello')
}
}
<button @click="func1($event), func2($event)">
提交
button>
template 中可以是多节点:
<template>
<div class="container">
<h1>titleh1>
<p>...p>
div>
template>
<template>
<h1>titleh1>
<p>...p>
template>
<MyCom :title.sync="title" />
<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')
})
}
})
<div>{{ message | formate }}div>
<div :id="rowId | formaId">div>
<button @click="ifOpen = true">全屏button>
<teleport to="body">
<div v-if="ifOpen" class="model">
<div>teleport 弹窗div>
<button @click="ifOpen = false">关闭button>
div>
teleport>
<Suspense>
<template #default>
<my-com />
template>
<template #fallback>
loading...
template>
Suspense>
useXxx
格式代码示例:
<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>
<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
在之前【前端面试专题】【4】Vue2 原理 有提到过 vue2 通过 Object.defineProperty 实现响应式的原理和缺陷,当时给出的总结是使用 Object.defineProperty 有这么几个缺点:
那么 vue3 使用的 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 // 是否删除成功
}
})
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 // 是否设置成功
}
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
// 创建响应式
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 // 是否设置成功
}
先来回顾一下 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>
代码示例:
<template>
<div>
<p>watch and watchEffectp>
<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)
代码示例:
<template>
<div>
<p>get instancep>
<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>
前面已经提到过,对比 Vue2 使用的 Object.defineProperty 有何优势
可以在这个网站可以看到 Vue3 模板编译的函数:
缓存事件即:在遇到事件的时候如果没有该缓存函数则定义一个缓存函数,后面触发该事件的时候就不用再定义了
模板编译时,根据不同的情况,引入不同的 API
使用之前需要安装依赖 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>
<div>
<p>Demo {{ numRef }}p>
div>
template>
<script>
import { ref } from 'vue'
export default {
name: 'Demo',
setup() {
const numRef = ref(100)
return {
numRef
}
}
}
script>
<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 中,插值是用 { }
,而 JSX 中没有这种冒号的写法,需要动态的话也只能在括弧中写
,而 JSX 中应该写成
,而 JSX 中应该写成 { flag.value && }
{{ item.name }}
,而 JSX 中应该写成 {list.map(item => {item.name})}
代码示例:
<template>
<tabs default-active-key="1" @change="onTabsChange">
<tab-panel key="1" title="title1">
<div>tab panel content 1div>
tab-panel>
<tab-panel key="2" title="title2">
<div>tab panel content 2div>
tab-panel>
<tab-panel key="3" title="title3">
<div>tab panel content 3div>
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>
<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 中,供父组件使用:
<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>Childp>
<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>
)
}
})
同时使用代码示例,Vue 版本需要大于等于 3.2.0:
<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>
<template>
<div>Childdiv>
template>
<script lang='ts' setup>
script>
代码示例:
<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')">changebutton>
<button @click="handleClickDelete">deletebutton>
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>
代码示例:
<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>Childdiv>
template>
<script setup>
import { ref } from 'vue'
const num1 = ref(100)
const num2 = ref(200)
defineExpose({
num1, num2
})
script>