编者按: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、测试开发和调试、源码解析及主流打包构建工具等。该书内容全面,讲解细致,示例丰富,适用于各层次的开发者。
评论