浅析vue2响应式原理

206 阅读4分钟

响应式:简而言之,就是当数据发生改变的时候,视图会重新渲染,更新为最新的值。

初识Object.defineProperty

Object.defineProperty()的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性

Object.defineProperty(obj, prop, desc)
  1. obj 需要定义属性的当前对象
  2. prop 当前需要定义的属性名
  3. desc 属性描述符

Object.defineProperty是 Vue 响应式系统的精髓。

Vue使用 Object.defineProperty 为对象中的每一个属性,设置 get 和 set 方法,进行数据劫持/监听;

get 值是一个函数,当属性被访问时,会触发 get 函数

set 值同样是一个函数,当属性被赋值时,会触发 set 函数

const data = {};
let name = 'Vueeee'

// 在data对象中定义name属性
Object.defineProperty(data,'name',{
  // 当访问data.name时自动调用此函数
  get(){
    console.log('🚀- 我是get');
    return name
  },
  // 当赋值data.name时自动调用此函数
  set(newValue){
    console.log('🚀- 我是set');
    name = newValue
    // vue会接着做一个视图重新渲染的操作
  }
})

// 调用了data.name属性的get方法
console.log(data.name)

// 调用了data.name属性的set方法
data.name = 'Hyyyyy'
console.log(data.name)

image.png

基本的响应式实现

上面说道可以使用Object.defineProperty来实现vue当中的响应式。 现在来使用Object.defineProperty来实现一个mini的vue响应式的例子

// 如果在vue中我们只需要这样书写,即是响应式数据。
export default {
    data() {
        return{
          name: 'Hyyy',
          age: 23,
        }
    }
}

那么vue是怎么做到呢?我们来写个简单的例子


// 首先模拟vue2中data
const data = {
  name: 'Hyyy',
  age: 23,
}

// 使data对象 变成响应式数据
observer(data)

// 为传入的对象做响应式
function observer(target){
  // 只处理对象
  if(typeof target !== 'object' || target === null) return target

  // 遍历,为对象中的每一个属性,设置 get 和 set 方法,进行数据劫持/监听
  for(let key in target){
    // 传入Object.defineProperty()方法 所需要的对象本身、key和value
    defineReactive(target, key, target[key])
  }
}

 // 为传入的对象做数据劫持/监听
function defineReactive(target, key, value){
  Object.defineProperty(target, key, {
    get(){
      return value
    },
    set(newValue){
      // 如果当前value不等于
      if(value !== newValue){
        value = newValue
        console.log('触发set,🔥更新视图操作')
      }
    }
  })
}

data.name = 'yHhhhhhhh'  

image.png 上面的例子就是如何使用Object.defineProperty()来实现一个定义mini响应式数据的过程。

vue的源码肯定更加复杂,会判断各种情况,但核心就是这样了.

处理值为复杂对象的情况

上面我们只处理了最简单的情况,对象中的属性只是数字、字符串

如果是复杂的对象又该如何呢?

const data = {
  name: 'Hyyy',
  age: 23,
  // 如果我们加上个对象
  friend: {
      friendName: 'xxx',
  }
}

function observer(){
    /* 之前代码 */
}
function defineReactive(){
    /* 之前代码 */
}


// 对新加入的friend属性中的name属性进行更改
data.friend.name = 'xxx2号'

此时会发现控制台中并没有和上个demo一样,出现'触发set,🔥更新视图操作'这句log

image.png 此时如果我们在上面定义的defineReactive方法中console.log打印传来的key,会发现

 function defineReactive(target, key, value){
  console.log(key) -> 只能打印出data对象中的 name、age、friend三个属性
  //并不能打印出friend属性中的firendName属性,所以我们其实是没有给对象中的属性做数据监听的
  
  Object.defineProperty(target, key, {
      /* 之前代码 */
  })
}

image.png

如何达到对象中的属性也可以监听到呢?简单,只需要在defineReactive()函数中加入一行observer(value)...

function defineReactive(target, key, value){
  // 深度观察,只需要将当前对象传给observer,也做个监听就好了
  observer(value)
  Object.defineProperty(target, key, {
      /* 之前代码 */
  })

image.png

但还没有完!!!

如果我们将data对象中的属性,赋值为一个新的对象,那这个对象还是没有受到监听的...

例如


const data = {
   age: 23,
    /* 之前代码 */
}
function observer(){
    /* 之前代码 */
}
function defineReactive(){
    /* 之前代码 */
}

data.age = { number: 23} // 会触发一次更新

//此时如果我们..
data.age.number = 21  // 又更改了,想想中会再触发一次更新


但并没有,只有data.age = { number: 23}触发了更新,因为我们没有监听data.age.number image.png

所以我们在set的时候,也就是data.age = { number: 23} 的时候,也要在set方法中对新传来的对象{number: 23}进行监听

set(newValue){
  // 加上这句!!!
  observer(newValue)
  /* 之前代码 */
  /* 之前代码 */
  /* 之前代码 */
  /* 之前代码 */
}

  /* 之前代码 */
  /* 之前代码 */

data.age = { number: 23} // 会触发一次更新

data.age.number = 21  // 再触发一次更新

image.png

也就是我们对对象中的对象属性也要进行深度的监听,才能在数据改变的时候及时更新。

所以这也是我们再使用Object.defineProperty()做响应式的一个问题,即使我们数据是个层级很深的对象,他也会在一开始对所有数据不断的深度监听,直到他是个普通的值为止。

所以Vue3中, 改用了Proxy来解决,Proxy就会在使用到这个数据的时候,才会去做这个监听的过程。

除了上述问题,如果我们做如下操作:


delete data.某属性

data.新属性 = 'xxx'

并不会被响应到,因为Object.defineProperty()是没法处理属性删除与属性新增的。

所以在vue中删除我们会使用Vue.delete,新增我们会使用Vue.set

算是Object.defineProperty()的一个弱点,我们要记一下。