Vue3
- Vue3简介
- 创建Vue3应用
- 分析工程结构
- 拉开序幕的
setup
函数 ref
函数reactive
函数Vue3
响应式原理reactive
和ref
的对比setup
函数注意点computed
计算属性函数watch
监视函数watchEffect
函数- 生命周期
Hook
toRef
和toRefs
shallowReactive
和shallowRef
readonly
和shallowReadonly
- 深度数据和浅度数据的理解
toRaw
和markRaw
customRef
函数: 自定义ref
provide
与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>