写点什么

Vue.js 源码解析:深入响应式原理

2016 年 10 月 15 日

编者按:InfoQ 开设新栏目“品味书香”,精选技术书籍的精彩章节,以及分享看完书留下的思考和收获,欢迎大家关注。本文节选自张耀春等著的《Vue.js 权威指南》一书。

Vue.js 最显著的功能就是响应式系统,它是一个典型的 MVVM 框架,模型(Model)只是普通的 JavaScript 对象,修改它则视图(View)会自动更新。这种设计让状态管理变得非常简单而直观,不过理解它的原理也很重要,可以避免一些常见问题。下面让我们深挖 Vue.js 响应式系统的细节,来看一看 Vue.js 是如何把模型和视图建立起关联关系的。

1 如何追踪变化

我们先来看一个简单的例子。代码示例如下:

复制代码
<span><span><<span>div</span> <span>id</span>=<span>"main"</span>></span>
<span><<span>h1</span>></span>count: </span><span>{{<span>times</span>}}</span><span><span></<span>h1</span>></span>
<span></<span>div</span>></span>
<span><<span>script</span> <span>src</span>=<span>"vue.js"</span>></span><span></<span>script</span>></span>
<span><<span>script</span>></span><span>
<span>var</span> vm = <span>new</span> Vue({
el: <span>'#main'</span>,
data: <span><span>function</span> <span>()</span> {</span>
<span>return</span> {
times: <span>1</span>
};
},
created: <span><span>function</span> <span>()</span> {</span>
<span>var</span> me = <span>this</span>;
setInterval(<span><span>function</span> <span>()</span> {</span>
me.times++;
}, <span>1000</span>);
}
});
</span><span></<span>script</span>></span></span>

运行后,我们可以从页面中看到,count 后面的 times 每隔 1s 递增 1,视图一直在更新。在代码中仅仅是通过 setInterval 方法每隔 1s 来修改 vm.times 的值,并没有任何 DOM 操作。那么 Vue.js 是如何实现这个过程的呢?我们可以通过一张图来看一下,如图 20-1 所示。

图 20-1 模型和视图关联关系图

图中的模型(Model)就是 data 方法返回的{times:1},视图(View)是最终在浏览器中显示的 DOM。模型通过 Observer、Dep、Watcher、Directive 等一系列对象的关联,最终和视图建立起关系。归纳起来,Vue.js 在这里主要做了三件事:

  • 通过 Observer 对 data 做监听,并且提供了订阅某个数据项变化的能力。
  • 把 template 编译成一段 document fragment,然后解析其中的 Directive,得到每一个 Directive 所依赖的数据项和 update 方法。
  • 通过 Watcher 把上述两部分结合起来,即把 Directive 中的数据依赖通过 Watcher 订阅在对应数据的 Observer 的 Dep 上。当数据变化时,就会触发 Observer 的 Dep 上的 notify 方法通知对应的 Watcher 的 update,进而触发 Directive 的 update 方法来更新 DOM 视图,最后达到模型和视图关联起来。

接下来我们就结合 Vue.js 的源码来详细介绍这三个过程。

20.1.1 Observer

首先来看一下 Vue.js 是如何给 data 对象添加 Observer 的。我们知道,Vue 实例创建的过程会有一个生命周期,其中有一个过程就是调用 vm._initData 方法处理 data 选项。_initData 方法的源码定义如下:

复制代码
<!
<span>Vue</span>.prototype._initData = function () {
var dataFn = this.$options.<span><span>data</span></span>
var <span><span>data</span> = this._data = dataFn ? dataFn<span>()</span> : <span>{}</span></span>
<span>if</span> (!isPlainObject(<span><span>data</span>)) <span>{
<span>data</span> = {}</span></span>
process.env.<span>NODE_ENV</span> !== 'production' && warn(
'<span><span>data</span> functions should return an object.',</span>
this
)
}
var props = this._props
// proxy <span><span>data</span> on instance</span>
var keys = <span>Object</span>.keys(<span><span>data</span>)</span>
var i, key
i = keys.length
while (i
key = keys[i]
// there are two scenarios <span>where</span> we can proxy a <span><span>data</span> key:</span>
// <span>1.</span> it's not already defined <span>as</span> a prop
// <span>2.</span> it's provided via a instantiation option <span>AND</span> there are no
// template prop present
<span>if</span> (!props || !hasOwn(props, key)) {
this._proxy(key)
} <span>else</span> <span>if</span> (process.env.<span>NODE_ENV</span> !== 'production') {
warn(
'<span>Data</span> field <span>"' + key + '"</span> is already defined ' +
'<span>as</span> a prop. <span>To</span> provide <span><span>default</span> value for a prop, use the "<span>default</span>" ' +</span>
'prop option; <span>if</span> you want to pass prop values to an instantiation ' +
'call, use the <span>"propsData"</span> option.',
this
)
}
}
// observe <span><span>data</span></span>
observe(<span><span>data</span>, this)</span>
}

在 _initData 中我们要特别注意 _proxy 方法,它的功能就是遍历 data 的 key,把 data 上的属性代理到 vm 实例上。_proxy 方法的源码定义如下:

复制代码
<!-- 源码目录:src/instance/internal/state.js-->
Vue.prototype._proxy = <span><span>function</span> <span>(key)</span> {</span>
<span>if</span> (!isReserved(key)) {
<span>var</span> <span>self</span> = this
Object.defineProperty(<span>self</span>, key, {
configurable: <span>true</span>,
enumerable: <span>true</span>,
get: <span><span>function</span> <span>proxyGetter</span> <span>()</span> {</span>
<span>return</span> <span>self</span>._data[key]
},
set: <span><span>function</span> <span>proxySetter</span> <span>(val)</span> {</span>
<span>self</span>._data[key] = val
}
})
}
}

_proxy 方法主要通过 Object.defineProperty 的 getter 和 setter 方法实现了代理。在前面的例子中,我们调用 vm.times 就相当于访问了 vm._data.times。

在 _initData 方法的最后,我们调用了 observe(data, this) 方法来对 data 做监听。observe 方法的源码定义如下:

复制代码
<!-- 源码目录:src/observer/index.js-->
export function observe (<span>value</span>, vm) {
<span>if</span> (!<span>value</span> || <span>typeof</span> <span>value</span> !== <span>'object'</span>) {
<span>return</span>
}
<span>var</span> ob
<span>if</span> (
hasOwn(<span>value</span>, <span>'__ob__'</span>) &&
<span>value</span>.__ob__ instanceof Observer
) {
ob = <span>value</span>.__ob__
} <span>else</span> <span>if</span> (
shouldConvert &&
(isArray(<span>value</span>) || isPlainObject(<span>value</span>)) &&
Object.isExtensible(<span>value</span>) &&
!<span>value</span>._isVue
) {
ob = <span>new</span> Observer(<span>value</span>)
}
<span>if</span> (ob && vm) {
ob.addVm(vm)
}
<span>return</span> ob
}

observe 方法首先判断 value 是否已经添加了ob属性,它是一个 Observer 对象的实例。如果是就直接用,否则在 value 满足一些条件(数组或对象、可扩展、非 vue 组件等)的情况下创建一个 Observer 对象。接下来我们看一下 Observer 这个类,它的源码定义如下:

复制代码
<!-- 源码目录:src/observer/index.js-->
export function Observer (<span>value</span>) {
<span>this</span>.<span>value</span> = <span>value</span>
<span>this</span>.dep = <span>new</span> Dep()
def(<span>value</span>, <span>'__ob__'</span>, <span>this</span>)
<span>if</span> (isArray(<span>value</span>)) {
<span>var</span> augment = hasProto
? protoAugment
: copyAugment
augment(<span>value</span>, arrayMethods, arrayKeys)
<span>this</span>.observeArray(<span>value</span>)
} <span>else</span> {
<span>this</span>.walk(<span>value</span>)
}
}

Observer 类的构造函数主要做了这么几件事:首先创建了一个 Dep 对象实例(关于 Dep 对象我们稍后作介绍);然后把自身 this 添加到 value 的ob属性上;最后对 value 的类型进行判断,如果是数组则观察数组,否则观察单个元素。其实 observeArray 方法就是对数组进行遍历,递归调用 observe 方法,最终都会调用 walk 方法观察单个元素。接下来我们看一下 walk 方法,它的源码定义如下:

复制代码
<!
Observer.prototype.walk = <span><span>function</span> (<span>obj</span>) {</span>
var <span>keys</span> = Object.<span>keys</span>(obj)
<span>for</span> (var i = <span>0</span>, l = <span>keys</span>.<span>length</span>; i < l; i++) {
this.<span>convert</span>(<span>keys</span>[i], obj[<span>keys</span>[i]])
}
}

walk 方法是对 obj 的 key 进行遍历,依次调用 convert 方法,对 obj 的每一个属性进行转换,让它们拥有 getter、setter 方法。只有当 obj 是一个对象时,这个方法才能被调用。接下来我们看一下 convert 方法,它的源码定义如下:

复制代码
Observer.prototype.convert = function (key, val) {
defineReactive(this.value, key, val)
}

convert 方法很简单,它调用了 defineReactive 方法。这里 this.value 就是要观察的 data 对象,key 是 data 对象的某个属性,val 则是这个属性的值。defineReactive 的功能是把要观察的 data 对象的每个属性都赋予 getter 和 setter 方法。这样一旦属性被访问或者更新,我们就可以追踪到这些变化。接下来我们看一下 defineReactive 方法,它的源码定义如下:

复制代码
<!-- 源码目录:src/observer/index.js-->
export <span><span>function</span> <span>defineReactive</span> <span>(obj, key, val)</span> {</span>
<span>var</span> dep = <span>new</span> Dep()
<span>var</span> property = Object.getOwnPropertyDescriptor(obj, key)
<span>if</span> (property && property.configurable === <span>false</span>) {
<span>return</span>
}
<span>var</span> getter = property && property.<span>get</span>
<span>var</span> setter = property && property.<span>set</span>
<span>var</span> childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: <span>true</span>,
configurable: <span>true</span>,
<span>get</span>: <span><span>function</span> <span>reactiveGetter</span> <span>()</span> {</span>
<span>var</span> value = getter ? getter.call(obj) : val
<span>if</span> (Dep.target) {
dep.depend()
<span>if</span> (childOb) {
childOb.dep.depend()
}
<span>if</span> (isArray(value)) {
<span>for</span> (<span>var</span> e, i = <span>0</span>, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
}
}
}
<span>return</span> value
},
<span>set</span>: <span><span>function</span> <span>reactiveSetter</span> <span>(newVal)</span> {</span>
<span>var</span> value = getter ? getter.call(obj) : val
<span>if</span> (newVal === value) {
<span>return</span>
}
<span>if</span> (setter) {
setter.call(obj, newVal)
} <span>else</span> {
val = newVal
}
childOb = observe(newVal)
dep.notify()
}
})
}

defineReactive 方法最核心的部分就是通过调用 Object.defineProperty 给 data 的每个属性添加 getter 和 setter 方法。当 data 的某个属性被访问时,则会调用 getter 方法,判断当 Dep.target 不为空时调用 dep.depend 和 childObj.dep.depend 方法做依赖收集。如果访问的属性是一个数组,则会遍历这个数组收集数组元素的依赖。当改变 data 的属性时,则会调用 setter 方法,这时调用 dep.notify 方法进行通知。这里我们提到了 dep,它是 Dep 对象的实例。接下来我们看一下 Dep 这个类,它的源码定义如下:

复制代码
<!-- 源码目录:src/observer/dep.js-->
export <span>default</span> <span><span>function</span> <span>Dep</span> <span>()</span> {</span>
<span>this</span>.id = uid++
<span>this</span>.subs = []
}
Dep.target = <span>null</span>

Dep 类是一个简单的观察者模式的实现。它的构造函数非常简单,初始化了 id 和 subs。其中 subs 用来存储所有订阅它的 Watcher,Watcher 的实现稍后我们会介绍。Dep.target 表示当前正在计算的 Watcher,它是全局唯一的,因为在同一时间只能有一个 Watcher 被计算。

前面提到了在 getter 和 setter 方法调用时会分别调用 dep.depend 方法和 dep.notify 方法,接下来依次介绍这两个方法。depend 方法的源码定义如下:

复制代码
Dep.prototype.depend = function () {
Dep.target.addDep(this)
}

depend 方法很简单,它通过 Dep.target.addDep(this) 方法把当前 Dep 的实例添加到当前正在计算的 Watcher 的依赖中。接下来我们看一下 notify 方法,它的源码定义如下:

复制代码
Dep.prototype.notify = function () {
// stablize the subscriber list first
var subs = toArray(this.subs)
for (var i = 0, l = subs.length; i <span>< <span>l</span>; <span>i</span>++) {
<span>subs</span>[<span>i</span>]<span>.update</span>()
}
}</span>

notify 方法也很简单,它遍历了所有的订阅 Watcher,调用它们的 update 方法。

至此,vm 实例中给 data 对象添加 Observer 的过程就结束了。接下来我们看一下 Vue.js 是如何进行指令解析的。

书籍介绍

Vue.js 是一个用来开发 Web 界面的前端库。本书致力于普及国内 Vue.js 技术体系,让更多喜欢前端的人员了解和学习 Vue.js。如果你对 Vue.js 基础知识感兴趣,如果你对源码解析感兴趣,如果你对 Vue.js 2.0 感兴趣,如果你对主流打包工具感兴趣,如果你对如何实践感兴趣,本书都是一本不容错过的以示例代码为引导、知识涵盖全面的最佳选择。全书一共 30 章,由浅入深地讲解了 Vue.js 基本语法及源码解析。主要内容包括数据绑定、指令、表单控件绑定、过滤器、组件、表单验证、服务通信、路由和视图、vue-cli、测试开发和调试、源码解析及主流打包构建工具等。该书内容全面,讲解细致,示例丰富,适用于各层次的开发者。

2016 年 10 月 15 日 03:252672

评论

发布
暂无评论
发现更多内容

架构师训练营 - 第十一周总结

一个节点

极客大学架构师训练营

《Linux就该这么学》PDF版免费下载

计算机与AI

Linux

java集合【10】——— LinkedList源码解析

秦怀杂货店

Java 集合 linkedlist

第七周作业

孤星

架构师训练营 -week11-总结

大刘

极客大学架构师训练营

月薪8k和月薪38K的程序员差距在哪里?学习Linux C/C++ 这些你就知道了

ShenDu_Linux

c++ Linux 程序员

【java基础】-- java接口和抽象类的异同分析

秦怀杂货店

Java 接口

Mybatis【6】-- Mybatis插入数据后自增id怎么获取?

秦怀杂货店

mybatis

11.2安全架构:加密与解密

张荣召

5 千字长文+ 30 张图解 | 陪你手撕 STL 空间配置器源码

herongwei

c++ 源码 内存 后端开发 stl

性能压测的时候,随着并发压力的增加,系统响应时间和吞吐量如何变化,为什么?

落朽

到手的股权,又没了 | 法庭上的CTO(2)

赵新龙

股权 CTO 28天写作

JDBC【4】-- jdbc预编译与拼接sql对比

秦怀杂货店

sql JDBC

第七周总结

孤星

11.1安全架构:Web攻击与防护

张荣召

架构师训练营第七周作业

丁乐洪

架构师训练营第11周总结

听夜雨

极客大学架构师训练营

架构师训练营第 1 期第 11 周学习总结

好吃不贵

极客大学架构师训练营

系统性能的主要技术指标以及变化

皮蛋

架构词典:缓存

lidaobing

缓存 架构

Mybatis【5】-- Mybatis多种增删改查那些你会了么?

秦怀杂货店

Java mybatis JDBC

架构师训练营第 1 期第11周作业

业哥

架构师训练营第11周课后作业

听夜雨

极客大学架构师训练营

从华为看VUCA时代如何让组织不断乘风破浪?

Alan

华为 战略思考 组织发展 组织活力

使用PicGo存储markdown图片(阿里云或者github)

秦怀杂货店

markdown 图床

架构师训练营第 1 期第 11 周作业

好吃不贵

极客大学架构师训练营

【Java基础】-- instanceof 用法详解

秦怀杂货店

Java

JVM,JRE,JDK之间的区别和联系

入门小站

JVM

IT做得好的时候,是什么状态?

boshi

职业

程序员入门之路

咸鱼杰克

程序人生

11.8作业

张荣召

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

Vue.js源码解析:深入响应式原理-InfoQ