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

最近因为公司要用到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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容