实现Vue双向绑定

Object.defineProperty()

这个API是实现双向绑定的核心,最主要的作用是重写数据的getset方法。

使用方法:

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
let obj = {
singer: "周杰伦"
};
let default_value = "青花瓷";
Object.defineProperty(obj, "music", {
// value: '七里香', // 设置属性的值,下面设置了get set函数,所以这里不能设置
configurable: false, // 是否可以删除属性,默认不能删除
// writable: true, // 是否可以修改对象,下面设置了get set函数,所以这里不能设置
enumerable: true, // music是否可以被枚举,默认是不能被枚举(遍历)
//get,set设置时不能设置writable和value,要一对一对设置,交叉设置/同时存在就会报错
get() {
// 获取obj.music的时候就会调用get方法
// let default_value = "强行设置get的返回值"; // 打开注释 读取属性永远都是‘强行设置get的返回值’
return default_value;
},
set(val) {
// 将修改的值重新赋给song
default_value = val;
}
});
console.log(obj.music); // 青花瓷
delete obj.music; // configurable设为false 删除无效
console.log(obj.music); // 青花瓷
obj.music = "听妈妈的话";
console.log(obj.music); // 听妈妈的话
for (let key in obj) {
// 默认情况下通过defineProperty定义的属性是不能被枚举(遍历)的
// 需要设置enumerable为true才可以 否则只能拿到singer 属性
console.log(key); // singer, music
}

实现思路

  1. 实现数据监听器Observer,用Object.defineProperty()重写数据的getset,值更新就在set中通知订阅者更新数据。
  2. 实现模板编译Compile,深度遍历dom树,对每个元素节点的指令模板进行替换数据以及订阅数据。
  3. 实现Watch用于连接ObserverCompile,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。

    流程图

    流程图

    代码实现

    html结构

    1
    2
    3
    4
    5
    6
    7
    <div id="wrap">
    <p v-html="test"></p>
    <input type="text" v-model="form">
    <input type="text" v-model="form">
    <button @click="changeValue">改变值</button>
    {{form}}
    </div>

JS调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
new Vue({
el: '#wrap',
data: {
form: '这是form的值',
test: '<strong>我是粗体</strong>',
obj: {
test: 123
}
},
methods: {
changeValue() {
console.log(this.form);
this.form = '改变了';
}
}
});

Vue结构

1
2
3
4
5
6
7
8
9
10
11
class Vue{
constructor(){}
proxyData(){}
observer(){}
compile(){}
compileText(){}
}
class Watcher{
constructor(){}
update(){}
}
  • Vue constructor 构造函数主要是数据的初始化
  • proxyData 数据代理
  • observer 劫持监听所有数据
  • compile 解析dom
  • compileText 解析dom里处理纯双花括号的操作
  • Watcher 更新视图操作

    Vue constructor 初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Vue{
    constructor(options = {}) {
    this.$el = document.querySelector(options.el);
    let data = this.data = options.data;
    // 代理data,使其能直接this.xxx的方式访问data,正常的话需要this.data.xxx
    Object.keys(data).forEach(key => {
    this.proxyData(key);
    });
    this.methods = options.methods; // 事件方法
    this.watcherTask = {}; // 需要监听的任务列表
    this.observer(data); // 初始化劫持监听所有数据
    this.compile(this.$el); // 解析dom
    }
    }

proxyData 代理data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Vue{
constructor(options = {}){
......
}
proxyData(key) {
let _this = this;
Object.defineProperty(_this, key, {
configurable: false,
enumerable: true,
get() {
return _this.data[key];
},
set(newVal) {
_this.data[key] = newVal;
}
});
}
}

上面主要是代理data到最上层,this.xxx的方式直接访问data

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
class Vue{
constructor(options = {}){
......
}
proxyData(key){
......
}
observer(data) {
let _this = this;
Object.keys(data).forEach(key => {
let value = data[key];
_this.watcherTask[key] = [];
Object.defineProperty(data, key, {
configurable: false,
enumerable: true,
get() {
return value;
},
set(newValue) {
if (newValue !== value) {
value = newValue;
_this.watcherTask[key].forEach(task => {
task.update();
});
}
}
})
})
}
}

同样是使用Object.defineProperty来监听数据,初始化需要订阅的数据。

把需要订阅的数据到push到watcherTask里,等到时候需要更新的时候就可以批量更新数据了。

compile 解析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
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
class Vue{
constructor(options = {}){
......
}
proxyData(key){
......
}
observer(data){
......
}
compile(el) {
let _this=this;
let nodes = el.childNodes;
for (let i = 0, len = nodes.length; i < len; i++) {
const node = nodes[i];
if (node.nodeType === 3) {
let text = node.textContent.trim();
if (!text) {
continue;
}
this.compileText(node, 'textContent');
} else if (node.type === 1) {
if (node.childNodes.length > 0) {
this.compile(node);
}
if (node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
node.addEventListener('input', (() => {
let attrVal = node.getAttribute('v-model');
this.watcherTask[attrVal].push(new Watcher(node, _this, attrVal, 'value'));
// node.removeAttribute('v-model');
return () => {
this.data[attrVal] = node.value;
}
})());
}
if (node.hasAttribute('v-html')) {
let attrVal = node.getAttribute('v-html');
this.watcherTask[attrVal].push(new Watcher(node, _this, attrVal, 'innerHTML'));
// node.removeAttribute('v-html');
}
this.compileText(node, 'innerHTMl');
if (node.hasAttribute('@click')) {
let attrVal = node.getAttribute('@click');
// node.removeAttribute('@click');
node.addEventListener('click', e => {
this.methods[attrVal] && this.methods[attrVal].bind(_this)()
})
}
}
}
}

compileText(node, type) {
let reg = /\{\{(.*?)\}\}/g;
let txt = node.textContent;
if (reg.test(txt)) {
node.textContent = txt.replace(reg, (matched, value) => {
let tpl = this.watcherTask[value] || [];
tpl.push(new Watcher(node, this, value, type));
if (value.split('.').length>1) {
let v=null;
value.split('.').forEach((val,i)=>{
v=!v?this[val]:v[val];
});
return v;
}else {
return this[value];
}
})
}
}
}

首先我们先遍历el元素下面的所有子节点,node.nodeType === 3 的意思是当前元素是文本节点,node.nodeType === 1 的意思是当前元素是元素节点。因为可能有的是纯文本的形式,如纯双花括号就是纯文本的文本节点,然后通过判断元素节点是否还存在子节点,如果有的话就递归调用compile方法。

Watcher

1
2
3
4
5
6
7
8
9
10
11
12
13
class Watcher {
constructor(el, vm, value, type) {
this.el = el;
this.vm = vm;
this.value = value;
this.type = type;
this.update();
}

update() {
this.el[this.type] = this.vm.data[this.value];
}
}