Vue.js双向绑定原理
Vue.js最核心的功能有两个,一个是响应式的数据绑定系统,另一个是组件系统。本文仅仅探究双向绑定是怎样实现的。先讲涉及的知识点,再用简化的代码实现一个简单的hello world示例。
一、访问器属性
访问器属性是对象中的一种特殊属性,它不能直接在对象中设置,而必须通过defineProperty()方法单独定义。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script>
var obj = {};
Object.defineProperty(obj, 'hello', {
get: function() {
console.log('get方法被调用了');
},
set: function(val) {
console.log('set方法被调用了,参数是' + val);
}
});
obj.hello; //get方法被调用了
obj.hello = 'abc'; //set方法被调用了,参数是abc
</script>
</body>
</html>
get和set方法内部的this都指向obj,这意味着get和set函数可以操作对象内部的值。另外,访问器属性的会“覆盖”同名的普通属性,因为访问器属性会被优先访问,与其同名的普通属性则会被忽略。
二、极简的双向绑定实现
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<input type="text" id="a" />
<span id="b"></span>
<script>
var obj = {};
Object.defineProperty(obj, 'hello', {
set: function(newval) {
document.getElementById('a').value = newval;
document.getElementById('b').innerHTML = newval
}
});
document.addEventListener('keyup', function(e) {
obj.hello = e.target.value;
})
</script>
</body>
</html>
此例实现的效果是:随着文本框输入文字的变化,span中会同步显示相同的内容。在js或者在控制台上显式的修改obj.hello的值,视图会相应的更新。这样就实现了model=>view以及view=>model的双向绑定。
以上就是Vue实现双向绑定的基本原理。
三、分解任务
上述示例仅仅是为了说明原理,我们最终要实现的是:
<div id="app">
<input type="text" v-model="text">
{{ text }}
</div> var vm = new Vue({
el:'#app',
data:{
text:'hello world'
}
})
首先将该任务分成几个子任务:
1、输入框以及文本节点与data中的数据绑定;
2、输入框内容变化时,data中的数据同步变化,即view =>model的变化;
3、data中的数据变化时,文本节点的内容同步变化,即model =>view的变化;
要实现任务1,需要对DOM进行编译,这里有一个知识点:DocumentFragment。
四、DocumentFragment
DocumentFragment(文档片段)可以看做节点容器,它可以包含多个子节点,当我们将它插入到DOM中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。使用DocumentFragment处理节点,速度和性能远远优于直接操作DOM。Vue进行编译时,就是将挂载目标的所有子节点劫持(真的是劫持,通过append方法,DOM中的节点会被自动删除)到DocumentFragment中,经过一番处理后,再将DocumentFragment整体返回插入挂载目标。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
<input type="text" id="a" />
<span id="b"></span>
</div>
<script>
var dom = nodeToFragment(document.getElementById('app'));
console.log(dom); function nodeToFragment(node) {
var flag = document.createDocumentFragment();
var child;
while(child == node.firstChild) {
flag.appendChild(child); //劫持node的所有子节点
}
return flag;
} document.getElementById('app').appendChild(dom); //返回到app中
</script>
</body>
</html>
五、数据初始化绑定
<!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 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.value = vm[name]; // 将 data 的值赋给该 node
node.removeAttribute('v-model');
}
};
}
// 节点类型为 text
if(node.nodeType === 3) {
if(reg.test(node.nodeValue)) {
var name = RegExp.$1; // 获取匹配到的字符串
name = name.trim();
node.nodeValue = vm.data[name]; //将data的值赋给该node
}
}
} 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 Vue(options) {
this.data = options.data;
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>
以上代码实现了任务一,我们可以看到,hello world已经呈现在输入框和文本节点中。
六、响应式的数据绑定
再来看任务2的是实现思路:当我们在输入框输入数据的时候,首先触发input事件或者keyup、change事件,在相应的事件处理程序中,我们获取输入框的value并赋值给vm实例的text属性。我们会利用defineProperty将data中的text设置为vm的访问器属性,因此给vm.text赋值就会触发set方法。在set方法中主要做两件事,第一是更新属性的值,第二留到任务3来说。
<!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) {
Object.defineProperty(obj, key, {
get: function() {
return val
},
set: function(newVal) {
if(newVal === val) return
val = newVal;
console.log(val); //方便看效果
}
});
} 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');
}
};
}
// 节点类型为 text
if(node.nodeType === 3) {
if(reg.test(node.nodeValue)) {
var name = RegExp.$1; // 获取匹配到的字符串
name = name.trim();
node.nodeValue = vm[name]; //将data的值赋给该node
}
}
} 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>
任务2也就完成了,text属性值会与输入框的内容同步变化(打开浏览器后台进行查看)。
七、订阅/发布模式(subscribe&publish)
text属性变化了,set方法触发了,但是文本节点的内容没有变化。如何让同样绑定到text的文本节点也同步变化呢?这里又有一个知识点:订阅发布模式。
订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。发布者发出通知 =>主题对象收到通知并推送给订阅者 =>订阅者执行相应操作。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Two-way-data-binding</title>
</head>
<body>
<script>
//一个发布者publisher
var pub = {
publish: function() {
dep.notify();
}
} //三个订阅者subscribers
var sub1 = {
update: function() {
console.log(1)
}
};
var sub2 = {
update: function() {
console.log(2)
}
};
var sub3 = {
update: function() {
console.log(3)
}
}; //一个主题对象
function Dep() {
this.subs = [sub1, sub2, sub3];
}
Dep.prototype.notify = function() {
this.subs.forEach(function(sub) {
sub.update();
})
} //发布者发布消息,主题对象执行notify方法,进而触发订阅者执行update方法
var dep = new Dep();
pub.publish(); //1,2,3
</script>
</body>
</html>
之前提到的,当set方法触发后做的第二件事就是作为发布者发出通知:“我是属性text,我变了”。文本节点则是作为订阅者,在收到消息后执行相应的更新操作。
八、双向绑定的实现
回顾一下,每当 new 一个 Vue,主要做了两件事:第一个是监听数据:observe(data),第二个是编译 HTML:nodeToFragement(id)。
在监听数据的过程中,会为data中的每一个属性生成一个主题对象dep。
在编译HTML的过程中,会为每个与数据绑定相关的节点生成一个订阅者watcher,watcher会将自己添加到相应属性的dep中。
我们已经实现:修改输入框内容 =>在事件回调函数中修改属性值 =>触发属性的set方法。接下来我们要实现的是:发出通知dep.notify() =>触发订阅者的update方法 =>更新视图。
这里的关键逻辑时:如何将watcher添加到关联属性的dep中。
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');
}
}
}
在编译HTML过程中,为每个与data关联的节点生成一个watcher,watcher函数中发生了什么呢?
function Watcher(vm, node, name, nodeType) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update: function() {
this.get();
this.node.nodeValue = this.value;
},
// 获取 data 中的属性值
get: function() {
this.value = this.vm[this.name]; // 触发相应属性的 get
}
}
首先,将自己赋给了一个全局变量Dep.target;
其次,执行了update方法,进而执行了get方法,get的方法读取了vm的访问器属性,从而触发了访问器属性的get方法,get方法中将该watcher添加到了对应访问器属性的dep中;
接着,获取属性的值,然后更新视图。
最后,将Dep.target设为空,因为它是全局变量,也是watcher与dep关联的唯一桥梁,任何时刻都必须保证Dep.target只有一个值。
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 Dep() {
this.subs = []
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
}
至此,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>
参考文章1:https://github.com/DDFE/DDFE-blog/issues/7
参考文章2:https://segmentfault.com/a/1190000006599500
原文地址
Vue.js双向绑定原理的更多相关文章
- Vue.js双向绑定的实现原理和模板引擎实现原理(##########################################)
Vue.js双向绑定的实现原理 解析 神奇的 Object.defineProperty 这个方法了不起啊..vue.js和avalon.js 都是通过它实现双向绑定的..而且Object.obser ...
- vue的双向绑定原理及实现
前言 使用vue也好有一段时间了,虽然对其双向绑定原理也有了解个大概,但也没好好探究下其原理实现,所以这次特意花了几晚时间查阅资料和阅读相关源码,自己也实现一个简单版vue的双向绑定版本,先上个成果图 ...
- vue数据双向绑定原理
vue的数据双向绑定的小例子: .html <!DOCTYPE html> <html> <head> <meta charset=utf-> < ...
- vue的双向绑定原理解析(vue项目重构二)
现在的前端框架 如果没有个数据的双向/单向绑定,都不好意思说是一个新的框架,至于为什么需要这个功能,从jq或者原生js开始做项目的前端工作者,应该是深有体会. 以下也是个人对vue的双向绑定原理的一些 ...
- vue的双向绑定原理浅析与简单实现
很久之前看过vue的一些原理,对其中的双向绑定原理也有一定程度上的了解,只是最近才在项目上使用vue,这才决定好好了解下vue的实现原理,因此这里对vue的双向绑定原理进行浅析,并做一个简单的实现. ...
- Vue.js双向绑定的实现原理
Vue.js最核心的功能有两个,一是响应式的数据绑定系统,二是组件系统.本文仅探究几乎所有Vue的开篇介绍都会提到的hello world双向绑定是怎样实现的.先讲涉及的知识点,再参考源码,用尽可能少 ...
- 西安电话面试:谈谈Vue数据双向绑定原理,看看你的回答能打几分
最近我参加了一次来自西安的电话面试(第二轮,技术面),是大厂还是小作坊我在这里按下不表,先来说说这次电面给我留下印象较深的几道面试题,这次先来谈谈Vue的数据双向绑定原理. 情景再现: 当我手机铃声响 ...
- Vue数据双向绑定原理及简单实现
嘿,Goodgirl and GoodBoy,点进来了就看完点个赞再go. Vue这个框架就不简单介绍了,它最大的特性就是数据的双向绑定以及虚拟dom.核心就是用数据来驱动视图层的改变.先看一段代码. ...
- Vue之双向绑定原理动手记
Vue.js的核心功能有两个:一是响应式的数据绑定系统,二是组件系统.本文是通过学习他人的文章,从而理解了双向绑定原理,从而在自己理解的基础上,自己动手实现数据的双向绑定. 目前几种主流的mvc(vm ...
随机推荐
- 洛谷 P2480 [SDOI2010]古代猪文 题解【欧拉定理】【CRT】【Lucas定理】
数论综合题. 题目背景 题目背景与题目无关因此省略.题目链接 题目描述 猪王国的文明源远流长,博大精深. iPig 在大肥猪学校图书馆中查阅资料,得知远古时期猪文文字总个数为 \(N\).当然,一种语 ...
- 转 在子线程中new Handler报错--Can't create handler inside thread that has not called Looper.prepare()
在子线程中new一个Handler为什么会报以下错误? java.lang.RuntimeException: Can't create handler inside thread that has ...
- 第一个hibernate程序HelloWorldHibernate
HelloWorldHibernate步骤: HelloWorld 1,新建java项目hibernate_0100_HelloWorld 2,学习User-library-hibernate,并加入 ...
- thinkPHP5配置nginx环境无法打开(require(): open_basedir restriction in effect. File(/mnt/hgfs/root/tp5/thinkphp/start.php) is not within the allowed path(s)
今天想把玩一下tp5,结果怎么都无法访问,每次都是报500错误,我把错误提示都打开看到下面的错误 require(): open_basedir restriction in effect. File ...
- 前后端完全分离开发模式Tomcat跨域问题处理
公司新项目实现方案采用前后端完全分离架构,后端采用spring boot框架,前端纯HTML5开发部署会采用同一台服务器,但是在实现过程中分工开发出现ajax请求跨域问题故为解决开发问题发现如下解决方 ...
- 控制反转(IOC) 和依赖注入(DI) 的理解
1. IOC(控制反转) inverseof control是spring容器的内核,AOP.声明事务等功能在此基础上开花结果. 2. 通过实例理解IOC概念: 实例:<墨攻 ...
- Javac的命令
关于命令,还可以查看<Java 7程序设计>一书后面的附录A As per javac source docs, there are 4 kinds of options: standar ...
- java当中的定时器
对于开发游戏项目的同胞来说,Timer 这个东西肯定不会陌生,今天对以前自己经常使用的定时进行了一番小小的总结!没有写具体实现的原理,只是列举出了其中的四种比较常见的使用方法,相对而言,所以只要按照其 ...
- [转]MAC:删除终端默认前缀的计算机名
MAC:删除终端默认前缀的计算机名 1.打开终端 输入 sudo vi /etc/bashrc,提示输入密码就是计算机的密码. 2.点击i将编辑模式改成insert修改文档,使用#注释PS1=’\h: ...
- NodeJS入门篇
在我印象里,“全栈工程师”这个词是NodeJS诞生后才逐渐火起来的,因为NodeJS赋予了JS服务器开发的能力.下面开始从一个小白的角度进军NodeJS... 前言:在学习NodeJS之前是需要安装的 ...