一、认识Vue3
1.相关信息
支持vue2.x大多特性
更好的支持typescript
………
2.性能提升
使用Proxy
重写虚拟DOM
………
3.新增特性
Composition API
响应性API
组件
………
二、创建Vue3项目
使用脚手架vue-cli
npm install -g @vue/cli
查看版本
vue –version
创建项目
vue create vue-demo
三、新特性学习
1.Composition API
1.1 Setup
所有的组合 API 函数都在此使用, 只在初始化时执行一次。
假设界面需要渲染一个变量count。
App.vue
<h1>setup的简单使用,{{count}}</h1>
1.函数一般返回一个对象,对象中的属性或方法。为模板提供数据, 也就是模板中可以直接使用此对象中的所有属性和方法。
2.setup返回对象中的方法会与 methods 中的方法合并成组件对象的方法。
App.vue
<template>
<h1>setup返回值,{{count}}</h1>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'App',
setup(){
let count = 0
const fn2=()=>{
console.log('setup fn2')
}
return {
count,
fn2
}
},
mounted(){
console.log('组件对象实例',this)
},
methods:{
fn1(){
console.log('methods fn1')
}
},
});
</script>
执行结果如下图:
3.setup返回对象中的属性会与 data 函数返回对象的属性合并成为组件对象的属性。
App.vue
<template>
<h1>setup返回值,{{count}}</h1>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'App',
setup(){
let count = 0
const fn2=()=>{
console.log('setup fn2')
}
return {
count,
fn2
}
},
data(){
return {
msg:10
}
},
mounted(){
console.log('组件对象实例',this)
},
methods:{
fn1(){
console.log('methods fn1')
}
},
});
</script>
执行结果如下图:
注意:
一般不要混合使用: methods 中可以访问 setup 提供的属性和方法, 但在 setup 方法中不能访问 data 和 methods(setup中无this)。
通过执行beforeCreate来判断setup的执行时间。
App.vue
<template>
<h1>setup执行时间,{{msg}}</h1>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'App',
beforeCreate(){
console.log('beforeCreate执行啦')
},
setup(){
let msg = 'setup执行啦'
console.log('setup执行啦')
return {
msg,
}
},
});
</script>
执行结果如下图:
通过执行结果可以得出,setup在beforeCreate之前就执行了,并且只执行了一次,由此可以推断此时组件并未创建,组件实例对象this不能够使用。这意味着,除了 props 之外,你将无法访问组件中声明的任何属性——本地状态、计算属性或方法。
setup 选项应该是一个接受 props 和 context 的函数。
假设有一个子组件Child.vue,从App.vue向子组件传递数据。
App.vue
<template>
<h1>setup参数:父组件</h1>
<Child :count="count" msg="hello" @countAdd="countAdd"></Child>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import Child from './components/Child.vue'
export default defineComponent({
name: 'App',
components:{
Child
},
setup(){
let count = 0
const countAdd=(value:number)=>{
console.log('count+value',count+value)
}
return {
count,
countAdd
}
},
});
</script>
Child.vue
<template>
<h1>子组件</h1>
<button @click="emitCountAdd">点击+</button>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name:'Child',
props:['count'],
setup(props,context){
console.log('child props',props)
console.log(props.count)
console.log('child context',context)
//context是一个对象,包含attrs,slots,emit
console.log('context attrs',context.attrs)
// 插槽 (非响应式对象)
console.log('context slots',context.slots)
// 触发事件 (方法)
console.log('context emit',context.emit)
//事件点击
const emitCountAdd=()=>{
context.emit('countAdd',3)
}
return {
emitCountAdd
}
},
})
</script>
Props: 是有一个对象,包含父组件向子组件传递的数据,并且是在子组件中使用props接受数据。setup函数中的props是响应式的,不能使用 ES6 解构,因为它会消除 prop 的响应性。如果需要解构 prop,可以通过使用 setup 函数中的 toRefs 来完成此操作。
Context: 是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对使用 ES6 解构。它暴露三个组件的 property,分别是attrs(获取当前组件的所有的标签属性对象,该属性是props中没有声明接收的所有的属性对象)、slot(插槽)、emit(触发事件)。
1.2 Computed
与vue2中的计算属性功能一致。
如果计算属性的回调函数中只传入一个参数,表示的是get。
在后续小案例代码中看见ref,reactive等响应式应用,请访问 响应性API 进行了解。
App.vue
<template>
<h1>计算属性:</h1>
<h3>姓名:{{fullname}}</h3>
姓氏:<input type="text" v-model="firstname">
<br/>
名字:<input type="text" v-model="lastname">
<br>
姓名:<input type="text" v-model="fullname">
</template>
<script lang="ts">
import { computed, reactive, ref } from 'vue'
export default {
setup() {
let firstname = ref('liao')
let lastname = ref('xin')
//如果计算属性的回调函数中只传入一个参数,表示的是get
// let fullname = computed(()=>{
// return firstname.value + '--' +lastname.value
// })
let fullname = computed({
get(){
return firstname.value + '--' +lastname.value
},
set(val:string){
firstname.value = val
}
})
console.log('fullname',fullname)
return {
firstname,
lastname,
fullname
}
}
}
</script>
我们可以通过打印fullname查看是什么样的数据。
1.3 Watch
与在组件中利用watch选项设置监听器功能相同。监听指定的一个或多个响应式数据, 一旦数据变化, 就自动执行监听回调。
watch函数接受三个参数:
1.是想要监听的响应式应用或者getter函数
2.是触发执行的回调函数
3.是可选的配置选项
watch(object,callback,options)
假设有一个user响应式应用,监听此对象。
App.vue
<template>
<h1>Watch:</h1>
<h3>姓名:{{fullname}}</h3>
姓氏:<input type="text" v-model="user.firstname">
<br/>
名字:<input type="text" v-model="user.lastname">
<br>
姓名:<input type="text" v-model="fullname">
</template>
<script lang="ts">
import { computed, reactive, ref, watch } from 'vue'
export default {
setup() {
let user = reactive({
firstname:'liao',
lastname:'xin'
})
let fullname = ref()
watch(user,()=>{
fullname.value = user.firstname + '--' + user.lastname
},{
immediate:true, //是否初始化立即执行一次, 默认是false
deep: true // 是否进行深度监听, 默认是false
})
return {
user,
fullname
}
}
}
</script>
每当user被修改时,比如user.firstname = ‘xinxin’,监听将会触发并执行回调函数。
第三个参数是可选的配置选项,immediate属性表示是否初始化立即执行一次回调,默认初始不执行回调函数,deep表示是否进行深度监听。
如果immediate属性为false,界面就会如下图
注意
如果watch监听的不是响应式数据,可以使用回调的写法
App.vue
<template>
<h1>Watch:</h1>
<h3>姓名:{{fullname}}</h3>
姓氏:<input type="text" v-model="user.firstname">
<br/>
名字:<input type="text" v-model="user.lastname">
<br>
姓名:<input type="text" v-model="fullname">
</template>
<script lang="ts">
import { computed, reactive, ref, watch } from 'vue'
export default {
setup() {
let user = reactive({
firstname:'liao',
lastname:'xin'
})
let fullname = ref()
watch(()=>user.firstname,()=>{
console.log('----')
})
return {
user,
fullname
}
}
}
</script>
1.4 watchEffect
接收一个回调函数。
watchEffect(callback)
监听user,fullname变量。
<template>
<h1>计算属性:</h1>
<h3>姓名:{{fullname}}</h3>
姓氏:<input type="text" v-model="user.firstname">
<br/>
名字:<input type="text" v-model="user.lastname">
<br>
姓名:<input type="text" v-model="fullname">
</template>
<script lang="ts">
import { computed, reactive, ref, watch, watchEffect } from 'vue'
export default {
setup() {
let fullname = ref()
let user = reactive({
firstname:'liao',
lastname:'xin'
})
watchEffect(()=>{
fullname.value = user.firstname + '--' + user.lastname
})
return {
user,
fullname
}
}
}
</script>
从如下执行结果中可以发现,watchEffect不用直接指定要监听的数据, 回调函数中使用的哪些响应式数据就监听哪些响应式数据,当这些数据发生更新时就会触发执行回调函数。
在查看执行界面时可以发现,在组件初始化的时候就会执行一次回调函数用以收集依赖,而后收集到的依赖发生变化,这个回调函数会再次执行。
1.5 生命周期
对比选项式API,组合式API使用setup进行代替beforeCreate以及created。
组合式API在使用时我们都需要使用import进行引入。
选项式 API | Hook inside setup |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
假设有一个App父组件和一个Child子组件,通过点击按钮对Child组件更新变量值以及卸载组件来观察Child组件的生命周期。
App.vue
<template>
<h1>App父组件</h1>
<button @click="isShow = !isShow">切换</button>
<hr />
<Child v-if="isShow" />
</template>
<script lang="ts">
import Child from './components/Child.vue'
export default {
data() {
return {
isShow: true
}
},
components: {
Child
}
}
</script>
Child.vue
<template>
<div class="about">
<h1>Child子组件</h1>
<h3>message:{{msg}}</h3>
<div>姓名:{{obj.name}}-----年龄:{{obj.age}}</div>
<hr />
<button @click="update">更新</button>
</div>
</template>
<script lang="ts">
import { ref, onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount, onRenderTracked, onRenderTriggered, reactive } from 'vue'
export default {
beforeCreate() {
console.log('beforeCreate()')
},
created() {
console.log('created')
},
beforeMount() {
console.log('beforeMount')
},
mounted() {
console.log('mounted')
},
beforeUpdate() {
console.log('beforeUpdate')
},
updated() {
console.log('updated')
},
beforeUnmount() {
console.log('beforeUnmount')
},
unmounted() {
console.log('unmounted')
},
setup() {
let msg = ref('hello world')
let obj = reactive(
{
name:'liao',
age:18
}
)
//数据更新
const update = () => {
msg.value += '--'
obj.name += 'xin'
}
onBeforeMount(() => {
console.log('--onBeforeMount')
})
onMounted(() => {
console.log('--onMounted')
})
onBeforeUpdate(() => {
console.log('--onBeforeUpdate')
})
onUpdated(() => {
console.log('--onUpdated')
})
onBeforeUnmount(() => {
console.log('--onBeforeUnmount')
})
onUnmounted(() => {
console.log('--onUnmounted')
})
onRenderTracked((event) => {
console.log("onRenderTracked---状态跟踪组件");
console.log(event);
})
onRenderTriggered((event) => {
console.log("onRenderTriggered---状态触发组件");
console.log(event);
})
return {
msg,
obj,
update
}
}
}
</script>
通过打印每个生命周期的变化,可以发现组合式API会比选项式API更快。
接下来讲解一下onRenderTracked和onRenderTriggered的使用。
onRenderTracked 字面意思状态跟踪,会跟踪页面上所有响应式变量和方法的状态。只要页面有更新的情况,它就会跟踪,然后生成一个event对象,我们通过event对象来查找程序的问题所在。
这里跟踪的是对象中的name。
这里跟踪的是对象中的age。
onRenderTriggered 字面意思状态触发,它不会跟踪每一个值,而是给你变化值的信息,并且新值和旧值都会给你明确的展示出来。然后生成一个event对象,我们通过event对象来调试程序。
这里触发的是msg的值。
这里触发的是对象中的name。
当这些钩子函数能够合理并且正确地使用我们可以快速的解决问题。
1.6 自定义Hook
使用 Vue3 的组合 API 封装的可复用的功能函数,功能有点类似于Vue2.x中的mixin。
使用自定义Hook函数让我们可以很清楚复用功能代码的来源, 更清楚易懂,并且代码更易维护。
假设有一个需求,需要在组件加载后在页面显示鼠标点击的坐标。常规操作如下代码。
App.vue
<template>
<div class="app-home">
<h1>自定义Hook函数</h1>
<h4>坐标x:{{x}}</h4>
<h4>坐标y:{{y}}</h4>
</div>
</template>
<script lang="ts">
import { defineComponent, onBeforeUnmount, onMounted, ref } from "vue";
export default defineComponent({
setup(){
const x = ref(0)
const y = ref(0)
//点击事件回调函数
const handleClick = (event:MouseEvent)=>{
x.value = event.pageX
y.value = event.pageY
}
//组件加载后
onMounted(()=>{
window.addEventListener('click',handleClick)
})
//组件卸载前
onBeforeUnmount(()=>{
window.removeEventListener('click',handleClick)
})
return {
x,
y
}
}
})
</script>
执行结果如下图:
如果在很多页面都有这样的功能,那么上述代码就没有复用的效果,该如何实现呢?自定义Hook函数。
在src/hooks文件夹下新建文件useMouseClick.ts。文件名通常以use开头,将可复用的代码都转入此文件中。
useMouseClick.ts
import { onBeforeUnmount, onMounted, ref } from "vue";
export default function(){
const x = ref(0)
const y = ref(0)
//点击事件回调函数
const handleClick = (event:MouseEvent)=>{
x.value = event.pageX
y.value = event.pageY
}
//组件加载后
onMounted(()=>{
window.addEventListener('click',handleClick)
})
//组件卸载前
onBeforeUnmount(()=>{
window.removeEventListener('click',handleClick)
})
return {
x,
y
}
}
然后在App.vue中引入定义的useMouseClick函数。
App.vue
<template>
<div class="app-home">
<h1>自定义Hook函数</h1>
<h4>坐标x:{{x}}</h4>
<h4>坐标y:{{y}}</h4>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import useMouseClick from './hooks/useMouseClick'
export default defineComponent({
setup(){
let {x,y} = useMouseClick()
return {
x,
y
}
}
})
</script>
执行结果与上图相同。
1.7 Provide / Inject
provide 和 inject 启用依赖注入。只有在使用当前活动实例的 setup 期间才能调用这两者。
Provide,接收两个参数,一个是property的name,一个是property的value
//需要引入provide
import { provide } from "vue";
provide(name,value)
Inject,接收两个参数,一个是 inject 的 property 的名称(name),一个是默认值(可选)
//引入inject
import { inject } from 'vue'
inject(name,initValue)
可以使用provide 和 inject进行跨层级组件的通信。
假设应用有三个组件,三者关系为App > Parent > Child,App.vue通过provide将数据传递给Child.vue,Child.vue通过inject将数据inject注入。点击App.vue组件的按钮,修改color数据,观察Child.vue组件的文本颜色变化。
App.vue
<template>
<div class="app-home">
<h1>App组件</h1>
<h4>color:{{color}}</h4>
<button @click="color = 'red'">红色</button>
<button @click="color = 'green'">绿色</button>
<button @click="color = 'purple'">紫色</button>
<Parent/>
</div>
</template>
<script lang="ts">
import { defineComponent, provide, ref } from "vue";
import Parent from './components/Parent.vue'
export default defineComponent({
components:{
Parent,
},
setup(){
let color = ref('red')
provide('color',color)
return {
color
}
}
})
</script>
<style scoped>
.app-home {
width: 500px;
height: 500px;
background-color: blue;
color: #fff;
}
</style>
Parent.vue
<template>
<div class="parent">
<h2>Parent组件</h2>
<Child/>
</div>
</template>
<script>
import { defineComponent } from 'vue'
import Child from './Child.vue'
export default defineComponent({
components:{
Child,
},
setup () {
return {
}
}
})
</script>
<style scoped>
.parent {
width: 300px;
height: 300px;
background-color: orange;
margin: 10px;
}
</style>
Child.vue
<template>
<div class="child">
<h1 :style="{color}">Child组件</h1>
</div>
</template>
<script lang="ts">
import { defineComponent, inject } from 'vue'
export default defineComponent({
setup() {
//注入
let color = inject('color')
return {
color
}
}
})
</script>
<style scoped>
.child {
width: 200px;
height: 200px;
background-color: yellow;
margin: 20px;
}
</style>
执行结果如下图:
2 响应性API
2.1 ref
定义一个响应式数据。创建一个包含响应式数据的引用(reference)对象。
假设界面有一个变量count,通过点击按钮,使得count值修改,视图更新。
App.vue
<template>
<h1>ref的使用:{{count}}</h1>
<button @click="countAdd">点击+</button>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'App',
setup(){
let count = 0
const countAdd=()=>{
console.log('count:',++count)
}
return {
count,
countAdd
}
},
});
</script>
如果直接通过如下代码进行修改数据,点击按钮,执行结果如下图:
会发现,视图没有进行更新,那么如何解决呢?使用ref。
App.vue
<template>
<h1>ref的使用:{{count}}</h1>
<button @click="countAdd">点击+</button>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
name: 'App',
setup(){
let count = ref(0)
console.log('count ref',count)
const countAdd=()=>{
console.log('count:',++count.value)
}
return {
count,
countAdd
}
},
});
</script>
再次点击按钮,会发现,视图更新了!!!我们可以打印count进行查看他的类型。
ref有另外的一个作用,利用 ref 函数获取组件中的标签元素。
假设有一个需求,在页面加载后,页面中的文本框自动获取焦点。
App.vue
<template>
<h1>App组件</h1>
<input type="text" ref="inputRef">
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from "@vue/runtime-core";
export default defineComponent({
setup(){
let inputRef = ref<HTMLElement | null>(null)
console.log(ref)
onMounted(()=>{
if(inputRef.value){
inputRef.value.focus()
}
})
return {
inputRef
}
}
})
</script>
注意
ref 接受参数,并将其包裹在一个带有 value property 的对象中返回,然后可以使用该 property 访问或更改响应式变量的值。
在模板中操作数据时,不需要value 属性。
2.2 reactive
定义多个数据的响应式。
接收一个普通对象然后返回该普通对象的响应式代理器对象。响应式转换是“深层的”:会影响对象内部所有嵌套的属性。内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的。
假设有组件内有一个对象obj,通过点击按钮使得obj对象中的某些属性值进行修改并且页面重新渲染。
App.vue
<template>
<h1>reactive使用,{{user.name}}----{{user.age}}</h1>
<h1>{{user.hobby}}--------{{user.score.chinese}}----{{user.score.math}}</h1>
<button @click="update">点击</button>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue';
export default defineComponent({
name: 'App',
setup(){
//返回的是Proxy代理对象,被代理者就是reactive传入的对象
let obj= {
name:'lx',
age:18,
score:{
chinese:99,
math:98
},
hobby:['pingpang','yumao']
}
let user = reactive(obj)
console.log('user reactive:',user)
function update(){
user.name += ' xinxin'
user.score.math = 100
user.hobby[0] = 'tiaocheng'
user.hobby.push('run')
//obj.name = 'hello' 直接使用目标对象来更新属性值不能够使数据改变
//obj.sex = '女'//添加这个属性,页面未重新渲染
//user.sex = '女'添加了这个属性,页面也重新渲染
//delete obj.name 删除属性,页面未重新渲染
//delete user.name 删除了属性,页面重新渲染
}
return{
user,
update
}
}
});
</script>
通过对目标对象属性的添加和删除可以发现,目标对象确实删除(添加)了属性,但是界面并未重新渲染。如果操作代理对象,目标对象中的数据也会随之修改,界面也会重新渲染。
2.3 ref与reactive比较
ref 可以用来处理基本类型数据, reactive 可以用来处理对象(递归深度响应式)。
如果用 ref 对象或数组, 内部会自动将对象或数组转换为 reactive 的代理对象。
ref 内部: 通过给 value 属性添加 getter/setter 来实现对数据的劫持。
reactive 内部: 通过使用 Proxy 来实现对对象内部所有数据的劫持, 并通过 Reflect 操作对象内部数据。
App.vue
<template>
<h2>ref与reactive</h2>
<p>m1: {{ m1 }}</p>
<p>m2: {{ m2 }}</p>
<p>m3: {{ m3 }}</p>
<button @click="update">更新</button>
</template>
<script lang="ts">
import { reactive, ref } from 'vue'
export default {
setup() {
const m1 = ref('lx')
const m2 = reactive({ name: 1, child: { name: 'xxx' } })
// 使用ref处理对象
const m3 = ref({ name: 2, child: { name: 'llxx' } })
console.log('m1',m1)
console.log('m2',m2)
console.log('m3',m3)
console.log('m3.value.child:',m3.value.child) // 也是一个proxy对象
const update=()=> {
m1.value += '--'
m2.name += 1
m2.child.name += '++'
m3.value = { name: 3, child: { name: 'lll' } }
m3.value.child.name += '=='
console.log(m3.value.child)
}
return {
m1,
m2,
m3,
update
}
}
}
</script>
ref 比 reactive 更全能,因为reactive能做的事情,ref 都能做,因为 ref 的 value 可以是Proxy。只是使用ref时需要用到.value,变量多了就比较麻烦,有点别扭。
2.4 toRefs
可以把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref。
假设在界面需要渲染一个对象的name值和age值,点击按钮可以修改对象的值,可以使用reactive。
APP.vue
<template>
<h1>App组件</h1>
<h3>姓名:{{person.name}}</h3>
<h3>年龄:{{person.age}}</h3>
<button @click="update">更新</button>
</template>
<script lang="ts">
import { defineComponent, reactive } from "@vue/runtime-core";
export default defineComponent({
setup(){
let person = reactive({
name: 'liaoxin',
age: 18
})
const update = ()=>{
person.name += '~'
}
return {
person,
update
}
}
})
</script>
如果有一个需求,需要将对象进行结构,并且修改属性值界面会重新渲染。我们不妨试一下直接将对象解构,可以发现,界面不会发生渲染,数据失去了响应性,reactive 对象取出的所有属性值都是非响应式的,那么该如何解决呢?
<template>
<h1>App组件</h1>
<h3>姓名:{{name}}</h3>
<h3>年龄:{{age}}</h3>
<button @click="update">更新</button>
</template>
<script lang="ts">
import { defineComponent, reactive } from "@vue/runtime-core";
export default defineComponent({
setup(){
let person = reactive({
name: 'liaoxin',
age: 18
})
let {name,age} = person
const update = ()=>{
name += '~'
}
return {
name,
age,
update
}
}
})
</script>
利用 toRefs 可以将一个响应式 reactive 对象的所有原始属性转换为响应式的 ref 属性。
App.vue
<template>
<h1>App组件</h1>
<h3>姓名:{{name}}</h3>
<h3>年龄:{{age}}</h3>
<button @click="update">更新</button>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from "@vue/runtime-core";
export default defineComponent({
setup(){
let person = reactive({
name: 'liaoxin',
age: 18
})
let {name,age} = toRefs(person)
console.log('name',name)
console.log('age',age)
const update = ()=>{
name.value += '~'
}
return {
name,
age,
update
}
}
})
</script>
试着打印一下name,age可以发现,对象的每个 property 都是指向原始对象相应 property 的 ref。
2.5 toRef
为源响应式对象上的某个属性创建一个 ref 对象, 二者内部操作的是同一个数据值, 更新时二者是同步的。
假设界面有一个status对象包含name,age属性,有name,age变量,通过点击按钮更新数据,查看页面数据的变化。status使用reactive进行响应式应用,name变量通过使用status对象中的name属性使用toRef进行响应式应用,age变量通过ref使用status中的age属性进行初始赋值。
App.vue
<template>
<h1>toRef</h1>
<h4>status:{{status}}</h4>
<h4>name:{{name}}</h4>
<h4>age:{{age}}</h4>
<hr />
<button @click="update">更新</button>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, toRef } from "@vue/runtime-core";
export default defineComponent({
setup(){
let status = reactive({
name:'liaoxin',
age: 18
})
let name = toRef(status,'name')
let age = ref(status.age)
console.log('status',status)
console.log('name',name)
console.log('age',age)
const update = ()=>{
//更新数据
status.name += '~~'
//name.value += '***'
//age.value = 19
}
return {
status,
name,
age,
update
}
}
})
</script>
首先我们将status对象中的name属性值进行修改,查看执行的界面效果。
const update = ()=>{
//更新数据
status.name += '~~'
}
会发现name变量的值也一起被修改了。
接着我们将变量name的值进行修改,查看执行的界面效果。
const update = ()=>{
//更新数据
name.value += '***'
}
会发现status对象中的name属性值也被修改了。
最后我们将变量age的值进行修改,查看执行的界面效果。
会发现只有age变量值被修改了,status对象中的age属性值并未修改。
我们可以打印一下上述name,age变量的响应式类型,可以发现两者皆是ref。
通过上述结果,可以得出:ref拷贝了一份新的数据值单独操作, 更新时相互不影响。
2.6 shallowRef
创建一个跟踪自身 .value 变化的 ref,但不会使其值也变成响应式的。
假设将界面的m1,m2响应式应用进行修改,查看页面数据是否重新渲染,分别使用ref和shallowRef进行响应式数据的应用。
App.vue
<template>
<h1>shallowRef</h1>
<hr/>
<h4>ref的m1:{{m1}}</h4>
<h4>shallowRef的m2:{{m2}}</h4>
<hr />
<button @click="update">更新</button>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, shallowRef } from "@vue/runtime-core";
export default defineComponent({
setup(){
let m1 = ref({
name:'liao',
friend:{
name:'xin',
age:18
}
})
let m2 = shallowRef({
name:'liao',
friend:{
name:'xin',
age:18
}
})
console.log('m1',m1)
console.log('m2',m2)
const update = ()=>{
//修改ref的m1的数据
//m1.value.friend.name += '~~'
//修改shallowRef的m2的数据
m2.value.friend.name += '~~'
console.log('update')
}
return {
m1,
m2,
update
}
}
})
</script>
可以发现,修改使用shallowRef定义响应式数据的m2页面并没有重新渲染。通过打印m1,m2变量,可以发现两者的不同。
只处理了.value的响应式,不对其对象进行reactive的处理。
2.7 shallowReactive
创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (暴露原始值)。
假设将界面的m1,m2响应式应用进行修改,查看页面数据是否重新渲染,分别使用reactive和shallowReactive进行响应式数据的应用。
App.vue
<template>
<h1>shallowReactive</h1>
<hr/>
<h4>reactive的m1:{{m1}}</h4>
<h4> shallowReactive的m2:{{m2}}</h4>
<hr />
<button @click="update">更新</button>
</template>
<script lang="ts">
import { defineComponent, reactive, shallowReactive } from "@vue/runtime-core";
export default defineComponent({
setup(){
let m1 = reactive({
name:'liao',
friend:{
name:'xin',
age:18
}
})
let m2 = shallowReactive({
name:'liao',
friend:{
name:'xin',
age:18
}
})
console.log('m1',m1)
console.log('m2',m2)
const update = ()=>{
//修改ref的m1的数据
//m1.friend.name += '~~'
//修改shallowRef的m2的数据
m2.friend.name += '~~'
console.log('update')
}
return {
m1,
m2,
update
}
}
})
</script>
可以发现,修改使用shallowReactive定义响应式数据的m2页面并没有重新渲染。
2.8 readonly
获取一个对象 (响应式或纯对象) 或 ref 并返回原始代理的只读代理。
只读代理是深层的:访问的任何嵌套 property 也是只读的。
应用:在某些特定情况下, 我们可能不希望对数据进行更新的操作, 那就可以包装生成一个只读代理对象来读取数据, 而不能修改或删除。
App.vue
<template>
<h1>readonly</h1>
<h4>status:{{status}}</h4>
<hr />
<button @click="update">更新</button>
</template>
<script lang="ts">
import { defineComponent, reactive, readonly } from "@vue/runtime-core";
export default defineComponent({
setup(){
let m1 = reactive({
name:'liao',
friend:{
name:'xin',
age:18
}
})
const status = readonly(m1)
const update = ()=>{
status.name += '~~' // 报错
status.friend.name += '~~' // 报错
}
return {
status,
update
}
}
})
</script>
创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值)。
应用:在某些特定情况下, 我们可能不希望对数据进行更新的操作, 那就可以包装生成一个只读代理对象来读取数据, 而不能修改或删除。
App.vue
<template>
<h1>shallowReadonly</h1>
<h4>status:{{status}}</h4>
<hr />
<button @click="update">更新</button>
</template>
<script lang="ts">
import { defineComponent, reactive, shallowReadonly } from "@vue/runtime-core";
export default defineComponent({
setup(){
let m1 = reactive({
name:'liao',
friend:{
name:'xin',
age:18
}
})
const status = shallowReadonly(m1)
console.log('status',status)
const update = ()=>{
//status.name += '~~' // 报错
status.friend.name += '~~'
console.log('update')
}
return {
status,
update
}
}
})
</script>
通过打印status,可以发现他的类型创建了一个proxy,使其自身的 property 为只读。
2.9 响应式数据的判断API
isRef: 检查一个值是否为一个 ref 对象
isReactive: 检查一个对象是否是由 reactive 创建的响应式代理
isReadonly: 检查一个对象是否是由 readonlt 创建的只读代理
isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理
<h1>响应式数据的判断API</h1>
3 Composition API 与Option API
4 手写实现组合API
暂不实现界面更新操作,只实现数据劫持操作。
4.1 shallowReactive
浅响应式,创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换。
index.js
const reactiveHandler = {
//获取属性值
get(target, key) {
if (key === '_is_reactive') return true
console.log('拦截读取数据',key)
return Reflect.get(target, key)
},
//修改属性值
set(target, key, value) {
const result = Reflect.set(target, key, value)
console.log('拦截修改数据',key,value)
return result
},
//删除属性值
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('拦截删除数据',key)
return result
}
}
/*
定义shallowReactive函数
*/
function shallowReactive(target) {
//判断当前目标对象类型是不是object
if (target && typeof target === 'object') {
return new Proxy(target, reactiveHandler)
}
return target
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./index.js"></script>
</head>
<body>
<script>
let shallowReactiveObj = shallowReactive({
name:'liao',
friend:{
name:'xin',
age:18
}
})
//拦截到了读和写数据
shallowReactiveObj.name += '**'
//拦截到了读数据
shallowReactiveObj.friend.name += '~~'
//拦截到了删除数据
delete shallowReactiveObj.name
//只拦截到了读取数据
delete shallowReactiveObj.friend.age
</script>
</body>
</html>
执行结果如下图:
4.2 reactive
index.js
const reactiveHandler = {
//获取属性值
get(target, key) {
if (key === '_is_reactive') return true
console.log('拦截读取数据',key)
return Reflect.get(target, key)
},
//修改属性值
set(target, key, value) {
const result = Reflect.set(target, key, value)
console.log('拦截修改数据',key,value)
return result
},
//删除属性值
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('拦截删除数据',key)
return result
}
}
/*
定义reactive函数
*/
function reactive(target) {
//判断当前目标对象类型是不是object
if (target && typeof target === 'object') {
//对数组或是对象中的所有数据进行reactive的递归处理
if (target instanceof Array) {
// 如果是数组
target.forEach((item, index) => {
target[index] = reactive(item)
})
} else {
// 如果是对象
Object.keys(target).forEach(key => {
target[key] = reactive(target[key])
})
}
return new Proxy(target, reactiveHandler)
}
return target
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./index.js"></script>
</head>
<body>
<script>
let reactiveObj = reactive({
name:'liao',
friend:{
name:'xin',
age:18
}
})
//拦截读和写数据
reactiveObj.name +='!!'
//拦截读和写操作
reactiveObj.friend.age = 19
//拦截删除操作
delete reactiveObj.name
//拦截读和删除操作
delete reactiveObj.friend.age
</script>
</body>
</html>
执行结果如下图:
4.3 ref
index.js
/*
定义ref函数
*/
function ref(target) {
// 因为如果用 ref 对象或数组, 内部会自动将对象或数组转换为 reactive 的代理对象。
if (target && typeof target === 'object') {
target = reactive(target)
}
const result = {
_value: target, // 用来保存数据的内部属性
_is_ref: true, // 用来标识是ref对象
get value() {
console.log('ref 拦截读取数据')
return this._value
}, //通过.value形式访问或修改数据
set value(val) {
this._value = val
console.log('ref 拦截更新数据')
}
}
return result
}
测试数据采用基本数据类型和对象类型进行测试。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./index.js"></script>
</head>
<body>
<script>
let ref1 = ref(0)
let ref2 = ref({
name:'liao',
friend:{
age:18
},
hobby:['run','play']
})
ref1.value++
ref2.value.hobby[0] = 'jump'
ref2.value.friend.age = 19
console.log(ref1,ref2)
</script>
</body>
</html>
执行结果如下图,可以直接观察看两个变量的响应式应用的区别。
4.4 shallowRef
index.js
/*
定义shallowRef函数
*/
function shallowRef(target) {
const result = {
_value: target, // 用来保存数据的内部属性
_is_ref: true, // 用来标识是ref对象
get value() {
console.log('shallowRef 拦截读取数据')
return this._value
}, //通过.value形式访问或修改数据
set value(val) {
this._value = val
console.log('shallowRef 拦截更新数据')
}
}
return result
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./index.js"></script>
</head>
<body>
<script>
let shallow1 = shallowRef(0)
let shallow2 = shallowRef({
name:'liao',
friend:{
name:'xin',
age:18
}
})
//拦截读取和写操作
console.log(++shallow1.value)
//拦截读取和写操作
shallow2.value = 'hello'
console.log(shallow2.value)
console.log('---')
//拦截读取操作
shallow2.value.friend = '**'
console.log(shallow2.value.friend)
</script>
</body>
</html>
执行结果如下图:
4.5 readonly
index.js
const readonlyHandler = {
get(target, key) {
if (key === '_is_readonly') return true
console.log('拦截读取数据')
return Reflect.get(target, key)
},
set() {
console.warn('拦截只读数据, 不能修改')
return true
},
deleteProperty() {
console.warn('拦截只读数据, 不能删除')
return true
}
}
/*
定义readonly函数
*/
function readonly(target) {
if (target && typeof target === 'object') {
if (target instanceof Array) {
// 数组
target.forEach((item, index) => {
target[index] = readonly(item)
})
} else {
// 对象
Object.keys(target).forEach(key => {
target[key] = readonly(target[key])
})
}
return new Proxy(target, readonlyHandler)
}
return target
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./index.js"></script>
</head>
<body>
<script>
let readonly1 = readonly({
name:'liao',
hobby:['run','jump']
})
console.log(readonly1.name)
readonly1.name = 'xin'
readonly1.hobby[0] = 'play'
</script>
</body>
</html>
执行结果如下图:
4.6 shallowReadonly
index.js
const readonlyHandler = {
get(target, key) {
if (key === '_is_readonly') return true
console.log('拦截读取数据')
return Reflect.get(target, key)
},
set() {
console.warn('拦截只读数据, 不能修改')
return true
},
deleteProperty() {
console.warn('拦截只读数据, 不能删除')
return true
}
}
/*
定义shallowReadonly函数
*/
function shallowReadonly(target) {
if (target && typeof target === 'object') {
return new Proxy(target, readonlyHandler)
}
return target
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./index.js"></script>
</head>
<body>
<script>
let shallow1 = shallowReadonly({
name:'liao',
hobby:['run','jump']
})
console.log(shallow1.name) //拦截读取数据
shallow1.name = 'xin' //拦截读取数据,不能修改数据
shallow1.hobby[0] = 'play' //拦截读取数据,并没有说不能修改数据
</script>
</body>
</html>
执行结果如下图:
5 组件
5.1 Fragment
在 Vue2 中: 组件必须有一个根标签。
在 Vue3 中: 组件可以没有根标签, 内部会将多个标签包含在一个 Fragment 虚拟元素中。
优点: 减少标签层级, 减小内存占用。
5.2 Teleport
5.3 Suspense
它们允许我们的应用程序在等待异步组件时渲染一些后备内容,可以提高用户体验。
当AsyncComp组件还未加载时,就会显示LOADING组件。
App.vue
<template>
<Suspense>
<template v-slot:default>
<AsyncComp />
</template>
<template v-slot:fallback>
<h1>LOADING...</h1>
</template>
</Suspense>
</template>
<script lang="ts">
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => import('./AsyncComp.vue'))
export default {
setup() {
return {}
},
components: {
AsyncComp,
}
}
</script>
- 本文作者: étoile
- 版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!