Vue源码分析之Observer

7

碎碎念

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

Vue中的观察者模式

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

Observer

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

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用于监控属性值的更新。

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

还有的是Vue对数组的处理跟对象还是有挺大的不同,length是数组的一个很重要的属性,无论数组增加元素或者删除元素(通过splice,push等方法操作)length的值必定会更新,那么岂不是一劳永逸,不需要拦截splice,push等方法就可以知道数组的状态更新,但是当我试着在数组length属性上用defineProperty拦截的时候,冒出了这样的错误:

Uncaught TypeError: Cannot redefine property: length

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

{
    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结构更新。

结束

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

你可能感兴趣的

zejuan · 2017年04月13日

66666

回复

北城以南 · 2017年04月17日

分析的很到位,想请教你一个问题,就是被监控的对象为什么在method中无法获取到其值!

回复

0

不应该啊 能否举个栗子

tain335 作者 · 2017年04月17日
0

@tain335 可以加个微信吗

北城以南 · 2017年04月17日
0

@北城以南 可以 我加你吧

tain335 作者 · 2017年04月18日
hcode · 2月21日

666666

回复

载入中...