vue响应式原理

7 篇文章 0 订阅

概述

在个人看来,vue响应式原理实际上就是对数据进行劫持,也称之为代理,具体的效果为:我们能够监测到数据被读取或改变的时机,并在这个时机执行处理相应情况的代码。

vue2.X

vue2.X使用Object.defineProperty()方法来实现数据劫持,该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

该方法接收三个参数,语法为Object.defineProperty(obj, prop, descriptor) ,其中obj是要定义属性的对象,prop是要定义或修改的属性的名称,descriptor是要定义或修改的属性描述符。在descriptor中我们可以定义getter和setter,当我们访问obj.prop时会调用get函数,当我们为obj.prop赋值时会调用set函数。

听起来有点抽象,但结合代码看一下就很明白了。

let obj = {}
let value = 1
Object.defineProperty(obj, 'prop', {
  get(){
    return value++
  },
  set(newValue){
    value = newValue * 10
  }
})
console.log(obj.prop) // 调用get函数,返回值就是obj.prop的值
console.log(obj.prop)
obj.prop = 10         //调用set函数,参数为10
console.log(obj.prop) 
console.log(obj.prop)
//控制台输出依次为 1 2 100 101

上述代码仅仅是对prop属性进行了劫持,要实现vue的响应式,我们需要对一个对象的所有属性均进行劫持,当属性的值是object类型时,还需要对该值进行递归操作,对它的所有子属性进行劫持。
具体实现代码如下,当我们想对obj对象进行劫持时,执行observe(obj)即可。

// 为object的key属性设置getter和setter
function toReactive(obj, key, value){
  // 这里形成了一个闭包,value的值不会被销毁,会一直存在于内存中
  Object.defineProperty(obj, key, {
    get(){
      // 模拟数据被读取时要执行的代码
      console.log(`读取属性${key}, 执行相关代码...`)
      return value
    },
    set(newValue){
      // 如果新值和旧值相同,直接return
      if(newValue === value){
        return
      }
      // 模拟数据被改变时要执行的代码
      console.log(`设置属性${key}的值为${newValue}, 执行相关代码...`)
      // 如果新值是一个对象,则调用observe方法,对所有属性进行劫持
      if(typeof newValue === 'object'){
        observe(newValue)
      }
      value = newValue
    }
  })
}

// 对obj的所有属性进行劫持
function observe(obj){
  if(typeof obj === 'object'){
    // 遍历所有属性
    for(let key in obj){
      // 该属性的值是对象,递归调用observe函数
      if(typeof obj[key] === 'object'){
        observe(obj[key])
      }
      // 为该属性设置getter和setter
      toReactive(obj, key, obj[key])
    }
  }
}

// 测试函数
function test(){
  // 定义测试对象
  const data = {
    id: 1,
    student:{
      name: 'tom',
      age: 10
    }
  }
  // 对data对象的所有属性进行劫持
  observe(data)

  // 读取和更改data的属性
  data.id
  data.id = 2
  console.log('--------------------分隔符1-------------------')

  data.student.age
  data.student.age = 20
  console.log('--------------------分隔符2-------------------')

  data.student
  data.student = {
    name: 'li',
    age: 30,
  }
  console.log('--------------------分隔符3-------------------')

  data.student.name
  data.student.name = 'jack'
  console.log('--------------------分隔符4-------------------')

  data.newProp = 'new' // 不能劫持到新增属性
  data.newProp
  console.log('--------------------分隔符5-------------------')

  console.log(data.id)
  console.log(data.student.name)
  console.log(data.student.age)
}
test()

ps:我感觉比较难理解的就是那个toReactive()方法内部作用域形成的闭包。

代码中测试函数test()的输出如下,可以看到,使用Object.defineProperty()对数据进行劫持时,不能劫持到新增属性

读取属性id, 执行相关代码...
设置属性id的值为2, 执行相关代码...
--------------------分隔符1-------------------
读取属性student, 执行相关代码...
读取属性age, 执行相关代码...
读取属性student, 执行相关代码...
设置属性age的值为20, 执行相关代码...
--------------------分隔符2-------------------
读取属性student, 执行相关代码...
设置属性student的值为[object Object], 执行相关代码...
--------------------分隔符3-------------------
读取属性student, 执行相关代码...
读取属性name, 执行相关代码...
读取属性student, 执行相关代码...
设置属性name的值为jack, 执行相关代码...
--------------------分隔符4-------------------
--------------------分隔符5-------------------
读取属性id, 执行相关代码...
2
读取属性student, 执行相关代码...
读取属性name, 执行相关代码...
jack
读取属性student, 执行相关代码...
读取属性age, 执行相关代码...
30

vue3.X

vue3.X的响应式是通过代理对象来实现的。

学过软件设计模式的话,应该对代理对象这个词非常熟悉。设计模式中有一种结构型模式叫做代理模式(Proxy Pattern) :给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英文叫做Proxy或Surrogate,它是一种对象结构型模式。

假如我们要监测数据对象data,当我们想读取或改变data的属性时,我们不直接引用data对象,而是创建一个data的代理对象dataProxy,在dataProxy中定义get和set函数,从而访问data对象里数据并实现数据的劫持。

具体代码如下,执行observe(data)的返回值就是data的代理对象。

// 返回obj的代理对象
function observe(obj){
  for(let key in obj){
    if(typeof obj[key] === 'object'){
      // 如果属性值是对象类型,则递归处理
      obj[key] = observe(obj[key])
    }
  }
  return new Proxy(obj, {
    get(target, key){
      console.log(`读取属性${key}, 执行相关代码...`)
      return target[key]
    },
    set(target, key, value){
      if(target[key] === value){
        return
      }
      console.log(`设置属性${key}的值为${value}, 执行相关代码...`)
      target[key] = (typeof value === 'object') ? observe(value) : value
      return true
    }
  })
}

// 测试函数
function test(){
  // 定义测试对象
  let data = {
    id: 1,
    student:{
      name: 'tom',
      age: 10
    }
  }
  // 获取data的代理对象
  data = observe(data)

  // 读取和更改data的属性
  data.id
  data.id = 2
  console.log('--------------------分隔符1-------------------')

  data.student.age
  data.student.age = 20
  console.log('--------------------分隔符2-------------------')

  data.student
  data.student = {
    name: 'li',
    age: 30,
  }
  console.log('--------------------分隔符3-------------------')

  data.student.name
  data.student.name = 'jack'
  console.log('--------------------分隔符4-------------------')

  data.newProp = 'new' // 可以劫持到新增属性
  data.newProp
  console.log('--------------------分隔符5-------------------')

  console.log(data.id)
  console.log(data.student.name)
  console.log(data.student.age)
}
test()

相比于Object.defineProperty(),使用Proxy类来劫持数据,写法要简单的多。

程序输出如下,可以看到,Proxy可以劫持到新增属性的变化

读取属性id, 执行相关代码...
设置属性id的值为2, 执行相关代码...
--------------------分隔符1-------------------
读取属性student, 执行相关代码...
读取属性age, 执行相关代码...
读取属性student, 执行相关代码...
设置属性age的值为20, 执行相关代码...
--------------------分隔符2-------------------
读取属性student, 执行相关代码...
设置属性student的值为[object Object], 执行相关代码...
--------------------分隔符3-------------------
读取属性student, 执行相关代码...
读取属性name, 执行相关代码...
读取属性student, 执行相关代码...
设置属性name的值为jack, 执行相关代码...
--------------------分隔符4-------------------
设置属性newProp的值为new, 执行相关代码...
读取属性newProp, 执行相关代码...
--------------------分隔符5-------------------
读取属性id, 执行相关代码...
2
读取属性student, 执行相关代码...
读取属性name, 执行相关代码...
jack
读取属性student, 执行相关代码...
读取属性age, 执行相关代码...
30
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值