VUE双向绑定
转自:http://www.cnblogs.com/qiuchuanji/p/8819697.html
Vue是一个提供了MVVM风格的双向数据绑定的Javascript库,专注于View层,也就是视图层。它的核心在于VM,不直接去操作DOM,而是将Model和View连接起来,保证视图和数据的一致性。
Vue的核心是采用 es6的Object.defineProperty来实现双向数据绑定的。
一、一个简单的双向数据绑定
<input type="text" value="我" class="txt">
输入:<span class="textSpan"></span>
let obj = {
foo: 'foo'
};
let textInput = document.querySelector('.txt');
let textSpan = document.querySelector('.textSpan');
Object.defineProperty(obj, 'foo', {
set: function (newVal) {
console.log('当前值为', newVal);
textInput.value = newVal;
textSpan.innerHTML = newVal;
},
get: function () {
console.log('将要读取obj.foo属性');
}
});
// 将要读取obj.foo属性
obj.foo;
// 当前值为 name
obj.foo = 'name';
textInput.addEventListener('keyup', function (e) {
obj.foo = e.target.value;
});
最终效果:
实现一个简单的数据双向绑定还是不难的: 使用Object.defineProperty()来定义属性的set函数,属性被赋值的时候,修改Input的value值以及span中的innerHTML;然后监听input的keyup事件,修改对象的属性值,即可实现这样的一个简单的数据双向绑定。
上述是一个单个对象的双向数据绑定,可是我们在项目中能看到那种对象有多个参数的,那这又是怎么实现的呢
<input type="text" class="txt">
let txtInput = document.querySelector('.txt');
let vue = {
data: {
name: '我是vue',
age: '2'
}
};
let data = vue.data;
for (let prop in data) {
data['__' + prop] = data[prop]; //存储私有属性
Object.defineProperty(data, prop, {
enumerable : true,
set: function (newVal) {
console.log('你正在修改'+prop + ' !...操作DOM...');
// 数据校验
this['__' + prop] = newVal;
},
get: function () {
console.log('getter 获取值 ...');
return this['__' + prop];
}
});
}
txtInput.addEventListener('keyup',function(e){
data.title = e.target.value;
console.log('这里会调用get方法'+data.title)
});
效果如下:
从上面的两个例子可以看出来,Object.defineProperty是vue的核心,下面我们来简单了解一下它:
语法
Object.defineProperty(obj, prop, descriptor)
参数:
obj
被定义或修改属性的对象;
prop
要定义或修改的属性名称;
descriptor
对属性的描述
描述符还提供了set和get方法,当被监控的对象发生了改变,那么就会调用set方法,调用对象就会触发get方法
vue是用什么方法来进行渲染的呢?
标准的Vue语法
div class="app">
<input type="text" class="txt" v-model="message">
{
{ message }}
</div>
<script>
let vm = new Vue({
el: '.app',
data: {
message: '我是vue'
}
})
</script>
其中el 获取挂载点里面所有的子节点,然后经过处理之后再将整体插入到挂载点中。文档片段上进行操作DOM,而不会影响到真实的DOM,操作完成之后,我们就可以添加到真实DOM上,这样的效率比直接在正式DOM上修
改要高很多 。
我们先来看个小例子:
<div id="app"></div>
<script>
var flag = document.createDocumentFragment(),
span = document.createElement('span'),
textNode = document.createTextNode('hello world');
span.appendChild(textNode);
flag.appendChild(span);
document.querySelector('#app').appendChild(flag)
</script>
这样,我们就可以得到下面的DOM树:
最终代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>双向绑定demo</title>
<style>
body{
margin: 200px auto;width: 800px;}
</style>
</head>
<body>
<div id="app">
<input type="text" id="input" v-model="text">
{
{ text }}
</div>
<script>
// Vue构造函数 --- 第一个执行
function Vue(options){
this.data = options.data;
//发布订阅模式,数据绑定
var data = this.data;
observe(data,this);
var id = options.el;
var dom = nodeToFragment(document.querySelector(id),this);
//编译完成后返回到app中
document.getElementById("app").appendChild(dom);
}
//遍历传入实例的data对象的属性,将其设置为Vue对象的访问器属性
function observe(obj,vm){
Object.keys(obj).forEach(function(key){
defineReactive(vm,key,obj[key]);
});
}
//访问器属性是对象中的一种特殊属性,它不能直接在对象中设置,而必须通过defineProperty()方法单独定义。
//访问器属性的"值"比较特殊,读取或设置访问器属性的值,实际上是调用其内部特性:get和set函数。
function defineReactive(obj,key,val){
//这里用到了观察者(订阅/发布)模式,它定义了一种一对多的关系,让多个观察者监听一个主题对象,这个主题对象的状态发生改变时会通知所有观察者对象,观察者对象就可以更新自己的状态。
//实例化一个主题对象,对象中有空的观察者列表
var dep = new Dep();
//将data的每一个属性都设置为Vue对象的访问器属性,属性名和data中相同
//所以每次修改Vue.data的时候,都会调用下边的get和set方法。然后会监听v-model的input事件,当改变了input的值,就相应的改变Vue.data的数据,然后触发这里的set方法
Object.defineProperty(obj,key,{
get: function(){
//Dep.target指针指向watcher,增加订阅者watcher到主体对象Dep
if(Dep.target){
dep.addSub(Dep.target);
}
return val;
},
set: function(newVal){
if(newVal === val){
return
}
val = newVal;
// console.log(val);
//给订阅者列表中的watchers发出通知
dep.notify();
}
});
}
//主题对象Dep构造函数
function Dep(){
this.subs = [];
}
//Dep有两个方法,增加观察者 和 发布消息
Dep.prototype = {
addSub: function(sub){
this.subs.push(sub);
},
notify: function(){
this.subs.forEach(function(sub){
sub.update();
});
}
};
//DocumentFragment(文档片段)可以看作节点容器,它可以包含多个子节点,当我们将它插入到DOM中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。使用DocumentFragment处理节点,速度和性能远远优于直接操作DOM。Vue进行编译时,就是将挂载目标的所有子节点劫持(真的是劫持)到DocumentFragment中,经过一番处理后,再将DocumentFragment整体返回插入挂载目标。
//var dom = nodeToFragment(document.getElementById("app"));
//console.log(dom);
//返回到app中
//document.getElementById("app").appendChild(dom);
function nodeToFragment(node,vm){
var flag = document.createDocumentFragment();
var child;
//劫持node的所有子节点(真的在dom树中消失了,所以要在下边重新返回搭到app中)
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;
//解析属性,不同的属性不用的处理方式,这里只写了v-model属性
for(var i=0;i<attr.length;i++){
if (attr[i].nodeName == "v-model") {
//获取节点中v-model属性的值,也就是绑定的属性名
var name = attr[i].nodeValue;
node.addEventListener("input",function(e){
//当触发input事件时改变vue.data中相应的属性的值,进而触发该属性的set方法
vm[name] = e.target.value;
});
//改变之后,通过属性名取得数据
node.value = vm.data[name];
//用完删,所以浏览器中编译之后的节点上没有v-model属性
node.removeAttribute("v-model");
}
}
}
//节点类型为text
if(node.nodeType === 3){
//text是否满足文本插值的写法:{
{任意内容}}
if(reg.test(node.nodeValue)){
//获取匹配到的字符串:这里的RegExp.$1是RegExp的一个属性
//该属性表示正则表达式reg中,第一个()里边的内容,也就是
//{
{任意内容}} 中的 文本【任意内容】
var name = RegExp.$1;
//去掉前后空格,并将处理后的数据写入节点
name = name.trim();
//node.nodeValue = vm.data[name];
//实例化一个新的订阅者watcher
new Watcher(vm,node,name);
return;
}
}
}
//观察者构造函数。
//上边实例化新的观察者的时候执行这个函数:通过get()取得Vue.data中对应的数据,然后通过update()方法把数据更新到节点中。
function Watcher(vm,node,name){
//让全局变量Dep的target属性的指针指向该watcher实例
Dep.target = this;
this.vm = vm;
this.node = node;
this.name = name;
//放入Dep.target才能update()?????????????????????????????????????????
this.update();
Dep.target = null;
}
// 观察者使用update方法,实际上是
Watcher.prototype = {
update: function(){
this.get();
this.node.nodeValue = this.value;
},
//获取data中的属性值
get: function(){
this.value = this.vm[this.name]; //触发相应属性的get
}
};
var vm = new Vue({
el: "#app",
data: {
text: "Hello Vue"
}
})
</script>
</body>
</html>
还没有评论,来说两句吧...