Vue源码分析之Observer

碎碎念

四月份真是慵懒无比的一个月份,看着手头上没啥事干,只好翻翻代码啥的,看了一会Vue的源码,忽而有点感悟,因而便记录一下。javascript

Vue中的观察者模式

观察者模式通常包含发布者(Publisher)和订阅者(Subscriber)两种角色;顾名思义发布者负责发布消息,订阅者经过订阅消息响应动做了。
回到Vue中,在Vue源码core/oberver目录下分析代码能够知道有三个类分别是Oberver,Watcher和Dep;那这三个类中谁是Publisher,谁是Subscriber尼?java

Observer

观察者,这个观察者究竟观察什么的尼?
仍是用最简单粗暴的方式,目录搜索一下哪里用到这个类,步步追寻,大体是这样一个调用过程。react

initState()-->observe(data)-->new Observer()

基本上Vue在咱们的data对象上都会定义一个__ob__属性指向新建立的Observer对象,就像这样子:数组

{
    a: {
        b: {
            d: 1
            __ob__: [Observer Object]
        }
        c: { e: 1, f: 2, g: 3 } //也是有__ob__属性的
        __ob__: [Observer Object]
    }
    __ob__: [Observer Object]
}

这里能够知道其实对象或者数组里面Vue都会帮你添加一个__ob__属性,可是这个__ob__属性或者这个Observer对象到底是干吗用的尼?
先举个栗子:浏览器

<div>
    <ul v-for="el in a.c">
        <li></li>
    </ul>
</div>

在模板里面咱们遍历数组内容,很明显数组有多少元素就会输出多少个li;那么咱们数组元素增长和删除的时候怎么通知到组件去从新渲染尼?
恩,答案就是经过这个__ob__属性。
好,直接上代码:异步

function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: Function
) {
  const dep = new Dep() //1. 为属性建立一个发布者
  ...
  let childOb = observe(val) //2. 获取属性值的__ob__属性
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      ...
      if (Dep.target) {
        dep.depend() //3. 添加订阅者
        if (childOb) {
          childOb.dep.depend() //4. 也为属性值添加一样的订阅者
        }
        if (Array.isArray(value)) {
          dependArray(value) // 同上
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      ...
      childOb = observe(newVal)
      dep.notify()
    }
  })
}

第4步至关重要,若是没有第4步,咱们添加或者删除元素,刚才那个组件是不会从新渲染的;咱们通常状况下都会想到去拦截属性的get和set方法,在get的方法咱们能够收集订阅者,set的方法咱们简单的判断旧的值和新的值是否相等咱们就能够通知订阅者去更新;可是对于引用的值(相似Object或者Array)这样就不行了,咱们得让他们内容发生变化(主要是增长删除内容,对象增长一个属性时候)的时候也要通知订阅者去更新,因此__ob__上的dep属性主要用于监控对象属性增长和删减而第1步所建立的dep用于监控属性值的更新。oop

但在这里的例子也致使另一个行为,咱们刚才在例子中很明显并无实际用到数组的内容,然而在for循环的过程当中,也就等同于咱们遍历对象全部内容,Vue就会认为咱们会“关心”这些内容的变化,因此当对象的内容(假设这个对象里的元素也是对象,在某个子对象上增长或者删除一个属性)发生变化的时候也会触发从新渲染;性能

还有的是Vue对数组的处理跟对象仍是有挺大的不一样,length是数组的一个很重要的属性,不管数组增长元素或者删除元素(经过splice,push等方法操做)length的值一定会更新,那么岂不是一劳永逸,不须要拦截splice,push等方法就能够知道数组的状态更新,可是当我试着在数组length属性上用defineProperty拦截的时候,冒出了这样的错误:学习

Uncaught TypeError: Cannot redefine property: length

不能重定义length属性??再用Object.getOwnPropertyDescriptor(arr, 'length')查看一下:this

{
    configurable: false
    enumerable: false
    value: 0
    writable: true
}

configurable为false,看来Object.defineProperty真的不行了,而MDN上也说重定义数组的length属性在不一样浏览器上表现也是不一致的,因此仍是老老实实拦截splice,push等方法,要么就等ES6的Proxy才能够作到了。
那么数组的下标可使用defineProperty拦截吗? 答案:是能够的。
那么Vue也是是对待普通对象同样对数组全部下标进行了拦截吗? 答案:是否认的。
因此像这样:

this.arr[0] = 1;

彻底不行的。
那么为啥不直接遍历数组而后拦截数组的下标尼,我大概想了一下答案:
性能的考虑,数组可能很大,一次性都对下标进行拦截,会有性能影响;数组可能运行时变化很大,增删频繁。
[2019.01.25]实际上是由于用Object.defineProperty方法拦截下标的话会让数组进入字典模式,效率会极其低下,参考文章最后一段
还有没有其余缘由尼,这个还有待学习,可是看到源码其中是这样收集数组的依赖的:

/**
 * Collect dependencies on array elements when the array is touched, since
 * we cannot intercept array element access like property getters.
 */
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

递归收集数组的依赖了,全部子数组的变化也会触发当前观察者,这是个值得注意的地方。

因此咱们能够再看添加一个元素的时候:

function set (target: Array<any> | Object, key: any, val: any): any {
  ...
  const ob = (target : any).__ob__
  ...
  ...
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

最终会让Observer的dep属性去通知更新。

Observer对象的做用可让一个普通的对象变成"Reactive",而Dep则是充当最终的发布者角色。

Dep

当Dep的notify方法调起时,便遍历subs(订阅者数组就是Array<Watcher>)调用订阅者的update方法。

Watcher

Watcher的update方法调起,便把Watcher压入schedule队列中,等待nextTick异步执行,固然咱们可使用同步模式,直接执行Watcher的run方法方便咱们调试。
Vue中主要有两种类型的Watcher,一种是Render Watcher,另一种是User Watcher;
User Watcher是经过vm.$watch 或者 options中的watch属性定义的。
Render Watcher又是啥尼,看了一下initRender()方法,追踪一下调用过程,来到Vue.prototype._mount方法,能够看到:

vm._watcher = new Watcher(vm, () => {
      vm._update(vm._render(), hydrating)
    }, noop)

这个就应该是Render Watcher了;
咱们定义在options中的watch对象是在initState方法中初始化,而initState又比initRender先调用,因此组件中User Watcher确定比Render Watcher优先级高(User Watcher的id比Render Watcher小);
可是咱们在mounted生命周期中使用vm.$watch定义的Watcher就不必定了(我的推测),由于Render Watcher已经建立。

Dep 和 Watcher

通常订阅者模式都是一对多的关系(一个发布者对应多个订阅者),可是在这里Dep和Watcher是多对多的关系,因此就有;

  1. 一个Watcher能够侦测多个属性的变化(在Render的时候,RenderWatcher就收集了咱们在模板里面所使用的各类属性的依赖,因此当咱们修改模板里面任意一个变量时都会触发RenderWatcher从新Render)
  2. Dep能够被多个Watcher收集(例如咱们能够定义多个vm.$watch同一个属性,当属性变化时就能够触发多个Watcher)
  3. 另外Props定义的属性默认是不会侦测的(可是若是Props有默认值,也是会调用Observe),由于Props的属性都是由父组件传递给子组件,当Props属性修改时,父组件会先本身从新Render,也会致使子组件Render,而后开始Diff流程。

关于渲染时依赖收集

在Render Watcher中Wachter.run方法会调起vm._render()方法,这样状况下咱们在模板中访问的属性例如a.b这样,会在对象的getter中把Render Watcher添加到订阅者列表中。

get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return value
    }

因此之后咱们改动相关的属性时,对象的setter自动会通知到Render Watcher让Dom结构更新。

结束

好了,基本结束,若有错漏,望指正。

相关文章
相关标签/搜索