创建vue3项目
1、vue-cli
## 安装或者升级
npm install -g @vue/cli
## 保证 vue cli 版本在 4.5.0 以上
vue --version
## 创建项目
vue create my-project
然后的步骤
- Please pick a preset - 选择 Manually select features
- Check the features needed for your project - 选择上 TypeScript ,特别注意点空格是选择,点回车是下一步
- Choose a version of Vue.js that you want to start the project with - 选择 3.x (Preview)
- Use class-style component syntax - 直接回车
- Use Babel alongside TypeScript - 直接回车
- Pick a linter / formatter config - 直接回车
- Use history mode for router? - 直接回车
- Pick a linter / formatter config - 直接回车
- Pick additional lint features - 直接回车
- Where do you prefer placing config for Babel, ESLint, etc.? - 直接回车
- Save this as a preset for future projects? - 直接回车
安装较慢,之后
npm run serve
2、vite
- vite 是一个由原生 ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发,
- 它做到了***本地快速开发启动***, 在生产环境下基于 Rollup 打包。
- 快速的冷启动,不需要等待打包操作;
- 即时的热模块更新,替换性能和模块数量的解耦让更新飞起;
- 真正的按需编译,不再等待整个应用编译完成,这是一个巨大的改变。
## 安装vite
npm install -g create-vite-app
## 利用 Vite 安装 Vue3.0 项目
npm init vite-app <project-name>
## 安装依赖运行项目
cd <project-name>
npm install
npm run dev
生成的vue2/vue3项目区别
-
vue2 中html模板必须有一对根标签,vue3可以没有根标签
-
script标签支持ts
-
defineComponent函数
<script lang="ts">
// lang="ts"表示 可以使用ts代码
// defineComponent函数用来定义一个组件,内部可以传入配置对象
import { defineComponent } from 'vue';
// 暴露defineComponent定义好的组件
export default defineComponent({
name: 'App', // 当前组件名字
});
</script>
组合Composition API(常用部分)
1、setup
setup执行时机
-
setup在beforeCreate生命周期回调之前执行,而且只执行一次。
-
setup执行的时候,组件还未创建,组件实例对象this为undefined,所以不能通过this去调用data/computed/methods/props内容
-
所有的composition API相关回调函数中也都不可以用this
setup返回值
- setup一般都是返回一个对象,对象中的属性和方法都可以在html模板中直接使用
- setup也可返回一个渲染函数,
return ()=>h('h1','我是h1标题')
此时页面直接渲染一个h1标题;h渲染函数需从vue引入 - setup中的属性与data中的属性会合并为组件对象的属性
- setup中的方法与methods中的方法会合并为组件对象的方法
- 属性或方法同名时,setup优先,但是尽量不要混用,methods可以访问setup中的属性与方法,反过来不行
- ??? setup不能是async函数:即不能写async setup()。因为返回值不再是return的对象,而是promise包裹的对象,模板中看不到return对象中的属性数据,只能通过
.then
获取数据
setup的参数
setup(props, context)或者setup(props, {attrs, emit, slots, expose})
-
props参数:是一个对象,包含子组件用props接收的父组件向子组件传递的属性
-
context参数对象
attrs
对象:没有在props中声明的父组件传过来的所有属性的对象,相当于this.$attrsemit
方法:用来分发自定义事件的函数, 相当于 this.$emitslots
:包含所有传入的插槽内容的对象, 相当于 this.$slotsexpose
:控制哪些内容会明确地公开暴露给组件使用者
父子组件示例:
父组件:
<template>
<h2>app父组件</h2>
<h3>父msg: {{ msg }}</h3>
<button @click="msg += '='">更新父级数据</button>
<h4>子组件expose给父组件的aaa:{{ aaa }}</h4>
<hr />
<Child :msg="msg" msg2='你猜' @emitData="xxx" ref="aaa"></Child>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import Child from './components/Child.vue'
export default defineComponent ({
name:'App',
components: {
Child
},
setup() {
const msg = ref('haha')
const aaa = ref()
function xxx(txt: string) {
msg.value += txt
}
return { msg, xxx, aaa }
},
mounted() {
// aaa 调用 expose 之后, 父组件 ref 拿到的结果为调用 expose 时的参数。而不再是组件实例了
console.log(this.aaa.count,'lkjhgfd',this.aaa)
// this.aaa.count = 10, this.aaa = Proxy {count: 10, __v_skip: true}
}
})
</script>
子组件:
<template>
<h2>child子组件</h2>
<h3>子msg: {{msg}}</h3>
<button @click="emitxxx">分发数据emit</button>
<h4>子组件expose的是:{{ count }}; 未expose{{ count1 }}</h4>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name:'Child',
props: ['msg'],
emits: ['emitData'],
// 数据初始化的生命周期回调
beforeCreate(){
console.log('beforeCreate执行')
},
setup(props,context) {
console.log('setup执行',this) // 先于beforeCreate执行 this是undefined
// console.log(context)
// console.log(context.attrs) // msg2: '你猜'
function emitxxx() {
context.emit('emitData', '++')
}
const count = 10
const count1 = 23
context.expose({ count })
return { emitxxx,count,count1 }
}
})
</script>
警告
1、props中未声明msg2警告:Extraneous non-props attributes (msg2) were passed to component but could not be automatically
inherited because component renders fragment or text root nodes.
2、Extraneous non-emits event listeners (emitData) were passed to component but could not be automatically inherited because component renders fragment or text root nodes. If the listener is intended to be a component custom event listener only, declare it using the “emits” option.
解决: emits: [‘emitData’]
2、ref
ref是一个函数
作用:定义一个响应式数据,返回一个Ref对象,对象中有一个value属性
语法: const xxx = ref(initValue)
- 创建一个包含响应式数据的引用(reference)对象
- js中操作数据:
xxx.value
- 模板中操作数据: 不需要
.value
基本使用
<template>
<h1>setup与ref --- 点击按钮次数增加</h1>
<h2>{{ count }}</h2>
<button @click="updateCount">更新数据</button>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
name: 'App',
// vue2实现
// data() {
// return {
// count: 0 // 属性
// }
// },
// methods: {
// updateCount() { // 方法
// this.count++
// }
// }
// vue3实现
setup() {
// let count = 0; // 直接定义数据并不是响应式的,当数据变化时,页面不会跟着渲染变化
let count = ref(0);
function updateCount() {
// 使用Ref对象调用value属性的方式进行数据的操作,但在html中不需要使用.value属性的写法
count.value++
}
return {
count,
updateCount
}
}
});
</script>
利用ref函数获取组件中的标签元素
功能需求: 让输入框自动获取焦点
<template>
<h2>ref自动获取标签元素</h2>
<input type="text" ref="inputRef" />
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue'
export default defineComponent({
name:'App',
setup() {
// 默认是空的null,页面加载完毕后说明组件已经存在了,这时可以获取到文本框元素
const inputRef = ref<HTMLElement | null>(null)
// 页面加载完毕,页面中的文本框可以直接获取焦点(自动获取焦点)
onMounted(()=>{
inputRef.value && inputRef.value.focus() // 自动获取焦点
})
return { inputRef }
}
})
</script>
3、reactive
-
作用: 定义多个数据的响应式
-
reactive接收一个普通对象,返回其响应式代理器对象(基于ES6的Proxy实现)
-
响应式转换是深层的即响应式代理器对象内部所有嵌套的属性都是响应式的
-
语法:
const proxy = reactive(obj)
— proxy是代理对象(其类型是Proxy),被代理的目标对象是reactive接收的对象obj -
操作代理对象,目标对象中的数据也会随之变化,页面重新渲染
<template>
<h1>reactive --- 点击按钮更新用户的相关信息数据</h1>
<h2>名字:{{ user.name }}</h2>
<h2>年龄:{{ user.age }}</h2>
<h2>性别:{{ user.gender }}</h2>
<h2>其他:{{ user.other }}</h2>
<button @click="updateUser">更新数据</button>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue';
export default defineComponent({
name: 'App',
setup() {
const obj: any = {
name: 'haha',
age: 20,
other: {
cars: ['ben','audio','MMW'],
likes: 'sports'
}
}
const user = reactive(obj)// user是代理对象(其类型是Proxy),被代理的目标对象是reactive接收的对象
const updateUser = () => {
// 目标对象不是响应式的,不能直接对其进行操作;代理对象是响应式的,可以进行操作
// user.age += 1
// user.other.likes += ': swim'
// obj对象添加一个属性:页面不重新渲染,obj对象中添加了该属性
// obj.gender = '女'
// obj对象移除一个已经存在的属性:页面没有更新渲染,obj对象中已没有该属性
// delete obj.age
// user对象添加一个属性:页面重新渲染,obj对象中添加了该属性
user.gender = '男'
// user对象移除一个已经存在的属性:页面更新渲染,obj对象中已没有该属性
delete user.age
// 总结:操作代理对象,目标对象中的数据也会随之变化,页面重新渲染
// 通过当前的代理对象把目标对象中的某个数组属性更改或添加一个新的属性
user.other.cars[1] += '=='
user.other.cars[4] = 'mashaji'
}
return {
user,
updateUser
}
}
});
</script>
4、reactive与ref的细节问题
- ref主要用来处理基本数据类型,reactive用来处理对象(递归深度响应式)
- 如果用ref处理 对象/数组,内部会自动将 对象/数组 转换为reactive的代理对象;reactive必须传入对象,不能传基本类型
- ref内部:通过给value属性添加getter/setter实现对数据的劫持
- reactive内部:通过Proxy实现对对象内部所有数据的挟持,并通过Reflect操作对象内部数据
- ref的数据操作:在ts中要加
.value
,在html中则不需要(内部解析模板时会自动添加)
<template>
<h2>reactive与ref的细节问题</h2>
<h3>m1: {{ m1 }}</h3>
<h3>m2: {{ m2 }}</h3>
<h3>m3: {{ m3 }}</h3>
<hr />
<button @click="update">更新数据</button>
</template>
<script lang="ts">
import { defineComponent, ref, reactive } from 'vue'
export default defineComponent({
name:'App',
setup() {
const m1 = ref('ref的abc')
const m2 = reactive({
name: '我是reactive创建的m2',
num: 2,
others: {
sports: 'jk'
}
})
const m3 = ref({
name: '我是ref创建的m3',
num: 3,
others: {
sports: 'hf'
}
})
const update = () => {
m1.value += '+m1'
m2.others.sports += '+m2'
m3.value.others.sports += '+m3'
}
return { m1, m2, m3, update }
}
})
</script>
5、vue2/vue3 响应式原理
vue2的响应式
- 核心:
- 对象: 通过
defineProperty
对对象的已有属性值的读取和修改进行劫持(监视/拦截) - 数组: 通过重写数组、更新数组一系列更新元素的方法来实现元素修改的劫持
- 对象: 通过
Object.defineProperty(data, 'count', {
get () {},
set () {}
})
- 问题
- 对象直接新添加的属性或删除已有属性, 界面不会自动更新
- 直接通过下标替换元素或更新length, 界面不会自动更新 arr[1] = {}
Vue3的响应式
- 通过
Proxy(代理)
: 拦截对data任意属性的任意(13种)操作, 包括属性值的读写, 属性的添加, 属性的删除等… - 通过
Reflect(反射)
: 动态对被代理对象的相应属性进行特定的操作
相同功能:
-
Reflect.get()
、target[prop]
、Object.create(target)[prop]
选择其一即可 -
Reflect.set()
、target[prop] = value
-
Reflect.deleteProperty()
、delete target[prop]
const user = {
name: 'haha',
age: 10,
other: {
likes: 'sport',
ball: 'pingpang'
}
}
// user --- 目标对象
// handler --- 代理器对象,用来监视数据以及数据的操作
const userProxy = new Proxy(user, {
// handler.get() 方法用于拦截对象的读取属性操作
get(target, prop) {
console.log('调用get')
// return target[prop]
// return Object.create(target)[prop] // 访问原型链上的属性
return Reflect.get(target, prop)
},
// handler.set() 方法是设置属性值操作的捕获器
set(target, prop, value) {
console.log('调用set')
// target[prop] = value
return Reflect.set(target, prop, value)
},
// handler.deleteProperty() 方法用于拦截对对象属性的 delete 操作。
deleteProperty(target, prop) {
console.log('调用delete')
// delete target[prop]
return Reflect.deleteProperty(target, prop)
}
})
console.log(userProxy.name) // 调用get haha
userProxy.age = 20 // 调用set
console.log(userProxy.age) // 调用get 20
userProxy.gender = '男' // 调用set
console.log(userProxy.gender) // 调用get 男
delete userProxy.name // 调用delete
console.log(userProxy.name) // 调用get undefined
// 更新目标对象中的某个属性对象中的属性值 --- 自己实现的只遍历了一层,没有深度遍历,所以没有调用set方法(vue3有深度遍历)
// get调用后是怎么改变值的???????
userProxy.other.ball = 'basketball' // 调用get
console.log(userProxy.other.ball) // 调用get basketball
6、计算属性与监视
computed函数:
- 只设置get:
computed(()=> {...}
- get、set:
computed({ get() {...}, set() {...}})
watch函数:
-
监听一个数据:
watch(user, (val)=>{...}, { immediate: true, deep: true})
- val 可解构
{firstName, lastName}
immediate: true
立即监视,不加的话fullName3第一次不显示,只有改变firstName, lastName后才显示,默认falsedeep: true
深度监视,默认false
- val 可解构
-
监听多个数据,使用数组来指定:
- 响应式数据(ref、reactive对象):
watch([fullName3, user], () => {...})
- 非响应式数据时(reactive对象中的属性),需要写成回调函数:
watch([() => user.firstName, () => user.lastName], () => {...})
- 响应式数据(ref、reactive对象):
watchEffect函数:watchEffect(()=> {...})
- 不用直接指定要监视的数据, 回调函数中使用的哪些响应式数据就监视哪些响应式数据
- 默认直接执行,不用配置immediate
<template>
<h2>计算属性与监视</h2>
<fieldset>
<legend>操作</legend>
姓氏:<input type="text" placeholder="请输入姓氏" v-model="user.firstName"><br />
名字:<input type="text" placeholder="请输入名字" v-model="user.lastName"><br />
</fieldset>
<fieldset>
<legend>演示</legend>
computed get姓名:<input type="text" placeholder="显示姓名" v-model="fullName1"><br />
computed set姓名:<input type="text" placeholder="显示姓名" v-model="fullName2"><br />
watch姓名:<input type="text" placeholder="显示姓名" v-model="fullName3"><br />
</fieldset>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, ref, watch, watchEffect } from 'vue'
export default defineComponent({
name:'App',
setup() {
const user = reactive({
firstName: '诸葛',
lastName: '亮'
})
const fullName1 = computed(()=> { // 返回的是一个Ref类型的对象
return user.firstName + '_' + user.lastName
})
const fullName2 = computed({
get() {
return user.firstName + '_' + user.lastName
},
set(val: string) {
const temp = val.split('_')
user.firstName = temp[0]
user.lastName = temp[1]
}
})
const fullName3 = ref('')
watch(user, ({firstName, lastName})=>{
fullName3.value = firstName + '_' + lastName
}, { immediate: true, deep: true})
watchEffect(()=> {
// fullName3.value = user.firstName + '_' + user.lastName
const names = fullName3.value.split('_')
user.firstName = names[0]
user.lastName = names[1]
})
// watch可以监听多个数据
// watch([user.firstName, user.lastName], () => {
// // 代码未执行,因为user.firstName, user.lastName不是响应式的数据,不是由ref,reactive创建的
// console.log('执行了吗?没有')
// })
// watch监听非响应式数据时,需要写成回调函数
watch([() => user.firstName, () => user.lastName], () => {
console.log('执行了吗?执行了')
})
return { user, fullName1, fullName2, fullName3}
}
})
</script>
7、生命周期
与 2.x 版本生命周期相对应的组合式 API
beforeCreate
-> 使用setup()
created
-> 使用setup()
beforeMount
->onBeforeMount
mounted
->onMounted
beforeUpdate
->onBeforeUpdate
updated
->onUpdated
beforeDestroy
->onBeforeUnmount
destroyed
->onUnmounted
组合式 API 还提供了以下调试钩子函数(???未研究):
errorCaptured
->onErrorCaptured
renderTracked
->onRenderTracked
renderTriggered
->onRenderTriggered
activated
->onActivated
deactivated
->onDeactivated
父组件:
<template>
<h2>父组件</h2>
<button @click="isShow = !isShow">切换显示子组件,不显示时触发beforeUnmount/unmounted生命周期</button>
<hr />
<Child v-if="isShow"></Child>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import Child from './components/Child.vue'
export default defineComponent({
name:'App',
components: {
Child
},
setup() {
const isShow = ref(true)
return { isShow }
}
})
</script>
子组件:
<template>
<h2>子组件</h2>
<h3>msg: {{ msg }}</h3>
<button @click="update">更新数据,触发beforeUpdate/updated生命周期</button>
</template>
<script lang="ts">
import { defineComponent, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, onUpdated, ref } from 'vue'
export default defineComponent({
name:'Child',
// vue2的生命周期钩子
// 执行顺序: beforeCreate --- created --- beforeMount --- mounted --- beforeUpdate --- updated
// --- beforeDestroy被取消了,在vue3语法中用beforeUnmount替代 --- destroyed被取消了,在vue3语法中用unmounted替代
beforeCreate() { console.log('vue2 --- beforeCreate') },
created() { console.log('vue2 --- created') },
beforeMount() { console.log('vue2 --- beforeMount') },
mounted() { console.log('vue2 --- mounted') },
beforeUpdate() { console.log('vue2 --- beforeUpdate') },
updated() { console.log('vue2 --- updated') },
// The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead
// The `destroyed` lifecycle hook is deprecated. Use `unmounted` instead
beforeUnmount() { console.log('vue2 --- beforeDestroy被取消了,在vue3语法中用beforeUnmount替代') },
unmounted() { console.log('vue2 --- destroyed被取消了,在vue3语法中用unmounted替代') },
// vue3的生命周期
setup() {
// vue3 的生命周期先于vue2执行。setup包括了beforeCreate与created两个生命周期
console.log('vue3 --- setup')
const msg = ref('abc')
const update = () => {
msg.value += '--'
}
onBeforeMount(() => { console.log('vue3 --- beforeMount') })
onMounted(() => { console.log('vue3 --- mounted') })
onBeforeUpdate(() => { console.log('vue3 --- beforeUpdate') })
onUpdated(() => { console.log('vue3 --- updated') })
onBeforeUnmount(() => { console.log('vue3 --- onBeforeUnmount') })
onUnmounted(() => { console.log('vue3 --- onUnmounted') })
return { msg, update}
}
})
</script>
8、hook
- 使用Vue3的组合API封装的可复用的功能函数
- 自定义hook的作用类似于vue2中的mixin技术
- 自定义Hook的优势: 很清楚复用功能代码的来源, 更清楚易懂
显示点击页面的坐标
hooks/useMousePosition.ts:
import { onBeforeUnmount, onMounted, ref } from 'vue'
export default function() {
const x = ref(-1)
const y = ref(-1)
// 点击事件的回调函数
const clickHandler = (event: MouseEvent) => {
x.value = event.pageX
y.value = event.pageY
}
// 页面加载完毕,再进行点击的操作
onMounted(()=>{
window.addEventListener('click', clickHandler)
})
// 页面卸载之前清除点击事件
onBeforeUnmount(()=>{
window.removeEventListener('click', clickHandler)
})
return { x, y }
}
APP.vue使用:
<template>
<h2>自定义hook函数:显示点击页面的坐标</h2>
<h3>横坐标:{{ x }}, 纵坐标:{{ y }}</h3>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import useMousePosition from './hooks/useMousePosition'
export default defineComponent({
name:'App',
setup() {
const { x, y } = useMousePosition()
return { x, y }
}
})
</script>
axios请求
hooks/useRequest.ts:
import { ref } from 'vue'
import axios from 'axios'
export default function<T> (url: string) {
const loading = ref(true) // 加载状态
const data = ref<T | null>(null) // 请求成功时的数据, data数据可能是数组或者对象,用TS的泛型强化类型检查
const errorMsg = ref('') // 错误信息
axios.get(url).then(response => {
loading.value = false
data.value = response.data
}).catch(error => {
loading.value = false
errorMsg.value = error.message || '位置错误'
})
return { loading, data, errorMsg }
}
App.vue使用:
<template>
<h2>自定义hook函数:axios请求</h2>
<h3 v-if="loading">加载中...</h3>
<h3 v-else-if="errorMsg">请求不成功,原因:{{ errorMsg }}</h3>
<div v-else>
<h4>对象类型的数据: </h4>
<ul>
<li>对象id: {{ data.id }}</li>
<li>对象名字: {{ data.name }}</li>
<li>对象年龄: {{ data.age }}</li>
</ul>
</div>
<div>
<h4>数组类型的数据: </h4>
<ul v-for="item in data" :key="item.id">
<li>id: {{ item.id }}</li>
<li>名字: {{ item.name }}</li>
<li>年龄: {{ item.age }}</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, watch } from 'vue'
import useRequest from './hooks/useRequest'
// 定义接口约束数组与对象的类型
interface ObjAndArray {
id: number,
name: string,
age: number
}
export default defineComponent({
name:'App',
setup() {
// const { loading, data, errorMsg} = useRequest<ObjAndArray>('/data/obj.json')
const { loading, data, errorMsg} = useRequest<ObjAndArray[]>('/data/array.json')
// 监视data数据,返回数组长度
watch(data, ()=>{
if(data.value) {
console.log(data.value.length)
}
})
return { loading, data, errorMsg }
}
})
</script>
9、toRefs
问题: reactive
对象取出的所有属性值都是非响应式
的
解决: 利用 toRefs
可以将一个响应式 reactive 对象
的所有原始属性转换为响应式的 ref 属性
应用: 当从合成函数返回响应式对象时,toRefs
非常有用,这样消费组件就可以在不丢失响应式的情况下对返回的对象进行分解使用
<template>
<h2>toRefs</h2>
<div style="border: 1px solid red;margin-bottom: 10px;">
<h3>属性值写成state1.name1</h3>
<span class="box">name1: {{ state1.name1 }}</span>
<span class="box">age1: {{ state1.age1 }}</span>
</div>
<div style="border: 1px solid green;margin-bottom: 10px;">
<h3>属性值写成name2</h3>
<span class="box">name2: {{ name2 }}</span>
<span class="box">age2: {{ age2 }}</span>
</div>
<div style="border: 1px solid blue;margin-bottom: 10px;">
<h3>属性值写成hook的name3</h3>
<span class="box">name3: {{ name3 }}</span>
<span class="box">age3: {{ age3 }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
// hook函数
function useState() {
const state3 = reactive({
name3: '我是3',
age3: 133
})
return {
...toRefs(state3)
}
}
export default defineComponent({
name:'App',
setup() {
const state1 = reactive({
name1: '我是1',
age1: 14
})
const state2 = reactive({
name2: '我是2',
age2: 178
})
// state2 --- Proxy {name2: '我是2', age2: 178}
// toRefs(state2) --- {name2: ObjectRefImpl, age2: ObjectRefImpl}
// toRefs把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref
const { name2, age2 } = toRefs(state2)
const { name3, age3 } = useState()
// 使用定时器更新数据,如果数据变化,页面随之变化就是响应式的数据
setInterval(()=>{
state1.name1 += '-1'
// state2.name2 += '-2' // 不是响应式的数据了
name2.value += '-2'
name3.value += '-3'
},1000)
return {
state1,
// ...state2 // 不是响应式的数据了,所以在setInterval里name2不会变化。用toRefs变成响应式数据
name2,
age2,
name3,
age3,
}
}
})
</script>
<style>
.box {
display: block;
margin-bottom: 10px;
}
</style>
组合Composition API(其他部分)
1、shallowReactive 与 shallowRef
- shallowReactive : 只处理了对象内最外层属性的响应式(也就是浅响应式)
- shallowRef: 只处理了
value
的响应式, 不进行对象的reactive
处理 - 什么时候用浅响应式呢?
- 一般情况下使用ref和reactive即可
- 如果有一个对象数据, 结构比较深, 但变化时只是外层属性变化 ===> shallowReactive
- 如果有一个对象数据, 后面会产生新的对象来替换 ===> shallowRef
<template>
<h2>shallowReactive 与 shallowRef</h2>
<h5>reactive深度响应 - m1: {{ m1 }}</h5>
<h5>shallowReactive浅响应式 - m2: {{ m2 }}</h5>
<h5>ref深度响应 - m3: {{ m3 }}</h5>
<h5>shallowRef浅响应式 - m4: {{ m4 }}</h5>
<button @click="update">更新数据</button>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, shallowReactive, shallowRef } from 'vue'
export default defineComponent({
name:'App',
setup() {
const m1 = reactive({
name: 'm1',
other: {
car: 'BBW',
likes: ['sports','balls']
}
})
const m2 = shallowReactive({
name: 'm2',
other: {
car: 'BBW',
likes: ['sports','balls']
}
})
const m3 = ref({
name: 'm3',
other: {
car: 'BBW',
likes: ['sports','balls']
}
})
const m4 = shallowRef({
name: 'm4',
other: {
car: 'BBW',
likes: ['sports','balls']
}
})
const update = () => {
// m1数据均改变
// m1.name += '-',
// m1.other.car += '='
// m1.other.likes[2] = 'swim'
// m2仅name改变,其他未变
// m2.name += '-'
// m2.other.car += '='
// m2.other.likes[2] = 'swim'
// m3均改变
// m3.value.name += '-'
// m3.value.other.car += '='
// m3.value.other.likes[2] = 'swim'
// m4均不会改变
m4.value.name += '-'
m4.value.other.car += '='
m4.value.other.likes[2] = 'swim'
// console.log(m3,'-',m4) // m3的value类型是Proxy,m4的value类型是Object
}
return { m1, m2, m3, m4, update}
}
})
</script>
2、readonly 与 shallowReadonly
- readonly:深度只读数据
- 获取一个对象 (响应式或纯对象) 或 ref 并返回原始代理的只读代理。
- 只读代理是深层的:访问的任何嵌套 property 都是只读的。
- shallowReadonly:浅只读数据
- 创建一个代理,使其
自身的 property 为只读
,但不执行嵌套对象的深度只读转换 reactive
:自身属性是只读的,嵌套属性可改变数据ref
:所有属性都可改变数据
- 创建一个代理,使其
- 应用场景:
- 在某些特定情况下, 我们可能不希望对数据进行更新的操作, 那就可以包装生成一个只读代理对象来读取数据, 而不能修改或删除
<template>
<h2>readonly 与 shallowReadonly</h2>
<h4>state1: {{ state1 }}</h4>
<h4>state3: {{ state3 }}</h4>
<button @click="update">更新数据</button>
</template>
<script lang="ts">
import { defineComponent, reactive, readonly, ref, shallowReadonly } from 'vue'
export default defineComponent({
name:'App',
setup() {
const state = reactive({
name: 'jjjj',
cars: {
name: 'nnn'
}
})
const state2 = ref({
name: 'hhhh',
cars: {
name: 'cds'
}
})
// const state1 = readonly(state) // 深度只读
const state1 = shallowReadonly(state) // 浅只读,reactive自身属性是只读的,嵌套属性可改变数据
// const state3 = readonly(state2) // 深度只读
const state3 = shallowReadonly(state2) // 浅只读,ref所有属性都可改变数据
const update = () => {
state1.name += '-' // readonly-reactive: 只读; shallowReadonly-reactive: 只读
state3.value.name += '-' // readonly-ref: 只读; shallowReadonly-ref: 改变数据
// state1.cars.name += '-' // readonly-reactive: 只读; shallowReadonly-reactive: 改变数据
// state3.value.cars.name += '-' // readonly-ref: 只读; shallowReadonly-ref: 改变数据
}
return { state1, state3, update }
}
})
</script>
3、toRaw 与 markRaw
- toRaw
- 把代理对象变成普通对象,
数据变化,页面不变化
- 把代理对象变成普通对象,
- markRaw
- 标记的对象数据,以后都不能再成为代理对象了,
数据变化,页面不变化
- 应用场景:
- 有些值不应被设置为响应式的,例如复杂的第三方类实例或 Vue 组件对象。
- 当渲染具有不可变数据源的大列表时,跳过代理转换可以提高性能。
- 标记的对象数据,以后都不能再成为代理对象了,
<template>
<h2>toRaw 与 markRaw</h2>
<h3>state: {{ state }}</h3>
<button @click="updateToRaw">更新toRaw</button>
<button @click="updateMarkRaw">更新markRaw</button>
</template>
<script lang="ts">
import { defineComponent, markRaw, reactive, toRaw } from 'vue'
interface UserInfo {
name: string
others: string[]
gender?: string[]
}
export default defineComponent({
name:'App',
setup() {
const state = reactive<UserInfo>({
name: '123',
others: ['bba','MMW']
})
const updateToRaw = () => {
// toRaw 把代理对象变成普通对象,数据变化,页面不变化
const user = toRaw(state)
user.name += '-'
console.log('toraw',user)
}
const updateMarkRaw = () => {
const gender = ['男','女']
// markRaw 标记的对象数据,以后都不能再成为代理对象了,数据变化页面不会变化
state.gender = markRaw(gender)
setInterval(()=> {
state.gender[0] += '+1'
console.log(state.gender)
},1000)
}
return { state, updateToRaw, updateMarkRaw}
}
})
</script>
4、toRef
为源响应式对象上的某个属性创建一个ref对象
, 二者内部操作的是同一个数据值
, 更新时二者是同步的
与ref
的区别: 拷贝了一份新的数据值单独操作, 更新时互不影响
应用: 当要将 某个prop
的 ref
传递给复合函数时,toRef
很有用
父组件:
<template>
<h2>toRef</h2>
<h3>state: {{ state }}</h3>
<h3>toRef-age: {{ age }}</h3>
<h3>ref-money: {{ money }}</h3>
<button @click="update">更新数据</button>
<hr />
<!-- 传给子组件的是age.value,在html中ref可以省略value。所以传给子组件的是age值,而不是age的ref对象 -->
<Child :msg="age"></Child>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, toRef } from 'vue'
import Child from './components/Child.vue'
export default defineComponent({
name:'App',
components: {
Child
},
setup() {
const state = reactive({
age: 0,
money: 0
})
const age = toRef(state, 'age') // 把响应式数据state对象中的属性age变成了ref对象
const money = ref(state.money) // 把响应式数据state对象中的属性money使用ref进行包装,变成了一个ref对象
const update = () => {
state.age += 20 // state里的age变化,age随之变化
// state.money += 2 // state里的money变化,money不会变化
// age.value += 2 // age变化时,state里的age会随之变化
// money.value += 10 // money变化时,state里的money不会变化
}
return { state, age, money, update }
}
})
</script>
子组件:
<template>
<h2>Child</h2>
<h3>父组件传过来的age: {{msg}}</h3>
<h3>age长度:{{ length }}</h3>
</template>
<script lang="ts">
import { computed, defineComponent, Ref, toRef } from 'vue'
// 假设定义的获取长度的hook函数要求传值的参数类型是ref对象
function useLength(age: Ref) {
// 数据变化时,立即计算长度
return computed(()=>{
return age.value.toString().length
})
}
export default defineComponent({
name:'Child',
props: ['msg'],
setup(props) {
const length = useLength(toRef(props,'msg'))
return { length }
}
})
</script>
5、customRef
创建一个自定义的 ref
,并对其依赖项跟踪
和更新触发
进行显式控制
需求: 使用 customRef 实现 debounce 的示例
<template>
<h2>customRef自定义防抖ref</h2>
<input type="text" v-model="keyword" />
<p>{{ keyword }}</p>
</template>
<script lang="ts">
import { customRef, defineComponent, ref } from 'vue'
// 自定义hook防抖函数
function useDebouncedRef<T>(value: T, delay=200) {
let timeout: number // 存储定时器变量
return customRef((track, trigger)=>{
return {
get() {
track() // 告诉vue追踪数据
return value
},
set(newValue:T) {
clearTimeout(timeout) // 清理定时器
timeout = setTimeout(()=>{ // 开启定时器
value = newValue
trigger() // 告诉vue更新页面
},delay)
}
}
})
}
export default defineComponent({
name:'App',
setup() {
// const keyword = ref('abc') // input输入框里的数据一变化,keyword就随之改变
const keyword = useDebouncedRef('abc', 500) // 防抖,input输入框输入数据时,keyword等500ms再变化
return { keyword }
}
})
</script>
6、provide 与 inject
provide
和inject
提供依赖注入,功能类似 2.x 的provide/inject
- 只能在使用当前活动实例的
setup()
期间被调用。
实现跨层级组件(祖孙)间通信:
父组件:
<template>
<h2>provide inject</h2>
<h3>颜色: {{ color }}</h3>
<button @click="color='red'">红色</button>
<button @click="color='green'">绿色</button>
<button @click="color='pink'">粉色</button>
<hr>
<Son />
</template>
<script lang="ts">
import { defineComponent, provide, ref } from 'vue'
import Son from './components/Child.vue'
export default defineComponent({
name:'App',
components: { Son },
setup() {
const color = ref('blue')
provide('color',color)
return { color }
}
})
</script>
子组件:
<template>
<h2>Son</h2>
<hr>
<Grandson />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Grandson from './Grandson.vue'
export default defineComponent({
name:'Son',
components: { Grandson }
})
</script>
孙组件:
<template>
<h2 :style="{color: color}">Grandson</h2>
<h2 :style="{color}">Grandson简写style样式</h2>
</template>
<script lang="ts">
import { defineComponent, inject } from 'vue'
export default defineComponent({
name:'Grandson',
setup() {
return {
color: inject('color')
}
}
})
</script>
7、响应式数据的判断
isRef
: 检查一个值是否为一个ref
对象isReactive
: 检查一个对象是否是由reactive
创建的响应式代理isReadonly
: 检查一个对象是否是由readonly
创建的只读代理isProxy
: 检查一个对象是否是由reactive
或者readonly
方法创建的代理
<template>
<h2>响应式数据的判断 </h2>
<h3>isRef: 检查一个值是否为一个 ref 对象</h3>
<h3>isReactive: 检查一个对象是否是由 reactive 创建的响应式代理</h3>
<h3>isReadonly: 检查一个对象是否是由 readonly 创建的只读代理</h3>
<h3>isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理</h3>
</template>
<script lang="ts">
import { defineComponent, isProxy, isReactive, isReadonly, isRef, reactive, readonly, ref } from 'vue'
export default defineComponent({
name:'App',
setup() {
console.log(isRef(ref('')))
console.log(isReactive(reactive({})))
console.log(isReadonly(readonly({})))
console.log(isProxy(reactive({})))
console.log(isProxy(readonly({})))
return {}
}
})
</script>
手写组合API
1、shallowReactive与 reactive
浅响应与深响应
Object.keys()
方法会返回一个由一个给定对象的自身可枚举属性组成的数组
-
target
为{name: 'test3',others: {likes: ['jk','gff'],aa: 's'}}
-
Object.keys(target)
为数组["name", "others"]
,由对象的属性名组成的字符串数组
const handlerReactive = {
get(target,prop) {
console.log('get')
return Reflect.get(target,prop)
},
set(target,prop,value) {
console.log('set')
return Reflect.set(target,prop,value)
},
deleteProperty(target,prop) {
console.log('delete')
return Reflect.defineProperty(target,prop)
}
}
function shallowReactive(target) {
// object类型(数组/对象)
if(target && typeof target === 'object') {
return new Proxy(target, handlerReactive)
} else {
// 基本数据类型,直接返回值
return target
}
}
function reactive(target) {
if(target && typeof target === 'object') {
// 数组
if(Array.isArray(target)) {
target.forEach((item, index) => {
target[index] = reactive(item)
})
} else {
// 对象
Object.keys(target).forEach(key => {
target[key] = reactive(target[key])
})
}
return new Proxy(target, handlerReactive)
} else {
return target
}
}
测试:
const test1 = shallowReactive({
name: 'shallow',
others: {
like: 'hgfd'
}
})
// test1.name += '=' // get set
// test1.others.like = '000' // get
// delete test1.name // delete
// delete test1.others.like // get
const test2 = reactive({
name: 'reac',
others: {
like: 'adf'
}
})
// test2.name += '=' // get set
// test2.others.like = '000' // get set
// delete test2.name // delete
// delete test2.others.like // get delete
2、shallowReadonly 与 readonly
浅只读与深只读
const handlerReadonly = {
get(target,prop) {
console.log('get只读',target)
return Reflect.get(target, prop)
},
set(target) {
console.log('set只读',target)
return true
},
deleteProperty(target) {
console.log('delete只读',target)
return true
}
}
function shallowReadonly(target) {
if(target && typeof target === 'object') {
return new Proxy(target,handlerReadonly)
} else {
return target
}
}
function readonly(target) {
if(target && typeof target === 'object') {
if(Array.isArray(target)) {
target.forEach((item,index) => {
target[index] = readonly(item)
})
} else {
Object.keys(target).forEach(key => {
target[key] = readonly(target[key])
})
}
return new Proxy(target,handlerReadonly)
} else {
return target
}
}
测试:
const test3 = shallowReadonly({
name: 'test3',
others: {
likes: ['jk','gff']
}
})
// console.log(test3.name) // get只读
// test3.name += '-' // get只读 set只读(不能修改,数据没有变化)
// delete test3.name // delete只读(不能删除,数据没有变化)
// test3.others.likes[4] = 'iuyf' // get只读,不调用set(可以修改)
// delete test3.others.likes[0] // get只读,不调用deleteProperty(可以删除)
const test4 = readonly({
name: 'test4',
others: {
likes: ['jk','gff']
}
})
// console.log(test4.name) // get只读
// test4.name += '-' // get只读 set只读(不能修改,数据没有变化)
// test4.others.likes[4] = 'iuyf' // get只读 get只读 set只读(不能修改,数据没有变化)
// delete test4.name // delete只读(不能删除,数据没有变化)
// delete test4.others.likes[0] // get只读 get只读 delete只读(不能删除,数据没有变化)
3、shallowRef 与 ref
function shallowRef(target) {
// ref 返回一个对象
return {
_value: target,
get value() {
console.log('get ref')
return this._value
},
set value(val) {
console.log('set ref')
this._value = val
}
}
}
function ref(target) {
target = reactive(target)
return {
_value: target,
get value() {
console.log('get ref')
return this._value
},
set value(val) {
console.log('set ref')
this._value = val
}
}
}
测试:
const test5 = shallowRef({
name: 'test5',
others: {
likes: ['jk','gff']
}
})
// console.log(test5.value) // get ref
// test5.value = '-' // set ref(劫持到)
// test5.value.others = '=' // get ref(劫持不到)
const test6 = ref({
name: 'test6',
others: {
likes: ['jk','gff']
}
})
// console.log(test6.value) // get ref
// test6.value = '-' // set ref(劫持到)
// test6.value.others = '=' // get ref - set(返回的是reactive中的set操作)(劫持到)
4、isRef, isReactive , isReadonly, isProxy
- 定义的 ref 函数返回对象中增加属性
_is_ref: true,
标识是否是 ref 对象 - handlerReactive 中的get方法增加
if(prop === '_is_reactive') return true
标识是否是 reactive 对象 - handlerReadonly 中的get方法增加
if(prop === '_is_readonly') return true
标识是否是readonly
function isRef(target) {
return target && target._is_ref
}
function isReactive(target) {
return target && target._is_reactive
}
function isReadonly(target) {
return target && target._is_readonly
}
function isProxy(target) {
return isReactive(target) || isReadonly(target)
}
新组件
1、Fragment(片断)
- 在Vue2中: 组件必须有一个
根标签
- 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个
Fragment虚拟元素
中 - 好处: 减少标签层级, 减小内存占用
2、Teleport(瞬移)
Teleport 提供了一种干净的方法, 让 组件的html 在 父组件界面外 的 特定标签 (很可能是body)下插入显示
例子:子组件中的按钮控制子组件的弹窗显示
父组件:
<template>
<h2>APP父组件</h2>
<Child />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Child from './components/Child.vue'
export default defineComponent({
name:'App',
components: { Child }
})
</script>
子组件:
- 不加Teleport,弹窗的div在
<div id="app">
里面,与父组件的<h2>APP父组件</h2>
并列。 - 加
<Teleport to="body">
弹窗的div在<body>
里面与id="app"
并列 - 用Teleport必须加
to="..."
表示把 html 放到哪个标签下
<template>
<button @click="showModal=true">显示弹窗</button>
<Teleport to="body">
<div v-if="showModal">
这是子组件弹窗
<button @click="showModal=false">关闭弹窗</button>
</div>
</Teleport>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
name:'Son',
setup() {
const showModal = ref(false)
return {
showModal
}
}
})
</script>
3、Suspense(不确定的)
应用程序在等待异步组件时渲染一些后备内容,可以创建一个平滑的用户体验
vue2与vue3动态引入
vue2:
const AsyncComp = () => import('./AsyncComp.vue')
- 在vue3中写vue2会报警告:
Invalid VNode type: undefined (undefined)
vue3:
const AsyncComp = defineAsyncComponent(() => import('./AsyncComp.vue'))
- defineAsyncComponent需引入
- AsyncComp 组件不用 setup 返回值的时候使用,若 AsyncComp 返回 promise 直接
静态引入
父组件:
使用Suspense放异步组件时需要放到一个根标签中,否则页面不会显示。
报警告:<Suspense> slots expect a single root node
<template>
<h2>APP父组件: suspense</h2>
<Suspense>
<template #default>
<div>
<AsyncComp />
<AsyncData />
</div>
</template>
<template v-slot:fallback>
<div style="width: 200px; height: 100px; border: 1px solid red; background-color: green;">
Loading...
</div>
</template>
</Suspense>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue'
// vue3 静态引入,AsyncComp组件用setup返回promise
import AsyncComp from './AsyncComp.vue'
import AsyncData from './AsyncData.vue'
export default defineComponent({
name:'App',
components: {
AsyncComp,
AsyncData
}
})
</script>
AsyncComp.vue:
<template>
<h2>AsyncComp</h2>
<h3>{{ msg }}</h3>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name:'AsyncComp',
setup() {
return new Promise ((resolve,reject)=> {
setTimeout(()=>{
resolve({
msg: 'daasdfghj'
})
},500)
})
}
})
</script>
AsyncData.vue:
<template>
<h2>AsyncData</h2>
<ul v-for="item in data" :key="item.id">
<li>{{item.id}} - {{ item.name }} - {{ item.age }}</li>
</ul>
</template>
<script lang="ts">
import axios from 'axios'
import { defineComponent } from 'vue'
export default defineComponent({
name:'AsyncData',
// setup() {
// return axios.get('/data/array.json').then(response => {
// return {
// data: response.data
// }
// })
// }
async setup() {
const result = await axios.get('/data/array.json')
return {
data: result.data
}
}
})
</script>
案例
输入框输入数据按回车后,生成一个已输入数据为标题的多选框,鼠标经过每栏多选框背景变色显示“删除”按钮;显示已选项与总项,包括全选与全不选,也可以清除所有已选项
技术点:div水平居中;li标签鼠标经过事件;父子组件与爷孙组件传值;本地缓存