Vue.js2.0源码图文剖析(1)--观察者系统

144
作者 与狼同行
2016.12.16 12:19 字数 1569

最近因为公司要用到Vue.js开发,想着看一看源码,知己知彼。


52B639BE-F771-440B-99D4-48FF6D98525F.png

从Vue对象的构造开始说起

本节目标,是能够理解下面这段代码背后发生的观察者系统所做的事情。

const v = new Vue({
  data:{
    a:1
  }
})
v.$watch("a",()=>console.log("Hello,Vue"))
v.a = 4

首先要理解的是Vue在构造时(即 new Vue)发生了哪些事情,这里直接可以翻看核心代码core文件夹下关于instance,描述Vue实例的相关部分。
在init.js中我们看到Vue初始化的部分。

function initMixin(){
    //...
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    callHook(vm, 'beforeCreate')
    initState(vm)
    callHook(vm, 'created')
    initRender(vm)
}

可以看到的是,初始化的过程依次是 :
初始化生命周期->事件->'beforeCreate'的回调->状态初始化-> 'created'的回调->渲染初始化。
这与官方的实例生命周期图有些不一致,原因不是很清楚。


573D0D32-E42D-4E53-9074-43F3366CCB78.png

本节讨论中我们重点观察一下几大初始化语句中关于 initState(vm)的部分。这里的vm应该是Vue Component,即Vue组件的意思。下面为initState(vm)代码:

export function initState (vm: Component) {
  vm._watchers = []
  initProps(vm)
  initMethods(vm)
  initData(vm)
  initComputed(vm)
  initWatch(vm)
}

这里State的作用其实很清晰了,initState的作业就可以概括为初始化props、data、computed、method、watched 5个成员,分别对应下面5个示例成员。

var vm = new Vue({
  el: '#example',
  props: ['message'],
  data: {
    message: 'Hello'
  },
  computed: { },
  methods: { },
  watch: { }
})

如果已本文开头的例子来说,我们这里就只分析一下initData(vm)部分即可。

const v = new Vue({
  data:{
    a:1
  }
})

我把initData稍微删除了一部分,看起来清爽一些。


33C10163-F3DF-4758-B28C-1A5D7DCEDB29.png


其中第二部分的proxy的作用是使访问自己的属性,就是访问子data的属性。
其作用具体体现为:

var data = { a: 1 }
var vm = new Vue({
  data: data
})
vm.a === data.a // -> true  访问自己的属性,就是访问子data的属性。

那么initData的作用可以概括为获取并代理data成员,并观察data成员
主线其实在于observe(data)这句话,正是它实现了观察数据。直接进入源码文件Observer文件夹下的index.js找到observe方法,这里我再略作删减,方便查阅。


ACA200E0-6D26-4299-BB34-20ECBBB65397.png

总结一下上面图片的代码的几个点或疑问:
1.我们可以说observe方法的作用就是遍历data成员并且使其响应式化
2.Observer构造函数对 data的数组成员和非数组成员,用了不同的处理方式
3.defineReactive就是关键,为Vue的data属性定义响应式的代码是使用Object.defineProperty方法。其中get可以暂时理解为简单的获取值,set可以理解为观察新装,并发送通知。
4.get中Dep.target因为没有介绍到Dep,暂时先跳过吧

于是我们现在对知识点的理解是这样的:


E86D6E34-67F5-4B4C-B322-CD95D4C717D7.png

Object.defineProperty -- 双向绑定

因为我之前对js了解的实在是少,后来了解才知道Object.defineProperty这么好用,它是前端中实现双向绑定的方法,show me the code 来介绍的话,下面是最直接的。


CBF16AC5-A243-454D-8F1B-51351625F441.png

实现响应式的方式通常会分为Pull 和 Push,而这里无疑是Push的方式,由下往上发送通知,简单的示意图如下所示:


42C7EA30-472D-4F03-BAD4-CF9FA617F8C8.png


于是现在就了解了Observer中的defineReactive就是使其响应式化。

Dep订阅容器的实现

现在理解了Object.defineProperty的作用,我们来理解一下观察者对应的订阅者Watcher和订阅容器Dep。

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

2B180DA4-3570-4617-9877-7FCCF67A18A2.png


这部分比较简单,一开始看不容易理解Dep中target的作用,后面会解释。

Watcher订阅者的实现

上一部分Dep只是订阅者的容器,现在来关注一下订阅者Watcher。
对于一个订阅者而言,最关心的功能无非以下几点:
1. Watcher是如何构造的?
2. 接受到观察者的通知时,如何实现 update 更新操作
3. 与Dep订阅容器相关的操作

首先直接看一下Watcher是如何构造的吧:


FAD57519-B756-4656-80A8-DCA0725BE0F4.png


我们可以说Watcher在构造时的两个部分总结:
第1部分除了基本的成员初始化外,主要是构造Watcher中的第二参数expOrFn当为表达式或函数时进行的分析,因为你必须得知道你监视的是哪一个值,然后解析后赋值给this.getter。

v.$watch("a",()=>console.log("Hello,Vue"))

举例来说,Watcher的构造成员有vmExpOrFncb,而这句例子中,v就是代码中的vmExpOrFn就是"a",cb就是后面的回调。而第一部分就是解析表达式,然后知晓你监视的是"a"。

而重点其实在于第2部分和第三部分,这两部分直接解释了上文中Observer.defineProperty中为什么要用到Dep.target
现在我们现在知道WatcherDep是包含关系,那么Dep容器在什么时机增加Watcher呢?答案就是通过触发 Observer.defineProperty 中关于属性的getter,并且通过标记一个唯一的Dep.target标签 , 在Observer.defineProperty.getter中做if判断,如果这个唯一的标签值被标记,就知道当前访问值的不是其他成员,而是一个订阅者Watcher,于是就把它放入容器。

其中get不仅仅是构造时会调用,在update的时候也会发生调用get方法,get方法一言以蔽之,就是拽取最新的观察值。下图解释了get的流程,以及Dep.target的作用。


CFC30F44-BD94-40C8-A44F-FE63D12F2EB9.png


这里的第三步addDep和第四步clearupDep,一起的作用就是刷新一轮容器关系。
上面的步骤已经解释了Watcher构造发生了什么,再总结一下就是从初始化Watcher的各个成员之后,把Watcher放入Vue组件对应的Dep容器中,通过get方法获取观察属性的最新值,其中get每次调用都会刷新一轮容器关系

现在我们来看一下Watcher是如何响应通知的的:


5F17174A-992B-4222-8116-4C1988D33323.png

于是我们现在可以把订阅者简单概括为:


75853085-998F-4926-98BC-615A37C34B23.png

因此了解了Observer、Watcher、Dep之后,整体的印象就会变成:


F35E00C2-3E52-455A-974F-5B4F1CEB0AAB.png

而实际执行开头那段代码的话,就会变成以下的步骤:


664FCDA7-4AAE-40D7-ADF4-F2BC817545CC.png
Vue.js源码解析