1

本篇文章主要想记录一下本人在学习vue中的响应式原理中遇到的一些问题,如有错误,请大佬们指正!

1. WatcherDepObserver分别负责什么?
  • Observer负责递归数据及所有子数据,通过Object.defineProperty为属性定义 getter/setter,监听数据的变化,将数据定义为响应式。但仅仅是把Object.defineProperty封装起来,监听数据的变化,是没有任何作用的,主要的目的是为了收集依赖

    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter() {
          var value = getter ? getter.call(obj) : val;
          /* 省略 */
          if (Dep.target) { // 收集依赖
              dep.depend();
          }
          return value
      },
      set: function reactiveSetter(newVal) {
          /* 省略 */
          dep.notify(); // 触发依赖
      }
    });
    
    // 总的来说就是,在`getter`中收集依赖,在`setter`中触发依赖。
  • 为什么要收集依赖,是为了数据属性发生变化时,通知使用了该数据的地方。
  • 依赖收集在哪里?依赖收集在Dep中,Dep用来收集依赖、删除依赖和通知依赖等。
  • 那么依赖是谁?依赖就是Watcher

为什么需要把依赖封装成Watcher类,《深入浅出vue.js》给出了很好的解释:

当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个。接着,它再负责通知其他地方。

2. Dep和Watcher的关系?

在我刚开始学习Vue源码的时候,对这里也很困惑:

  1. Dep.target是干什么的?

Dep.target其实就是当前Watcher实例,是一个全局唯一的静态变量,读取数据的时候会触发这个数据的getter,触发了getter就会执行

if (Dep.target) {
    dep.depend();
}

把自己收集到该数据的Dep中,大致代码如下:

 get () {
    /*将自身watcher观察者实例设置给Dep.target,用以依赖收集。*/
    pushTarget(this)
    let value
    const vm = this.vm

    value = this.getter.call(vm, vm)
    
    /*将观察者实例从target栈中取出,置空Dep.target*/
    popTarget()
    return value
  }

  /*将watcher观察者实例设置给Dep.target,用以依赖收集。同时将该实例存入target栈中*/
  function pushTarget (_target: Watcher) {
    if (Dep.target) targetStack.push(Dep.target)
    Dep.target = _target
  }
  1. 为什么Dep收集依赖是用Watcher中的addDep方法,并在Watcher中有记录dep的deps数组?

要回答这个问题,让我们来举个例子🌰,我们来看一下Vue中的vm.$watch

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ): Function {
    const watcher = new Watcher(vm, expOrFn, cb, options)
    /*省略*/
    return function unwatchFn () {
      /*将自身从所有依赖收集订阅列表删除*/
      watcher.teardown()
    }
  }
}

/* Watcher/teardown大致代码如下 */
/* 将自身从所有依赖收集订阅列表删除 */
  function teardown () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].removeSub(this)
    }
    this.active = false
  }

这个方法返回一个unwatchFn方法用于取消观察数据,本质就是要把Watcher实例从当前正在观察数据的依赖列表中移除。
如果通过getter中的依赖收集,仅仅是把Watcher记录到了该数据的Dep(依赖列表)中,那么只是在Dep中知道了哪些Watcher在监听这个数据,但在Watcher中却不知道自己都订阅了谁(Watcher实例被收集进了哪些Dep中),不知道自己订阅了谁,那该通知谁来取消观察呢?所以我们需要在Dep和Watcher中都记录一下对方,行成多对多的关系

所以我们在Dep中收集依赖是用Dep.target.addDep(this)

function addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}

通过记录newDepIds,newDeps,既在Watcher中记录自己被收集进了哪些Dep中,最后又通过dep.addSub(this)把自己记录到了Dep中,是不是很巧妙呢!

3. __ob__作用是什么?Observer实例上的dep是干什么的?

首先,__ob__属性上保存的就是Observer实例,__ob__的第一个作用是可以用来标记当前value是否已经被Observer转换成了响应式数据,避免后续重复操作。

其次,Vue对于Obejct的变化侦测,收集依赖和触发依赖都在一个闭包内可以访问到dep实例

function defineReactive () {
  /*在闭包中定义一个dep对象*/
  const dep = new Dep()

  Object.defineProperty(obj, key, {
    get: function reactiveGetter () {
        /*进行依赖收集*/
        dep.depend()
    },
    set: function reactiveSetter (newVal) {
      /*dep对象通知所有的观察者*/
      dep.notify()
    }
  })
}

但对于Array,是在getter中收集依赖,在拦截器上触发的依赖,不熟悉的小伙伴请自行看Vue处理数组的代码,大致代码如下:

[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    /*数组新插入的元素需要重新进行observe才能响应式*/
    const ob = this.__ob__
    
    /*dep通知所有注册的观察者进行响应式处理*/
    ob.dep.notify()
    return result
  })
})

我们可以看到,在拦截器中,没有dep实例给我们访问,但由于拦截器是对原型的一种封装,所以可以在拦截器中访问到当前正在操作的数组(this),所以要把__ob__属性设置到每个被侦测的数据上,这里就可以通过this.__ob__.dep获取到dep实例。


最后我想说,学习源码一方面是为了更加熟练的运用Vue这个框架去开发,但更重要的是理解框架为什么这么去设计,并运用到以后的日常开发中去,更好的提升自己!

结尾

我是周小羊,一个前端萌新,写文章是为了记录自己日常工作遇到的问题和学习的内容,提升自己,如果您觉得本文对你有用的话,麻烦点个赞鼓励一下哟~

小绵羊
70 声望517 粉丝