vue的数据双向绑定原理

vue数据双向绑定原理

目前几种主流的框架都实现了数据绑定,大致做法有:

1
2
3
发布者-订阅者模式(backbone.js)
脏值检测(angular.js)
数据劫持+发布者-订阅者模式(vue.js)

数据劫持

要实现数据劫持,我们需要了解Object.defineProperty()方法。当我们创建一个普通属性时,可以看到属性描述符的各种性质的默认值,如果需要修改这些默认值,可以使用Object.defineProperty()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var myObject = {
a:1
};
Object.getOwnPropertyDescriptor(myObject, "a");
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
Object.defineProperty( myObject, "a", {
value: 3,
writable: true,
configurable: true,
enumerable: true
} );
myObject.a //3

对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。访问器描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。如果一个描述符不具有value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有(value或writable)和(get或set)关键字,将会产生一个异常。
Vue就是通过Object.defineProperty()来实现数据劫持的。看下面这段代码:设置a属性时会触发set函数,读取a属性时会触发get函数

1
2
3
4
5
6
7
8
9
10
11
12
13
var myObject = {};
var score;
Object.defineProperty(myObject,"a", {
get() {
console.log('新的分数');
},
set(val) {
score = val + '分';
console.log('分数加上单位');
}
})
myObject.a = 60; //分数加上单位
myObject.a //新的分数

思路原理

MVVM数据双向绑定大致是两方面,一是视图改变更新数据,二是数据改变更新视图。视图更新数据通过事件监听实现,比如input标签监听’input’事件,在事件触发函数内修改数据;数据更新视图就运用到上节提到的Object.defineProperty()方法,当数据变化是会触发改方法内的set函数,然后在set函数内更新视图。要实现MVVM数据绑定,需要实现以下几点:
1.实现一个数据监听器Observer,用来监听数据对象上的所有属性,一旦数据变化就通知订阅者
2.实现一个订阅者watcher,订阅并收到属性变化的通知,然后执行绑定的回调函数更新视图
3.实现一个指令解析器,对每个元素节点上的指令进行扫描和解析,根据指令模板替换数据,以及绑定响应的回调函数
'vue-wwo-way-binding'

实现Observer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
function Observer(data) {
this.data = data;
this.walk(data);
}
Observer.prototype = {
walk(data) {
const keys = Object.keys(data);
for (let i = 0, l = keys.length; i < l; i++) {
defineReactive(data, keys[i], data[keys[i]])
}
},
defineReactive(data, key, value) {
var dep = new Dep();
var childObj = observe(value); // 递归遍历所有子属性
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: true,
get() {
Dep.depend();
return value;
},
set(newVal) {
if (val === newVal) return;
value = newVal;
console.log('监听的属性'+ key +'变化了,现在的值为'+ newVal);
Dep.notify();
}
})
}
}
function observe(data) {
if(!data || typeOf data !== 'Object') {
return;
}
const keys = Object.keys(data);
for (let i = 0, l = keys.length; i < l; i++) {
defineReactive(data, keys[i], data[keys[i]])
}
}
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub(sub) {
this.subs.push(sub);
},
removeSub(sub) {
var index = this.subs.indexOf(sub);
if (index !== -1) {
this.subs.splice(index, 1);
}
},
depend() {
if(Dep.target) {
Dep.addSub(watcher);
}
},
notify() {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
Dep.target = null;

实现watcher

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function watcher(vm, exp, cb) {
this.vm = vm;
this.cb = cb;
this.exp = exp;
this.value = this.get();
}
watcher.prototype = {
update() {
this.run();// 属性值变化收到通知
},
run() {
var value = this.get();
var oldValue = this.value();
if (oldValue !== value) {
this.value = value;
this.cb.call(this.vm, value, oldVal);// 执行Compile中绑定的回调,更新视图
}
},
get() {
Dep.target = this;
var value = this.vm.data[this.exp];
Dep.target = null;
return value;
}
}

实现compile(后续补充)

Object.defineProperty的缺陷

数组漏洞

Object.defineProperty无法监听数组变化。

  1. 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength
    作者对数组的8类方法进行了hack处理,使用它们可以实现视图更新。
    1
    2
    3
    4
    5
    6
    7
    push()
    pop()
    shift()
    unshift()
    splice()
    sort()
    reverse()

对象漏洞

Vue 不能检测对象属性的添加或删除,所以添加属性的时候需要使用推荐的vm.$set方法。数据劫持时,需要对对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历。

Proxy实现数据的双向绑定

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

Proxy可以直接监听对象而非属性

我们用Proxy改造上文中用Object.defineProperty实现的数据绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var myObject = {};
var score;
var proxy = new Proxy(myObject, {
get(target, key, receiver) {
console.log('新的分数');
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
score = value + '分';
console.log('分数加上单位');
return Reflect.set(target, key, receiver);
}
})
proxy.a = 60; //分数加上单位
proxy.a //新的分数

Proxy直接可以劫持整个对象,并返回一个新对象,不管是操作便利程度还是底层功能上都远强于Object.defineProperty。

Proxy可以直接监听数组的变化

当我们对数组进行操作(push、shift、splice等)时,会触发对应的方法名称和length的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
var arr = [1,2,3,4];
var newArr = new Proxy(arr, {
get(target, key, receiver) {
console.log(key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log('原始数组变了');
return Reflect.set(target, key, value, receiver);
}
})
newArr[4] = 5; //原始数组变了
arr //[1,2,3,4,5]

Proxy 支持的拦截操作有13种:

1
2
3
4
5
6
7
8
9
10
11
12
13
get(target, key, receiver):拦截对象属性的读取。
set(target, key, value, receiver):拦截对象属性的设置。
has(target, key):拦截key in proxy的操作,返回一个布尔值。
deleteProperty(target, key):拦截delete proxy[propKey]的操作,返回一个布尔值。
ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
getOwnPropertyDescriptor(target,propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。