要想实现双向绑定需要做到两点:1.如何监听data对象是否改变;2.对象变化后如何去更新视图
一、如何监听data对象是否改变
在Vue监听data对象是否改变主要通过defineReactive方法来做到的,就是利用Object.defineProperty的get和set方法。如下代码所示:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: Function
) {
const dep = new Dep()//每个监听的对象都新建一个dep对象,dep相当于观察者模式中的被观察对象,watcher相当于观察者对象
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()//这里是进行订阅操作
if (childOb) {
childOb.dep.depend()
}
if (Array.isArray(value)) {
dependArray(value)
}
}
return value
},
set: function reactiveSetter (newVal) {
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.call(obj, newVal)
} else {
val = newVal
}
childOb = observe(newVal)
dep.notify()//通知所有订阅了该dep的观察者watcher对象去更新视图,watcher会调用updateComponent方法去更新视图
}
})
}
但是Object.defineProperty也不是没有缺点,就是它无法监听数组的push,shift等方法。那么如何监听数组的这一些方法呢?
我们知道数组对象本身没有push这一些方法,都是继承至Array.prototype对象的,所以我们想到可以重写这一些方法,但不能直接修改Array.prototype对象上的方法,否则将影响所有数组的使用。那Vue是如何做到的呢?分为两种情况:
1.如果浏览器支持__proto__熟悉的话,vue采用的是代理模式实现的,实现方式如下:
首先Vue创建了一个继承至Array.prototype的arrayMethods代理对象,在该代理对象中重写了相关的数组原生方法,
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
/**
* Intercept mutating methods and emit events
*/
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator () {
// avoid leaking arguments:
// http://jsperf.com/closure-with-arguments
let i = arguments.length
const args = new Array(i)
while (i--) {
args[i] = arguments[i]
}
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
inserted = args
break
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
然后再将数组对象继承至arrayMethods对象
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
2.如果浏览器不支持__proto__属性,就只需在数组对象上新增了push等方法,这样当数组调用push等方法时将不再使用Array.prototype的方法,而是这一些新增的方法。
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
二、对象变化后如何去进行视图渲染
首先对象变化,会触发对于的defineReactive中的set方法,在set方法中会触发dep.notify(),该方法会触发所有订阅了该dep的watcher去只执行视图渲染,可以参考http://blog.csdn.net/zhoulu001/article/details/79372555
总结如下,其实vue的双向绑定主要基于观察者模式实现的。Dep相当于被观察对象,Watcher相关于观察者对象,在页面首次渲染后,defineReactive中的get方法就让watcher进行了对dep的订阅,一旦data数据变化,就调用dep.notify()去通知所有的订阅了该dep的watcher,去执行渲染操作。