在Vue中我们经常修改数据,然后视图就直接修改了,那么这些究竟是怎么实现的呢?
其实Vue使用了E5的语法Object.defineProperty来实现的数据驱动。
那么Object.defineProperty究竟是怎么实现的呢?
我们先来看一下一个简单的demo
<template>
<div class="hello">
{{test}}
</div>
</template>
<script>
export default {
data () {
test: '123'
},
created () {
console.log(this.test === this._data.test) // true
}
}
</script>
在vue这段小代码中,this.test === this._data.test其实是等价的。这是为什么呢
其实就是通过Object.defineProperty做的一个小代理实现的。
原理如下:
var obj = {
_data: {
x: 123,
y: 467
}
}
function proxy (target, sourceKey, key) {
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get: function() {
return this[sourceKey][key]
},
set: function(val) {
this[sourceKey][key] = val
}
})
}
proxy(obj, '_data', 'x')
proxy(obj, '_data', 'y')
console.log(obj.x) // 123
console.log(obj.y) // 467
console.log(obj._data.x) // 123
console.log(obj._data.y) // 467
以上一个demo就是对obj对象的一个代理,通过访问obj.x直接代理到obj._data.x。 Object.defineProperty具体用法可以自行搜索。
那么其实数据双向绑定也是根据Object.defineProperty里面的get和set来实现的,通过set的时候去做一些视图更新的操作。
接下来我们就来看一下Vue源码吧。
双向数据绑定,将分为以下3个部分:
1. Observer。这个模块是用于监听对象上的所有属性,即使用Object.defineProperty来实现get和set
2. Watcher。 这个模块是观察者,当监听的数据值被修改时,执行对象的回调函数。
3. Dep。 连接Observe和Watcher的桥梁,每个Observer对应一个Dep,内部维护一个数组,保存与该Observer相关的Watcher
Observer
首先来看下oberser。
路径: src/core/observer/index.js
export class Observer {
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
// 添加__ob__来标示value有对应的Observer
def(value, '__ob__', this)
// 对数组的处理
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
// 处理对象
this.walk(value)
}
}
// 给每个属性添加getter/setters
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// 观察数组的每一项
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
所以从源码可以看出数据类型主要分为2种,一种是数组,一种是对象。对应的处理方法分别树observe和defineReactive
那我们再来看看这2个函数做了些什么呢?
这2个函数也是在这个文件内部
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
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
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
// 这边作出get和set的动态响应的处理
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 = !shallow && observe(newVal)
dep.notify()
}
})
}
// 这个方法具体是为对象的key添加get,set方法;如果用户传入自己本身传入get和set方法也会保留其方法。它会为每一个值都创建一个Dep,在get函数中 dep.depend做了2件事,一是向Dep.target的内部添加dep,二是将dep。target添加到dep内部的subs数组中,也就是建立关系。
在set函数中,如果新值和旧值相同则不处理,如果不同,则通知更新。
接下来看observe
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
对数组中的每一项进行检测,该方法用于观察一个对象,如果不是对象则直接返回,如果是对象则返回该对象Observer对象。
但是这样紧紧对数组中的每一项的对象进行了观察,如果数组本身的长度修改那么又如何触发呢。Vue专门对数组做了特出的处理。
回过头来看Observer的类中有这么一段,在处理数组的时候,处理了这些代码
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
我们来细看这些函数,见如下
function protoAugment (target, src: Object, keys: any) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
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])
}
}
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
我们可以看到protoAugment很简单,就是执行了一段value._proto_ = arrayMethods
copyAugment中循环把arrayMethods上的arrayKeys方法添加到value上。
arrayMethods又是重写了数组的操作方法['push','pop','shift','unshift','splice','sort','reverse']。
通过调用数组这些方法的时候,通知dep.notify。 至此Observer部分已经结束
Dep
Dep相对就简单点。
源码路径:src/core/observer/dep.js
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)
}
// 调用watcher
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()
}
}
}
内部有个唯一的id标识,还有一个保存watcher的数组subs。
Watcher
let uid = 0
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
vm._watchers.push(this)
...
this.cb = cb
this.id = ++uid
...
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
}
}
this.value = this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
...
value = this.getter.call(vm, vm)
...
popTarget()
this.cleanupDeps()
return value
}
...
update () {
...
queueWatcher(this)
}
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
...
}
Watcher就是用于把变化放入观察,并通知其变化更新。
queueWatcher就是把变化者放入数组queue,然后通过nextTick去更换新数组queue中的变化。
在生命周期挂载元素时,就会通过创建Watcher,然后来更新创新模块。
vm._watcher = new Watcher(vm, updateComponent, noop)
这边数据双向绑定差不多就结束了。最后再附上一张简要的流程图来进一步清晰自己的思路。
下一章节通过数据绑定原理结合jquery来实现数据驱动更新的demo。之所以采用jquery操作dom是因为现在Vue源码还没到解析html模板那一步。
所以一步步来。等之后学完模板解析后。再去制作一个MVVM的简易demo。
如果对您有帮助,请点个赞,谢谢!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。