Vue响应式原理:Observer、Dep、Watcher

868 阅读3分钟

Vue的一大特点是数据响应式,数据的变化会作用于UI而不用进行DOM操作。原理上来讲是利用了js的defineProperty(obj,key,{})的特性。通过定义对象属性setter方法拦截对象属性变更,从而通过Watcher通知视图更新。在Watcher中会调用updateComponent,而updateComponent方法中会调用render.js中的_render()方法和_update()方法,这样视图UI也就进行了更新(vdom转化为了真实dom)

defineReactive()方法

这个方法中用到了js的defineProperty属性,大家都知道Vue的响应式是通过defineProperty实现的,只要是被defineProperty改造过的属性就具备了**【响应式】**的,原因是改变这个对象的时候会触发getter和setter方法,通过这两个方法就可以知道什么时候数据变化了,从而就可以在这个时候通知watcher中的render方法进行视图更新了。代码如下:

// 对象响应式
function defineReactive(obj, key, val) {
  // 对传入obj进行访问拦截
  Object.defineProperty(obj, key, {
    get() {
      console.log('get:传入的'+key+'这个属性被获取了,这里可以做些什么呢?' + key);
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        console.log('set :' + key + '属性被改变了,我要把新值放到界面上' + newVal);
        
        val = newVal
      }
    }
  })
}

let data = {msg:'我是一条数据'}
defineProperty(data,msg,data.msg)
console.log(data.msg) //传入的msg这个属性被获取了,这里可以做些什么呢?

//set
data.msg = '修改这个数据'  //console.log输出 :set :msg属性被改变了,我要把新值放到界面上

Observer

通过上面的代码就可以实现对date的属性进行监听,Vue中通过添加一个Observer来监听数据的变化,这称为观察者。


function observe(obj) {
  if (typeof obj !== 'object' || obj == null) {
    // 希望传入的是obj
    return
  }

  new Observer(obj)

}
class Observer {
  constructor(value) {
    this.value = value
    if (typeof value === 'object') {
      this.walk(value)

    }
  }

  walk(obj) {
    //对象数据响应化
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    });

    //数组数据响应化  
    if (Array.isArray(obj)) {
      //覆盖原型,替换7个数组操作方法
      obj._pro_ = arrayProto
      //对数组元素执行响应化
      for (var i = 0; i < obj.length; i++) {
        observe(obj[i])
      }
    }
  }
}

Dep (依赖收集)

有了监听数据变化的Observer,那怎么把这些变化都收集起来呢,因为vue是把所有的变化都收集起来,一次进行更新的,不然变化一次就更新一次,这效率也太低了,再说了也太消耗性能。这样就有了Dep来收集依赖。这些依赖就是用来管理data中所有属性变化的。

//依赖收集:管理某个key相关的所有Watcher(目前这里我用的还是vue1的实现,作一下简单说明,所以还是每个key对应一个Watcher,vue2.0就是一个组件对应一个Watcher了)
class Dep {
  constructor() {
    this.deps = []
  }

  addDep(dep) {
    this.deps.push(dep)
  }
  notify() {
    this.deps.forEach(dep => dep.update())
  }
}

怎么进行依赖收集的呢?

顺便把上面的defineReactive()方法改一下:

// 对象响应式
function defineReactive(obj, key, val) {
  // 递归,如果传入的val是还是一个对象,再执行observe(val),比如:{foo:'foo',bar:'bar',baz:{a:1}}中的baz:{a:1}
  observe(val)

  //创建一个Dep和当前key一一对应
  const dep = new Dep()

  // 对传入obj进行访问拦截
  Object.defineProperty(obj, key, {
    get() {
      console.log('get ' + key);
      //依赖收集在这里
      console.log('Dep:===', dep)
      Dep.target && dep.addDep(Dep.target)
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        console.log('set ' + key + ':' + newVal);
        // 如果传入的newVal依然是obj,需要做响应化处理,如obj.baz.a = 10000
        observe(newVal)
        val = newVal

        /* //粗糙的看下效果
        watchers.forEach(w => w.update()) */
        //通知更新
        dep.notify()
      }
    }
  })
}

Watcher

通过上面的Observer和Dep已经可以对数据的变化作出监听并把这些依赖都收集起来了,但是怎么通知视图进行更新呢?所以就出现了Watcher这个构造函数。Watcher就是在数据变化时通知render和update去更新视图的。

class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm
    this.key = key
    this.updateFn = updateFn

    // watchers.push(this)
    //Dep.target静态属性上设置为当前Watcher实例
    Dep.target = this
    this.vm[this.key]   //读取触发了getter
    Dep.target = null //收集完成就置空
  }

  update() {
    this.updateFn.call(this.vm, this.vm[this.key])
  }
}

总结

参考链接:segmentfault.com/a/119000000…