响应式数据的实现: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的区别

  1. 从定义数据角度对比:
  • ref用来定义:基本类型数据,但是ref也可以定义对象数据类型。
  • reactive用来定义对象(或数组)类型数据,且reactive不能用来定义基本数据类型。

备注: ref也可以用来定义对象(或数组)类型数据,它内部会自动通过reactive转为代理对象。

  1. 从原理角度对比:
  • ref通过object.defineProperty()的get与set来实现响应式(数据劫持)。
  • reactive通过使用Proxy来实现响应式(数据劫持),并通过Reflect操作源对象内部的数据。

其实ref底层还是调用的reactive

  1. 从使用角度对比:
  • ref定义的数据:操作数据需要.value,模板中读取数据时直接读取不需要.value 。
  • reactive定义的数据:操作数据与读取数据均不需要.value 。
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐