- 实现原理:
- 通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
//源数据 let person = { name:'张三', age:18 } //模拟Vue3中实现响应式 //Proxy对属性的增删改查都可以监测得到 //#region const p = new Proxy(person,{ //有人读取p的某个属性时调用 get(target,propName){ console.log(`有人读取了p身上的${propName}属性`) return target[propName] }, //有人修改p的某个属性、或给p追加某个属性时调用 set(target,propName,value){ console.log(`有人修改了p身上的${propName}属性,我要去更新界面了!`) target[propName] = value }, //有人删除p的某个属性时调用 deleteProperty(target,propName){ console.log(`有人删除了p身上的${propName}属性,我要去更新界面了!`) return delete target[propName] } }) //#endregion
- 通过Reflect(反射): 对源对象的属性进行操作。
// Reflect(反射) // 读取对象指定属性的值 Reflect.get(object, '属性名') // 修改对象指定属性的值 Reflect.set(object, '属性名', '新属性值') // 删除对象指定属性 Reflect.deleteProperty(object, '属性名')
//模拟Vue3中实现响应式 //#region const p = new Proxy(person,{ //有人读取p的某个属性时调用 get(target,propName){ console.log(`有人读取了p身上的${propName}属性`) return Reflect.get(target,propName) }, //有人修改p的某个属性、或给p追加某个属性时调用 set(target,propName,value){ console.log(`有人修改了p身上的${propName}属性,我要去更新界面了!`) Reflect.set(target,propName,value) }, //有人删除p的某个属性时调用 deleteProperty(target,propName){ console.log(`有人删除了p身上的${propName}属性,我要去更新界面了!`) return Reflect.deleteProperty(target,propName) } }) //#endregion
- MDN文档中描述的Proxy与Reflect:
- Proxy:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
- Reflect:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
new Proxy(data, { // 拦截读取属性值 get (target, prop) { return Reflect.get(target, prop) }, // 拦截设置属性值或添加新属性 set (target, prop, value) { return Reflect.set(target, prop, value) }, // 拦截删除属性 deleteProperty (target, prop) { return Reflect.deleteProperty(target, prop) } }) proxy.name = 'tom'
🥽 reactive对比ref
- 从定义数据角度对比:
- ref用来定义:基本类型数据。
- reactive用来定义:对象(或数组)类型数据。
- 备注:ref也可以用来定义对象(或数组)类型数据, 它内部会自动通过
reactive
转为代理对象。
- 从原理角度对比:
- ref通过
Object.defineProperty()
的get
与set
来实现响应式(数据劫持)。 - reactive通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据。
- 从使用角度对比:
- ref定义的数据:操作数据需要
.value
,读取数据时模板中直接读取不需要.value
。 - reactive定义的数据:操作数据与读取数据:均不需要
.value
。
- 一般将数据封装在一个data对象中,利用reactive函数将该对象变为响应式数据对象
🥽 setup的两个注意点
- setup执行的时机
- 在beforeCreate之前执行一次,this是undefined,在setup无法使用this。
- setup的参数
- props:值为对象,包含:组件外部传递过来,且组件内部声明接收了的属性。
- context:上下文对象
- attrs: 值为对象,包含:组件外部传递过来,但没有在props配置中声明接收的属性, 相当于
this.$attrs
。存放没有被组件props配置项接收的数据,如果组件外部传递过来的数据都被组件props配置项接收,则该对象为空。 - slots: 收到的插槽内容, 相当于
this.$slots
。Vue3中具名插槽使用v-slot:插槽名
- emit: 分发自定义事件的函数, 相当于
this.$emit
。Vue3绑定的自定义事件在组件中需要使用emits配置项接收。
App.vue
<template> <Demo @hello="showHelloMsg" msg="你好啊" school="尚硅谷"> <!-- Vue3中具名插槽使用 `v-slot:插槽名` --> <template v-slot:qwe> <span>尚硅谷</span> </template> <template v-slot:asd> <span>尚硅谷</span> </template> </Demo> </template> <script> import Demo from './components/Demo' export default { name: 'App', components:{Demo}, setup(){ // 自定义事件的处理函数 function showHelloMsg(value){ alert(`你好啊,你触发了hello事件,我收到的参数是:${value}!`) } return { showHelloMsg } } } </script>
Demo.vue
<template> <h1>一个人的信息</h1> <h2>姓名:{{person.name}}</h2> <h2>年龄:{{person.age}}</h2> <button @click="test">测试触发一下Demo组件的Hello事件</button> </template> <script> import {reactive} from 'vue' export default { name: 'Demo', props:['msg','school'], emits:['hello'], // 绑定的自定义事件在组件中需要使用emits配置项接收 setup(props,context){ // console.log('---setup---',props) // console.log('---setup---',context) // console.log('---setup---',context.attrs) //相当与Vue2中的$attrs // console.log('---setup---',context.emit) //触发自定义事件的。 console.log('---setup---',context.slots) //插槽 //数据 let person = reactive({ name:'张三', age:18 }) //方法 function test(){ // 触发自定义事件 context.emit('hello',666) } //返回一个对象(常用) return { person, test } } } </script>
🥽 computed函数
- 与Vue2.x中computed配置功能一致,在Vue3中可以使用Vue2中的写法(向下兼容)。在Vue3中计算属性使用computed函数。
- 写法
import {computed} from 'vue' setup(){ ... //计算属性——简写 let fullName = computed(()=>{ return person.firstName + '-' + person.lastName }) //计算属性——完整 let fullName = computed({ get(){ return person.firstName + '-' + person.lastName }, set(value){ const nameArr = value.split('-') person.firstName = nameArr[0] person.lastName = nameArr[1] } }) }
<template> <h1>一个人的信息</h1> 姓: <input type="text" v-model="person.firstName"><br/> 名: <input type="text" v-model="person.lastName"><br/> <p>全名: {{person.fullName}}</p> </template> <script> import {reactive, computed} from 'vue' export default { name: 'Demo', setup(){ //数据 let person = reactive({ firstName: '张', lastName: '三' }) // 计算属性 person.fullName = computed(()=>{ return person.firstName + '-' + person.lastName }) //返回一个对象 return { person, } } } </script>
🥽 watch函数
- 与Vue2.x中watch配置功能一致,在Vue3中可以使用Vue2中的写法(向下兼容)。
- 两个小“坑”:
- 监视reactive定义的响应式数据时:oldValue无法正确获取、强制开启了深度监视(deep配置失效)。
- 监视reactive定义的响应式数据中某个属性时:deep配置有效。
//数据 let sum = ref(0) let msg = ref('你好啊') let person = reactive({ name:'张三', age:18, job:{ j1:{ salary:20 } } }) //情况一:监视ref定义的响应式数据 // 第一个参数为监视的数据,第二个参数为回调函数,第三个参数为配置属性 watch(sum,(newValue,oldValue)=>{ console.log('sum变化了',newValue,oldValue) },{immediate:true}) //情况二:监视多个ref定义的响应式数据 // newValue,oldValue为数组类型数据 watch([sum,msg],(newValue,oldValue)=>{ console.log('sum或msg变化了',newValue,oldValue) }) /* 情况三:监视reactive定义的响应式数据 若watch监视的是reactive定义的响应式数据,则无法正确获得oldValue!! 如果需要获取监视对象中某个属性的oldValue,可以采用将该属性单独取出使用ref的形式 若watch监视的是reactive定义的响应式数据,则强制开启了深度监视 */ // 如果监视的对象为使用ref函数实现响应式,则需要监视`变量.value`,对于对象类型数据ref借助了reactive watch(person,(newValue,oldValue)=>{ console.log('person变化了',newValue,oldValue) },{immediate:true,deep:false}) //此处的deep配置不再奏效 //情况四:监视reactive定义的响应式数据中的某个属性 watch(()=>person.job,(newValue,oldValue)=>{ console.log('person的job变化了',newValue,oldValue) },{immediate:true,deep:true}) //情况五:监视reactive定义的响应式数据中的某些属性 watch([()=>person.job,()=>person.name],(newValue,oldValue)=>{ console.log('person的job变化了',newValue,oldValue) },{immediate:true,deep:true}) //特殊情况 // 监视reactive所定义的对象中的某个属性,且该属性也为对象类型 watch(()=>person.job,(newValue,oldValue)=>{ console.log('person的job变化了',newValue,oldValue) },{deep:true}) //此处由于监视的是reactive所定义的对象中的某个属性,所以deep配置有效
- 对于使用ref函数定义的数据,如果为基本数据类型,则使用watch监视时,不使用
变量.value
的形式,因为不能监视一个字面量,监视的需要为一个存放数据的结构;如果为对象类型,此时value的值为Proxy对象,如果没有开启深度监视,只要value对应的对象的引用没有改变相当于值没有修改,解决方法:
- 开启深度监视,监视Proxy对象中属性的变化
- 监视
变量.value
,由于Proxy对象时ref函数借助reactive函数生成的,监视变量.value
相当于监视reactive函数生成的Proxy对象
🥽 watchEffect函数
- watch的套路是:既要指明监视的属性,也要指明监视的回调。
- watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。
- watchEffect有点像computed:
- 两个函数中所依赖的数据发生变化,都会重新执行一次回调。
- 但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
- 而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。
//watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。 watchEffect(()=>{ const x1 = sum.value const x2 = person.age console.log('watchEffect配置的回调执行了') })
🥽 生命周期
- Vue3.0中可以继续使用Vue2.x中的生命周期钩子,但有有两个被更名:
beforeDestroy
改名为beforeUnmount
destroyed
改名为unmounted
App.vue
<template> <button @click="isShowDemo = !isShowDemo">切换隐藏/显示</button> <Demo v-if="isShowDemo"/> </template> <script> import {ref} from 'vue' import Demo from './components/Demo' export default { name: 'App', components:{Demo}, setup() { let isShowDemo = ref(true) return {isShowDemo} } } </script>
- Demo.vue
<template> <h2>当前求和为:{{sum}}</h2> <button @click="sum++">点我+1</button> </template> <script> import { ref } from 'vue' export default { name: 'Demo', setup() { //数据 let sum = ref(0) //返回一个对象(常用) return { sum } }, //通过配置项的形式使用生命周期钩子 //#region 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---') } //#endregion } </script>
- Vue3.0也提供了 Composition API 形式的生命周期钩子,与Vue2.x中钩子对应关系如下:
beforeCreate
===>setup()
created
=======>setup()
beforeCreate
与created
没有对应组合式API,setup()
相当于beforeCreate
与created
,对于配置项setup()
的执行时机早于配置项beforeCreate
与created
。beforeMount
===>onBeforeMount
mounted
=======>onMounted
beforeUpdate
===>onBeforeUpdate
updated
=======>onUpdated
beforeUnmount
==>onBeforeUnmount
unmounted
=====>onUnmounted
- 如果组合式API的生命周期与配置项形式的生命周期一起写,组合式API的生命周期的执行时机优先于配置项形式的生命周期。
- 一般情况下统一使用组合式API的生命周期,或者统一使用配置项形式的生命周期
<template> <h2>当前求和为:{{sum}}</h2> <button @click="sum++">点我+1</button> </template> <script> import {ref,onBeforeMount,onMounted,onBeforeUpdate,onUpdated,onBeforeUnmount,onUnmounted} from 'vue' export default { name: 'Demo', setup(){ console.log('---setup---') //数据 let sum = ref(0) //通过组合式API的形式去使用生命周期钩子 onBeforeMount(()=>{ console.log('---onBeforeMount---') }) onMounted(()=>{ console.log('---onMounted---') }) onBeforeUpdate(()=>{ console.log('---onBeforeUpdate---') }) onUpdated(()=>{ console.log('---onUpdated---') }) onBeforeUnmount(()=>{ console.log('---onBeforeUnmount---') }) onUnmounted(()=>{ console.log('---onUnmounted---') }) //返回一个对象(常用) return {sum} }, //通过配置项的形式使用生命周期钩子 //#region 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---') }, //#endregion } </script>