手写v-model之简造

** 很多都是自己的理解,”谨”供参考,如果错误,烦请指正

什么是双向绑定

 双向绑定的概念最早是从Angularjs1.0版出现,这也是当年Angularjs能称为顶级框架的原因之一
 所谓的双向绑定,实际是为了实现数据与视图的”联动”
 它的一端连着数据仓库,另一端连着视图,而它站在中间帮你完成所有的同步(绑定)工作
 你要做的,就是向上帝管理河流一样,保证你数据格式正确、清晰它的来源和去向、以及保证每种数据变化的合理
 当然,这不代表你应该过分控制你的河流

 想要将数据仓库与试图同步起来,你要做2件事,这就是双向绑定的核心任务
    ① 数据变动,通知视图变化
    ② 视图变化,修改数据仓库

 第2件事是我们最擅长的,监听视图即事件监听,修改相应的仓库数据
 而第1件事,想要监听数据变动,我们就需要借助相应的API
 下一步,我们了解一下可以监听数据的两个API:ES5的Object.defineProperty 与ES6的Proxy

Object.defineProperty

 Object.defineProperty(obj, prop, descriptor)
   obj:要在其上定义属性的对象
   prop:要定义或修改的属性的名称
   descriptor:将被定义或修改的属性描述符
      ① configurable: 该属性描述符是否可通过delete删除,默认false
      ② enumerable: 是否可枚举,默认false(不可枚举属性无法通过for…in遍历)
      ③ value: 属性值,默认undefined
      ④ writable: 是否可被赋值运算符改变,默认false
      ⑤ get: 方法名,属性被获取时调用,默认undefined
      ⑥ set: 方法名,属性被改变时调用,默认undefined

 使用方式如下:

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

// 监听对象 obj 监听属性 name
const obj = {};
Object.defineProperty(obj,'name',{
get: function () {
// 此处获取对象属性值
return this._name;
},
set: function (newValue) {
this._name = '皮一下 ' + newValue;
}
});

obj.name = '张三'';
console.log(obj.name);

 defineProperty可以劫持数据,但只能监听对象中的某个属性
 IE8以下不支持该属性
 Vue底层通过defineProperty实现双向绑定

Proxy

 new Proxy(target, handler);
   ① target: 监听对象
   ② handler: 操作对象
   ③ 返回该对象
  ES6吸收了强类型语言的精华,为JavaScript新增了代理Proxy与反射Reflect
  Proxy可以监听整个对象的变化

  用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var obj = new Proxy({},{
get (target,propKey,receiver){
console.log('获取属性'+propKey + '的值');
return Reflect.get(target,propKey,receiver);
},
set(target,propKey,value,receiver){
// target: 监听对象
// target下的改变的属性名
// 新赋值
// Proxy对象
return Reflect.set(target,propKey,'皮一下 '+value,receiver);
}
});

// 注: obj是Proxy对象

obj.name = '张三';
console.log(obj.name);

简单数据绑定

 初级数据绑定,我们只需要满足数据与视图的捆绑即可

1
2
3
4
<div id="app">
<div id="divText">{{name}}</div>
<input id="inputText" />
</div>
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

// 监听对象
const obj = { name: 'HelloWorld' };

// 获取DOM元素
const $divText = document.querySelector('#divText');
const $inputText = document.querySelector('#inputText');

// 页面渲染逻辑
function pageInit(target){
$divText.innerHTML = obj[target] || '';
$inputText.value = obj[target] || '';
}

// 初始化视图
pageInit('name');


Object.defineProperty(obj,'name',{
get(){
return obj._name;
},
set(newValue){
obj._name = newValue;

// 粗暴通知视图变化
pageInit('name');
}
});

// 监听事件
$inputText.addEventListener('input',function(){
obj.name = this.value;
},false)

仿源码建造数据绑定

 仿Vue搭建可扩展的数据绑定,调用如下

1
2
3
4
5
6
// 无需再为DOM绑定ID
<div id="app">
<div>姓名:{{name}}</div>
<div>年龄:{{age}}</div>
<input type="text" v-model=' name ' />
</div>

1
2
3
4
5
6
7
Vue({
el: '#app', // 根DOM
data: { // 绑定数据
name: '小明',
age: 18
}
})

 Vue与字符串模版相似,将根DOM下的元素经过处理后将其替换
 但不同于简单粗暴的字符串模版,Vue会通过DOM Diff算法来进行优化
 首先,我们初始化页面,将初始数据置入,并替换DOM

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

// DOM结点处理
function cloneNode(node,vm) {
const flag = document.createDocumentFragment();
let child;

while(child = node.firstChild) {
compile(child,vm);
flag.appendChild(child);
}

return flag
}


// Vue 构造函数
function Vue(opts){
this.$data = opts.data || {};
const _root = document.querySelector(opts.el || '#app');
const _dom = cloneNode(_root,this);
_root.appendChild(_dom);
}


// 处理模版DOM中的绑定数据
function compile(node,vm) {
const attr = node.attributes;

// 元素为input且双向绑定
if(~~node.nodeType === 1 && node.nodeName == 'INPUT' && attr['v-model']) {
const _name = attr['v-model'].value.trim();
node.value = vm.$data[_name];
node.addEventListener('input', function () {
// 更新后的数据绑定到了vm中,而不是vm.$data;
vm[_name] = this.value;
},false);
node.removeAttribute('v-model');
}else {
// 全局替换对象
node.nodeValue = node.nodeValue.replace(/\{\{(.*)\}\}/g, function (reg,$1) {
return vm.$data[$1.trim()];
})
}
}

 我们已经绑定了input事件,及时更改了Vue实例对象中的数据值,但我们还没有做数据监听

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
function Vue(opts){
this.$data = opts.data || {};
const _id = document.querySelector(opts.el || "#app");

observe(this.$data,this); // 新加

const _dom = cloneNode(_id,this); // 获取劫持DOM
_id.appendChild(_dom); // 将替换为劫持后的DOM
}


function observe() {
Object.keys(obj).forEach(key=>{
defineReactive(vm,key,obj[key]);
})
}


// 数据监听处理 obj--> 历史值
function defineReactive(vm,key,val) {
Object.defineProperty(vm,key,{
get(){
//
return val;
},
set(newValue){
if(newValue == value) return;
val = newValue;

// 视图更新
// 留白
}
})
}

 视图更新我们用发布-订阅者模式

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

// 发布者
function Dep(){
this.subs = [];
}

Dep.prototype = {
// 添加监听对象
addSub(sub){
this.subs.push(sub);
},
// 通知所有被监听者
notify(){
this.subs.forEach(sub=>sub.updata());;
},
};

// 订阅者
function Watcher(vm,node,name,nodeType){
this.vm = vm;
this.node = node;
this.name = name;
this.nodeType = nodeType;
};

Watcher.prototype = {
// 更新视图
update(){
// 获取最新数值
this.value = this.vm[this.name];
// 更新相应的DOM
}
};

 最后就是何时订阅,何时发布了
 DOM处理数据时->订阅,监听到数据变化是->发布
 此外,将监听数据全部扩展到vm中
 完整代码如下

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
function Dep(){
this.subs = [];
}

Dep.prototype = {
addSub(sub){
this.subs.push(sub);
},
notify(){
this.subs.forEach(sub=>sub.update());
},
};


function Watcher(vm,node,name,nodeType){
this.vm = vm;
this.node = node;
this.name = name;
this.nodeType = nodeType;
};

Watcher.prototype = {
update(){
this.value = this.vm[this.name];
if(this.nodeType == 'input'){
this.node.value = this.value;
}else{
this.node.nodeValue = this.value;
}
}
};

let dep = new Dep();

function cloneNode(node,vm) {
const flag = document.createDocumentFragment();
let child;

while(child = node.firstChild) {
compile(child,vm);
flag.appendChild(child);
}

return flag;
}

function compile(node,vm) {
const attr = node.attributes;
let nodeType = 'text';
let _name;

// 元素为input且双向绑定
if(~~node.nodeType === 1 && node.nodeName == 'INPUT' && attr['v-model']) {
_name = attr['v-model'].value.trim();
node.value = vm.$data[_name];
node.addEventListener('input', function () {
// 更新后的数据绑定到了vm中,而不是vm.$data;
vm[_name] = this.value;
},false);
node.removeAttribute('v-model');
nodeType = 'input'
}else {
// 全局替换对象
node.nodeValue = node.nodeValue.replace(/\{\{(.*)\}\}/g, function (reg,$1) {
_name = $1.trim();
return vm[$1.trim()];
})
};

dep.addSub(new Watcher(vm,node,_name,nodeType));

}


function observe(obj,vm){
Object.keys(obj).forEach(key=>{
defineReactive(vm,key,obj[key]);
})
}

function defineReactive(vm,key,val) {
Object.defineProperty(vm,key,{
get(){
return val;
},
set(newValue){
if(newValue == val) return;
val = newValue;
dep.notify();
}
})
}


function Vue(opts){
this.$data = opts.data || {};
const _root = document.querySelector(opts.el || '#app');

observe(this.$data,this);


const _dom = cloneNode(_root,this);
_root.appendChild(_dom);
}