Vue作为当前国内使用广泛的前端MVVM框架,其中的双向数据绑定大大减少了前端代码维护数值变化的难度,显得高效而神秘,那么今天就来解开其神秘面纱!动手实现简单的双向数据绑定,本项目源码请猛戳这里。
1. 实现简单的vue双向数据绑定
1.1 基本原理
-
首先看原理图如下
其中主要部分及其功能(首字母大写为课实例化的类,小写为函数)
-
MVVM
即Vue实例,主要包括data
和template
两部分(其他暂不考虑) -
data
对象数据模型Model,template
对应视图View -
observe
为数据劫持模块,主要实现数据的getter
和setter
,并为属性绑定订阅者,在属性值发生变化是通知订阅者 -
Watcher
为订阅者,通过depend
将自己添加至订阅者管理模块Dep
实例中,主要实现属性值变化时调用回调函数更新视图 -
Dep
为订阅者管理模块,是建立observe
与Watcher
的桥梁.通过notify
通知所有订阅者数据发生变化 -
compile
为模板解析模块,解析v-
指令以及模板字面量等,并为相应属性添加订阅者Watcher
和回调函数
1.2 基本步骤如下
-
Vue
包括data
和template
两部分,分别对应Model与View - 通过
observe
为data
的每一个属性和其子属性添加getter
和setter
- 通过
Dep
实例来管理订阅者,其中data的每一个属性拥有一个Dep
实例(data
与Dep
实例为一对多的关系) - 通过
compile
解析模板template
,分析出那些是data
的属性并创建Watcher
实例,添加至属性对应Dep
实例中 - 当
data
属性值发生变化时,即调用属性的getter
时会触发Dep
实例的notify
方法,接着出发Watcher
实例的update
方法,刷新视图(Dep
实例与Watcher
实例同样是一对多的关系`) - 当视图数据发生变化时,改变
data
对应属性值,继续步骤5,实现视图刷新
2. 用法
<div id='wu-app'>
<input type="text" v-model='text'>
<br>
<label for="">Input value:{{text}}</label>
<br>
<input type="button" v-on:click='btnClick' value='Click Me'>
</div>
<script>
window.onload = function () {
var app = new W.Wu({
el: '#wu-app',
data: {
text: 'Hello World!'
},
methods: {
btnClick(e) {
this.text = 'You clicked the button!'
}
}
})
}
</script>
3. 代码分析
3.1 主模块:入口
export function Wu(options) {
this.$options = options;
this.$data = options.data || {};
this.$methods = options.methods || {};
this.$watched = options.watched;
// 将data和methods以及computed中的属性方法代理在自己身上
proxy(this, this.$data);
proxy(this, this.$methods);
// 初始化数据劫持
observe(this.$data);
// 模板解析
compile(options.el || document.body, this);
}
3.2 数据劫持
function observe(data) {
if (!data || typeof data !== "object") return;
Object.keys(data).forEach(function(key) {
let val = data[key],
dep = new Dep();
//观察子属性
observe(val);
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get: function() {
// console.log(`i get ${key}:${val}`);
//添加订阅者
Dep.target && dep.addSub(Dep.target);
return val;
},
set: function(newVal) {
// console.log(`i set ${key}:${val}-->${newVal}`);
val = newVal;
//通知所有订阅者数据变更
dep.notify();
}
});
});
}
3.3 模板解析
let compileUtil = {
elementNodeType: 1,
textNodeType: 3,
isDirective(attr) {
let reg = /v-|:|@/;
return reg.test(attr);
return (
attr.indexof("v-") == 0 ||
attr.indexof(":") == 0 ||
attr.indexof("@") == 0
);
},
// 将原生节点拷贝到fragment
node2Fragment(node) {
let frag = document.createDocumentFragment();
[].slice.call(node.childNodes).forEach(child => {
frag.appendChild(child);
});
return frag;
},
// 更新回调函数
update(node, dir, newVal, oldVal) {
switch (dir) {
case "model":
node.value = typeof newVal === "undefined" ? "" : newVal;
break;
case "class":
break;
case "html":
break;
case "text":
node.textContent = typeof newVal === "undefined" ? "" : newVal;
break;
}
},
// 获取属性值(当表达式为不只是key,而是一各需要运算的语句是如何处理?)
getVMVal(vm, exp) {
let src = vm;
exp.split(".").forEach(k => {
src = src[k];
});
return src;
},
// 设置属性值
setVMVal(vm, exp, val) {
let src = vm,
keys = exp.split(".");
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
if (i < keys.length - 1) {
src = src[k];
} else {
src[k] = val;
}
}
return src;
},
// 解析节点
compileNode(node, vm) {
[].slice.call(node.childNodes).forEach(child => {
switch (child.nodeType) {
// 节点
case compileUtil.elementNodeType:
let attrs = child.attributes;
[].slice.call(attrs).forEach(attr => {
// 判断是否为内部指令
let attrName = attr.name;
if (this.isDirective(attrName)) {
let dir = attrName.split(/v-|:|@/).join(""),
exp = attr.value;
// 事件
if (dir.substring(0, 2) === "on") {
child.addEventListener(
dir.substring(2),
//注意 this 指向
this.getVMVal(vm, exp).bind(vm)
);
} else {
// 其他指令model bind text等
this.update(child, dir, this.getVMVal(vm, exp));
// 订阅者
new Watcher(vm, exp, (newVal, oldVal) => {
// 更新回调
this.update(child, dir, newVal, oldVal);
});
// model
if (dir === "model") {
let oldVal = this.getVMVal(vm, exp);
// 注册对于表单输入项的input事件
child.addEventListener("input", e => {
var newVal = e.target.value;
if (newVal !== oldVal) {
// 更改数值
this.setVMVal(vm, exp, newVal);
}
});
}
}
// 移除指令
// child.removeAttributes(attr);
}
});
if (child.childNodes && child.childNodes.length > 0) {
this.compileNode(child, vm);
}
break;
// 文本
case compileUtil.textNodeType:
var text = child.textContent,
reg = /\{\{(.*)\}\}/;
if (reg.test(text)) {
var exp = reg.exec(text)[1];
this.update(child, "text", this.getVMVal(vm, exp));
new Watcher(vm, exp, (newVal, oldVal) => {
this.update(child, "text", newVal, oldVal);
});
}
break;
}
});
}
};
// 模板解析
function compile(template, vm) {
let el =
template.nodeType == compileUtil.elementNodeType
? template
: document.querySelector(template); // 取出id为el的第一个节点作为容器
if (el) {
// 将原始节点存为fragment进行操作 减少页面渲染次数 提升效率
let fragment = compileUtil.node2Fragment(el);
compileUtil.compileNode(fragment, vm);
// 处理完后 重新添加至容器
el.appendChild(fragment);
}
}
3.4 订阅者
let _uid = 0;
export function Watcher(vm, exp, cb) {
// 唯一标识
this.id = _uid++;
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.depend();
}
Watcher.prototype = {
constructor: Watcher,
depend() {
Dep.target = this;
//通过触发getter,添加自己为订阅者
var value = this.vm[this.exp];
Dep.target = null;
return value;
},
// 更新
update() {
let oldVal = this.value,
newVal = this.vm[this.exp];
if (oldVal !== newVal) {
this.value = newVal;
this.cb(newVal, oldVal);
}
}
};
3.5 订阅者管理
export function Dep() {
// 键值对
this.subs = new Map();
}
Dep.prototype = {
constructor: Dep,
// 添加订阅者
addSub(watcher) {
// 通过订阅者id作为唯一标识 避免重复订阅
this.subs.set(watcher.id, watcher);
},
// 通知订阅者
notify() {
this.subs.forEach(watcher => {
watcher.update();
});
}
};
Dep.target = null;
参考
如果您感觉有所帮助,或者有问题需要交流,欢迎留言评论,非常感谢!
前端菜鸟,还请多多关照!