@hanting003
2016-10-31T10:37:57.000000Z
字数 6172
阅读 889
Vue.js最显著的功能就是响应式系统,它是一个典型的MVVM框架,模型(Model)只是普通的JavaScript对象,修改它则视图(View)会自动更新。这种设计让状态管理变得非常简单而直观,不过理解它的原理也很重要,可以避免一些常见问题。下面让我们深挖Vue.js响应式系统的细节,来看一看Vue.js是如何把模型和视图建立起关联关系的。
我们先来看一个简单的例子。代码示例如下:
<div id="main">
<h1>count: {{times}}</h1>
</div>
<script src="vue.js"></script>
<script>
var vm = new Vue({
el: '#main',
data: function () {
return {
times: 1
};
},
created: function () {
var me = this;
setInterval(function () {
me.times++;
}, 1000);
}
});
</script>
运行后,我们可以从页面中看到,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在这里主要做了三件事:
接下来我们就结合Vue.js的源码来详细介绍这三个过程。
首先来看一下Vue.js是如何给data对象添加Observer的。我们知道,Vue实例创建的过程会有一个生命周期,其中有一个过程就是调用vm._initData方法处理data选项。_initData方法的源码定义如下:
<!--源码目录:src/instance/internal/state.js-->
Vue.prototype._initData = function () {
var dataFn = this.$options.data
var data = this._data = dataFn ? dataFn() : {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object.',
this
)
}
var props = this._props
// proxy data on instance
var keys = Object.keys(data)
var i, key
i = keys.length
while (i--) {
key = keys[i]
// there are two scenarios where we can proxy a data key:
// 1. it's not already defined as a prop
// 2. it's provided via a instantiation option AND there are no
// template prop present
if (!props || !hasOwn(props, key)) {
this._proxy(key)
} else if (process.env.NODE_ENV !== 'production') {
warn(
'Data field "' + key + '" is already defined ' +
'as a prop. To provide default value for a prop, use the "default" ' +
'prop option; if you want to pass prop values to an instantiation ' +
'call, use the "propsData" option.',
this
)
}
}
// observe data
observe(data, this)
}
在_initData中我们要特别注意_proxy方法,它的功能就是遍历data的key,把data上的属性代理到vm实例上。_proxy方法的源码定义如下:
<!--源码目录:src/instance/internal/state.js-->
Vue.prototype._proxy = function (key) {
if (!isReserved(key)) {
// need to store ref to self here
// because these getter/setters might
// be called by child scopes via
// prototype inheritance.
var self = this
Object.defineProperty(self, key, {
configurable: true,
enumerable: true,
get: function proxyGetter () {
return self._data[key]
},
set: function proxySetter (val) {
self._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 (value, vm) {
if (!value || typeof value !== 'object') {
return
}
var ob
if (
hasOwn(value, '__ob__') &&
value.__ob__ instanceof Observer
) {
ob = value.__ob__
} else if (
shouldConvert &&
(isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (ob && vm) {
ob.addVm(vm)
}
return ob
}
observe方法首先判断value是否已经添加了ob属性,它是一个Observer对象的实例。如果是就直接用,否则在value满足一些条件(数组或对象、可扩展、非vue组件等)的情况下创建一个Observer对象。接下来我们看一下Observer这个类,它的源码定义如下:
<!--源码目录:src/observer/index.js-->
export function Observer (value) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
Observer类的构造函数主要做了这么几件事:首先创建了一个Dep对象实例(关于Dep对象我们稍后作介绍);然后把自身this添加到value的ob属性上;最后对value的类型进行判断,如果是数组则观察数组,否则观察单个元素。其实observeArray方法就是对数组进行遍历,递归调用observe方法,最终都会调用walk方法观察单个元素。接下来我们看一下walk方法,它的源码定义如下:
<!--源码目录:src/observer/index.js-->
Observer.prototype.walk = function (obj) {
var keys = Object.keys(obj)
for (var i = 0, l = keys.length; i < l; i++) {
this.convert(keys[i], obj[keys[i]])
}
}
walk方法是对obj的key进行遍历,依次调用convert方法,对obj的每一个属性进行转换,让它们拥有getter、setter方法。只有当obj是一个对象时,这个方法才能被调用。接下来我们看一下convert方法,它的源码定义如下:
<!--源码目录:src/observer/index.js-->
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 function defineReactive (obj, key, val) {
var dep = new Dep()
var property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get
var setter = property && property.set
var childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
if (isArray(value)) {
for (var e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val
if (newVal === value) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
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 default function Dep () {
this.id = uid++
this.subs = []
}
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
Dep类是一个简单的观察者模式的实现。它的构造函数非常简单,初始化了id和subs。其中subs用来存储所有订阅它的Watcher,Watcher的实现稍后我们会介绍。Dep.target表示当前正在计算的Watcher,它是全局唯一的,因为在同一时间只能有一个Watcher被计算。
前面提到了在getter和setter方法调用时会分别调用dep.depend方法和dep.notify方法,接下来依次介绍这两个方法。depend方法的源码定义如下:
<!--源码目录:src/observer/dep.js-->
Dep.prototype.depend = function () {
Dep.target.addDep(this)
}
depend方法很简单,它通过Dep.target.addDep(this)方法把当前Dep的实例添加到当前正在计算的Watcher的依赖中。接下来我们看一下notify方法,它的源码定义如下:
<!--源码目录:src/observer/dep.js-->
Dep.prototype.notify = function () {
// stablize the subscriber list first
var subs = toArray(this.subs)
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
notify方法也很简单,它遍历了所有的订阅Watcher,调用它们的update方法。
至此,vm实例中给data对象添加Observer的过程就结束了。接下来我们看一下Vue.js是如何进行指令解析的。