先来三道题



以上三个问题的答案分别为
- 不会触发
- 会被触发
- 3s之后不会触发
如果以上三个问题都答对了,下面的就不用看了,说明你对vue响应式原理理解非常深刻。
赋予数据响应式能力
声明以下代码基于vue@2.6.10,对于源码有一定删减。那就从new Vue()
开始说起




vue响应式数据初始化是在initData
这个函数中,并且我们可以看出data实际是可以为对象的,只是最好是一个函数,保证多个组件不会共享同一份data,导致组角相互干扰。
从名字上也能看出,将普通对象变成响应式数据的核心在于 observe
这个方法。

observe
会先检查对象上是否有 __ob__
这个属性,如果有说明已经是响应式数据了,不用在observe了。第二个分支比较啰嗦,如果服务端渲染或者是vue组件对象,就不用变成响应式数据了,因为没有必要。命中的话就将数据当场参数传入,创建 Observer
实例,从名字上看就是讲普通对象变成了可观察对象。

Observe
构造函数中 new Dep()
先简单理解为创建一个用来放依赖(dependency)的地方,然后把Observer
这个实例本身挂到 value
的 __ob__
上,避免之后被重复 observe
。如果是Array
类型,调用observeArray
,这是因为vue用到了 Object.defineProperty提供的get和set访问器。而Array的index是索引属性,并不是访问器属性,所以没有办法触发get和set方法。
如果是对象,则会遍历对象中的可访问属性,对于每个属性,执行defineReactive,让每个属性变得reacttive起来。

如果说要举一段非常有js味道的代码,我觉得这里就再好不过了。先创建一个dep
实例,使用Object.defineProperty
定义当前属性的get和set操作。get
和set
分别是两个新定义的函数,并且引用了函数外部的变量dep
,这里是闭包的典型应用场景。
当get
被调用时,如果取值的时候Dep.target
不为空,调用 dep.depend()
,可以先简单理解为该属性收集到了依赖。如果是深层次的观察,子对象也应该收集依赖。
当set
被调用时,会先检查 newVal
与旧的值是否有变化,没变化什么都不做,直接结束函数。有变化就调用 dep.notify()
,通知收集到的依赖。所以对于对象上的任意一个属性,都会有一个依赖容器,当属性发生变化时,通知依赖容器中的依赖。


举例可以看出 d.m.a
上有 __ob__
属性,并且dep.subs
不为空,这里就解释了第二题。因为只要属性被观察了,当属性改变的时候就会通知相关的依赖。
观察数据,收集依赖的过程

梳理一下整个 observe
的过程就是如上所示,data通过Observer与Dep关联起来,至此数据赋予了响应式的能力。
但是如果数据从未用过,没有调用过触发过属性的 get
方法,没有收集到依赖,那么改变的时候也没有依赖需要通知,自然也不会产生额外的副作用
了。
因为响应式的数据还需要一个watch操作才能完成任务,有两种watch使用姿势
- render 方法本质上就是调用了watch api
- vue 暴露出来的$watch方法


new Watch
的第二参数是要观察的内容,通常是表达式或者函数,第三个参数是执行回调函数。
在render 的场景下,watch了需要render的函数,而我们知道render函数其实就是模板编译成了函数。在函数执行的过程中,会触发vm.$data的取值操作,也就是get函数。


如果是renderWatcher, 则会把vm.watcher 设为this。computed的模式暂时先不用关注,因为普通情况就是执行了 this.get()


在get执行之前做一次 pushTarget,之后结束做一个PopTarget,因为有多个依赖要收集,并且可能是递归的。因此使用栈来保存之前的target,并把当前的taget挂载到Dep上。执行结束之后,把之前的target重新设置回来。

getter函数也就是此时的渲染函数就会执行,渲染函数执行的时候涉及了属性的取值,此时触发了 get操作。

我们现在再回头看get中的依赖收集,其实就是把Watcher.addDep(dep),而addDep同时也会调用dep.addSub(this),使用Dep和Watch保持相互引用关系,并且是多对多的关系。

等Dep调用notify的时候,会调用所有 this.subs 的update方法,此时可以把Watcher理解为依赖,而前面也正是new Watcher
开启了观察。因此当调用 watcher.get
方法时,实质是观察者,触发了观察。

由于是多对多的关系,因为可以同时观察同一个属性,一个属性有多个观察者;也可以一次性观察多个属性,每个属性都有各自的依赖容器,watcher充当多个属性的依赖,任意一个属性改变时都会通知到watcher。
watcher.get
方法的finally 执行了cleanupDeps
, 从字面意思就是清理依赖。

从上述代码中我们可以看出Watcher 会维护一个旧依赖和新依赖的表,并且有选择性的进行更新。
因为这里就可以回答第三题,不会触发,因为3s之后Watcher观察的属性有变化,对应在watcher 这里的deps会发生更新。

因此主要可以分为如上三大块儿,initData只是赋予了数据响应式的能力,但此时还没有建立起响应的关系。

只有当data touch 的时候才会收集到依赖,这里理解的依赖是指被通知去执行update的那个。因为第一题也有了答案。
通知依赖


如果有sync,则表示同步执行,直接执行this.run。默认情况下调用quueWatcher(this),将当前watcher当到队列中。

queueWatcher 会区分几种情况去消费队列中的任务,因为我们可以看出当数据改变后,会在下一个tick才清空任务队列。或者只有一个watcher回调触发的另一个watcher,才会立即同步加入队列中执行。因此在watcher的回调中触发另一个watcher,是有可能触发死循环的。同时也解释了为什么改变数据之后必须在nextTick中才能获取dom变化,这样也是出于性能考虑,一次性执行update。