Vue2数据响应式浅析

96 阅读2分钟

Object.defineProperty()

Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

var obj = {}

Object.defineProperty(obj, 'a', {
    get() {
        console.log('访问obj的a属性')
    },
    set() {
        console.log('改变obj的a属性')
    }
})

console.log(obj.a)
obj.a = 10

get:
getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象。该函数的返回值会被用作属性的值。默认为 undefined
set:
属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined

//设置 temp 变量来完成数据响应式
var temp

Object.defineProperty(obj, 'a', {
    get() {
        console.log('访问obj的a属性')
        return temp
    },
    set(newValue) {
        console.log('改变obj的a属性', newValue)
        temp = newValue
    }
})

defineReactive()

为了避免声明多余变量的问题

function defineReactive(data, key, val) {
  if(arguments.length == 2){ val = data[key] }
  Object.defineProperty(data, key, {
    get() {
      console.log(`访问${data}${key}属性`)
      return val
    },
    set(newValue) {
      console.log('改变${data}的${key}属性', newValue)
      if(val === newValue) return;
      val = newValue
    }
  })
}

defineReactive(obj, 'a', 10)

递归监测对象的全部属性

当监测的对象有多层时,上面的方法就监测不到了

obj: { a: {  m: { n: 1 } } }

这时候我们就需要一个 Observer 类将每一个正常的Object转换为每个层级的属性都是响应式的

image.png

function observe(value){
  if(typeof value !== 'Object') return
  var ob
  if(typeof value.__ob__ !== 'undefined'){
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}

observe(obj)
const def = function(obj, key, value, enuertable){ 
  Object.defineProperty(obj, key, { 
    value, 
    enumerable, 
    writable: true, 
    configurable: true 
  }) 
}

class Observer(){
  constructor(value){
    //给实例添加__ob__属性
    def(value,'__ob__', this, false)
    this.walk(value)
  }
  
  walk(value){
    for(let k in value){
      defineReactive(value, k)
    }
  }
}

此时我们对defineReactive进行改造,使有多层对象的obj也能被监测到

function defineReactive(data, key, val) {
  if(arguments.length == 2){ val = data[key] }
  
  let childOb = observe(val)
  
  Object.defineProperty(data, key, {
    get() {
      return val
    },
    set(newValue) {
      if(val === newValue) return;
      val = newValue
      childOb = observe(newValue)
    }
  })
}

数组的响应式处理

在上面写的响应式中,是不能对数据进行响应式的,所以Vue改写了7个数组方法
Vue中的做法: 以Array.prototype为原型,创建了一个新对象,将需要监听的数组指向这个新对象

const ayrrayPrototype = Array.prototype

export const arrayMethods = Object.create(arrayPrototype)

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

methodsNeedChange.forEach(methodName => {
  //备份原来的方法
  const original = arrayPrototype[methodName]
  
  def(arrayMethods,methodName,function(){ 
    const args = [...arguments]
    const ob = this.__ob__
    
    let inserted = []
    switch(methodName){
      case 'push':
      case 'unshft':
        inserted = args
        break;
      case 'splice':
        inserted = args.slice(2)
        break;
    }
    
    if(inserted){
      //让新项也变为响应式
      ob.observeArray(inserted)
    }
    
    return original.apply(this, arguments) 
  },false)
})

此时也要完善Observer类中对数组的检查

class Observer(){
  constructor(value){
    //给实例添加__ob__属性
    def(value,'__ob__', this, false)
    if(Array.isArray(value)) {
      Object.setPrototypeOf(value, arrayMethods)
      //让数组变成observe
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  
  walk(value){
    for(let k in value){
      defineReactive(value, k)
    }
  }
  
  observeArray(arr) {
    for(let i = 0, l = arr.length; i < l; i++) {
      observe(arr[i])
    }
  }
}

依赖收集

需要用到数据的地方,称为依赖
Vue1.x中,细粒度依赖,用到数据的DOM都是依赖
Vue2.x中,中等粒度依赖,用到数据的组件都是依赖
在getter中收集依赖,在setter中触发依赖

Dep类和Watcher类

把依赖收集的代码封装成一个Dep类,它专门用来管理依赖,每个Observer的实例,成员中都有一个Dep的实例

Watcher是一个中介,数据发生变化时通过Watcher中转,通知组件

image.png

  • 依赖就是Watcher。只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中
  • Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍
  • 代码实现的巧妙之处:Watcher把自己设置到全局的一个指定位置,然后读取数据,因为读取了数据,所以会触发这个数据的getter。在getter中就能得到当前正在读取数据的Watcher,并把这个Watcher收集到Dep中

Dep类

var uid = 0
class Dep{
  constructor(){
  this.id = uid++
    this.subs = []
  }
  //添加订阅
  addSub(sub){
    this.subs.push(sub)
  }
  //添加依赖
  depend(){
    if(Dep.target) {
      this.addSub(Dep.target)
    }
  }
  //通知更新
  notify(){
    const subs = this.subs.slice()
    for(let i = 0, l = subs.length; i < 1; i++){
      subs[i].update()
    }
  }
}

Watcher类

var uid = 0
class Watcher{
  constructor(target, expression, callback){
    this.id = uid++
    this.target = target
    this.getter = parsePath(expression)
    this.callback = callback
    this.value = this.get()
  }
  update(){
    
  }
  get(){
    Dep.target = this
    
    const obj = this.target
    try{
      value = this.getter(obj)
    } finally {
      Dep.target = null
    }
    
    return value
  }
  run(){
    this.getAndInvoke(this.callback)
  }
  getAndInvoke(cb){
    const value = this.get()
    
    if(value !== this.value || typeof value == 'object'){
      const oldValue = this.value
      this.value = value
      cb.callback(this.target, value , oldValue)
    }
  }
}

function parsePath(str){
  var segments = str.split('.')
  
  return (obj) => {
    for(let i = 0; i < segments.length; i++){
      if(!obj) return 
      obj = obj[segments[i]]
    }
    return obj
  }
}

在Observer类和defineReactive中实例化,以及数组和set中使用发布订阅模式(notify()),get中收集依赖

class Observer(){
  constructor(value){
    this.dep = new Dep()
    def(value,'__ob__', this, false)
    if(Array.isArray(value)) {
      Object.setPrototypeOf(value, arrayMethods)
      //让数组变成observe
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  } 
 //...
}

function defineReactive(data, key, val) {
  this.dep = new Dep()
  if(arguments.length == 2){ val = data[key] }
  
  let childOb = observe(val)
  
  Object.defineProperty(data, key, {
    get() {
      if(Dep.target){
        dep.depend()
        if(childOb){
          childOb.dep.depend()
        }
      }
      return val
    },
    set(newValue) {
      if(val === newValue) return;
      val = newValue
      childOb = observe(newValue)
      dep.notify()
    }
  })
}

methodsNeedChange.forEach(methodName => {
  //备份原来的方法
  const original = arrayPrototype[methodName]
  
  def(arrayMethods,methodName,function(){ 
    const args = [...arguments]
    const ob = this.__ob__
    
    let inserted = []
    switch(methodName){
      case 'push':
      case 'unshft':
        inserted = args
        break;
      case 'splice':
        inserted = args.slice(2)
        break;
    }
    
    if(inserted){
      //让新项也变为响应式
      ob.observeArray(inserted)
    }
    
    ob.dep.notify()
    
    return original.apply(this, arguments) 
  },false)
})