资讯专栏INFORMATION COLUMN

Vue源码分析之Observer

CoderBear / 3197人阅读

摘要:中的观察者模式观察者模式一般包含发布者和订阅者两种角色顾名思义发布者负责发布消息,订阅者通过订阅消息响应动作了。中主要有两种类型的,一种是另外一种是是通过或者中的属性定义的。结束好了,基本结束,如有错漏,望指正。

碎碎念

四月份真是慵懒无比的一个月份,看着手头上没啥事干,只好翻翻代码啥的,看了一会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对象究竟是干嘛用的尼?
先举个栗子:

在模板里面我们遍历数组内容,很明显数组有多少元素就会输出多少个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) {
  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 | 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)调用订阅者的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是多对多的关系,所以就有;

一个Watcher可以侦测多个属性的变化(在Render的时候,RenderWatcher就收集了我们在模板里面所使用的各种属性的依赖,所以当我们修改模板里面任意一个变量时都会触发RenderWatcher重新Render)

Dep可以被多个Watcher收集(例如我们可以定义多个vm.$watch同一个属性,当属性变化时就可以触发多个Watcher)

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

结束

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/82459.html

相关文章

  • vue 源码分析如何实现 observer 和 watcher

    摘要:所以,我们是不是应该写一个消息订阅器呢这样的话,一触发方法,我们就发一个通知出来,然后,订阅这个消息的,就会怎样。。。截止到现在,在我们只考虑最简单情况下。。关于的新文章行代码,理解和分析的响应式架构 本文能帮你做什么?。。好奇vue双向绑定的同学,可以部分缓解好奇心还可以帮你了解如何实现$watch 前情回顾 我之前写了一篇没什么干货的文章。。并且刨了一个大坑。。今天。。打算来填一天...

    shiina 评论0 收藏0
  • vue2.0源码分析理解响应式架构

    摘要:分享前啰嗦我之前介绍过如何实现和。我们采用用最精简的代码,还原响应式架构实现以前写的那篇源码分析之如何实现和可以作为本次分享的参考。到现在为止,我们再看那张图是不是就清楚很多了总结我非常喜欢,以上代码为了好展示,都采用最简单的方式呈现。 分享前啰嗦 我之前介绍过vue1.0如何实现observer和watcher。本想继续写下去,可是vue2.0横空出世..所以 直接看vue2.0吧...

    chenatu 评论0 收藏0
  • vue源码分析系列响应式数据(二)

    摘要:巴拉巴拉省略大法,去除无关代码巴拉巴拉省略大法,去除无关代码核心就这一句话。文章链接源码分析系列源码分析系列之环境搭建源码分析系列之入口文件分析源码分析系列之响应式数据一 前言 接着上一篇的初始化部分,我们细看initData中做了什么。 正文 initData function initData (vm: Component) { let data = vm.$options.d...

    CKJOKER 评论0 收藏0
  • Vue原理】依赖收集 - 源码引用数据类型

    摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理依赖收集源码版之引用数据类型上 写文章不容易,点个赞呗兄弟专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于...

    vvpvvp 评论0 收藏0
  • 源码分析Vue的双向数据绑定

    摘要:所以最近攻读了其源码的一部分,先把双向数据绑定这一块的内容给整理一下,也算是一种学习的反刍。设计思想观察者模式的双向数据绑定的设计思想为观察者模式,为了方便,下文中将被观察的对象称为观察者,将观察者对象触发更新的称为订阅者。 虽然工作中一直使用Vue作为基础库,但是对于其实现机理仅限于道听途说,这样对长期的技术发展很不利。所以最近攻读了其源码的一部分,先把双向数据绑定这一块的内容给整理...

    JeOam 评论0 收藏0

发表评论

0条评论

CoderBear

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<