Vue2响应式原理总结

224 阅读1分钟
  • vue响应式用了观察者的设计模式,响应式data的数据被修改,观察者会进行视图更新或者执行回调

1.用Observer类将对象变成响应式

  • 遍历对象的每个属性:

  • 给对象的每个属性创建Dep依赖收集器

  • Object.defineProperty给对象的每个属性定义set、get方法:

 get:使用Dep来收集观察者
 set:Dep派发通知给收集到的观察者
  • 如果对象的属性也是一个对象,进行递归,重复以上操作
class Observer {
  constructor (value: Object) {
    this.walk(value)
  }
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) { // 遍历对象的每个key
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
}

const observe = (value: any) => {
  if (!isObject(value)) {
    return
  }
  return new Observer(value)
}

function defineReactive (
  obj: Object,
  key: string,
  val: any
) {
  const dep = new Dep() //给该属性创建依赖收集器

  observe(val) // 该属性可能是一个对象,用observe递归
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      dep.depend() // 进行依赖收集,收集观察者
      return val
    },
    set: function reactiveSetter (newVal) {
      val = newVal
      observe(newVal) // 新的属性值可能是一个对象,用observe进行响应式处理,也就是重复以上的流程
      dep.notify() // 收集器派发通知给观察者
    }
  })
}

2.Dep依赖收集器,收集观察者

  • 上面get方法里面使用了dep.depend收集观察者,我们假设观察者是一个函数,保存在window.target上

那么dep收集观察者的逻辑就可以这么写:

export default class Dep {
  constructor () {
    this.id = uid++
    this.subs = [] //收集器用来存放观察者的数组
  }

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

  depend () {
    if (window.target) {
      this.addSub(window.target) // 把观察者添加到subs数组上
    }
  }

  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

3.Watcher观察者

  • Watcher是一个中介,当对象变化时通过dep.notify() 通知它,它再通知其他的地方
  • 先看一下Watcher经典的使用方式
vm.$watch('user.name', function() {
  // doing sometings
})
  • 该函数代表user对象的name属性发生变化时,执行回调函数
  • 上面有说过对象的每个属性都会创建一个Dep,并且在该属性的get方法上进行依赖收集。我们只需要触发name属性的get方法,把这个watcher实例添加到name属性的Dep上就行了
class Watcher {

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
  ) {
    this.vm = vm
    this.cb = cb
    this.getter = parsePath(expOrFn) // 解析user.name,生成访问name属性的函数
    this.value = this.get()
  }

  get () {
    window.target = this
    const vm = this.vm
    // getter是可以访问name属性的函数
    // 此时会触发name属性的get函数执行dep.depend收集观察者
    // 观察者在上面一步已经赋值到了window.target上了,dep.depend可以进行收集
    let value = this.getter.call(vm, vm)
    window.target = undefined // 收集完毕,清空window.target
    return value
  }
  
  // 当name属性被修改时,触发dep.notify。notify函数会执行观察者的update方法
  update () {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue) // 执行$watch('user.name', cb)的回调函数
  }

}

4.Array响应式

  • Array能够改变自身内容的方法有7个:push、pop、shift、unshfit、sort、splice、reverse
  • 我们可以对数组的这些函数进行修改、在这些函数当中执行dep.notify通知观察者:
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    //  todo。这里执行通知观察者的逻辑
    return result
  })
})
  • 我们微调一下Observer的代码,把以上重写后的数组方法赋值给数组
class Observer {
  constructor (value: Object) {
    if(Array.isArray(value)) {
      value.__proto__ = arrayMethods // __proto__ 有兼容性问题 兼容性处理省略
    }else {
      this.walk(value)
    }
  }
}
  • 我们重写了数组的方法,在这些方法里面执行Dep的通知。但我们还没有为数组创建Dep。
  • 上面说到对象是遍历每个属性,为每个属性创建一个Dep,那么数组的Dep应该在那里创建呢?
  • 我们可以在存放在Observer的实例上,Observer再次调整如下:
class Observer {
  constructor (value: Object) {
    this.dep = new Dep() // 新增
    def(value, '__ob__', this) // 新增 在数组上添加__ob__属性指向当前的Observer实例
    if(Array.isArray(value)) {
      value.__proto__ = arrayMethods
    }else {
      this.walk(value)
    }
  }
}
  • 这样数组就可以通过__ob__属性访问到dep了,调整arrayMethods如下:
methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    this.__ob__.dep.notify() // 这里执行通知观察者的逻辑
    return result
  })
})
  • 通知观察者的逻辑实现完了,现在实现收集观察者的逻辑,我们调整defineReactive的代码如下:
function defineReactive (
  obj: Object,
  key: string,
  val: any
) {
  const dep = new Dep() 

  const childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      if(childOb) { // 新增
        childOb.dep.depend() // 新增
      }
      dep.depend()
      return val
    },
    set: function reactiveSetter (newVal) {
      val = newVal
      observe(newVal)
      dep.notify()
    }
  })
}
  • 以上已经处理好了数组的响应式,但是数组里面的每个元素却不是响应式的,我们再一次调整Observer:
class Observer {
  constructor (value: Object) {
    if(Array.isArray(value)) {
      value.__proto__ = arrayMethods 
      this.observerArray(value) // 新增
    }else {
      this.walk(value)
    }
  }
}

observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
  }
}

Peng_YT的csdn

Peng_YT的语雀