data.png数据响应式的通俗点解释就是我们改变了数据,应用对我们改变的行为做出响应。在Vue中响应式的实现基本原理可以用上面这张官方原理图来解释,下面我们将分为一步步解释这张原理图。

数据对象代理

Vue中数据是以一个data对象的形式存储的,那么我们基本的需求自然就为改变对象中的某个键值,应用对我们的行为做出响应,由此我们需要某种方法在改变键值的时候能插入我们自定义的行为,下面我们将这种行为成为代理。es2015提供了 Object.defineProperty 方法实现代理, Object.definePropery 可以定义对象每个key的 gettersetter ,分别能在获取和设定值的时候插入我们需要的行为。
下面以一个最基本的需求开始:改变获取对象中的某个键值,应用打印我们改变获取了什么键值

  1. function def(obj, key, val) {
  2. Object.defineProperty(obj, key, {
  3. get() {
  4. console.log('getting', key)
  5. return val
  6. },
  7. set(newVal) {
  8. console.log('setting', key)
  9. val = newVal
  10. }
  11. }

下面需求升级:接受一个对象,代理其中全部的key

  1. function proxy(obj) {
  2. Object.keys(obj).forEach(key => {
  3. let val = obj[key]
  4. Object.defineProperty(obj, key, {
  5. get() {
  6. console.log('getting', key)
  7. return val
  8. },
  9. set(newVal) {
  10. console.log('setting', key, newVal)
  11. val = newVal
  12. }
  13. })
  14. })
  15. }

需求再升级:我们需要一个新对象,它代理另一个对象的所有属性,但这个代理只是简单引用

  1. function getSimpleProxyObj(obj) {
  2. Object.keys(obj).forEach(key => {
  3. Object.defineProperty(obj, key, {
  4. get() {
  5. return obj[key]
  6. },
  7. set(newVal) {
  8. obj[key] = newVal
  9. }
  10. })
  11. })
  12. }

需求最终版:我们给出一个对象obj,改变对象中的某个键值,应用打印我们改变获取了什么键值,然后我们需要一个新对象newObj,这个对象能通过点操作符直接访问obj的属性,譬如newObj.name会返回obj.name的值

  1. data = {
  2. x: 1,
  3. y: 2,
  4. z: 3
  5. }
  6. function proxy(obj) {
  7. Object.keys(obj).forEach(key => {
  8. let val = obj[key]
  9. Object.defineProperty(obj, key, {
  10. get() {
  11. console.log('getting', key)
  12. return val
  13. },
  14. set(newVal) {
  15. console.log('setting', key, newVal)
  16. val = newVal
  17. }
  18. })
  19. })
  20. }
  21. function getSimpleProxyObj(obj) {
  22. const newObj = {}
  23. Object.keys(obj).forEach(key => {
  24. Object.defineProperty(newObj, key, {
  25. get() {
  26. return obj[key]
  27. },
  28. set(newVal) {
  29. obj[key] = newVal
  30. }
  31. })
  32. })
  33. return newObj
  34. }
  35. proxy(data)
  36. const newObj = getSimpleProxyObj(data)

上面的需求最终版就是类似数据代理在Vue中的应用了。Vue接受一个data对象,将它转换成代理对象A(对应着原理图当中的紫色data),然后再将Vue实例简单代理这个代理对象A,这个我们就能通过vm.x直接访问到代理对象中的x属性了。
你可能发现上面我们只是讲了怎么代理,并没有讲 gettersetter 到底做了什么。下面我们先补充一个设计模式:观察者模式,然后再解释 gettersetter 的具体行为。

观察者模式

一句话概括:A观察B,B能通知A进行更新。这里称A为观察者,B为被观察,两者分别称作Observer,和Subject,它们分别有以下基本方法:

  • Observer
    • update:当Subject通知时调用Observer此方法
  • Subject
    • register(observer):注册一个observer
    • delete(observer):删除一个observer
    • notify():通知所有注册在本Subject上面的observer进行更新

简易实现如下:

  1. class Observer {
  2. constructor(subject) {
  3. subject && subject.register(this)
  4. }
  5. update() {
  6. console.log('update')
  7. }
  8. }
  9. class Subject {
  10. constructor() {
  11. this.observers = new Set()
  12. }
  13. register(observer) {
  14. this.observers.add(observer)
  15. }
  16. unregister(observer) {
  17. this.observers.delete(observer)
  18. }
  19. notify() {
  20. [...this.observers].forEach(observer => {
  21. observer.update && observer.update()
  22. })
  23. }
  24. }

这个模式被应用在Vue的响应式原理当中,看下面的原理图,Dep对应着模式中的Subject,在Vue的经过代理的data对象中的每个key都一个Dep,也就是每个key都是一个被观察者,而需要观察这个key值变化的统称为watcher,对应着模式中的Observer。Dep对象能通知所有注册到自己上面的watcher进行更新。

v2-01e4b5455cdf992a5af457f03c1f37b6_r.jpg

依赖收集与消息派发

依赖收集

上面我们知道对象中的每个key对应着一个dep,那么很自然就需要一个过程来让各种watcher注册到key的dep当中,而Vue把这个过程称为依赖收集,也就是第一张图中的Collect as Dependency过程。

Vue把watcher注册到dep的行为放到了对应key的getter上,也就是说,如果这个key被读取了,那么就会把对应的watcher注册到自己的dep上面。

以原理图为例,当组件渲染时,会新建一个渲染的watcher,渲染时需要读取响应式对象的key值(对应原理图的”touch”),此时渲染watcher会注册到key的dep上面。因此,所有渲染过程中引用到的key对应的dep都会收集到渲染watcher。到此,依赖收集的含义更清晰了,就是dep收集依赖于自己的watcher

消息派发

消息派发比较简单,以原理图为例,响应式对象某个key值发生了变化,对应的dep通知收集到的watcher进行update操作(对应原理图的”notify”),而这个通知的行为自然要放到key的setter上面,watcher的update执行re-render操作,对组件进行重新渲染。

总结

简单总结一下响应式原理的关键点:

  1. 利用 Object.defineProperty 劫持获取和设定key值的过程
  2. 每个key值对应一个dep,dep负责收集所有依赖自己的watcher,收集的过程在key的getter当中
  3. 当key值更新时,对应dep会通知所有收集到的watcher进行更新

优点:精准对watcher进行更新
缺点:对于数组和对象新增属性,直接赋值则无法实现响应式,需要用Vue.set或者vm.$set来实现