vue.js是MVVM(模型到视图和视图到模型,模型是指数据模型,视图是指展示数据用的界面,即数据变化后,界面显示的数据变。界面输入的数据变化后,模型中的数据变)结构的,同类的还有AngularJs;至于MVC、MVP、MVVM的比较网上已经有很多了,这样不再重复。这篇文章将给大家深入的介绍vue.js双向绑定的实现原理,有需要的朋友们可以参考借鉴,下面跟着小编一起来看看吧。
大家都知道Vue.js最核心的功能有两个,一是响应式的数据绑定系统,二是组件系统。本文仅探究几乎所有Vue的开篇介绍都会提到的hello world双向绑定是怎样实现的。先讲涉及的知识点,再参考源码,用尽可能少的代码实现那个hello world开篇示例。
一、访问器属性
访问器属性是对象中的一种特殊属性,它不能直接在对象中设置,而必须通过defineProperty()
方法单独定义。
var obj = { };
// 为obj定义一个名为hello的访问器属性
Object.defineProperty(obj, "hello", {
get: function () {return sth},
set: function (val) {/* do sth */}
})
obj.hello
// 可以像普通属性一样读取访问器属性
访问器属性的"值"比较特殊,读取或设置访问器属性的值,实际上是调用其内部特性:get
和set
函数。
obj.hello
// 读取属性,就是调用get
函数并返回get
函数的返回值
obj.hello = "abc"
// 为属性赋值,就是调用set
函数,赋值其实是传参
get
和set
方法内部的this都指向obj,这意味着get
和set
函数可以操作对象内部的值。另外,访问器属性的会"覆盖"同名的普通属性,因为访问器属性会被优先访问,与其同名的普通属性则会被忽略(也就是所谓的被"劫持"了)。
二、极简双向绑定的实现
此例实现的效果是:随文本框输入文字的变化,span中会同步显示相同的文字内容;在js或控制台显式的修改obj.name
的值,视图会相应更新。这样就实现了model =>view
以及view => model
的双向绑定,并且是响应式的。(e是event,e.target是该事件的目标对象)
以上就是Vue实现双向绑定的基本原理。
三、分解任务
上述示例仅仅是为了说明原理。
我们最终要实现的是:
首先将该任务分成几个子任务:
1、输入框以及文本节点与data中的数据绑定
2、输入框内容变化时,data中的数据同步变化。即view => model
的变化。
3、data中的数据变化时,文本节点的内容同步变化。即model => view
的变化。
要实现任务一,需要对DOM进行编译,这里有一个知识点:DocumentFragment。
注:el:element
四、DocumentFragment
DocumentFragment(文档片段)可以看作节点容器,它可以包含多个子节点,当我们将它插入到DOM中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。使用DocumentFragment处理节点,速度和性能远远优于直接操作DOM。Vue进行编译时,就是将挂载目标的所有子节点劫持(真的是劫持)到DocumentFragment中,经过一番处理后,再将DocumentFragment整体返回插入挂载目标。
五、数据初始化绑定
以上代码实现了任务一,我们可以看到,hello world已经呈现在输入框和文本节点中。
六、响应式的数据绑定
再来看任务二的实现思路:当我们在输入框输入数据的时候,首先触发input
事件(或者keyup
、change
事件),在相应的事件处理程序中,我们获取输入框的value
并赋值给vm实例的text
属性。我们会利用defineProperty
将data中的text
劫持为vm的访问器属性,因此给vm.text
赋值,就会触发set
方法。
在set
方法中主要做两件事,第一是更新属性的值,第二留到任务三再说。
任务二也就完成了,text属性值会与输入框的内容同步变化:
七、订阅/发布模式(subscribe&publish)
text属性变化了,set
方法触发了,但是文本节点的内容没有变化。如何让同样绑定到text的文本节点也同步变化呢?这里又有一个知识点:订阅发布模式。
订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。
发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应操作
之前提到的,当set
方法触发后做的第二件事就是作为发布者发出通知:“我是属性text,我变了”。文本节点则是作为订阅者,在收到消息后执行相应的更新操作。
八、双向绑定的实现
回顾一下,每当new
一个Vue,主要做了两件事:第一个是监听数据:observe(data)
,第二个是编译HTML:nodeToFragement(id)
。
在监听数据的过程中,会为data中的每一个属性生成一个主题对象dep。
在编译HTML的过程中,会为每个与数据绑定相关的节点生成一个订阅者watcher,watcher会将自己添加到相应属性的dep中。
我们已经实现:修改输入框内容 => 在事件回调函数中修改属性值 => 触发属性的set
方法。
接下来我们要实现的是:发出通知dep.notify()
=> 触发订阅者的update
方法 => 更新视图。
这里的关键逻辑是:如何将watcher添加到关联属性的dep中。
在编译HTML过程中,为每个与data关联的节点生成一个Watcher。Watcher函数中发生了什么呢?
首先,将自己赋给了一个全局变量Dep.target
;
其次,执行了update
方法,进而执行了get
方法,get
的方法读取了vm的访问器属性,从而触发了访问器属性的get
方法,get
方法中将该watcher添加到了对应访问器属性的dep中;
再次,获取属性的值,然后更新视图。
最后,将Dep.target
设为空。因为它是全局变量,也是watcher与dep关联的唯一桥梁,任何时刻都必须保证Dep.target
只有一个值。
至此,hello world双向绑定就基本实现了。文本内容会随输入框内容同步变化,在控制器中修改vm.text
的值,会同步反映到文本内容中。
完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Two-way-data-binding</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text">
{{ text }}
</div>
<script>
function observe (obj, vm) {
Object.keys(obj).forEach(function (key) {
defineReactive(vm, key, obj[key]);
})
}
function defineReactive (obj, key, val) {
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function () {
// 添加订阅者 watcher 到主题对象 Dep
if (Dep.target) dep.addSub(Dep.target);
return val
},
set: function (newVal) {
if (newVal === val) return
val = newVal;
// 作为发布者发出通知
dep.notify();
}
});
}
function nodeToFragment (node, vm) {
var flag = document.createDocumentFragment();
var child;
// 许多同学反应看不懂这一段,这里有必要解释一下
// 首先,所有表达式必然会返回一个值,赋值表达式亦不例外
// 理解了上面这一点,就能理解 while (child = node.firstChild) 这种用法
// 其次,appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
// 所以,第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
while (child = node.firstChild) {
compile(child, vm);
flag.appendChild(child); // 将子节点劫持到文档片段中
}
return flag
}
function compile (node, vm) {
var reg = /\{\{(.*)\}\}/;
// 节点类型为元素
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; // 获取 v-model 绑定的属性名
node.addEventListener('input', function (e) {
// 给相应的 data 属性赋值,进而触发该属性的 set 方法
vm[name] = e.target.value;
});
node.value = vm[name]; // 将 data 的值赋给该 node
node.removeAttribute('v-model');
}
};
new Watcher(vm, node, name, 'input');
}
// 节点类型为 text
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1; // 获取匹配到的字符串
name = name.trim();
new Watcher(vm, node, name, 'text');
}
}
}
function Watcher (vm, node, name, nodeType) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.nodeType = nodeType;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update: function () {
this.get();
if (this.nodeType == 'text') {
this.node.nodeValue = this.value;
}
if (this.nodeType == 'input') {
this.node.value = this.value;
}
},
// 获取 data 中的属性值
get: function () {
this.value = this.vm[this.name]; // 触发相应属性的 get
}
}
function Dep () {
this.subs = []
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
}
function Vue (options) {
this.data = options.data;
var data = this.data;
observe(data, this);
var id = options.el;
var dom = nodeToFragment(document.getElementById(id), this);
// 编译完成后,将 dom 返回到 app 中
document.getElementById(id).appendChild(dom);
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
})
</script>
</body>
</html>
总结
以上就是关于vue.js双向绑定实现原理的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助。