Vue 2.x中的响应式实现原理-2

162 阅读2分钟

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

前言

MVVM模式最核心的特性就是数据双向绑定,Vue构建了一套响应式系统,可以实现用声明的方式绑定数据,从而在数据发生变化时自动渲染视图

Vue 2.x 响应式系统的实现原理

考虑一下下面的代码

let user = {name: 'jack', address: {city: '北京'}}
observer(user)
user.address = {city: '河北'}
user.adress.city = '天津'

上述代码有两次属性变化,一次是为address属性设置了一个对象字面量,一次是修改address对象的city属性值,但如果运行代码,就会发现只能看到一次“视图更新”。这是因为在修改address属性时是为它赋值了一个新的对象,而这个新对象的属性并没有被侦测,因此后面对这个新对象属性值的更改就没有被侦测到。

针对这种情况,可以在set() 方法中为新的值添加 observer() 调用,代码如下:

// 对Object.defineProperty() 方法进行封装
function defineReactive(obj, key, value){
  // 通过递归调用解决多层对象嵌套的属性侦测问题
  observer(value)
  Object.defineProperty(obj, key, {
    get() {
      return value
    },
    set(newValue){
      if(newValue !== value) {
        //如果newValue是对象类型,则继续侦测该对象的所有属性变化
        //observer() 函数中已经有对象参数是否是对象类型的判断代码,此处可省略
        //observer(newValue)
         updateView(); // 在set()方法中触发更新
         value = newValue
       }
    } 
  })
}

至此,我们已经解决了对象侦测的问题,但还需要考虑数组侦测的问题。代码如下:

let user = {name: 'jack', address: {city: '北京'}, emails: ['123456@qq.com']}
// 对user对象的所有属性变化进行侦测
observer(user)
user.emails.push('456789@qq.com')

emails属性是数组类型,当通过push()方法改变数组内容时,并不会触发独享的set()方法的调用。如果我们想在调用数组方法修改数组内容时得到通知,就需要替换数组的原型对象,代码如下:

const arrayPrototype = Array.prototype
// 使用数组的原型对象创建一个新对象
const proto = Object.create(arrayPrototype)

// 改变数组自身内容的方法只有如下7个,对它们进行拦截
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
.forEach(method => {
  Object.defineProperty(proto, method, {
    get(){
      updateView()
      // 返回数组原有的方法
      return arrayPrototype[method]
    }
  }  
})

// 对一个对象中所有属性的变化进行侦测
function observer(target){
  // 如果不是对象数据类型,则直接返回
  if(typeof target !== 'object' ) {
    return target
  }
  
  /* 新增对数组的处理 */
  if(Array.isArray(target)){
    // 如果target是数组,则将数组的原型对象设置为proto
    Object.setPrototypeOf(target, proto)
    // 对数组中的元素进行侦测
    for(let i = 0 ; i < target.length; i ++){
      observer(target[i])
    }
  }
  
  
  // 循环遍历对象的所有属性,并将他们转化为getter和setter形式
  for(let key in target){
    difineReactive(target, key, target[key])
  }
}