Vue3
- Vue3简介
- 创建Vue3应用
- 分析工程结构
- 拉开序幕的
setup函数 ref函数reactive函数Vue3响应式原理reactive和ref的对比setup函数注意点computed计算属性函数watch监视函数watchEffect函数- 生命周期
HooktoRef和toRefsshallowReactive和shallowRefreadonly和shallowReadonly- 深度数据和浅度数据的理解
toRaw和markRawcustomRef函数: 自定义refprovide与inject函数: 实现祖先组件向后代组件通信- 响应式数据的判断
- Composition API的优势
- vue3模板中可以直接使用
$emit、$attrs、$slots、$refs等 - vue3为子组件添加系统事件
- 新的组件
Suspense组件- Vue3中其他改变
Vue3简介
优势
1.打包体积减少
2.性能提升
3.占用内存减少
4.支持TS
源码升级
1.使用Proxy代替defineProperty,实现数据响应式
2.重写虚拟DOM的实现和Tree-Shaking
Tree-Shaking:代码摇树,即摇掉无用的代码,比如:我们定义了一个模块,里面有10个方法,但是只用到了3个,那么Tree-Shaking就会把10个方法摇掉,只保留3个方法。
新特性
1.composition API(组合式API)
- setup配置
- ref和reactive
- computed和watch
- watch与watchEffect
- provide和inject
2.新的内置组件
- Fragment
- Teleport
- Suspense
3.其他改变
- 新的声明周期钩子
- 移除了keyCode值作为键盘事件修饰符,例如,
@keyup.13已不支持
创建Vue3应用
前提:
1.node.js version >= 18.3
2.vue-cli version >= 4.5.0 脚手架版本大于4.5.0才能创建Vue3应用
# 查看node版本
➜ vue_demo git:(master) node -v
v23.11.0
# 查看@vue-cli版本
➜ vue_demo git:(master) vue -V
@vue/cli 5.0.8
# 如果小于4.5则重新安装
➜ vue_demo git:(master) npm install -g @vue/cli
脚手架vue-cli创建
使用脚手架创建vue3应用,选择Vue3创建
➜ vue_demo git:(master) vue create vue3-test
Vue CLI v5.0.8
? Please pick a preset: (Use arrow keys)
❯ Default ([Vue 3] babel, eslint)
Default ([Vue 2] babel, eslint)
Manually select features
########################################
Vue CLI v5.0.8
✨ Creating project in /Users/cancanliu/Documents/code/vue_demo/vue3-test.
⚙️ Installing CLI plugins. This might take a while...
yarn install v1.22.19
info No lockfile found.
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
success Saved lockfile.
✨ Done in 8.38s.
🚀 Invoking generators...
📦 Installing additional dependencies...
yarn install v1.22.19
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
✨ Done in 4.53s.
⚓ Running completion hooks...
📄 Generating README.md...
🎉 Successfully created project vue3-test.
👉 Get started with the following commands:
$ cd vue3-test
$ yarn serve
安装时错误
error @achrinza/node-ipc@9.2.9: The engine "node" is incompatible
@achrinza/node-ipc和node版本不兼容,解决办法,重新安装node版本为稳定版本,不要安装最新版本
使用vite创建
Vite: 是一种新型前端构建工具,能够显著提升前端开发体验。其作者就是vue的创建者尤雨溪。
优势:
1.在开发环境中无需打包,可快速冷启动
2.轻量快速热重载
3.真正的按需引入,不再等待整个应用编译完成
传统构建和vite构建对比:
传统构建: 构建时需要将所有文件进行打包,然后再进行运行。
vite构建: 运行时需要什么文件就动态导入什么文件,不需要等待所有文件打包完成。

# 1.使用vite创建项目 npm init vite-app <项目名>
➜ vue_demo git:(master) npm init vite-app vue3-test-vite
Done. Now run:
cd vue3-test-vite
npm install (or `yarn`)
npm run dev (or `yarn dev`)
# 2.下载依赖包
➜ vue_demo git:(master) ✗ cd vue3-test-vite
➜ vue3-test-vite git:(master) ✗ npm i
added 305 packages in 9s
44 packages are looking for funding
run `npm fund` for details
# 3.启动项目
➜ vue3-test-vite git:(master) ✗ npm run dev
> vue3-test-vite@0.0.0 dev
> vite
[vite] Optimizable dependencies detected:
vue
Dev server running at:
> Local: http://localhost:3000/
> Network: http://172.21.3.176:3000/
分析工程结构
main.js
1.不再引入Vue构造函数创建vue实例,而是引入了createApp工厂方法创建应用实例对象app
vue2
import Vue from 'vue'
import App from './App.vue'
new Vue({
render: h => h(App)
}).$mount('#app')
vue3
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
createApp(App): 创建一个app实例,类似于new Vue()创建的vm实例,但是要比vm实例更简洁轻量
App.vue组件
组件的<teamplate>标签中可以有多个根标签
<template>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</template>
eslint仍然检查语法错误:

在vetur扩展配置中,取消勾选

拉开序幕的setup函数
1.setup函数: 是一个新的组件选项,是所有Composition API表演的舞台
2.组件中所有资源,包括:data、methods、computed、watch、生命周期钩子等,都配置在setup函数中
3.setup函数的两种返回值:
- 若返回一个对象,则对象中的属性、方法,模板中均可以直接使用
- 若返回一个渲染函数,则可以自定义组件渲染内容
setup返回一个对象:
<template>
读取setup返回的对象: 姓名: {{name}} 年龄: {{age}} 性别: {{sex}}
<button @click="sayVue3">说</button>
</template>
<script>
export default {
name: 'App',
setup() {
let name = '张三';
let age = 18;
let sex = '男';
function sayVue3() {
console.log('Vue3');
}
return {
name,
age,
sex,
sayVue3,
};
},
};
</script>


setup返回一个渲染函数:
返回的渲染函数,渲染函数中指定容器,将完全替换掉组件中<template>标签中的内容
<template>
读取setup返回的对象: 姓名: {{name}} 年龄: {{age}} 性别: {{sex}}
<button @click="sayVue3">说</button>
</template>
<script>
export default {
name: 'App',
setup() {
return h=>('h1','Vue3')
},
};
</script>

vue3环境中仍然可以定义data,methdos等属性,并正常使用
<template>
读取data返回的对象: {{nameVue2}}
<button @click="sayVue2">说vue2</button>
</template>
<script>
data(){
return {
nameVue2: 'vue2数据',
}
},
methods:{
sayVue2(){
console.log('vue2')
}
},
</script>

vue2配置中可以访问setup数据,但是setup中不能访问vue2配置的数据,因此vue2和vue3不能混合使用写法
<template>
<button @click="testV3">vue2配置中读取vue3配置数据</button>
<button @click="testV2">vue3配置中读取vue2配置数据</button>
</template>
<script>
export default {
data() {
return {
nameVue2: '张三vue2',
};
},
methods: {
sayVue2() {
console.log('vue2');
},
testV3() {
console.log('读取vue3数据', this.name);
console.log('读取vue3方法', this.sayVue3);
},
},
setup() {
let name = '张三vue3';
function sayVue3() {
console.log('Vue3');
}
function testV2() {
console.log('读取vue2数据', this.nameVue2);
console.log('读取vue2方法', this.sayVue2);
}
return {
name,
sayVue3,
testV2,
};
},
};
</script>
vue2的配置可以读取vue3的数据和方法,但vue3的配置无法读取vue2的数据和方法

总结
1.vue2配置中可以访问setup数据,但是setup中不能访问vue2配置的数据,因此vue2和vue3不能混合使用写法
2.setup返回对象中属性会交给vue管理,因此插值语法中可以直接使用,其他配置中通过this访问,而setup没有返回的属性,则不能使用
3.vue2配置的数据如果和setput配置的数据有重名,则优先使用setup配置数据
<template>{{name}}</template>
<script>
export default {
data() {
return {
name: 'vue2',
};
},
setup() {
let name = 'vue3';
return {
name,
};
},
};
</script>

4.setup函数前不能添加async,一旦添加async那么setup的返回值是一个Promise对象,因此模板中无法使用
// 错误写法
async setup() {
let name = 'vue3';
return {
name,
};
},
ref函数
ref函数用于创建一个响应式对象,对象中有一个属性value,value属性的值就是接收的参数的值。
其接收参数的值可以是基本数据类型,也可以是对象类型
<template>
<button @click="showRefValue">查看ref函数返回值</button>
<button @click="changeName">修改name</button>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
let name = ref('vue3');
function showRefValue() {
console.log(name);
};
return {
name,
showRefValue
};
},
};
</script>
通过ref函数将属性name包装为一个引用实现的实例,简称为引用对象,其属性值通过代理value属性调用getter和setter方法,从而实现数据代理,
和vue2中重新解析模板动作相同,当修改name时,调用setter方法,并触发视图更新

在setup方法中通过属性.value获取属性值或修改属性值,而插值模板中直接使用{{属性}}即可获取属性值,插值模板省略了.value
元素指令中的表达式也可以直接获取属性值,不需要.value
<template>
{{ name }}
<button @click="showRefValue">查看ref函数返回值</button>
<button @click="changeName(name)">修改name</button>
</template>
function changeName(nameValue) {
console.log(nameValue);
name.value = 'vue3 hello';
}

基本数据类型和对象类型实现响应式原理不同
基本数据类型:通过ref函数创建响应式对象,通过getter和setter方法实现响应式(数据劫持)
对象类型: 将ref()函数接收的对象,通过reactive函数转换为响应式对象(Proxy类型),然后再把Proxy类型的数据赋值给RefImpl的value属性
setup() {
let person = ref({
hobby: ['篮球', '足球', '羽毛球'],
age: 18,
});
function showObj() {
console.log(person);
}
return {
showObj,
};
},


ref传入ref对象
<template>
<input type="text" v-model="name" placeholder="姓名" />
<br />
<button @click="name+='!'">修改姓名</button>
<br />
<span>{{otherName}}</span>
<br />
<button @click="otherName+='!'">修改otherName</button>
</template>
<script>
import { ref, customRef } from 'vue';
export default {
setup() {
let name = ref('张三');
let otherName = ref(name);
console.log(name, otherName);
console.log(name === otherName);
return {
name,
otherName,
};
},
};
</script>
将ref包装过的对象,传入ref函数后会原样返回。也就是name和otherName指向同一个对象,修改一个另一个也会修改

总结
作用: 定义响应式数据
语法: const xxx = ref(初始化值)
读取值:
(1): 插值语法中直接使用{{xxx}}
(2): 元素标签指令表达式中直接使用xxx,@click="show(xxx)"
(3): js中使用xxx.value,读取对象中属性: xxx.value.属性
reactive函数
作用: 定义对象类型的响应式数据,不能定义基本类型响应式数据
基本类型数据无法响应式
引入reactive函数并使用
import { ref, reactive } from 'vue';
export default {
setup() {
let name = reactive('vue3');
function changeName() {
console.log(name);
name = 'reactive';
}
return {
name,
changeName
};
},
};
使用reactive函数定义基本数据类型为响应式数据,虽然页面可使用,但是vue出现已警告,不能用作响应式数据
将基本类型数据传递给reactive函数,返回值仍是传入的基本类型数据,没有任何变化

对象类型数据响应式
传入对象类型数据,返回值是一个Proxy对象,不需要.value.属性,可直接.属性使用响应式数据
reactive对象数据: 姓名: {{person.name}} 年龄:{{ person.age }}
<button @click="changePerson">修改person</button>
<script>
import { ref, reactive } from 'vue';
export default {
setup() {
let person = reactive({
name: 'vue3',
age: 18,
});
function changePerson() {
console.log('person', person);
person.name = '张三';
person.age = 20;
};
return {
person,
changePerson,
};
},
};

数组类型响应式
reactive可以检测到通过脚标修改的元素,使元素成为响应式数据,而vue2中必须通过破坏性方法修改元素才能响应
function changePerson() {
person.name = '张三';
person.age = 20;
person.hobby[0] = 'football';
};

总结
作用: 定义对象类型的响应式数据,不能定义基本类型响应式数据
语法: const 代理对象 = reactive(对象)
通过reactive代理的对象,能够作为响应式数据使用,可以深层代理数据,不需要.value写法获取对象属性,而是直接使用.属性读取对象属性
内部基于ES6的Proxy对象实现,通过Proxy对象代理对象中每个属性,从而实现数据代理
数组数据可以直接通过脚标进行修改元素,也能响应式
Vue3响应式原理
Vue2响应式原理
通过Object.defineProperty对对象中的属性进行劫持,当读取属性时,会调用getter方法,当修改属性时,会调用setter方法,从而实现数据劫持
let person = {
name: '张三',
sex: '男',
}
Object.defineProperty(person, 'value', {
get() {
console.log('get被调用了');
return person.name;
},
set(newValue) {
console.log('set被调用了');
person.name = newValue;
}
})
存在的问题:
1.新增属性、删除属性不能响应
2.通过下标修改元素不能响应
添加/删除属性进行响应式,应该使用$set/Vue.set和$delete/Vue.delete方法,否则不会触发视图更新
对数组的操作通过调用vue包装的数组破坏性方法进行操作,如push/pop/shift/unshift/splice/sort/reverse,否则不会触发视图更新
methods: {
add(){
this.$set(this.person, 'hobby', 'football');
},
delete(){
this.$delete(this.person, 'hobby');
}
changeArr(){
this.arr.push(4);
}
}
Vue3响应式原理
Proxy构造函数
使用Proxy构造函数创建原始对象的代理对象,读取代理对象的属性会调用代理对象的get方法,写入代理对象的属性会调用代理对象的set方法,在这些处理方法中对源数据进行处理。
参数:
1.代理目标对象 必填
2.配置对象 必填 可以使用{}占位
let person = {
name: "张三",
age: 18,
};
let proxy = new Proxy(person, {
get(target, key) {
console.log("get", key);
return target[key];
},
set(target, key, value) {
console.log("set", key, value);
target[key] = value;
},
});
console.log(proxy.name);
proxy.name = "李四";
proxy.name读取时调用get方法,proxy.name赋值时调用set方法

Proxy中的配置对象
代理对象组成:
Handler对象: 代理对象中处理函数
Target对象: 被代理对象

get处理函数
调用时机: 读取代理对象属性时调用
作用: 根据该函数的return读取源对象属性值
参数:
1.目标对象
2.被读取的属性名
get(target, key) {
console.log("get", key);
return target[key];
}
set处理函数
调用时机: 修改/新增代理对象属性时调用
作用: 修改/新增源对象属性值
返回值: 修改成功返回true, 修改失败返回false
参数:
1.目标对象
2.被修改的属性名
3.修改后的属性值
set(target, key, value) {
console.log("set", key, value);
target[key] = value;
},
deleteProperty处理函数
调用时机: 删除代理对象属性时调用
作用: 删除源对象属性
参数:
1.目标对象
2.被删除的属性名
返回值: 删除成功返回true, 删除失败返回false
deleteProperty(target, key) {
return delete target[key];
},
删除代理对象属性时,调用deleteProperty处理函数,而后删除源对象属性,将删除结果return出去

操作代理对象,调用处理函数,处理函数中操作原始对象,从而修改原始对象数据

Reflect反射对象
Reflect: Reflect对象也是 ES6 为了操作对象而提供的新 API。
ECMA规范有意将Object中的方法,放到Reflect对象上。
Reflect对Object中方法进行优化,像defineProperty方法具有返回值,明确调用definedProperty是否成功,而不关心具体异常的原因
而Object调用definedProperty方式时,还要防止抛出异常,因此需要try...catch捕获异常,以免影响程序的正常执行。
例如,使用Object对对象进行操作时,方法出现错误只能使用try...catch捕获,而Reflect对象则提供了更优雅的处理方式。
let person = {
name: "张三",
age: 18,
};
try {
Object.defineProperty(person, "hobby", {
get() {
return "football";
},
});
Object.defineProperty(person, "hobby", {
get() {
return "football";
},
});
} catch (e) {
console.log(e);
}
console.log("下段逻辑");
Object调用方法时报错,影响后面逻辑执行,只能使用try catch捕获错误

let person = {
name: "张三",
age: 18,
};
let r1 = Reflect.defineProperty(person, "hobby", {
get() {
return "football";
},
});
let r2 = Reflect.defineProperty(person, "hobby", {
get() {
return "football";
},
});
console.log(r1, r2);
console.log("下段逻辑");
使用Reflect调用defineProperty方法时,如果存在错误则会返回false,不会影响后面逻辑执行

Proxy中使用Reflect操作原始数据
let proxy = new Proxy(person, {
get(target, key) {
return Reflect.get(target, key);
},
set(target, key, value) {
Reflect.set(target, key, value);
},
deleteProperty(target, key) {
return Reflect.deleteProperty(target, key);
},
});

总结
vue3响应式实现原理:
Proxy(代理构造): 拦截对象属性的任意操作,包括,增删改查
Reflect(反射对象): 在Proxy处理函数中对原始对象进行操作
必须在setup函数中返回ref或者reactive函数的返回值才能响应式,否则无法响应式,即使返回对象的属性也会无法响应式
setup() {
let person = reactive({
firstName: '张',
name: '三',
});
setInterval(() => {
console.log('修改响应对象属性值');
person.firstName = '王';
}, 1000);
return {
firstName: person.firstName,
};
},


当setup返回reactive函数返回值时,该返回值才是响应式

reactive和ref的对比
reactive和ref都可以使数据实现响应式
从定义角度对比:
reactive: 定义对象类型数据,基本数据类型无效
ref: 定义基本类型数据
注意:
ref也可以定义对象类型和数组类型数据,对于对象和数组类型,其内部也是借助reactive来实现响应式
从原理角度对比:
reactive: 通过Proxy代理对象来实现响应式(数据劫持),通过Reflect反射对象操作源数据
ref: 基本类型数据通过Object.defineProperty的get和set来实现响应式(数据劫持),对象和数据组借助reactive
从使用角度对比:
reactive: js操作数据和模板中读取数据都不需要.value
ref: js操作数据时需要.value,在模板中读取数据不需要.value
setup函数注意点
1.setup执行时机优于beforeCreate,因此无法在setup中使用this
setup函数在beforeCreate之前执行,此时组件实例对象还未创建,因此不能通过this访问组件实例对象
setup() {
console.log('setup');
console.log('this', this);
},
beforeCreate() {
console.log('beforeCreate');
console.log('this', this);
},

2.setup函数参数
第一个参数: Proxy类型对象,用于接收props属性中的参数
Proxy类型对象,对象中的属性为props接收的属性。因此需要在子组件中使用props属性接收父组件传递的参数,否则setup第一个参数为空的Proxy对象
export default {
props: ['name'],
setup(props) {
console.log(props);
},
};

当子组件没有使用props属性接收父组件传递的任何参数时,setup第一个参数为空的Proxy对象
export default {
// props: ['name'],
setup(props) {
console.log(props);
},
};

当模板中使用了未通过props接收该参数时,出现警告
<template>
<div>
<h2>学生姓名:{{ name }}</h2>
</div>
</template>
<script>
export default {
// props: ['name','age'],
};
</script>
name属性没有通过props接收,并且在模板中使用name属性,则出现警告

第二个参数: context上下文对象,用于接收attrs、slots、emit属性
context上下文对象,对象中的属性为attrs、slots、emit属性。
export default {
setup(props, context) {
console.log(context);
},
};

attrs属性
attrs属性用于接收父组件传递的未通过props接收的参数
props: ['age']
父组件传递过来的name属性,未使用props接收时,会存放到context.attrs属性中

emit属性
自定义hello事件
<template>
<student @hello="showValue" name="张三" age="18"></student>
</template>
<script>
import Student from './components/Student.vue';
export default {
components: { Student },
setup() {
function showValue(value) {
console.log('触发了自定义事件', value);
}
return {
showValue,
};
},
};
</script>
触发hello事件
context.emit('hello', '触发事件');
slots属性
App组件,必须使用v-slot:插槽名指令指定插槽
<Student @hello="showValue" name="张三" age="18">
<template v-slot:asd>
<div>
<p>这是插槽内容</p>
</div>
</template>
</Student>
Student组件
setup(props, context) {
console.log(context.slots);
},
App组件中设置Student组件的asd插槽内容,Student组件setup的context对象的slots属性中存放了asd插槽内容,
slots存放的是插槽函数,函数名为插槽名,调用插槽函数返回虚拟节点

console.log(context.slots.asd()):

总结
1.setup优于beforeCreated执行,因此setup中的this为undefined
2.setup函数的参数
(1).props: 父组件传递的属性
(2).context: 上下文对象,包含attrs、slots、emit属性
attrs: 值为对象,没有在props配置中声明的属性,会放到该对象中,相当于this.$attrs
slots: 值为对象,存放了插槽函数,调用插槽函数返回虚拟节点,相当于this.$slots
emit: 值为函数,用于触发自定义事件,相当于this.$emit
computed计算属性函数
vue3中仍然可以使用vue2的计算属性配置,vue2通过this获取setup返回的数据
<template>
学生姓:
<input type="text" v-model="person.firstName" />
<br />学生名:
<input type="text" v-model="person.name" />
<br />
学生全名: {{ fullName }}
</template>
<script>
import { reactive } from 'vue';
export default {
computed: {
fullName() {
return this.person.firstName + this.person.name;
},
},
setup() {
let person = reactive({
firstName: '张',
name: '三',
});
return {
person,
};
},
};
</script>

计算属性只读写法,入参为get方法的回调函数
vue3中的计算属性,引入computed计算属性函数,该函数接收一个回调函数,回调函数中书写数算逻辑return结果
import { reactive, computed } from 'vue';
export default {
setup() {
let person = reactive({
firstName: '张',
name: '三',
});
person.fullName = computed(() => {
return person.firstName + person.name;
});
return {
person,
};
},
};
计算属性读和写,入参为对象,对象中包含get和set属性
let fullName = computed({
get() {
return person.firstName + person.name;
},
set(value) {
person.firstName = value.substring(0, 1);
person.name = value.substring(1);
},
});

watch监视函数
vue2中的watch
export default {
watch:{
sum(newVal, oldVal) {
console.log('sum的值发生了变化', '新值', newVal, '旧值', oldVal);
},
},
setup() {
let sum = ref(1);
return {
sum,
};
},
};

vue3中的watch函数
watch函数接收三个参数,第一个参数为要监视的响应数据,第二个参数为回调函数,第三个为监视特性配置对象
watch的第一个参数类型可以是: ref,reactive对象,getter函数,多个类型数据源数组
数据源为ref变量
import { ref, watch } from 'vue';
export default {
setup() {
let sum = ref(1);
watch(sum, (newVal, oldVal) => {
console.log('sum的值发生了变化', '新值', newVal, '旧值', oldVal);
});
return {
sum,
};
},
};
sum作为第一个参数

数据源为getter函数
getter函数结果作为watch回调函数参数,在getter函数中获取ref响应数据源使用.value
如下,newScore和oldScore分别是触发前和触发后getter函数的返回值
setup() {
let sum = ref(1);
let subject = ref('英语');
watch(
() => subject.value + ': ' + sum.value,
(newScore, oldScore) => {
console.log('score的值发生了变化', '新值', newScore, '旧值', oldScore);
}
);
return {
sum,
subject,
};
},
改变非响应数据时,getter函数不会触发,回调函数不会执行
setup() {
let sum = ref(1);
let subject = ref('英语');
let name = '张三';
watch(
() => name + '| ' + subject.value + ': ' + sum.value,
(score, oldScore) => {
console.log('score的值发生了变化', '新值', score, '旧值', oldScore);
}
);
function changeName() {
name = '李四';
console.log('name的值发生了变化');
}
return {
sum,
subject,
changeName,
};
},
只有修改了getter回调函数中用到的任意响应数据时,watch回调才会执行

数据源为reactive对象
setup() {
let sum = 1;
let subject = '英语';
let name = '张三';
let person = reactive({
name,
sum,
subject,
});
watch(person, (newPerson, oldPerson) => {
console.log('person的值发生了变化', '新值', newPerson, '旧值', oldPerson);
});
return {
person,
};
},

监听数据为对象类型时,即使对象中属性值发生了改变,并触发了回调函数,但是新值和旧值相同都是新值对象
vue2监视对象类型时,对象中属性值改变,也会触发回调函数,但是新值和旧值相同都是新值对象

原因: 由于监视的是一个对象,这个对象中的属性发生改变通过Proxy回调捕获到,但是新值和旧值仍是该对象的引用地址,因此新值和旧值相同
内存地址 0x1234: { name: "Alice", age: 30 }
│
│ 修改 age 属性
▼
内存地址 0x1234: { name: "Alice", age: 31 } // 仍是同一地址
watch 回调参数:
newVal → 指向 0x1234
oldVal → 指向 0x1234 // 所以值相同
监听reactive对象类型数据时,vue3新版本中开始支持了deep配置,之前的版本中默认为deep:true且deep配置无效
即监听了person对象,修改obj中的属性值改变时,也能触发回调函数
let person = reactive({
name,
sum,
subject,
obj: {
a: 1,
b: 2,
},
});
在vue3版本中默认为deep: true
watch(
person,
(newPerson, oldPerson) => {
console.log('person对象发生了变化', '新值', JSON.stringify(newPerson), '旧值', JSON.stringify(oldPerson));
}
);

watch(
person,
(newPerson, oldPerson) => {
console.log('person对象发生了变化', '新值', JSON.stringify(newPerson), '旧值', JSON.stringify(oldPerson));
}
, {deep: false}
);
修改了person.obj.a深度属性值,vue3版本开始可以使用deep属性控制是否深度监视
deep:false: 修改深度属性不监视对象的变化

在`vue3之前版本中,默认为deep:true,而deep:fales无效
当监听对象属性时,应该使用getter函数
watch(
person.sum,
(newPerson, oldPerson) => {
console.log('person对象发生了变化', '新值', newPerson, '旧值', oldPerson);
}
);
报错,因为person.sum是基本数据类型,它不是ref、reactive对象、getter函数、多个类型数据源数组,即使使用了深度监视也无效

应该使用getter函数
setup() {
let sum = 1;
let subject = '英语';
let name = '张三';
let person = reactive({
name,
sum,
subject,
});
watch(()=>person.sum, (newPerson, oldPerson) => {
console.log('person的sum值发生了变化', '新值', newPerson, '旧值', oldPerson);
});
return {
person,
};
},

数据源为多个数据源数组
监视多个数据源,只要有一个数据源发生变化,就会执行回调函数
回调函数中两个参数,它们都是数组
第一个参数为新值数组,每个元素就是数据源的新值
第二个参数为旧值数组,每个元素就是数据源的旧值
setup() {
let sum = ref(1);
let msg = ref('hello watch');
watch([sum, msg], (newVal, oldVal) => {
console.log('sum的值发生了变化', '新值', newVal, '旧值', oldVal);
});
return {
sum,
msg,
};
},

当回调函数只接收一个参数时,该参数为数组,每个元素为数据源的新值
当回调函数只有一个参数时,该参数也是一个数组,每个元素就是数据源的新值
setup() {
let sum = ref(1);
let msg = ref('hello watch');
watch([sum, msg], ([newVal, oldVal]) => {
console.log('sum的值发生了变化', '新值', newVal, oldVal);
});
return {
sum,
msg,
};
},

watch的监听配置项
watch的第三个参数为配置项,配置项中可以配置immediate和deep属性
immediate属性为布尔值,表示是否立即执行回调函数,默认为false,不立即执行回调函数
deep属性为布尔值,表示是否深度监视
深度监视: 监视reactive对象类型时,当该对象中对象属性值发生改变时,也能触发回调函数
当使用getter函数监听对象时,需要使用deep: true,否则只能检测到该对象被整体替换(内存地址改变),无法检测到对象中的属性值改变
由于getter返回的是响应数据对象中对象,该对象并不是reactive函数直接返回的对象,那么该对象就是一个普通对象,因此需要配置deep: true才能深度监视
let person = reactive({
name,
sum,
subject,
obj: {
a: 1,
b: 2,
},
});
watch(
() => person.obj,
(newPerson, oldPerson) => {
console.log('person对象发生了变化', '新值', JSON.stringify(newPerson), '旧值', JSON.stringify(oldPerson));
}
);

应使用deep: true配置深度监视对象属性值的改变
watch(
() => person.obj,
(newPerson, oldPerson) => {
console.log('person对象发生了变化', '新值', JSON.stringify(newPerson), '旧值', JSON.stringify(oldPerson));
}
, {
deep: true,
}
);

总结
1.监视数据源有哪些: ref返回值(RefImpl对象),reactive返回值(Proxy对象),getter函数,多数据源的数组
2.监视reactive定义的对象类型数据时,和vue2一样,对象中的属性改变了,触发回调方法,但新值和旧值相同,都为新值
3.监视reactive定义的对象类型数据时,默认为deep:true开启深度监视,在新的vue3版本中也可以设置deep:false取消深度监视
4.监视reactive定义的某一个非对象属性时,应该使用getter方法返回对象中的属性
5.使用getter函数返回reactive定义的某一个属性,该属性是对象时,该对象就是普通对象,deep默认为false,设置deep:true开启深度监视,才能检测到属性值的变化,否则只能检测到该对象整体被替换(内存地址发生改变)
6.不能在回调函数中修改监视属性值,否则产生递归调用,vue3中会报出超出最大递归次数

监视ref定义数据时.value问题
let person = ref({
name,
sum,
subject,
obj: {
a: 1,
b: 2,
},
});

想要监视ref中的对象属性值改变有两种方式:
1.监视Ref对象.value 因为value值其实就是reactive定义的Proxy响应式数据
2.直接监视Ref对象,并开启深度监视
watch(
person.value,
(newPerson, oldPerson) => {
console.log('person对象发生了变化', '新值', JSON.stringify(newPerson), '旧值', JSON.stringify(oldPerson));
}
);
watch(
person,
(newPerson, oldPerson) => {
console.log('person对象发生了变化', '新值', JSON.stringify(newPerson), '旧值', JSON.stringify(oldPerson));
}, {
deep: true,
}
);
watchEffect函数
watchEffect函数和watch函数功能基本一致,都是用于监视数据源,当数据源发生变化时,执行回调函数,但watchEffect函数不需要指定监视的数据源,它会自动监视watchEffect回调函数中使用到的所有响应式数据,一旦使用到的某一个响应数据发生了改变,回调函数将会执行
watch: 既要指明监视数据,又要既定回调函数
watchEffect: 不需要指明监视数据,监视的回调函数中用到哪个响应式数据,就监视哪个响应式数据
watchEffect表现形式有点像computed,一旦所依赖到的响应数据发生改变,就会触发回调函数的执行
不同的是:
1.computed注重return返回值
2.watchEffect注重的是过程(回调函数的函数体)
let person = ref({
name,
sum,
subject,
obj: {
a: 1,
b: 2,
},
});
watchEffect(() => {
console.log('执行了监视器');
person.value.obj.a;
});
挂载后立即执行一次回调函数,只要用到了任何响应数据,即使没有赋值给其他变量,也会触发回调函数的执行
回调函数中用到了person中深度属性a,因此只要a数据发生改变,就会触发回调函数的执行

watchEffect中使用异步函数监听响应数据无效
如下代码,watchEffect使用setTimeout异步回调函数监听了响应数据name的变化,但监听无效
let name = ref('');
let otherName = ref('');
watchEffect(() => {
console.log('name 改变');
setTimeout(() => {
otherName.value = name.value;
});
});
return {
name,
otherName,
};
只有初始化时执行一次监视回调,回调函数中使用异步函数,而在异步函数中使用到了响应数据则无法监听,因此监听回调不再执行

watch和watchEffect回调函数中使用到的setTimeout回调函数只执行一次
let name = ref('');
let otherName = ref('');
watchEffect(() => {
console.log('name 改变');
setTimeout(() => {
console.log('otherName 改变');
otherName.value = name.value;
}, 3000);
});
return {
name,
otherName,
};

let name = ref('');
let otherName = ref('');
watch(name, (newValue)=>{
console.log('name 改变');
setTimeout(() => {
console.log('执行了setTimeout');
otherName.value = newValue;
},1000)
})
而使用watch进行监听,因为已经指定了监听数据,因此只要一改变就能执行监视回调从而执行了setTimeout的回调

总结watchEffect特点
1.页面挂载完成立即执行回调
2.无法获取旧值和新值
3.回调函数中用到了哪个响应数据,就监视哪个响应数据
4.可以深度监视响应属性值改变
5.只要用到了响应属性,即使不赋值给其他变量,也会监视该属性
6.watchEffect和watch都不返回回调函数的返回值
7.watchEffect回调函数中修改监视属性的值,不会触发回调函数执行,也就是不会造成递归调用。只有在回调函数外修改了监视属性才能触发回调函数
watchEffect(() => {
console.log('执行了监视器');
person.value.obj.a++;
});

8.watch和watchEffect回调函数中使用setTimeout存在的问题
watch和watchEffect都是只能使setTimetout的回调执行一次
区别在watch可以执行多次,从而执行多次setTimeout回调
而watchEffect则不能执行多次,从而不能执行多次setTimeout回调
生命周期

渲染遇到组件: 必须指定挂载到的容器后(.mount(容器)),才渲染组件,走后续流程
而vue2生命周期中new Vue()后就可以执行beforeCreate、created
配置项回调函数
export default {
setup() {
let sum = ref(1);
return {
sum,
};
},
// 创建app实例前
beforeCreate() {
console.log('beforeCreate');
},
// 创建app实例后,组合API加载完毕
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');
},
};

相比vue2的生命周期钩子,有两个钩子名称改变:
beforeDestroy -> beforeUnmount
destroyed -> unmounted
组合API生命周期
beforeCreate -> setup
created -> setup
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeUnmount -> onBeforeUnmount
unmounted -> onUnmounted
vue3设计中并没有提供beforeCreate和created两个组合式API钩子,而是将setup组合式API作为它们两钩子的组合式API
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted} from 'vue';
export default {
setup() {
let sum = ref(1);
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,
};
},
};
组合式API生命周期钩子执行优先于配置项生命周期钩子执行
Hook
Hook: 本质是一个函数,把setup中使用到Composition API进行了封装,在setup执行了该hook后,那么该组件也就加载了该hook所封装的Composition API
优势: 复用代码使setup中的逻辑更清晰
hook函数命名规范
hook函数名称必须以use开头,如usePoint、useMouse、usePoint等
hook函数使用

1.在src目录下创建hooks文件夹,并在该文件夹下创建usePoint.js文件
2.在usePoint.js文件中编写hook函数
暴露一个匿名函数,该函数就是hook函数,包含了数据和组合api逻辑
import { reactive, onMounted, onBeforeUnmount } from 'vue';
export default function () {
let point = reactive({
x: 0,
y: 0,
});
function position(e) {
point.x = e.pageX;
point.y = e.pageY;
console.log(point);
}
onMounted(() => {
window.addEventListener('click', position);
});
onBeforeUnmount(() => {
window.removeEventListener('click', position);
});
return point;
}
3.在组件中使用hook函数,使用hook函数返回数据
<template>点击时坐标: Y:{{point.y}} X:{{point.x}}</template>
<script>
import userPoint from '../hooks/userPoint';
export default {
setup() {
let point = userPoint();
return {
point,
};
},
};
</script>

toRef和toRefs
toRef函数
<template>
<span>person信息: {{person}}</span>
<hr />
姓名: {{newName}}
<hr />
<button @click="newName = '王五'">修改姓名</button>
</template>
setup() {
let person = reactive({
name: '张三',
age: 18,
});
return {
person,
newName: person.name,
};
},
setup返回了响应式对象中属性name值,并使用newName变量接收,实际上这个newName就是普通的字符串,不是响应式数据,因此修改newName的值,不会触发视图更新

要想让newName变成响应式数据,并且和person中的name值同步修改,可以使用toRef函数
toRef函数接收两个参数,第一个参数是响应式对象,第二个参数是响应式对象中的属性名,返回一个响应式数据,这个响应式数据会与响应式对象中的属性值同步修改
setup() {
let person = reactive({
name: '张三',
age: 18,
});
return {
person,
newName: toRef(person, 'name'),
};
},

toRef函数的作用: 可以把响应式对象中的属性变成响应式数据并返回,并且该响应式属性和响应式对象中的属性值同步修改

toRefs函数
toRefs函数的作用: 可以把响应式对象中所有属性变成响应式数据,并且该响应式属性和响应式对象中的属性值同步修改
let person = reactive({
name: '张三',
age: 18,
});
console.log(toRefs(person));
将响应式对象中所有属性变成响应式数据

<template>
<span>person信息: {{person}}</span>
<hr />
姓名: {{name}}
<hr />
<button @click="name = '王五'">修改姓名</button>
</template>
<script>
import { toRef, reactive,toRefs } from 'vue';
export default {
setup() {
let person = reactive({
name: '张三',
age: 18,
});
return {
person,
...toRefs(person),
};
},
};
</script>
setup返回了响应式对象person,并返回了所有具有响应式功能的person属性

当模板中使用了未定义(setup未返回)的属性,该属性不是响应式数据,则出现警告
<template>
<span>person信息: {{age}}</span>
<hr />
姓名: {{name}}
<hr />
<button @click="person.name = '王五'">修改姓名</button>
</template>
<script>
import { reactive } from 'vue';
export default {
setup() {
let person = reactive({
age: 18,
});
return {
person,
...person
};
},
};
</script>
模板中使用了name属性,但是setup的返回的对象中并没有name属性,因此name不再是响应式数据,即使动态添加person对象中的name属性,也不会再次解构...person对象,因此name属性不是影响式数据

总结
1.toRef(): 创建一个ref对象,其value值就是响应式对象中的属性值,并且和响应式对象中的属性值保持同步
2.语法: toRef(对象,属性名)
3.使用场景: 想单独对响应式对象中属性进行响应式使用
4.toRefs(): 创建一个对象,对象中的属性值都是ref对象,并且和响应式对象中的属性值保持同步
5.toRef和toRefs函数操作的数据必须是响应式数据,否则它们加工过属性也不是响应式,但是原始数据是可以修改的
let person = {
name: '张三',
age: 18,
};
return {
person,
...toRefs(person),
};
由此可见toRef和toRefs返回的包装数据地址仍然指向了原数数据,但由于原始数据不是响应式,不能触发vue重新解析模板,但是原始数据已经改变

shallowReactive和shallowRef
shallowReactive函数
shallowReactive函数作用: 功能和reactive函数相同,把一个对象转为响应式对象,浅层响应式处理
不同点:
1.shallowReactive只对对象类型数据的第一层属性进行响应式处理
2.shallowReactive只对数组类型数据中元素进行响应式处理,不会对元素内部数据进行响应式处理
<template>
<span>person信息: {{person}}</span>
<br />
姓名: {{person.name}}
<br />
<button @click="person.name = '王五'">修改姓名</button>
<br />
个人爱好: {{person.hobby}}
<br />
<button @click="person.hobby.splice(0,1,'打游戏')">修改个人爱好</button>
<hr />
书籍: {{books}}
<br />
<button @click="books.splice(0,1,'红楼梦')">修改书籍</button>
<br />
第三本书详细信息: {{books[2]}}
<br />
<button @click="books[2].author = '曹雪芹2'">修改第三本书作者</button>
</template>
<script>
import { shallowReactive } from 'vue';
export default {
setup() {
let person = shallowReactive({
name: '张三',
hobby: ['看电影', '看小说'],
});
let books = shallowReactive(['西游记', '三国演义', { name: '红楼梦', author: '曹雪芹' }]);
return {
person,
books,
};
},
};
</script>
对对象类型数据person中的name进行响应式处理:

不能对person中的hobby属性中数据进行响应式处理:

对数组类型中的元素进行响应式处理,不能对数组中的元素内部的数据进行响应式处理:

shallowRef函数
shallowRef函数作用: 将原始数据包装为RefImpl对象,当原始数据是基本数据类型时其功能和ref一样,如果是引用类型(对象和数组),shallowRef则不会将其转为响应式数据,只对基本数据类型进行响应式处理。
<template>
<span>姓名: {{name}}</span>
<br />
<button @click="name = '李四'">修改姓名</button>
<hr />
<span>person信息: {{person}}</span>
<br />
<button @click="person.name = '王五'">修改姓名</button>
<br />
<hr />
书籍: {{books}}
<br />
<button @click="books.splice(0,1,'红楼梦')">修改书籍</button>
</template>
<script>
import { ref,shallowRef } from 'vue';
export default {
setup() {
let name = shallowRef('张三');
console.log('shallow-name', name);
let person = shallowRef({
name: '张三',
hobby: ['看电影', '看小说'],
});
console.log('shallowRef-person', person);
let refPerson = ref({
name: '张三',
hobby: ['看电影', '看小说'],
});
console.log('ref-person', refPerson);
let books = shallowRef(['西游记', '三国演义', { name: '红楼梦', author: '曹雪芹' }]);
console.log('shallowRef-books', books);
let refBooks = ref(['西游记', '三国演义', { name: '红楼梦', author: '曹雪芹' }]);
console.log('ref-books', refBooks);
return {
name,
person,
books,
};
},
};
</script>
对象类型数据包装为RefImpl对象,但是value值就是普通对象,不是Proxy对象,因此不会对对象类型的数据进行响应式处理
数组也是引用类型,同样使用shallowRef包装的RefImpl对象的value就是普通数组,和ref函数创建的RefImpl对象的value只不同

只对基本数据类型进行响应式处理

注意
在使用shallowRef或shallowReactive时,只是对某些属性不能响应式,但其值仍然被修改成功了,如果触发了vue重新解析模板,则会重新渲染页面,显示最新的值
如下: shallowRef对对象类型的数据不能响应式,但是修改其属性值是成功的,修改基本数据类型name时,由于name是响应式的数据,触发了vue重新渲染模板,因此刚刚修改的对象中的name值被重新渲染到页面中

总结
1.shallowReactive只处理对象最外层的响应式(浅响应),例如对象类型的第一层属性,和数组类型中元素值改变,而不能把数组元素内部的属性转变为响应式
2.shallowRef只处理基本数据类型,例如字符串,数字等,不能处理对象类型和数组类型内部数据响应式,如果对象类型和数组类型,则其value值就是普通对象和数组,而不是响应式对象和响应式数组
3.shallowRef({})时,如果对象的引用地址改变了,也会触发响应式,例如person = {name:'张三'},person的引用地址改变了,也会触发响应式
readonly和shallowReadonly
readonly函数
对象类型数据变成只读数据,无法修改属性值(深度限制)
<template>
<span>对象: {{person}}</span>
<br>
<span>姓名: {{person.name}}</span>
<br />
<button @click="person.name += '!'">修改对象中的属性</button>
<br />
<span>个人爱好: {{person.hobby}}</span>
<br />
<button @click="person.hobby.splice(0,1,'打游戏')">修改个人爱</button>
<hr />
<span>书籍: {{books}}</span>
<br />
<button @click="books.push('JavaScript权威指南')">修改书籍</button>
</template>
<script>
import { reactive, readonly } from 'vue';
export default {
setup() {
let person = reactive({
name: '张三',
hobby: ['看电影', '看小说'],
});
person = readonly(person);
console.log('readonly-person', person);
let books = reactive(['JavaScript高级程序设计', 'Vue.js实战']);
books = readonly(books);
console.log('readonly-books', books);
return {
person,
books,
};
},
};
</script>

修改响应式对象类型属性以及深度属性时修改被限制,修改响应式数组类型数据时也会限制修改

let a = 1;
let readOnlyB = readonly(a);
a = ref(a);
return {
a
};
readonly对对象类型数据才能生效,对基本类型数据无效,使用readonly包装基本数据后,仍然可以使用ref进行响应式

readonly处理普通对象后,返回值为Proxy对象实例
let person = {
name: '张三',
age: 18,
};
let readOnly = readonly(person);
console.log(readOnly); // Proxy(Object) {name: '张三', age: 18}
shallowReadonly函数
让对象类型数据变成只读数据,无法修改属性值(浅层限制)
<template>
<span>对象: {{person}}</span>
<br />
<span>姓名: {{person.name}}</span>
<br />
<button @click="person.name += '!'">修改对象中的属性</button>
<br />
<span>个人爱好: {{person.hobby}}</span>
<br />
<button @click="person.hobby.splice(0,1,'打游戏')">修改个人爱</button>
<hr />
<span>书籍: {{books}}</span>
<br />
<button @click="books.push('JavaScript权威指南')">添加书籍</button>
<br />
<button @click="books[2].name += '!'">修改第三本书籍</button>
</template>
<script>
import { reactive, shallowReadonly } from 'vue';
export default {
setup() {
let person = reactive({
name: '张三',
hobby: ['看电影', '看小说'],
});
person = shallowReadonly(person);
console.log('shallowreadonly-person', person);
let books = reactive(['JavaScript高级程序设计', 'Vue.js实战', { name: 'Vue.js 3企业级应用开发实战', author: '柳伟卫' }]);
books = shallowReadonly(books);
console.log('shallowreadonly-books', books);
return {
person,
books,
};
},
};
</script>
当修改对象第一层属性,和数组元素值时,无法进行响应式,但是修改对象内部属性和数组元素内部属性时,可以响应式修改

总结
1.readonly和shallowReadonly作用是限制源数据修改,和源数据是否是响应数据无关
let person = {
name: '张三',
hobby: ['看电影', '看小说'],
};
person = readonly(person);
也能对person普通对象进行限制

2.外部组件引入组件数据时,可以在组件中设置readonly或shallowReadonly,防止外部组件修改组件内部数据
3.使用readonly或shallowReadonly后,再对数据进行响应式处理无效,该数据仍然是只读数据
let person = {
name: '张三',
hobby: ['看电影', '看小说'],
};
person = shallowReadonly(person);
person = reactive(person);
一旦对数据进行修改的限制,那么该数据响应式无效

4.readonly和shallowReadonly都只能对对象类型数据生效
5.readonly和shallowReadonly处理普通对象后,返回值为Proxy对象实例
深度数据和浅度数据的理解
深度数据: 对象类型的深度属性,数组类型的元素内部属性
浅度数据: 对象类型的第一层属性,数组类型的元素
toRaw和markRaw
toRaw函数
toRaw将reactive包装过的响应式数据转换为普通数据,原始响应式数据仍然能够修改,但是不再响应
toRaw对ref包装过的响应式数据不能转为普通数据,即使ref包装的Proxy数据也不会转为普通数据,仍然可以响应
<template>
<span>person信息: {{person}}</span>
<hr />
姓名: {{name}}
<hr />
<button @click="changePerson">修改姓名</button>
<hr />
<span>城市: {{city}}</span>
<button @click="changeCity">修改城市</button>
</template>
<script>
import { toRef, reactive, toRefs, toRaw } from 'vue';
export default {
setup() {
let city = toRef('北京');
let person = reactive({
name: '张三',
age: 18,
});
function changeCity() {
let rawCity = toRaw(city);
console.log('rawCity', rawCity);
// 仍然可以修改基本类型响应式数据
rawCity.value = '上海';
}
function changePerson() {
let rawPerson = toRaw(person);
// 不能修改对象类型响应式数据,但通过toRaw()方法加工后,原始数据不再响应
rawPerson.name += '!';
console.log('rawPerson', rawPerson, 'rawPerson === person', rawPerson === person);
console.log('person', person);
}
return {
person,
...toRefs(person),
city,
changeCity,
changePerson,
};
},
};
</script>
toRaw对对象类型数据不再响应:

toRaw对ref基本类型响应数据无效,仍然响应:

toRaw对ref加工过的对象类型数据无效,仍然可以响应
<template>
<span>城市: {{city}}</span>
<button @click="changeCity">修改城市</button>
</template>
<script>
import { ref, reactive, toRefs, toRaw } from 'vue';
export default {
setup() {
let city = ref({ city: '北京' });
function changeCity() {
let rawCity = toRaw(city);
console.log('rawCity', rawCity);
// 仍然可以修改基本类型响应式数据
rawCity.value.city = '上海';
console.log('city', city);
}
return {
city,
changeCity,
};
},
};
</script>
toRaw对ref加工过的对象类型数据修改时无效,仍然可以使Proxy对象进行响应式

toRaw()对ref()包装过的响应式数据无效,不能返回原始数据,而是返回了ref包装的响应式对象
let city = ref('北京');
function changeCity() {
let rawCity = toRaw(city);
console.log('rawCity', rawCity, 'city === rawCity', city === rawCity);
// 对ref包装过的响应式数据不能返回原始数据
rawCity.value = '上海';
}

markRaw函数
<template>
<span>person信息: {{person}}</span>
<br />
<button @click="addCar">为个人添加一辆车</button>
<button @click="person.car.price = parseInt(person.car.price) + 10 + 'w'">修改车的价格</button>
<br />
</template>
<script>
import { reactive, markRaw } from 'vue';
export default {
setup() {
let person = reactive({
name: '张三',
age: 18,
});
function addCar() {
person.car = { name: '奔驰', price: '40w' };
}
return {
person,
addCar,
};
},
};
</script>
为person对象中追加car属性,那么car属性仍然响应式数据

当不想让后来添加的对象中的属性响应时,使用marRaw对象该对象进行标记,使该属性不再是响应式数据,但原始数据仍可以修改成功
function addCar() {
person.car = markRaw({ name: '奔驰', price: '40w' });
}

同样markRaw标记的数组,数组中的元素属性值修改时,不会被响应式处理

function addTall() {
person.tall = markRaw(2.0);
}
function changeTall() {
person.tall = 1.88;
}
markRaw对基本数据类型进行标记,基本数据类型修改时,仍然会响应,因此markRaw只能使引用类型数据不再响应

总结
toRaw()将proxy对象包装为原始对象,proxy对象由reactive()、readonly()、shallowReactive() 或者 shallowReadonly() 创建。修改原始对象值时,vue不再响应,除非其他响应式数据修改触发了vue重新渲染,此时才会显示原始对象最新值
使用场景: 限制响应式对象不再响应(整体限制),只能包装proxy对象,不能包装refImpl对象
markRaw(): 标记一个永远不是响应式对象,返回值为永不响应式对象,使该对象中所有属性或元素不再响应式,后续再对该对象进行reactive包装也不会具有响应式
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false
markRaw对对象类型中属性和数组类型中元素生效,对基本数据类型无效
使用场景:
1.操作响应式对象时,不想对后续添加的数据进行响应式,可以使用markRaw标记(部分限制)
2.当渲染确定不可变的大数据列表时,可以对该大列表数据标记为markRaw,告诉vue不必再去监听该对象跳过响应式处理,从而提升性能
也适用于嵌套在其他响应性对象
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false
customRef函数: 自定义ref
创建一个自定义的ref,显示声明对其依赖追踪和更新触发的控制方式
如下需求: 修改input框的值,延迟1s同步到其他地方
常规做法: 使用watch监听输入框值的变化,在watch监听属性回调方法中使用setTimeout延迟1s修改其他属性值为输入框的值
这种做法并没有对name进行延迟响应渲染,而是延迟修改其他响应式数据实现
let name = ref('');
let otherName = ref('');
watch(name, newValue => {
console.log('name 改变');
setTimeout(() => {
otherName.value = newValue;
}, 1000);
});
return {
name,
otherName,
};

当不使用其他变量,使其1s延迟响应时,就需要使用customRef来实现该功能,可以自定义响应逻辑,实现真正意义的控制响应式延迟
创建一个自定义的响应式函数名字叫myRef
myRef返回customRef API的返回值
customRef API接收两个参数,一个是track,一个是trigger
track: 告诉vue什么时候开始追踪属性,在return响应式数据前调用
trigger: 告诉vue什么时候开始触发更新,当属性值发生改变时调用
customRef返回一个配置对象,配置对象中包含get和set方法,当属性被访问时,调用get方法,当属性值发生改变时,调用set方法
<template>
<input type="text" v-model="name" placeholder="姓名" />
<br />
<button @click="name+='$'">修改姓名</button>
<br />
<span>{{name}}</span>
</template>
<script>
import { ref, customRef } from 'vue';
export default {
setup() {
let timer;
let myRef = value => {
return customRef((track, trigger) => {
return {
get() {
console.log(`读取了属性值: ${value}`);
// 在get方法return的前一步告诉追踪器,return的属性被访问了,需要被追踪,当value只发生改变时,get方法会再次被调用
track();
return value;
},
set(newValue) {
console.log(`修改了属性,旧值: ${value} 新值: ${newValue}`);
value = newValue;
clearTimeout(timer);
// 通知vue重新解析模板
timer = setTimeout(() => {
trigger();
}, 1000);
},
};
});
};
let name = myRef('');
return {
name,
};
},
};
</script>
延迟1s通知vue重新解析模板

在通知vue重新解析模板前停止上一次的定时器回调立即中断,这样就实现了vue只渲染最近一次更新,可以解决渲染防抖问题

总结
customerRef可以让vue延迟解析(读取)响应式数据,本质上数据已经修改,只是延迟了通知vue重新解析模板,从而实现真正的延迟响应式
provide与inject函数: 实现祖先组件向后代组件通信
官方推荐祖先向孙子以及后代组件通信使用provide和inject,父组件向子组件通信使用props

使用方式
父组件使用provide提供数据
import { ref, provide } from 'vue';
export default {
components: {
Child,
},
setup() {
let name = ref('张三');
provide('name', name);
return {
name,
}
},
};
子组件或孙子组件使用inject接收数据
import {inject, provide} from 'vue';
export default {
setup() {
provide('son', 'son');
return {
name: inject('name'),
};
},
};

后代使用provide不能向祖先组件通信:
子组件提供数据:
provide('child', 'child');
父组件则无法接受后代提供的数据
setup() {
return {
child: inject('child'),
};
},
祖先组件不能通过后代组件provide获取数据

总结
1.父组件向子组件通信使用props
2.父组件向deepChild(孙子以及孙子后代组件)通信使用provide和inject,provide和inject也能实现父向子通信但官方并不推荐,而是推荐props
3.provide和inject不直接支持后代向祖先通信,但可以通过父组件向子组件传递函数引用,通过子组件调用触发父组件函数执行实现子组件向父组件通信
父组件
<template>
<div class="child">
我是子组件
<Son />
</div>
</template>
<script>
import Son from './Son.vue';
import { provide } from 'vue';
export default {
components: {
Son,
},
setup() {
function parentMethod(value) {
console.log('触发父组件方法,接收参数:', value);
}
provide('parentMethod', parentMethod);
return {};
},
};
</script>
<style scoped>
.child {
background-color: #f0f0f0;
padding: 20px;
border-radius: 10px;
}
</style>
子组件
<template>
<button @click="parentMethod('子组件参数')">触发父组件方法</button>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
let parentMethod = inject('parentMethod');
return {
parentMethod,
};
},
};
</script>
<style scoped>
.son {
background-color: #afd38c;
padding: 10px;
}
</style>

响应式数据的判断
1.isRef 判断是否由ref函数创建的响应式数据
2.isReactive 判断对象是否由reactive函数成功创建的Proxy代理对象实例
3.isReadonly 判断对象是否由readonly函数成功创建的Proxy代理对象只读实例
4.isProxy 判断是否由reactive或readonly函数创建的Proxy代理对象实例
import { ref, reactive, isReactive, isReadonly, isProxy, isRef, readonly, shallowReadonly, } from 'vue';
export default {
setup() {
let sum = ref(1);
let person = {
name: '张三',
age: 18,
};
let personReactive = reactive({
name: '张三',
age: 18,
});
let readOnly = readonly(person);
let readOnlyReactive = readonly(personReactive);
console.log('sum isRef', isRef(sum));
console.log('personReactive isReactive', isReactive(personReactive));
console.log('readOnlyReactive isReadonly', isReadonly(readOnlyReactive));
console.log('readOnly isReadonly', isReadonly(readOnly));
console.log('readonly(响应式对象) isProxy', isProxy(readOnlyReactive));
console.log('readonly(普通对象) isProxy', isProxy(readOnly));
console.log('shallowReadonly(普通对象) isProxy', isProxy(shallowReadonly(person)));
return {
};
},
};

isReadonly(): readonly无法对基本类型数据进行只读,因此isReadonly(readonly(基本类型数据))返回false
let a = 1;
let readOnlyB = readonly(a);
console.log('isReadonly', isReadonly(readOnlyB)); // false
总结
reactive(普通对象)和readonly(普通对象)返回值都是proxy对象,因此isProxy返回true
Composition API的优势
使用传统的Options API中,新增或修改一个功能时,需要在data、methods、computed、watch中来回修改,而使用Composition API则只需要在setup函数中修改即可,代码复用性更高
组合式API可以将功能代码进行归纳,并使用hook封装功能到一个函数中

vue3模板中可以直接使用$emit、$attrs、$slots、$refs等
获取组件实例对象
import {getCurrentInstance} from 'vue'
export default {
setup(){
console.log(getCurrentInstance());
return {}
},
};
使用getCurrentInstance()获取组件实例对象

模板中则隐式的获取到了ctx组件上下文对象,因此在模板中可以直接使用$emit等
<template>
<div>
<button @click="$emit('click', 'hello')">点击</button>
</div>
</template>
vue3为子组件添加系统事件
vue3中不再推荐使用@系统事件.native修饰符,而是直接使用@系统事件
父组件
<template>
父组件
<Child @click="customEvent" />
</template>
在子组件标签中直接使用@系统事件="回调函数"
使用场景: 子组件中必须只有一个根元素
事件将添加到子组件的根元素上


子组件指定节点使用v-bind="$attrs",来触发事件
使用场景: 子组件有多个根元素,添加到指定节点上
<template>
<div>子组件</div>
<div v-bind="$attrs">子组件其他根节点</div>
<div v-bind="$attrs">子组件其他根节点</div>
</template>
$attrs属性中包含了该组件的事件,使用v-bing将事件绑定到具体的元素上


子组件中使用$emit触发事件
使用场景: 指定子组件某些节点触发事件
<template>
<div>子组件</div>
<div @click="$emit('click')">子组件自定义事件</div>
</template>

出现警告提示: 在使用emit时应该使用emits属性声明事件名称,声明后不再出现警告
<template>
<div>子组件</div>
<div @click="$emit('click')">子组件自定义事件</div>
</template>
<script>
export default {
emits: ['click'],
};
</script>
系统事件仍有冒泡机制
<template>
<div @click="parentEvent">
父组件
<Child @click="customEvent" />
</div>
</template>
setup() {
function customEvent() {
console.log('customEvent');
}
function parentEvent() {
console.log('parentEvent');
}
return {
customEvent,
parentEvent,
};
},

新的组件
Fragment组件
在vue2模板标签中必须有一个根标签
在vue3模板标签中可以没有根标签,vue3会自动创建一个Fragment组件作为虚拟根标签
当组件模板中有多个标签时,vue3会自动将多个标签渲染为Fragment虚拟组件

Teleport组件
在Son组件中使用Dialog弹窗组件,Dialog弹窗组件会渲染到Son组件中

如果想让该弹窗脱离Son组件,传统做法使用定位布局对弹窗位置进行调整,定位元素参考包含块进行定位,而Son组件中不断添加相对定位元素,那么包含块的就会改变,因此定位元素则不再准确
Teleport组件可以将Dialog弹窗组件渲染到body标签中,Teleport组件的to属性指定渲染位置,这样减少了包含块和元素之间的层级嵌套,从而避免了定位元素位置不准确的问题
<template>
<button @click="show = !show">展示/隐藏弹窗</button>
<teleport to="body">
<div v-if="show" class="dialog">
我是弹窗组件:
<p>弹窗内容</p>
<p>弹窗内容</p>
<p>弹窗内容</p>
</div>
</teleport>
</template>
将指定的元素使用teleport组件包裹起来,teleport组件会自动将元素移动到指定的位置
to: 指定要移动的元素位置(元素选择器)

使用teleport组件实现弹窗

Dialog弹窗组件
<template>
<button @click="showDialog">展示弹窗</button>
<teleport to="body">
<div v-if="show" class="dialog">
我是弹窗组件:
<p>弹窗内容</p>
<p>弹窗内容</p>
<p>弹窗内容</p>
<button @click="showDialog">隐藏弹窗</button>
</div>
</teleport>
</template>
<script>
import { ref, inject } from 'vue';
export default {
setup() {
let changeMask = inject('changeMask');
let show = ref(false);
function showDialog() {
show.value = !show.value;
changeMask();
}
return {
show,
showDialog,
};
},
};
</script>
<style>
.dialog {
width: 200px;
background-color: #72cce0;
padding: 10px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
App组件
<template>
<div class="app" :class="mask">
我是父组件
<Child />
</div>
</template>
<script>
import Child from './components/Child.vue';
import { ref, provide } from 'vue';
export default {
components: {
Child,
},
setup() {
let mask = ref('');
function changeMask() {
mask.value ? mask.value = '' : mask.value = 'mask';
}
provide('changeMask', changeMask);
return {
mask,
};
},
};
</script>
<style>
.app {
background-color: #ccc;
padding: 10px;
}
.mask {
opacity: 0.5;
pointer-events: none;
}
</style>

最佳实战:
1.在弹窗组件中定义遮罩层为整个body
2.为弹窗内容添加一个父元素
3.父元素添加遮罩层样式,以及控制显示和隐藏
App组件
<template>
<div class="app">
我是父组件
<Child />
</div>
</template>
<script>
import Child from './components/Child.vue';
export default {
components: {
Child,
},
};
</script>
<style>
.app {
background-color: #ccc;
padding: 10px;
}
</style>
Dialog弹窗组件:
<template>
<button @click="showDialog">展示弹窗</button>
<teleport to="body">
<div v-if="show" class="mask">
<div class="dialog">
我是弹窗组件:
<p>弹窗内容</p>
<p>弹窗内容</p>
<p>弹窗内容</p>
<button @click="showDialog">隐藏弹窗</button>
</div>
</div>
</teleport>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
let show = ref(false);
function showDialog() {
show.value = !show.value;
}
return {
show,
showDialog,
};
},
};
</script>
<style>
.mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.dialog {
width: 200px;
background-color: #72cce0;
padding: 10px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>

Suspense组件
Suspense组件用于等待异步组件加载完成,在等待过程中显示fallback属性指定的内容
Suspense组件底层定义了两个插槽:default和fallback
default: 默认插槽,用于显示正常内容
fallback: 备用插槽,用于显示加载过程的内容
默认情况下,组件需要等待其所有的子组件加载完成才会展示,例如,调整浏览器网速使之等待加载:

加载页面速度取决于最慢的组件然选速度,而后一并渲染组件
Suspense组件可以让组件在等待渲染期间展示备用组件
步骤:
1.使用Suspense组件包裹异步组件
2.使用defineAsyncComponent动态定义异步组件代替静态引入组件
<template>
<div class="app">
我是父组件
<Suspense>
<template #default>
<Child />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</div>
</template>
<script>
// import Child from './components/Child.vue'; // 静态加载组件
import { defineAsyncComponent } from 'vue';
const Child = defineAsyncComponent(() => import('./components/Child.vue')); // 动态异步加载组件
export default {
components: {
Child,
},
};
</script>

程序员控制组件等待加载时间
1.父组件仍然使用Suspense组件包裹异步组件并动态异步引入子组件
2.子组件中的setup则使用异步async关键字修饰,定义为异步方法,setup方法return时使用await关键字获取异步结果并返回
<template>
<div class="child">
我是子组件
{{sum}}
</div>
</template>
<script>
export default {
async setup() {
let sum = 0;
let p = new Promise((resolve, reject) => {
setTimeout(() => {
sum = 123;
resolve({ sum });
}, 1000);
});
return await p;
},
};
不再使用浏览器调整网速的方式延迟加载组件,而是自定义延迟加载组件时间
等待1s后,await p的结果并return出去

Vue3中其他改变
vu3中全局API的改变
Vue.xxx调整到app.xxx
| Vue2 | Vue3 |
|---|---|
| Vue.config.xxx | app.config.xxx |
| Vue.config.productionTip | 移除 |
| Vue.component | app.component |
| Vue.directive | app.directive |
| Vue.mixin | app.mixin |
| Vue.use | app.use |
| Vue.prototype | app.config.globalProperties |
其他改变
1.vue2中非组件化时data可以是一个对象,组件化时是一个函数,而vue3中是否为组件化都必须把data定义为函数
2.过渡类名的更改
vue2中过渡类名:
.fade-enter,
.fade-leave-to {
opacity: 0;
}
vue3中过渡类名:
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
3.移除keyCode作为v-on的修饰符,同时不再支持config.keyCodes
4.移除v-on.native修饰符
vue2使用.native修饰为原生事件
vue3中默认为原生事件,不再需要.native修饰符,如果是自定义事件,如果父组件中为子组件自定义了事件,子组件应该使用emits配置项声明事件名称
父组件为子组件定义haha事件:
<template>
父组件
<Child @haha="customEvent" />
</template>
子组件应使用emits声明事件名称:
<template>
<div>子组件</div>
<div @click="$emit('haha')">子组件自定义事件</div>
</template>
<script>
export default {
emits: ['haha'],
};
如果不使用emits声明事件,控制台则出现警告: 提示使用emits声明

5.移除filters
过滤器需要一个自定义语法,打破了大括号内表达式只能是js的表达式的设计,这不仅有学习成本而且有实现成本!官方推荐使用方法调用或计算属性去替换过滤器
例如: 使用过滤器时,{{date | dateFormat('YYYY-MM-DD')}}过滤器语法含义: |前面的结果作为|后面的函数的参数 这种语法含义和js不同
<div id="root">
今天的日期是: {{date | dateFormat}}
<br>
今天的日期是: <input type="text" :value="currentDate | dateFormat('YYYY年MM月DD日 HH:mm:ss')">
</div>
<script type="text/javascript">
new Vue({
el: '#root',
data: {
currentDate: 1749202679233,
date: 1749202679233
},
filters: {
dateFormat(value, format='YYYY-MM-DD HH:mm:ss') {
return dayjs(value).format(format);
}
}
})
</script>

浙公网安备 33010602011771号