之前了解过一点点angularjs的双向绑定,采用的是脏检查。而我们的vue采用的是数据挟持的方式。
这些双向绑定的方式究竟有什么不同呢?

双向绑定就是说:就是在数据和UI之间建立双向的通信通道,当用户通过Function改变了数据,那么这个改变也会立即反应到UI上;或者说用户通过UI的操作,那么这些操作也会随之引起对应的数据变动。

简介

双向绑定一般有三种方法:
Knockout / Backbone — 发布-订阅模式
Angular — ‘脏检查’
Vue — ‘Object.defineProperty’
Vue新 — es6 proxy

vue实现方式

Object.defineProperty 仅仅是实现了对数据的监控,后续实现对UI的重新渲染并不是它做的,所以这里还涉及到 发布-订阅模式。
过程是,当监控的数据对象被更改后,这个变更会被广播给所有订阅该数据的watcher,然后由该 watcher实现对页面的重新渲染。

  1. 首先,Vue的Compile模块会对Vue的 template 代码进行编译解析并生成一系列的watcher,也可以称之为“更新函数”,它负责把变更后的相关数据重新渲染到指定的地方。

<input v-model="message"> Compile会解析出 v-model 这个指令并且生成 watcher 并连接数据中的 message 和当前这个Dom对象,一旦收到这个message被变更的通知,watcher就会根据变更对这个Dom进行重新渲染。

  1. observer模块通过object.defineProperty对数据进行监控。

  2. 当然一个页面或者一个项目中肯定有很多watcher,因此Vue使用了Dep这个对象来存储每一个watcher,当数据发生变更,Observer会调用Dep的notify方法以通知所有订阅了该数据的watcher

js实现简单的双向绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<div id="app">
<input type="text" id="txt">
<p id="show"></p>
</div>
</body>
<script type="text/javascript">
var obj = {}
Object.defineProperty(obj, 'txt', {
get: function () {
return obj
},
set: function (newValue) {
document.getElementById('txt').value = newValue
document.getElementById('show').innerHTML = newValue
}
})
document.addEventListener('keyup', function (e) {
obj.txt = e.target.value
})
</script>

下面进行实现完整过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Object.defineProperty实现双向绑定</title>
</head>

<body>
<h1 id='h1'></h1>
<input type="text" id="inp" onkeyup="inputChange(event)">
<input type="button" value="加" onclick="btnAdd()" />
</body>
<script src="index.js"></script>

</html>

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
//实现这个index.js

//数据源
let vm = {
value: 0
}

//然后定义一个Dep,用于存储watcher
let Dep = function() {
this.list = [];
this.add = function(watcher) {
this.list.push(watcher);
};
this.notify = function(newValue) {
this.list.forEach(function (fn) {
fn(newValue)
})
}
}

// 模拟compile,生成watcher
function renderIput(newValue) {
let el = document.getElementById('inp');
if (el) {
el.value = newValue
}
}

function renderTitle(newValue) {
let el = document.getElementById('h1');
if (el) {
el.innerHTML = newValue
}
}

//将解析出来的watcher存入Dep中待用
let dep = new Dep();
dep.add(renderInput);
dep.add(renderTitle)

//使用Object.defineProperty 定义一个Observer
function observer(vm, key, value) {
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get: function () {
console.log('Get');
return value
},
set: function (newValue) {
if (value !== newValue) {
value = newValue
console.log('Update')

//将变动通知给相关的订阅者
dep.notify(newValue)
}
}
})
}

//数据初始化方法
function initMVVM(vm) {
Object.keys(vm).forEach(function (key) {
observer(vm, key, vm[key])
})
}

//初始化数据源
initMVVM(vm)

//初始化页面,将数据源渲染到UI
dep.notify(vm.value);


//实现html调用的事件方法
function inputChange(ev) {
let value = Number.parseInt(ev.target.value);
vm.value = (Number.isNaN(value)) ? 0 : value;
}

function btnAdd() {
vm.value = vm.value + 1;
}

object.defineProperty

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。

  • Object.defineProperty()的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性
  • Object.defineProperty(obj, prop, desc), prop属性名,desc 属性描述符。

javacript 有三种类型的属性:

  • 命名数据属性:拥有一个确定的值的属性。这也是最常见的属性
  • 命名访问器属性:通过getter和setter进行读取和赋值的属性
  • 内部属性:由JavaScript引擎内部使用的属性,不能通过JavaScript代码直接访问到,不过可以通过一些方法间接的读取和设置。比如,每个对象都有一个内部属性[[Prototype]],你不能直接访问这个属性,但可以通过Object.getPrototypeOf()方法间接的读取到它的值。虽然内部属性通常用一个双吕括号包围的名称来表示,但实际上这并不是它们的名字,它们是一种抽象操作,是不可0见的,根本没有上面两种属性有的那种字符串类型的属性

描述符有:

数据描述符:value, writable
存取描述符:get(一个给属性提供getter的方法,如果没有getter则为undefined。该方法返回值被用作属性值。默认为undefined。)
set(一个给属性提供setter的方法,如果没有setter则为undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认值为undefined。)
都有这两个描述符:configrable 描述属性是否配置,以及可否删除;enumerable 描述属性是否会出现在for in 或者 Object.keys()的遍历中

(当configrable为true,而writable为false, 可以通过配置方式修改属性值)

1
2
3
4
5
6
7
8
9
10
11
12

let Person = {}
let temp = null

Object.defineProperty(Person, 'name', {
get: function () {
return temp
},
set: function (val) {
temp = val
}
})

es6 proxy

3.0 将带来一个基于 Proxy 的 observer 实现,它可以提供覆盖语言 (JavaScript——译注) 全范围的响应式能力,消除了当前 Vue 2 系列中基于 Object.defineProperty 所存在的一些局限,如:

对属性的添加、删除动作的监测
对数组基于下标的修改、对于 .length 修改的监测
对 Map、Set、WeakMap 和 WeakSet 的支持

还有以下特性:https://juejin.im/post/5bb719b9f265da0ab915dbdd

  1. 公开的用于创建 observable (即响应式对象——译注) 的 API。就像new Vue({data: {…}})。
  2. 默认是惰性检测。2.x版本启动时必然被监测。新版本只有应用的初始可见部分所用的数据会被检测。
  3. 更精准的变动通知:在 2.x 系列中,通过 Vue.set 强制添加一个新的属性,将导致所有依赖于这个对象的 watch 函数都会被执行一次;而在 3.x 中,只有依赖于这个具体属性的 watch 函数会被通知到。
  4. 不可变监测对象(Immutable observable),可以创建一个对象的“不可变”版本
  5. 更良好的可调试能力。

proxy与设计模式
Proxy实现前端中3种代理模式的使用场景,分别是:缓存代理、验证代理、实现私有属性。

脏检查

以典型的mvvm框架angularjs为代表,angular通过检查脏数据来进行UI层的操作更新。
关于angular的脏检测,有几点需要了解些:

  • 脏检测机制并不是使用定时检测。 只有当UI事件、ajax请求或者timeout延迟事件,才会触发脏检查。
  • 脏检测的时机是在数据发生变化时进行。
  • angular对常用的dom事件,xhr事件等做了封装,在里面触发进入angular的digest流程。
  • 在digest流程里面,会从rootscope开始遍历,检查所有的watcher。

Angular每一个绑定到UI的数据,就会有一个$watch对象。

1
2
3
4
5
6
7
8
9
10
11
watch = {
name:'', //当前的watch对象观测的数据名
getNewValue:function($scope){ //得到新值
...
return newValue;
},
listener:function(newValue,oldValue){ //当数据发生改变时需要执行的操作
...
}
}
getNewValue() 可以得到当前$scope上的最新值,listener函数得到新值和旧值并进行一些操作。

一次脏检查就是调用一次$apply()或者$digest(),将数据中最新的值呈现在界面上。