响应式原理(二)

依赖收集

        通过上一节的分析我们了解 Vue 会把普通对象变成响应式对象,响应式对象 getter 相关的逻辑就是做依赖收集,这一节我们来详细分析这个过程。

我们先来回顾一下 getter 部分的逻辑:

defineReactive之get依赖收集

        这段代码我们只需要关注 2 个地方,一个是 const dep = new Dep()实例化一个 Dep 的实例,另一个是在 get 函数中通过 dep.depend 做依赖收集,这里还有个对 childObj 判断的逻辑,我们之后会介绍它的作用。

Dep

Dep 是整个 getter 依赖收集的核心,它的定义在 src/core/observer/dep.js 中:

Dep

        Dep 是一个 Class,它定义了一些属性和方法,这里需要特别注意的是它有一个静态属性 target,这是一个全局唯一 Watcher,这是一个非常巧妙的设计,因为在同一时间只能有一个全局的 Watcher 被计算,另外它的自身属性 subs 也是 Watcher 的数组

        Dep 实际上就是对 Watcher 的一种管理,Dep 脱离 Watcher 单独存在是没有意义的,为了完整地讲清楚依赖收集过程,我们有必要看一下 Watcher 的一些相关实现,它的定义在 src/core/observer/watcher.js 中:

一部分Watcher代码

Watcher 是一个 Class,在它的构造函数中,定义了一些和 Dep 相关的属性:

Watcher 中和 Dep 相关的属性

        其中,this.deps 和 this.newDeps 表示 Watcher 实例持有的 Dep 实例的数组;而 this.depIds 和 this.newDepIds 分别代表 this.deps 和 this.newDeps 的 id Set(这个 Set 是 ES6 的数据结构,它的实现在 src/core/util/env.js 中)。那么这里为何需要有 2 个 Dep 实例数组呢,稍后我们会解释。

        Watcher 还定义了一些原型的方法,和依赖收集相关的有 get、addDep 和 cleanupDeps 方法,单个介绍它们的实现不方便理解,我会结合整个依赖收集的过程把这几个方法讲清楚。

过程分析

        之前我们介绍当对数据对象的访问会触发他们的 getter 方法,那么这些对象什么时候被访问呢?还记得之前我们介绍过 Vue 的 mount 过程是通过mountComponent函数,其中有一段比较重要的逻辑,大致如下:

mountComponent

        当我们去实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,然后会执行它的this.get() 方法,进入 get 函数,首先会执行:pushTarget(this),pushTarget 的定义在 src/core/observer/dep.js 中:

get()
pushTarget

        实际上就是把 Dep.target 赋值为当前的渲染 watcher 并压栈(为了恢复用)。接着又执行了:value = this.getter.call(vm, vm),this.getter 对应就是 updateComponent 函数,这实际上就是在执行:vm._update(vm._render(), hydrating)

        它会先执行 vm._render() 方法,因为之前分析过这个方法会生成 渲染 VNode,并且在这个过程中会对 vm 上的数据访问,这个时候就触发了数据对象的 getter

        那么每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)

        刚才我们提到这个时候 Dep.target 已经被赋值为渲染 watcher,那么就执行到addDep方法:

addDep

        这时候会做一些逻辑判断(保证同一数据不会被添加多次)后执行 dep.addSub(this),那么就会执行 this.subs.push(sub),也就是说把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。

        所以在 vm._render() 过程中,会触发所有数据的 getter,这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了么,其实并没有,再完成依赖收集后,还有几个逻辑要执行,首先是:if (this.deep) { traverse(value)}这个是要递归去访问 value,触发它所有子项的 getter,这个之后会详细讲。接下来执行:popTarget(),popTarget 的定义在 src/core/observer/dep.js 中:Dep.target = targetStack.pop()实际上就是把 Dep.target 恢复成上一个状态,因为当前 vm 的数据依赖收集已经完成,那么对应的渲染Dep.target 也需要改变(会一直保持当前Dep.target的值是当前正在处理的watcher)。最后执行:this.cleanupDeps()

        其实很多人都分析过并了解到 Vue 有依赖收集的过程,但我几乎没有看到有人分析依赖清空的过程,其实这是大部分同学会忽视的一点,也是 Vue 考虑特别细的一点。

依赖清空

        考虑到 Vue 是数据驱动的,所以每次数据变化都会重新 render,那么 vm._render() 方法又会再次执行,并再次触发数据的 getters,所以 Wathcer 在构造函数中会初始化 2 个 Dep 实例数组,newDeps 表示新添加的 Dep 实例数组,而 deps 表示上一次添加的 Dep 实例数组

        在执行cleanupDeps函数的时候,会首先遍历 deps,移除对 dep 的订阅,然后把 newDepIds 和 depIds 交换,newDeps 和 deps 交换,并把 newDepIds 和 newDeps 清空。

        那么为什么需要做 deps 订阅的移除呢,在添加 deps 的订阅过程,已经能通过 id 去重避免重复订阅了。

        考虑到一种场景,我们的模板会根据 v-if 去渲染不同子模板 a 和 b,当我们满足某种条件的时候渲染 a 的时候,会访问到 a 中的数据,这时候我们对 a 使用的数据添加了 getter,做了依赖收集,那么当我们去修改 a 的数据的时候,理应通知到这些订阅者。那么如果我们一旦改变了条件渲染了 b 模板,又会对 b 使用的数据添加了 getter,如果我们没有依赖移除的过程,那么这时候我去修改 a 模板的数据,会通知 a 数据的订阅的回调,这显然是有浪费的。

        因此 Vue 设计了在每次添加完新的订阅,会移除掉旧的订阅,这样就保证了在我们刚才的场景中,如果渲染 b 模板的时候去修改 a 模板的数据,a 数据订阅回调已经被移除了,所以不会有任何浪费,真的是非常赞叹 Vue 对一些细节上的处理。

总结

        通过这一节的分析,我们对 Vue 数据的依赖收集过程已经有了认识,并且对这其中的一些细节做了分析。收集依赖的目的是为了当这些响应式数据发送变化,触发它们的 setter 的时候,能知道应该通知哪些订阅者去做相应的逻辑处理,我们把这个过程叫派发更新,其实 Watcher 和 Dep 就是一个非常经典的观察者设计模式的实现,下一节我们来详细分析一下派发更新的过程。

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

推荐阅读更多精彩内容