Vue原理解析——数据的响应化
上一篇文章从整体上梳理了一遍Vue构造实例的过程,从这篇文章开始将从细节上探讨Vue各个特性的实现,其中最核心的自然是Vue响应式数据的实现原理了。就先从这里开始。
initData
我们已经知道了Vue的初始化是通过_init(options)
这个函数入口开始的。在这个函数内部的created钩子被调用之前,有两个初始化方法:_initState
和_initEvents
被调用。前者就是负责数据的初始化和响应化操作。
大家都知道Vue的响应化特点,对于data对象的任何更改我们都能监听到并重渲染模板,其基础思路就是遍历data的每一个属性,然后使用Object.defineProperty将其设为响应式。
遍历遇到普通数据属性就直接处理,遇到对象和数组则递归进行处理。对于一个对象我们可以执行Object.defineProperty设置响应,但对于数组就有问题了。也许可以通过遍历下标再执行Object.defineProperty,但问题在于数组有许多原生方法的操作是无法监听的。所以Vue重写了数据的push,pop等改变数组的8个方法,让他们在执行后通知数组更新。该部分的相关代码都在_initData
这个函数中,具体如下:
Vue.prototype._initData = function() {
var dataFn = this.$options.data;
var data = this._data = dataFn ? dataFn() : {};
var props = this._props;
var keys = Object.keys(data);
var i, key;
i = key.length;
while(i--) {
key = keys[i];
this._proxy(key);
}
observe(data, this);
}
这个函数主要做了两件事,一是将data属性的内容都代理到vm上去,二就是通过observe这个函数来将data变为响应式的。
Observe
该函数具体如下:
export function observe (value, vm) {
if (!value || typeof value !== 'object') {
// 保证只有对象会进入到这个函数
return
}
var ob
if (
//如果这个数据身上已经有ob实例了,那observe过了,就直接返回那个ob实例
hasOwn(value, '__ob__') &&
value.__ob__ instanceof Observer
) {
ob = value.__ob__
} else if (
shouldConvert &&
(isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 是对象(包括数组)的话就深入进去遍历属性,observe每个属性
ob = new Observer(value)
}
if (ob && vm) {
// 把vm加入到ob的vms数组当中,因为有的时候我们会对数据手动执行$set/$delete操作,
// 那么就要提示vm实例这个行为的发生(让vm代理这个新$set的数据,和更新界面)
ob.addVm(vm)
}
return ob
}
可以看到Vue的响应式数据都会有一个__ob__
属性作为标记,里面存放的就是Observer的实例了,防止重复绑定。
接下来再来看Observer这个构造函数:
/**
* Observer class that are attached to each observed
* object. Once attached, the observer converts target
* object's property keys into getter/setters that
* collect dependencies and dispatches updates.
*/
export class {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
/*
将Observer实例绑定到data的__ob__属性上面去,之前说过observe的时候会先检测是否已经有__ob__对象存放Observer实例了,def方法定义可以参考https://github.com/vuejs/vue/blob/dev/src/core/util/lang.js#L16
*/
def(value, '__ob__', this)
if (Array.isArray(value)) {
/*
如果是数组,将修改后可以截获响应的数组方法替换掉该数组的原型中的原生方法,达到监听数组数据变化响应的效果。
这里如果当前浏览器支持__proto__属性,则直接覆盖当前数组对象原型上的原生数组方法,如果不支持该属性,则直接覆盖数组对象的原型。
*/
const augment = hasProto
? protoAugment /*直接覆盖原型的方法来修改目标对象*/
: copyAugment /*定义(覆盖)目标对象或数组的某一个方法*/
augment(value, arrayMethods, arrayKeys)
/*Github:https://github.com/answershuto*/
/*如果是数组则需要遍历数组的每一个成员进行observe*/
this.observeArray(value)
} else {
/*如果是对象则直接walk进行绑定*/
this.walk(value)
}
}
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
/*walk方法会遍历对象的每一个属性进行defineReactive绑定*/
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
/*数组需要遍历每一个成员进行observe*/
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
这部分主要对数组和对象的响应式绑定进行了区分,如果是对象则通过walk方法深度遍历每个属性绑定,如果是数组则替换掉该数组的原生方法并为每一个成员继续递归调用observe绑定。
区分了数组和对象之后,最终就要进入到defineReactive函数中进行正式的处理了。
defineReactive
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: Function
) {
/*在闭包中定义一个dep对象*/
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
/*如果之前该对象已经预设了getter以及setter函数则将其取出来,新定义的getter/setter中会将其执行,保证不会覆盖之前已经定义的getter/setter。*/
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
/*对象的子对象递归进行observe并返回子节点的Observer对象*/
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
/*如果原本对象拥有getter方法则执行*/
const value = getter ? getter.call(obj) : val
if (Dep.target) {
/*进行依赖收集*/
dep.depend()
if (childOb) {
/*子对象进行依赖收集,其实就是将同一个watcher观察者实例放进了两个depend中,一个是正在本身闭包中的depend,另一个是子元素的depend*/
childOb.dep.depend()
}
if (Array.isArray(value)) {
/*是数组则需要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归。*/
dependArray(value)
}
}
return value
},
set: function reactiveSetter (newVal) {
/*通过getter方法获取当前值,与新值进行比较,一致则不需要执行下面的操作*/
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
/*如果原本对象拥有setter方法则执行setter*/
setter.call(obj, newVal)
} else {
val = newVal
}
/*新的值需要重新进行observe,保证数据响应式*/
childOb = observe(newVal)
/*dep对象通知所有的观察者*/
dep.notify()
}
})
}
在该函数中首先通过闭包定义了一个Dep对象。该类的定义很简单,就是一个观察者模式中发布者的实现。
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
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)
}
/*依赖收集,当存在Dep.target的时候添加观察者对象*/
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
/*通知所有订阅者*/
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// 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.target设为null,防止后面重复添加依赖。*/
其sub属性用来存放他的观察者。观察者是什么呢?回顾一下,每当生成一个Vue对象时,首先是我们现在在谈的数据响应化,该过程会为每个属性生成一个dep对象。然后会编译HTML模板,在编译过程中会为每个与数据绑定相关的节点生成一个观察者,观察者在取值时会先将Dep.target设置为自己,然后触发其依赖属性的getter,这样依赖属性就能把自己添加到其sub中了。
具体来说,当每一次调用属性的getter时,执行了dep.depend(),该函数又会调用Dep.target的addDep方法将观察者添加到属性的观察者数组中。需要注意Dep.target和每个属性的dep对象不同,是全局的管理观察者的地方。
举个例子,假如某个观察者节点的值是a+b的返回值,那么在计算前它会把a+b封装成函数放在全局Dep的target属性中,然后计算时触发a,b属性的getter,在a,b的dep的sub数组中就会存放Dep.target,让Dep.target来订阅这两个属性。
而调用setter时,会通过dep的notify方法通知观察者节点去进行响应式更新。至此就完成了data对象的响应式绑定。
-- EOF --