一. 通过简单代码模拟Vue的实现
在网上找到一个Vue简单实现的例子, 为了更好的理解这个例子,
通过对这段代码下断点调试, 理解了每一行代码, 并且对代码做了详细的注释,
方便以后阅读.
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body>
<div id="test_bind">
<input v-model="xxx">
<input v-model="xxx">
<div>{{xxx}}</div>
</div>
<script>
function nodeContainer(node, vm, flag) { // 分析模板, 需要填充和需要绑定的
var flag = flag || document.createDocumentFragment(); // 准备一个节点用于返回
var child;
while (child = node.firstChild) { // 遍历
compile(child, vm); // 分析当前子模板节点
flag.appendChild(child); // 将已分析的节点挂在返回节点下
if (child.firstChild) { // 还有子节点就递归
nodeContainer(child, vm, flag); // 递归
}
}
return flag; // 返回挂了分析后的节点, 他们会被watcher更新
}
function compile(node, vm) { // 分析单个模板节点
if (node.nodeType === 1) {
var attr = node.attributes;
for (var i = 0; i < attr.length; i++) { // 遍历属性
if (attr[i].nodeName == 'v-model') { // 发现绑定标识
var name = attr[i].nodeValue; // 绑定的变量名
node.addEventListener('input', function (e) { // 监听输入事件
vm[name] = e.target.value; // 触发这个变量名的set事件
});
node.value = vm[name]; // 将实例中的data数据赋值给节点
node.removeAttribute('v-model'); // 移除该属性
new Watcher(vm, node, name); // 添加watcher
}
}
}
if (node.nodeType === 3) { // 文本类型
if (/\{\{(.*)\}\}/g.test(node.nodeValue)) { // 发现{{xxx}}
var name = RegExp.$1;
name = name.trim(); // 取出xxx名字
new Watcher(vm, node, name); // 添加watcher
}
}
}
function defineReactive(obj, key, value) { // 监听data的get和set
var dep = new Dep(); // 每个key单独提供一个闭包的Dep对象存watcher
Object.defineProperty(obj, key, { // 替换get, set
get: function () { // 监听到get
if (Dep.global) { // 判断是否有新的watcher
dep.add(Dep.global); // 存入新的watcher
Dep.global = null; // 避免重复存入
}
return value; // 返回value, 先是默认值, set后就是新值
},
set: function (newValue) { // 监听到set
if (newValue === value) return; // 相同值不处理
value = newValue; // 设置新值
dep.notify(); // 通知当前key对应的watcher
}
})
}
function observe(obj, vm) { // 监听data每个属性的get,set
Object.keys(obj).forEach(function (key) {
defineReactive(vm, key, obj[key]);
})
}
function Vue(options) { // Vue构造函数
this.data = options.data; // 保存数据
var data = this.data;
observe(data, this); // 对data每个属性监听get,set
var id = options.el;
var element = document.getElementById(id); // 取出目标节点对应的dom元素
var dom = nodeContainer(element, this); // 分析dom模板, 填充dom模板, 返回
element.appendChild(dom); // 将新的子树附加到目标节点
}
function Dep() { // Dep构造函数
this.subs = []; // 含数组缓存watcher
}
Dep.prototype = {
add: function (sub) { // 添加新的watcher
this.subs.push(sub);
},
notify: function () { // 循环通知每一个watcher
this.subs.forEach(function (sub) {
sub.update();
})
}
}
function Watcher(vm, node, name) { // Watcher构造函数
Dep.global = this; // 准备传递watcher
this.name = name; // xxx的名字
this.node = node; // 待填充的dom元素
this.vm = vm; // 当前的Vue对象
this.update(); // 首次更新
}
Watcher.prototype = {
update: function () { // 将值更新到需要填充的node
this.value = this.vm[this.name]; // 先触发get获取
switch (this.node.nodeType) {
case 1:
this.node.value = this.value; // 填充到节点
break;
case 3:
this.node.nodeValue = this.value; // 填充到文本
break;
default: break;
}
}
}
var Demo = new Vue({ // 构造Vue对象
el: 'test_bind', // 绑定id
data: { // 数据
xxx: '123', // 给xxx默认值"123"
}
})
</script>
</body>
</html>
二. 整体流程描述
1. 编写dom模板:
dom写id, input写v-model绑定data.xxx, div显示data.xxx;
2. 创建Vue对象:
填入dom的id, data.xxx的默认值;
3. 定义Vue的构造函数:
保存data数据,
调用observe对data的全部子属性都监听get, set;
调用nodeContainer递归遍历分析目标节点下的模板, 对每个需要填充xxx数据的节点添加watcher;
5. 实现函数defineReactive
对每个属性创建闭包的变量dep, 每个dep有一个数组存watcher;
6. 实现函数nodeContainer
遍历+递归,
解析目标dom子节点包含的xxx名字需要填充的, 或者v-model绑定了事件的.
compile解析v-model, 监听输入事件, 将xxx名字和对应的node添加watcher,
compile解析目标中的{{xxx}}, 将xxx名字添加watcher;
创建临时节点, 把目标id的子节点作为临时节点的子节点返回.
7. 定义Watcher的构造函数:
通过Dep.global传递当前watcher, 记下xxx, node, vm, 触发第一次update,
update访问vm[xxx]触发了get (将Dep.global缓存的watcher存入xxx对应dep内的数组),
update根据节点类型设置node.value;
三. 验证
- 操作:
将以上代码保存为html, 在浏览器打开, input控件输入文本;
- 代码层面的处理:
- 触发complie时设置的对input事件的监听函数;
- 监听函数设置vm[xxx], 触发defineReactive定义的set;
- set触发闭包的dep的notify, notify对数组内的watcher触发update;
- update操作更新node.value, 文本node.nodeValue;
- 看到的现象:
两个输入框不管输入哪个, 三个dom元素子显示的内容都同步发生变化.
四. 总结
以上代码详细分析了Vue的实现过程,
首先是创建Vue对象, 监听data的属性的get, set;
然后是分析模板中需要填充的node用watcher记下, 后续触发watcher更新该节点;
分析模板中的v-model, 对input输入事件做监听, 当input发生时, 触发set, set又触发watcher;
watcher对缓存的xxx对应的node更新; 最终实现了双向绑定;