# 什么是变化侦测

变化侦测的作用是侦测数据的变化。当数据变化时,会通知视图进行相应的更新。变化侦测是响应式系统的核心,没有它就没有重新渲染。框架在运行时,视图就无法随着状态的变化而变化。

Vue中可以进行更细粒度的更新,因为粒度越细,每个状态所绑定的依赖就越多,依赖追踪再内存上的开销就会越大。因为Vue2.0开始引入了虚拟DOM,将粒度调整为中等粒度,即一个状态所绑定的依赖不再是具体的DOM节点,而是一个组件。这样状态变化后,会通知到组件,组件内部再使用虚拟DOM对比。这样可以大大降低依赖数量,从而降低依赖追踪所消耗的内存。

# 如何追踪变化

JS中如何侦测一个对象的变化?有两种方法可以侦测到变化:

  • Object.defineProperty
  • Proxy

这里我们先说通过Object.defineProperty这种方式追踪变化

function defineReactvie(data, key ,val) {
  Object.defineProperty(data, key, {
    get() {
      return val
    },
    set(newVal) {
      if (val === newVal) {
        return
      }
      val = newVal
    }
  })
}

# 如何收集依赖

前面我们定义了defineReactvie方法,可以看出当我们读取属性的时候会调用getter方法,给属性设置值的时候会调用setter方法,我们可以在这两个方法里收集、触发依赖

# 依赖收集在哪里

因为每个数据属性对应的依赖可能并不是一个,可以在getter中用数组来存储每个可以的key的依赖,当调用属性的setter方法时,依次触发依赖,更新下界面

export default class Dep {
  constructor () {
    // 存储依赖
    this.subs = []
  }

  addSub (sub) {
    this.subs.push(sub)
  }

  removeSub (sub) {
    remove(this.subs, sub)
  }

  depend () {
    if (window.target) {
      this.addSub(window.target)
    }
  }

  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
        subs[i].update()
      }
    }
  }
  
  function remove (arr, item) {
    if (arr.length) {
      const index = arr.indexOf(item)
      if (index > -1) {
        return arr.splice(index, 1)
      }
    }
}

function defineReactive (data, key, val) {
  let dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      // 收集依赖
      dep.depend()
      return val
    },
    set: function (newVal) {
      if(val === newVal){
        return
      }
      val = newVal
      // 触发依赖
      dep.notify()
    }
  })
}

# 依赖是谁

<div id="app">
  <p>{{name}}</p>
  <p>{{name}}</p>
  <p>{{name}}</p>
</div>

如上name属性可能在多个地方被多次调用,如果当name发生变化的时候我们逐个去通知未免太繁琐,Vue中使用了Watcher类,由它负责通知,我们收集的依赖也就是Watcher。

# 什么是Watcher

Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。

export default class Watcher {
  constructor (vm, expOrFn, cb) {
    this.vm = vm
    // 执行this.getter(),就可以读取data.name的内容
    this.getter = parsePath(expOrFn)
    this.cb = cb
    this.value = this.get()
  }

  get() {
    window.target = this
    let value = this.getter.call(this.vm, this.vm)
    window.target = undefined
    return value
  }

  update () {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}

上述代码为window.target赋值了this也就是Watcher的实例,当我们读取data.name时就会触发getter方法,在getter中会调用dep.dend方法将这个watcher添加到Dep中,当data.name属性值发生变化的时候会触setter方法,执行dep.notify()方法依次执行依赖列表中所有watcher的update方法

# 递归侦测所有key

export class Observer {
  constructor (value) {
    this.value = value

    if (!Array.isArray(value)) {
      this.walk(value)
    }
  }
  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
}

function defineReactive (data, key, val) {
  // 新增,递归子属性
  if (typeof val === 'object') {
    new Observer(val)
  }
  let dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      dep.depend()
      return val
    },
    set: function (newVal) {
      if(val === newVal){
        return
      }

      val = newVal
      dep.notify()
    }
  })
}

Observer类会附加到每一个被侦测的object上。一旦被附加上,Observer会将object的所有属性转换为getter/setter的形式来收集属性的依赖,并且当属性发生变化时会通知这些依赖。

walk会将每一个属性都转换成getter/setter的形式来侦测变化 这个方法只有在数据类型为Object时被调用。

# 关于Object的问题

var vm = new Vue({
  el: '#app',
  data: {
    obj: {
      name: 'f'
    }
  },
  methods: {
    del() {
      delete this.obj.name
    },
    add() {
      this.obj.age = 18
    }
  }
})

Vue无法检测到上述代码中age和name属性的增删,因为:

Vue通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性。因为JS在ES6之前没有元编程的能力,无法侦测到一个属性是被添加了还是被删除了。为了解决这个问题,Vue提供了两个API: vm.$set和vm.$delete

# 总结

Data通过Observer转化成了geter/setter的形式来追踪变化。当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。

当数据发生变化时,会触发setter,从而向Dep中的依赖(Watcher)发送通知。

Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。