Vue响应式原理(一)

176 阅读5分钟

这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战

1. 什么是响应式?

数据变化,视图(也就是DOM)会自动变化。

我们在使用vue差值表达式时,改变数据后,页面上相应数据会自动改变。

2. 实现一个响应式系统,需要做些什么?

我们要实现的是:数据A改变时,视图中用到数据A的所有地方,都要发生改变。

现在将上面的步骤拆为3步:

  • 监听数据A的变化 --- 数据劫持 (当数据变化时,我们可以做一些特定的事情)
  • 收集所有依赖于数据A的元素 --- 依赖收集 (我们要知道那些视图层的内容(DOM)依赖了哪些数据(state))
  • 通知上述这些依赖于数据A的元素更新 --- 派发更新 (数据变化后,如何通知依赖这些数据的DOM)

vue实现响应式主要做了这么几件事:

  • 数据劫持:new Vue的时候遍历data对象,用Object.defineProperty给所有属性加上了getter和setter
  • 依赖收集:render的过程(执行render()时),会触发数据的getter,在getter的时候把当前的watcher对象收集起来
  • 派发更新:setter的时候,遍历这个数据的依赖对象(watcher对象),进行更新

3. 数据劫持

Vue2的响应式是利用Object.defineProperty()实现的,vue3改成了proxy, 本文先讨论Vue2。

在读取某个属性值时,会触发Object.defineProperty()中的get方法;

在设置某个属性的值时,会触发Object.defineProperty()中的set方法;

我们可以给某个对象的属性,通过Object.defineProperty()设置get和set方法,来监听这个属性的变化,也就实现了对这个属性的数据劫持。

举例:

let obj = {}
let val = 1;
Object.defineProperty( obj, 'a', {
    get() {
        console.log('有元素访问a属性啦!')
        return val;
    },
    set(newVal) {
        if (val === newVal) return
        console.log('设置a属性的值为:'+newVal)
    return newVal;
    }
} )
console.log('obj.a', obj.a);
obj.a = 4

响应式defineProperty.png 从这个例子中我们可以看到,需要一个全局变量val来保存这个属性的值,周转get和set,因此,我们可以定义一个名为defineReactive() 函数(用于提供一个闭包的环境)。

// value使用了参数默认值
function defineReactive(data, key, value = data[key]) {
  Object.defineProperty(data, key, {
    get: function reactiveGetter() {
      return value
    },
    set: function reactiveSetter(newValue) {
      if (newValue === value) return
      value = newValue
    }
  })
}
​
defineReactive(obj, a, 1)

闭包有内外两层函数,上述代码中,defineReactive是外层函数,get和set是内层函数。val替代了上面临时的全局变量的作用。

上面的代码只能侦测对象的某一个属性,我们需要递归侦测对象的全部属性。

递归侦测对象的全部属性

新建一个Observer类,将一个正常的object转换为全部属性都被侦测的对象。

class Observer {
    constructor(value) {  // 这里的value是new Observer传入的参数,也就是某个正常obj
        this.value = value;
        this.walk();
    }
    walk() {
        Object.keys(this.value).forEach((key) => defineReactive(this.value, key))
    }
 }
 
 const obj = { a:1, b:2 }
 new Observer(obj)

Observer类中,接收的参数value是要监听的对象,Object.keys(对象),会返回一个由对象所有属性组成的数组。对此数组进行遍历,让监测的对象带着每一个属性都执行一遍defineReactive函数,即可让对象obj的全部属性都被侦测。

深度侦测对象的多层嵌套属性

上述的Observer类,已经可以侦测将某个对象的全部属性,但如果某个属性的值,还是一个对象,那么无法侦测这个位于属性值的对象。我们需要使用递归来完成嵌套属性的数据劫持。

递归函数需要:

  • 结束条件: 属性值不是对象时,结束
  • 递推关系:对象使用new Oberser来处理;对象的属性值也使用new Oberser来处理;对象的属性值的属性值也使用new Oberser来处理...

使用递归来完成嵌套属性的数据劫持:

// 入口函数
function observe(data) {
  if (typeof data !== 'object') return
  // 调用Observer
  new Observer(data)
}
​
class Observer {
  constructor(value) {
    this.value = value
    this.walk()
  }
  walk() {
    // 遍历该对象,并进行数据劫持
    Object.keys(this.value).forEach((key) => defineReactive(this.value, key))
  }
}
​
function defineReactive(data, key, value = data[key]) {
  // 如果value是对象,递归调用observe来监测该对象
  // 如果value不是对象,observe函数会直接返回
  // value是对象的属性值
  observe(value)
  Object.defineProperty(data, key, {
    get: function reactiveGetter() {
      return value
    },
    set: function reactiveSetter(newValue) {
      if (newValue === value) return
      value = newValue
      observe(newValue) // 设置的新值也要被监听
    }
  })
}
​
const obj = {
  a: 1,
  b: {
    c: 2
  }
}
​
observe(obj)

完成嵌套属性的数据劫持的代码,是defineReactive函数里的observe(value),在被监测对象的某个属性下,再次监测这个属性的属性值,如果这个属性值是对象,再调用Observer,对这个是对象的属性值 的 属性进行数据劫持;... 直到某个属性下,属性值不是对象,直接return;

注意: 设置的新值也要被监听。执行了observe(obj)后,用户可能再对obj添加新属性。

递归函数是指自己调用自己,首先调用observe(obj)函数,通过observe函数,调用Oberver类,在Observer类中,对obj的每个属性都调用defineReactive函数 实现对obj的全部属性进行侦测。

接着,在defineReactive函数中,又对每个属性的属性值,调用observe(属性值)函数,如果某个属性的属性值A不是对象,那么observe(属性值A)函数return,不做处理。如果某个属性的属性值A是对象,那么observe(属性值A)函数,通过observe函数,调用Oberver类,在Observer类中,对属性值A的每个属性都调用defineReactive函数 实现对属性值A的全部属性进行侦测。

接着,在defineReactive函数中,又对属性值A的每个属性的属性值,调用observe(属性值)函数,...

observe、Observer、defineReactiv的调用关系:

数据劫持.png