个人理解: 数据双向绑定就是让数据Model展示到视图View上,同时视图View的变化改变数据Model。
在Vue中代码,很轻松就能实现输入框绑定数据,并随输入而动态改变数据
<input v-model="msg" />
<p>{{msg}}</p>
效果图
光是用还不够,接下来我们来了解Vue实现图示效果的逻辑思维,这样更能理解有时数据渲染与个人预期不同的原因。
数据绑定
常见的架构模式:
- MVC:通过业务逻辑的Controller层,对应用数据Model进行更改,然后在View视图展示(jsp页面,ejs嵌入式js模板引擎)。
- MVP:P指的是Presenter,负责View和Model之间数据流,作为二者之间的中间人
- MVVM:MVVM 可以分解成(Model-View-ViewModel),ViewModel是P的进阶版,通过事件监听响应View中用户修改Model的数据,减少DOM的操作。
它们设计的目标都是为了解决Model和View的耦合问题。
目前前端框架基本上都是采用 MVVM 模式实现双向绑定,Vue 自然也不例外。但是各个框架实现双向绑定的方法略有所不同,目前大概有三种实现方式。
- 发布订阅模式
- Angular 的脏查机制
- 数据劫持
Vue 则采用的是数据劫持与发布订阅相结合的方式实现双向绑定,数据劫持主要通过 Object.defineProperty 来实现。
Object.defineProperty
Vue主要用到了其中的 set 和 get 方法
get
属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为 undefined。
set
属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 默认为 undefined。
分析实现
MVVM模式在于数据和视图保持同步,一方改变就会更新另一方
如何检测数据变化
视图的标签发生变化从而去更新数据,这个利用标签绑定到对应的事件监听即可;而数据的变化利用Object.defineProperty的set方法,属性发生变化自动触发set函数,在函数内部通知更新视图。
实现
根据描述和分析,Vue的双向绑定是通过数据劫持和发布订阅模式来实现的。数据劫持是通过Object.defineProperty方法实现的,而发布订阅则需要监听器Observer监听标签属性的变化,Watcher订阅者来更新视图,同时还需要compile对Vue的特定指令进行解析。
- Observer监听器: 用来监听属性的变化通知订阅者
- Watcher订阅者:收到属性的变化,然后更新视图
- compile解析器:解析Vue的特定指令,初始化并绑定标签属性到订阅者(如{{}},v-model)
模拟实现双向绑定
MyVue实例
// vue接收一个对象参数
function MyVue(options = {}) {
this.$options = options;
// 获取到 el元素
this.$el = document.querySelector(options.el);
this._data = options.data;
// 设置订阅池保存订阅器
this._watcherTpl = {};
// 设置observer函数,对数据重写,实现数据变化的监听
this._observer(this._data);
// 编译模板和指令,生成订阅器发布订阅
this._compile(this.$el);
}
创建vm实例
const vm = new MyVue({
el: "#app",
data: {
msg: "这是仿·Vue",
},
});
监听器 Observer
将实例中的data对象进行遍历:
- 在订阅器中添加对应的属性,并赋值一个初始的订阅器属性为数组
- 给每个属性使用Object.defineProperty添加set、get方法
MyVue.prototype._observer = function (obj) {
Object.keys(obj).forEach((key) => {
// 添加每个属性的订阅器位置
this._watcherTpl[key] = {
_directives: [],
};
// 方便调用
let watcherTpl = this._watcherTpl[key];
// 获取当前key的值
let value = obj[key];
Object.defineProperty(this._data, key, {
configurable: true,
enumerable: true,
get() {
// console.log(`${key}获取的值是:${value}`);
return value;
},
set(newVal) {
// 数据发生了变化才触发更新
if (newVal === value) return;
value = newVal;
//触发更新
// console.log("将订阅池对应的属性进行更新");
watcherTpl._directives.forEach(item => {
item.update()
})
},
});
});
};
订阅者 Watcher
将对应元素节点的属性值关联到实例的data的属性值
function Watcher(el, vm, val, attr) {
this.el = el;
this.vm = vm;
this.val = val;
this.attr = attr;
this.update();
}
Watcher.prototype.update = function () {
this.el[this.attr] = this.vm._data[this.val];
};
解析器 Compile
解析指令初始化模板,添加标签属性到订阅者并绑定更新函数
示例解析Input和模板语法{{}}
MyVue.prototype._compile = function (el) {
var nodes = el.children;
var len = nodes.length;
for (var i = 0; i < len; i++) {
var node = nodes[i];
// 如果节点存在子节点则进行递归
if (node.children.length) this._compile(node);
// 判断是否有v-model指令并且是输入框
if (node.hasAttribute("v-model") && node.tagName === "INPUT") {
node.addEventListener("input", ((key) => {
var attrVal = node.getAttribute("v-model");
// 创建watcher对象, 并且将订阅器根据属性放入对应的订阅器集合
var watcher = new Watcher(node, this, attrVal, "value");
this._watcherTpl[attrVal]._directives.push(watcher);
return () => {
this._data[attrVal] = nodes[key].value;
};
})(i)
);
}
// 正则匹配模板语法 {{}}
var reg = /{{\s*(.*?)\s*}}/igs;
var txt = node.textContent;
if (reg.test(txt)) {
node.textContent = txt.replace(reg, (matched, placeholder) => {
let attVal = placeholder;
var watcher = new Watcher(node, this, attVal, "innerHTML");
this._watcherTpl[attVal]._directives.push(watcher)
return placeholder.split(".").reduce((val, key) => {
return val[key];
}, this._data);
});
}
}
};
效果
HTML
<div id="app">
<input v-model="msg" />
<p>{{msg}}</p>
</div>
js
//将MyVue、Observer、Watcher、Compile依次放进来
const vm = new MyVue({
el: "#app",
data: {
msg: "这是仿·Vue",
},
});
就能实现简单的双向绑定逻辑