关于vue响应式原理的一点理解:observer,watcher,dep

1,465 阅读6分钟

一直以来都搞不清,或者是勉强理解过后又忘记了,observer, dep, watcher 这三个傻傻分不清,结合vue源码和自己的理解,特写此文记录一下。

响应式原理的思路

  1. 知道data什么时候改变
  2. 知道哪些dom需要用到data,当触发setter时把所有用到该数据的dom更新

Dep可以看做是书店,Watcher就是书店订阅者,而Observer就是书店的书,订阅者在书店订阅书籍,就可以添加订阅者信息,一旦有新书就会通过书店给订阅者发送消息。

Dep收集watcher,ob监听数据变化,一有变化,dep会通知收集的watcher,watcher再去触发对应的视图更新

看下vue 源码的处理

initData 初始化数据

  1. 对data.key作完验证后执行proxy(vm,_data, key),把_data代理到vm实例上
  2. 执行observe(data, true /* asRootData */),把data变成响应式

Observer 观察者

observe()

来看下observe()方法的定义

export function observe (value: any, asRootData: ?boolean): Observer | void {
  // value必须是对象且不是vnode实例
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // data已经有__ob__属性 且是observer实例则直接取该值
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&//非服务端渲染
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue// 不是vue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

这里先对data作类型校验和__ob__属性检测,没有__ob__时在对data实例化一个Observer,返回该实例

new Observer()

  1. 对this.value,this.dep,this.vmCount赋值
  2. def(value, '__ob__', this)对属性作一层封装,不直接赋值,是因为该属性设定为不可枚举
  3. value作类型判断,分别为数组和对象
  4. 数组:this.observeArray(value),递归对每个item作observe处理..
  5. 对象:this.walk(value),对value的每个属性作defineReactive处理

defineReactive

  1. 每个Key都会实例化一个Dep(收集watcher)
  2. 获取属性描述符
  3. 没有getter或者有setter并且没有传入初始化值时对val初始化
  4. 对子值递归调用observe childOb = !shallow && observe(val)
  5. 重新定义属性描述符的getter和setter 这使得当data中有数据变化时可以通过getter/setter监测到

Dep

Dep是什么呢,通俗的说它是一个依赖,记录了某个key订阅了哪个watcher,它还有一些对添加、删除watcher的方法。 看到Dep的定义,在src/core/observer/dep.js中,static target: ?Watcher;它有一个静态属性 target ,是一个 watcher , 这是全局唯一的 watcher ,因为同一时间只能有一个全局 watcher 被计算。它有个属性subs是watcher的数组,以及id。

Watcher

和响应式密切相关的是watcher,定义在src/core/observer/watcher.js中,其中定义了些和dep相关的属性

this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()

还有一些和依赖收集相关的方法,像 addDep

还记得这段代码吗?

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

执行mountComponent函数中,传入uddateComponent函数,实例化watcher,此时进入到watcher方法中,执行this.get(),首先pushTarget(this)

// 把当前target(Watcher)赋给Dep,并压到targetStack中
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

接着执行value = this.getter.call(vm, vm)//updatecomponent,this.getter 对应就是 updateComponent 函数,这实际上就是在执行vm._update(vm._render(), hydrating)

它会先执行 vm._render()方法,因为之前分析过这个方法会生成 渲染 VNode,并且在这个过程中会对 vm 上的数据访问,这个时候就触发了数据对象的 getter

vm._render() 过程中,会触发所有数据的 getter ,这样实际上已经完成了一个依赖收集的过程(下面将会作具体分析)。

收集完依赖后,执行

if (this.deep) {
  traverse(value)//和嵌套子值相关,此处先不分析
}
popTarget()
this.cleanupDeps()

popTarget:

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

实际上就是把 Dep.target 恢复到上个状态, 因为当前 vm 的数据依赖收集已经完成

再执行this.cleanupDeps()

/**
  * Clean up for dependency collection.
  * 性能优化,遍历新依赖,删除旧依赖的订阅者watcher
  * 当数据更新时没有订阅watcher则不会触发update
  */
cleanupDeps () {
  let i = this.deps.length
  while (i--) {
    const dep = this.deps[i]
    // 删除dep中有,newdep中没有的dep的subs中的当前watcher
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)
    }
  }
  // 存储newdeps到deps中
  let tmp = this.depIds
  this.depIds = this.newDepIds
  this.newDepIds = tmp
  this.newDepIds.clear()
  tmp = this.deps
  this.deps = this.newDeps
  this.newDeps = tmp
  this.newDeps.length = 0
}

依赖清空的过程:遍历deps,移除对watcher的订阅(这是为了避免当旧的依赖订阅了watcher,而新依赖不需要订阅该watcher时watcher也会渲染的浪费),再互换newDeps和deps,

收集依赖,订阅watcher

目的:当数据变化时,触发setter时,可以知道要通知哪些watcher去做相应的处理

依赖可以理解成当前数据绑定的视图组(watcher),一个key有一个deps 我们来看下依赖收集过程:

get: function reactiveGetter () {
  const value = getter ? getter.call(obj) : val
  // 依赖收集过程
  if (Dep.target) {//通过watcher触发的getter才收集依赖
    dep.depend()//把当前watcher收集起来
    if (childOb) {
      childOb.dep.depend()
      if (Array.isArray(value)) {
        dependArray(value)
      }
    }
  }
  return value
},
  1. 当Dep.target存在时才会去收集依赖,即是通过watcher触发的getter才收集依赖。 如果存在嵌套属性也是响应式对象,也会对它进行依赖收集,此处先不考虑这种情况

执行dep.depend()

depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}
  1. 执行Dep.target.addDep(this)
addDep (dep: Dep) {
  const id = dep.id
  // 判断是否已经有这个依赖
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)//添加新依赖id
    this.newDeps.push(dep)//添加新依赖
    // 旧依赖中没有这个依赖时,添加当前watcher到dep.subs
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}

如果当前新依赖没有这个id则把它加到新依赖中(确保同一数据不会被添加多次),如果旧依赖没有这个依赖,则把当前watcher加到它的subs队列中(一开始subs为空,旧依赖有这个id说明已经添加过watcher了),也就是说把当前的 watcher 订阅到这个数据持有的 depsubs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。

简单来说这个过程就是在当前watcher里把涉及到的数据依赖存起来,数据依赖也把当前watcher记录储存起来。 这样子我们就帮所有触发了getter的key的dep订阅了相应的watcher(有点绕)

派发更新

订阅了watcher后,我们来看下数据更改后是如何派发更新到对应watcher的

当数据改变时,触发setter:

set: function reactiveSetter (newVal) {
  const value = getter ? getter.call(obj) : val
  /* eslint-disable no-self-compare */
  if (newVal === value || (newVal !== newVal && value !== value)) {
    return
  }
  /* eslint-enable no-self-compare */
  if (process.env.NODE_ENV !== 'production' && customSetter) {
    customSetter()
  }
  // #7981: for accessor properties without setter
  if (getter && !setter) return
  if (setter) {
    setter.call(obj, newVal)
  } else {
    val = newVal
  }
  childOb = !shallow && observe(newVal)
  // 派发更新
  dep.notify()
}

对value进行求值,对比newVal,设置新值,派发更新dep.notify()

  1. watcher.update
  2. queueWatcher
  3. flushSchedulerQueue

总结

vue的响应式原理核心是观测对应数据变化,变化时通知到对应观察者。最核心的是dep的实现,它是连接数据和观察者的桥梁。

在vue的初始化阶段,对vue上挂载的data/props通过definereactive做一些处理, 使他们的属性变成响应式的,同时内部会设一个dep实例,当属性被访问到时会触发dep对应方法来收集依赖(当前watcher作为订阅者来收集依赖)、添加订阅者;修改属性时就会触发notify方法来通知订阅者做update处理。

computed 创建了_computedWatchers的特殊watcher,也会设一个dep实例,当属性被访问到时会调_computedWatchers.evaluate, 就会触发内部的depend去收集依赖,当它依赖的值变化时就会通知订阅者做相应处理。

watcher 创建了Userwatcher``,render watcher, 当它执行render时,访问到某些属性数据,render watcher就订阅这些属性的变化,当属性变化时,就会触发render watcher的updateComponent,重新做一次渲染