响应式数据的实现:ref和reactive
在vue2中data配置
中的属性会通过Object.defineProperty
原理最终包装成响应式数据_data。
vue3中为我们提供了两种包装响应式数据的方法:ref和reactive
ref函数
注意这里的ref和vue2中的ref不一样,这里是一个ref函数
。
ref的引入
上面使用setup包裹页面数据,但是这样编写出的数据不是响应式的,即数据改变页面不会被重新加载。从vue2中我们知道一个数据要实现响应式一定有对应的响应式得getter和setter方法(实现原理是Object.defineProperty
),在vue3中通过ref函数
帮我们生成响应式的getter和setter:
// 伪代码,不是真正的实现
const myRef = {
_value: 0,
get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}
ref的实现原理也是通过Object.defineProperty
实现的
ref的作用
定义一个响应式的数据
基本类型ref的使用
- 引入 :
import { ref } from 'vue'
- 格式:
const xxx = ref(数据)
(创建一个包含响应式数据的引用对象(reference对象,简称ref对象))
ref负责将传入的数据包装成一个带有响应式的getter和setter的对象
eg:
可以发现具体值在该RefImpl的.value
属性上 - 值的获取:
js中:变量名.value
模板中读取数据:不需要.value
,直接:<div>{{变量名}}</div>
(因为vue3在模板中自动帮我们读取其中的.value)
例子:
<template>
<h1>app组件</h1>
<h2>姓名{{ name }}</h2>
<h2>年龄{{ age }}</h2>
<button @click="sayHello">hello</button>
<button @click="changeInfo">修改人的信息</button>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'App',
setup() {
// 数据
let name = ref('yang')
let age = ref(18)
// 方法
function sayHello() {
alert(`你好呀,我叫${name.value}`)
}
function changeInfo() {
console.log(name)
name.value = 'cheng',
age.value = 20
}
return {
name,
age,
sayHello,
changeInfo
}
}
}
</script>
对象属性ref的使用
ref当然也可以包装对象,那就有一个问题。他是否会包装对象中的数据,让对象中的数据也成为响应式的,这样对象中的数据改变才能重新渲染页面。
答案是:对象中的数据vue也帮我们设置成了响应式的但是不是通过ref实现的,是通过Proxy
(ES6语法)实现的。(——vue3中封装了reactive()函数
来实现Proxy
)
所以获取ref包装的对象的属性时: 对象.value.属性
(属性后面无需再加value了,属性不是用ref封装的)
eg:app.vue
<template>
<h1>app组件</h1>
<h2>姓名{{ name }}</h2>
<h2>年龄{{ age }}</h2>
<h2>工作种类{{ job.type }}</h2>
<h2>薪资{{ job.salary }}</h2>
<button @click="sayHello">hello</button>
<br />
<button @click="changeInfo">修改人的信息</button>
<button @click="changeJob">修改工作信息</button>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'App',
setup() {
// 数据
let name = ref('yang')
let age = ref(18)
let job = ref({
type: '前端工程师',
salary:'30k'
})
// 方法
function sayHello() {
alert(`你好呀,我叫${name.value}`)
}
function changeInfo() {
console.log(name)
name.value = 'cheng',
age.value = 20
}
function changeJob() {
job.value.type = 'UI设计师'
job.value.salary = '100k'
}
return {
name,
age,
job,
sayHello,
changeInfo,
changeJob
}
}
}
</script>
放弃ref的深层响应性_shallowRef
我们将ref可以实现对象的属性的响应性叫做ref的深层响应性。
同时我们可以放弃ref的深层响应性,通过shallow ref
实现,对于浅层的ref只有.value的访问会被追踪。浅层的ref可以于避免对大型数据的响应性开销来优化性能。
eg:
const state = shallowRef({ count: 1 })
// 不会触发更改
state.value.count = 2
// 会触发更改
state.value = { count: 2 }
shallowRef的数据类型:
function shallowRef<T>(value: T): ShallowRef<T>
interface ShallowRef<T> {
value: T
}
ref的响应式原理
- 基本类型的数据:响应式依然是靠
object.defineProperty()
的get 与set
完成的。 - 对象类型的数据:最外层使用的是
object.defineProperty()
的get 与set
,内部“求助”了Vue3.0中的一个新函数——reactive函数
。
ref在模板中解包的注意事项
- 在模板渲染上下文中,只有顶级的 ref 属性才会被解包。
const count = ref(0)
const object = { id: ref(1) }
//因此,这个表达式按预期工作:
{{ count + 1 }}
//...但这个不会:
{{ object.id + 1 }}
- 想要在模板中使用
object.id
的时候我们可以先解构 id,然后再模板中使用{{ id + 1 }}
- 或者直接使用
.value
进行取值,在模板中使用{{object.id.value+1}}
const { id } = object
//使用
{{ id + 1 }}
- 特殊情况:如果
ref 是文本插值的最终计算值
(即 {{ }} 标签),那么它将被解包,因此以下内容将渲染为 1
{{ object.id }}
上述写法等价于 {{ object.id.value }}
reactive函数
reactive作用
作用:定义一个对象类型
的响应式数据(基本类型不要用它,要用ref函数)
对象属性reactive使用
- 语法:
const 代理对象 = reactive(源对象)
- 接收一个对象(或数组),返回一个代理对象(Proxy的实例对象,简称proxy对象)
通过代理对象操作源对象内部数据进行操作。
eg:
let job = reactive({
type: '前端工程师',
salary:'30k'
})
console.log(job)
例子:
app.vue
<template>
<h1>app组件</h1>
<h2>姓名{{ name }}</h2>
<h2>年龄{{ age }}</h2>
<h2>工作种类{{ job.type }}</h2>
<h2>薪资{{ job.salary }}</h2>
<h2>c:{{ job.a.b.c }}</h2>
<button @click="sayHello">hello</button>
<br />
<button @click="changeInfo">修改人的信息</button>
<button @click="changeJob">修改工作信息</button>
</template>
<script>
import { ref,reactive } from 'vue'
export default {
name: 'App',
setup() {
// 数据
let name = ref('yang')
let age = ref(18)
let job = reactive({
type: '前端工程师',
salary: '30k',
a:{
b: {
c:6
}
}
})
// 方法
function sayHello() {
alert(`你好呀,我叫${name.value}`)
}
function changeInfo() {
console.log(name)
name.value = 'cheng',
age.value = 20
}
function changeJob() {
// console.log(job)
job.type = 'UI设计师'
job.salary = '100k'
job.a.b.c = 666
}
return {
name,
age,
job,
sayHello,
changeInfo,
changeJob
}
}
}
</script>
reactive定义的响应式数据是“深层次的”。
数组属性reactive使用
Proxy封装的数组,可以直接通过下标修改数据,同时实现响应式布局。
定义数据
let hobby = reactive(['吃饭', '睡觉', '打豆豆'])
修改数据
function changeHobby() {
hobby[0] = 'study'
}
当数组数据改变可以引起页面重新渲染
例子,reactive实现上述ref例子
app.vue:
<template>
<h1>app组件</h1>
<h2>姓名{{ person.name }}</h2>
<h2>年龄{{ person.age }}</h2>
<h2>工作种类{{ person.job.type }}</h2>
<h2>薪资{{ person.job.salary }}</h2>
<h2>c:{{ person.job.a.b.c }}</h2>
<h3>hobby:{{ person.hobby}}</h3>
<button @click="changeInfo">修改人的信息</button>
</template>
<script>
import { ref,reactive } from 'vue'
export default {
name: 'App',
setup() {
let person = reactive({
name: 'yang',
age: 18,
job: {
type: '前端工程师',
salary: '30k',
a: {
b: {
c: 6
}
}
},
hobby: ['吃饭', '睡觉', '打豆豆']
})
function changeInfo() {
person.name = 'cheng',
person.age = 18,
person.job.type = 'UI设计师'
person.job.salary = '100k'
person.job.a.b.c = 666
person.hobby[0] = 'study'
}
return {
person,
changeInfo
}
}
}
</script>
放弃reactive的深层响应性_shallowReactive
Vue 能够拦截对响应式对象所有属性的访问和修改,以便进行依赖追踪和触发更新。reactive() 将深层地
转换对象:当访问嵌套对象时,它们也会被 reactive()
包装。与浅层 ref 类似,这里也有一个 shallowReactive()
API 可以选择退出深层响应,浅层的reactive只会对第一层对象实现响应式:
const state = shallowReactive({
foo: 1,
nested: {
bar: 2
}
})
// 更改状态自身的属性是响应式的
state.foo++
// ...但下层嵌套对象不会被转为响应式
isReactive(state.nested) // false
// 不是响应式的
state.nested.bar++
shallowReactive()
和 reactive()
不同,这里没有深层级的转换:一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包
了(需要使用.value进行访问,并且值为ref的属性的响应式不会消失)。
reactive的响应式原理
直接源数据封装成Proxy代理对象(ES6语法中的代理对象),Proxy代理对象是响应式的。
reactive() 返回的是一个原始对象的 Proxy,它和原始对象是不相等的,为保证访问代理的一致性:
- 对同一个原始对象调用 reactive() 会总是返回同样的代理对象
- 对一个已存在的代理对象调用 reactive() 会返回其本身:
const raw = {}
const proxy = reactive(raw)
// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false
// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true
依靠深层响应性,响应式对象内的嵌套对象依然是代理,默认添加reactive
实现响应式:
const proxy = reactive({})
const raw = {}
proxy.nested = raw
console.log(proxy.nested === raw) // false
reactive的局限性
- 有限的值类型:它只能用于
对象类型
(对象、数组和如 Map、Set 这样的集合类型)。它不能持有如 string、number 或 boolean 这样的原始类型
。 - 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时响应式消失。
- 不能替换整个对象:替换整个对象为新的reactive()对象,之前数据的响应式数据消失,并且新替换的数据不会渲染到页面上;替换整个对象为普通对象,所有数据响应式消失。无论替换成什么数据页面的响应式都会显示。
let state = reactive({ count: 0 })
// 上面的 ({ count: 0 }) 引用将不再被追踪
// (赋值一个新的响应式对象,但页面上连接的是原来的state对象,之前的state对象连接被断开,所以不会触发响应式)
state = reactive({ count: 1 })
//不会触发响应式,响应性连接已丢失
state.count++
//赋值普通对象响应性连接丢失
state = { count: 3 }
// 不会触发响应式,响应性连接已丢失
state.count++
ref 和 reactive的混合使用
ref作为reactive的对象属性值使用
一个 ref 会在作为响应式对象的属性被访问或修改时自动解包,无需使用.value
访问值。
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
ref作为reactive的数组值使用
当 ref 作为reactive的数组或原生集合类型
(如 Map) 中的元素被访问时,它不会被解包
:
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)
Dom的更新时机
当修改了响应式状态时,DOM 会被自动更新。但是DOM 更新不是同步的。
Vue 会在“next tick”更新周期
中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。
nextTick
要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API:
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// 现在 DOM 已经更新了
}
vue3的响应式原理
vue2的响应式原理
- 实现原理:
(1)对象类型:通过object.defineProperty()
对属性的读取、修改进行拦截(数据劫持)。
(2)数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。
eg:对象类型
<script type='text/javascript'>
let person ={
name:'yang',
age:18
}
// vue2的响应式原理
let p ={}
Object.defineProperty(p,'name',{
get(){
return person.name
},
set(value){
console.log("name被修改了,触发了响应式")
person.name = value
}
})
Object.defineProperty(p,'age',{
get(){
return person.age
},
set(value){
console.log("age被修改了,触发了响应式")
person.age = value
}
})
</script>
- 存在问题:
(1)新增、删除对象属性,不会触发响应式。
但是vue2中也可以解决:
新增属性this.$set(this.person, 'sex','女')
删除属性this.$delete(this.person, 'sex'')
(2)直接通过下标修改数组,不会触发响应式。
但是vue2中也可以解决:
修改数组hobby第0个元素:this.$set(this.person.hobby, 0,'学习')
修改数组hobby第0个元素:this.person.hobby.splice(0,1,'学习')
vue3的响应式原理
vue3的响应式优点
vue2中存在的问题在vue3中都解决了:
(1)新增属性、删除属性,界面会更新。
(2)直接通过下标修改数组,界面会自动更新。
eg:实现添加sex属性和删除name属性
<template>
<h1>app组件</h1>
<h2 v-show="person.name">姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<h2 v-show="person.sex">性别:{{ person.sex }}</h2>
<h2>工作种类:{{ person.job.type }}</h2>
<h2>薪资:{{ person.job.salary }}</h2>
<h2>c:{{ person.job.a.b.c }}</h2>
<h3>hobby:{{ person.hobby}}</h3>
<button @click="changeInfo">修改人的信息</button>
<button @click="addsex">添加一个sex属性</button>
<button @click="deleteName">删除一个name属性</button>
</template>
<script>
import {reactive } from 'vue'
export default {
name: 'App',
setup() {
//数据
let person = reactive({
name: 'yang',
age: 18,
job: {
type: '前端工程师',
salary: '30k',
a: {
b: {
c: 6
}
}
},
hobby: ['吃饭', '睡觉', '打豆豆']
})
// 方法
function changeInfo() {
person.name = 'cheng',
person.age = 18,
person.job.type = 'UI设计师'
person.job.salary = '100k'
person.job.a.b.c = 666
person.hobby[0] = 'study'
}
function addsex() {
person.sex = '女'
}
function deleteName (){
delete person.name
}
return {
person,
changeInfo,
addsex,
deleteName
}
}
}
</script>
vue3的响应式原理1——Proxy
通过Proxy
实现,Proxy
是es6中的语法,是window身上的一个内置函数window.Proxy
,可以直接使用
proxy
的意思是代理,
- 语法格式:
const p =new Proxy(person,{})
第一个参数:
可以使p映射person的操作,即p代理着person,当p的值发生变化时person的值也会发生变化(和Object.defineProperty相似之处),而且增删改
变化都可以被检测到(和Object.defineProperty不同之处,Object.defineProperty只能检测到改
的变化)。
——这就形成了数据代理,但是还没有完成响应式
第二个参数:
用于实现响应式,里面可以编写增删改
操作的响应式
<script type='text/javascript'>
let person ={
name:'yang',
age:18
}
// vue3的响应式原理
const p =new Proxy(person,{
// 读取p的属性的响应式
get(target,propName){
// target代表person源对象
// propName代表读取的属性名
console.log(`有人读取了person身上的${propName}属性`)
// propName是一个变量需要使用数组形式读取
return target[propName]
},
// 修改p或给p追加属性时的响应式
set(target,propName,value){
console.log(`有人修改了了person身上的${propName}属性,我要去修改页面了`)
target[propName] = value
},
// 删除p的属性时的响应式
deleteProperty(target,propName){
console.log(`有人删除了person身上的${propName}属性,我要去修改页面了`)
return delete target[propName]
}
})
</script>
vue3的响应式原理2——Reflect
Reflect也是ES6新增的一个属性,在Window身上,可以直接调用。
- 作用:可以实现对对象属性的增删改查
let person ={
name:'yang',
age:18
}
// 读取
Reflect.get(person,"name")
// 修改
Reflect.set(person,"name","cheng")
// 添加
Reflect.set(person,"sex","男")
// 删除
Reflect.deleteProperty(person,"name")
- Reflect身上也有 defineProperty 属性:
let person ={
name:'yang',
age:18
}
Reflect.defineProperty(person,"school",{
get(){
return "nefu"
},
set(value){
person.school = value
}
})
//Object写法
/*Object.defineProperty(person,"school",{
get(){
return "nefu"
},
set(value){
person.school = value
}
})*/
Reflect.defineProperty 和 Object.defineProperty的区别:
Object.defineProperty
对一个代理对象设置两个相同的属性会直接报错。
Reflect.defineProperty
对一个代理对象设置两个相同的属性不会报错,且以第一次设置的属性为准。
vue3的响应式原理(Proxy和Reflect共同实现)
<script type='text/javascript'>
let person ={
name:'yang',
age:18
}
// vue3的响应式原理
const p =new Proxy(person,{
// 读取p的属性的响应式
get(target,propName){
// target代表person源对象
// propName代表读取的属性名
console.log(`有人读取了person身上的${propName}属性`)
// propName是一个变量需要使用数组形式读取
return Reflect.get(target,propName)
},
// 修改或追加p属性时的响应式
set(target,propName,value){
console.log(`有人修改了了person身上的${propName}属性,我要去修改页面了`)
Reflect.set(target,propName,value)
},
// 删除p的属性时的响应式
deleteProperty(target,propName){
console.log(`有人删除了person身上的${propName}属性,我要去修改页面了`)
return Reflect.deleteProperty(target,propName)
}
})
</script>
vue3的响应式原理就是通过Proxy代理对象
和 Reflect反射对象
实现的
reactive和ref的区别
- 从定义数据角度对比:
ref
用来定义:基本类型
数据,但是ref也可以定义对象数据类型。reactive
用来定义对象(或数组)
类型数据,且reactive不能用来定义基本数据类型。
备注: ref也可以用来定义对象(或数组)类型数据,它内部会自动通过reactive
转为代理对象。
- 从原理角度对比:
- ref通过
object.defineProperty()
的get与set来实现响应式(数据劫持)。 - reactive通过使用
Proxy
来实现响应式(数据劫持),并通过Reflect
操作源对象内部的数据。
其实ref底层还是调用的reactive。
- 从使用角度对比:
- ref定义的数据:操作数据需要
.value
,模板中读取数据时直接读取不需要.value 。 - reactive定义的数据:操作数据与读取数据均不需要.value 。