Vue双向绑定

  双向绑定一直是一个高频考点,今天就来实现一个简单的Vue双向绑定。设框架名称为Wue,双向绑定指令为w-model。主要方法是observer、watcher、complier。分别用来给属性绑定setter,连接observer与complier,根据模版语法添加对应的watcher并且更新模版中的值

observer思路

1
2
3
4
5
6
7
8
9

start=>start: 开始
end=>end: 结束
op1=>operation: 遍历Wue.data中的所有属性,为每个属性添加getter/setter
op2=>operation: 手动设置data中的属性值,触发setter
op3=>operation: setter触发watcher

start->op1->op2->op3->end

watcher(在本文中更像是writer)相当于中间人。手动设置data属性后,setter触发对应data属性的watcher,watcher更新UI;input等加了双向绑定指令”w-model”的值更新后,触发对应绑定事件,在回调方法中触发watcher,更新data属性

watcher思路

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

start=>start: 开始
end=>end: 结束
op1=>operation: 触发watcher,更新input的value
op2=>operation: 触发watcher,更新data的属性
condition=>condition: data属性更新
dataIo=>inputoutput: data属性更新
inputIo=>inputoutput: input属性更新

start->condition
condition(yes)->dataIo->op1->end
condition(no)->inputIo->op2->end

watcher是在分析dom元素时添加的。先遍历dom节点,找到添加了w-model或使用指定的插值语法输入的属性,给它们添加watcher。这部分的实现者是complier

complier思路

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

start=>start: 开始
end=>end: 结束
op1=>operation: 遍历dom节点
op2=>operation: input标签添加input事件,更新数据后更改data中的属性
op3=>operation: 给双大括号语法中的属性添加watcher,将其值替换为data属性中的值
condition=>condition: 是否有w-model或双大括号语法

start->op1->condition
condition(yes)->op2->op3->end
condition(no)->end

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!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>手写 vue 双向绑定</title>
</head>
<body>
<div id="root">
w-model-name: <input type="text" w-model="name">
<div>{{ name }}</div>
w-model-age: <input type="text" w-model="age">
<div>{{ age }}</div>
no w-model: <input type="text">
<input class="submit" type="button" value="setData">
</div>
</body>
</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
// main.js
// 初始化
import './index.html';
import observer from './observer';
import getElement from './complier';

function Wue(options = {}) {
this.$el = document.querySelector(options.el);
this.$data = options.data;
this._watchers = {}; // 监听池,存放data和w-model绑定的中每个数据的watcher
this._observer(this.$data);
this._complier(this.$el);
}
Wue.prototype._observer = observer;
Wue.prototype._complier = getElement;
// 创建新的Wue实例
const app = new Wue({
el: '#root',
data: {
name: 'Alice',
age: 18
}
});
window.app = app;
// 手动修改Data数据,触发UI更新
document.querySelector('.submit').addEventListener('click', function() {
app.$data.name = 'Tom';
}, false)
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
// complier.js
// 从根节点开始,遍历所有节点,获取所有带v-model和{{}}中的属性(已在data定义或w-model这种默认指令),为它们添加watcher。并且input更新后手动修改data
import Watcher from './watcher';

function getElement(element) {
const _this = this;
const nodeList = element.children; // 获取每一级的根节点: HTMLCollection
for (let i = 0; i < nodeList.length; i += 1) {
complier.call(_this, nodeList[i]);
}
}
function complier(element) {
const _this = this;
if (element.children.length) {
complier(element); // 递归深度遍历dom树
}
if (element.hasAttribute('w-model') && element.tagName === 'INPUT') {
const attr = element.getAttribute('w-model');
_this._watchers[attr].push(
new Watcher({
el: element,
val: attr,
vm: _this,
attr: 'value',
}),
);
element.addEventListener(
'input',
function() {
_this.$data[attr] = element.value;
},
false,
);
}
// 遍历节点,获取{{}}中的值,在Wue.data中查找对应的属性,添加watcher
const tagReg = /^\{\{\s*(.*\S)\s*\}\}$/;
let textNode = element.textContent; // 当前节点的文本内容
if (tagReg.test(textNode)) {
textNode = textNode.replace(tagReg, (matched, matchedVal) => {
let watcher = _this._watchers[matchedVal];
if (!watcher) {
// 没有事件池 创建事件池
watcher = [];
}
watcher.push(
new Watcher({
el: element,
vm: _this,
val: matchedVal,
attr: 'innerHTML',
}),
);
});
}
}

export default getElement;

observer.js

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
// 为data中的每个属性添加getter、setter。数据变化后触发watcher
function observer(obj) {
const _this = this;
if (!obj || typeof obj !== 'object') {
return;
}
Object.keys(obj).forEach(key => {
initWatcher.call(_this, key);
defineObjProperty.call(_this, obj, key, obj[key]);
})
}
// 初始化数据订阅池
function initWatcher(key) {
this._watchers[key] = [];
}
// 设置对象成员的访问器属性,监听数据变化
function defineObjProperty(obj, key, value) {
observer(value);
const watchersPool = this._watchers;
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
return value;
},
set(newVal) {
if (newVal !== value) {
value = newVal;
Object.keys(watchersPool).forEach(key => {
watchersPool[key].forEach(item => {
item.update();
});
})
}
}
})
}
export default observer;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// watcher.js
// Watcher,个人觉得更应该叫Writer。用来将complier与observer连接起来。当input数据发生变化时,触发input事件,再触发watcher,修改data中的数据
// 反过来,data数据变化,触发setter,进而触发watcher,修改input的数据
function Watcher({el, vm, val, attr} = options) {
this.el = el;
this.vm = vm;
this.val = val;
this.attr = attr; // dom获取值,如value获取input的值 / innerHTML获取dom的值
this.update();
}
Watcher.prototype.update = function () {
// 更新input值
this.el[this.attr] = this.vm.$data[this.val];
}

export default Watcher;