1. 实现一个极简的双向绑定效果
1.1 访问器属性
访问器属性是对象中的一种特殊属性,它不能直接在对象中设置,而必须通过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
函数可以操作对象内部的值。另外,访问器属性的会"覆盖"同名的普通属性,因为访问器属性会被优先访问,与其同名的普通属性则会被忽略(也就是所谓的被"劫持"了)。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>vueTest</title>
<script>
var obj ={};
//关键是object对象的defineProperty方法
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>
</head>
<body>
<div>
<input type="text" id="a">
<span id="b"> </span>
</div>
</body>
</html>
效果:
此例实现的效果是:随文本框输入文字的变化,span中会同步显示相同的文字内容;在js或控制台显式的修改obj.name
的值,视图会相应更新。这样就实现了model =>view
以及view => model
的双向绑定,并且是响应式的。
以上就是Vue实现双向绑定的基本原理。
2. 复现Vue框架的双向绑定效果
上面一个例子只是一个简单的例子,我们要完全实现vue的双向绑定功能(如下代码)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>VueTest2</title>
<script src="https://cdn.bootcss.com/vue/2.5.16/vue.min.js"></script>
<script>
var vue = new Vue({
el:'#app',
data:{
text:'hello world'
}
})
</script>
</head>
<body>
<div id = "app">
<input type="text" v-model = "text">{{text}}}
</div>
</body>
</html>
要实现Vue双向数据绑定的效果,需要三步:
1、输入框以及文本节点与data中的数据绑定
2、输入框内容变化时,data中的数据同步变化。即
view => model
的变化。3、data中的数据变化时,文本节点的内容同步变化。即
model => view
的变化。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>VueTest2</title>
</head>
<body>
<div id ="app">
<input type="text">
</div>
<script>
//劫持id = “app” 节点的子节点、放到节点容器dom中
var dom = nodeToFragment(document.getElementById('app'));
console.log(dom)
function nodeToFragment(node) {
var flag = document.createDocumentFragment();
var child;
while(child = node.firstChild){
flag.append(child);//劫持node的所有子节点
}
return flag;
}
//把劫持到的子节点添加回app节点去。
document.getElementById('app').appendChild(dom);
//数据初始化绑定
function compile(node,vue) {
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) {
vue[name] = e.target.value;
});
node.value = vue[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 = vue[name];//将值赋给该node
new Watcher(vm,node,name);
}
}
}
function nodeToFragment(node, vue) {
var flag = document.createDocumentFragment();
var child;
while(child = node.firstChild){
compile(child,vue);
flag.append(child);
}
return flag;
}
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);
}
//发布者
function defineReactive(obj, key, val) {
var dep = new Dep();
Object.defineProperty(obj,key,{
get: function () {
if (Dep.target) dep.addSub(Dep.target);
return val;
},
set:function (newVal) {
if (newVal == val ) return
val = newVal;
console.log(val);//
dep.notify();
}
});
}
function observe(obj, vm) {
Object.keys(obj).forEach(function (key) {
defineReactive(vm, key, obj[key]);
})
}
//主题对象
function Dep() {
this.subs = [];
}
Dep.prototype={
addSub:function (sub) {
this.subs.push(sub);
},
notify:function () {
this.subs.forEach(function (sub) {
sub.update();
})
}
}
//观察者
function Watcher(vm, node, name){
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
}
}
var vue = new Vue({
el:'app',
data:{
txt:'hello world'
}
});
</script>
</body>
</html>
发布者-订阅者逻辑:
//发布者-订阅者示例:
//发布者
var pub ={
publish :function () {
dep.notify();
}
}
//订阅者
var sub1 = {update:function (){console.log(1)}};
var sub2 = {update:function (){console.log(2)}};
//主题对象
function Dep() {
this.subs = [sub1,sub2];
}
Dep.prototype={
notify:function () {
this.subs.forEach(function (sub) {
sub.update();//notify方法通知观察者update
})
}
}
//发布者发布消息,主题对象执行notify方法,进而触发订阅者执行update方法
var dep = new Dep();
pub.publish();