上面已经实现了单向绑定,也就是将数据通过模板编译的原理显示在了视图上,但如图3-1所示,单向数据的改变并没有再刷新到视图上。
图3-1
因为还没有对数据做监控,监控到改变之后,执行更新视图操作。这个概念似乎不陌生了,这就是观察者模式,有一部分也说这是发布订阅模式。
关于观察者模式和发布订阅模式,这两者有没有不同之处,总是被人议论纷纷。有的人认为两者就是一个模式,有的人认为有点不同,在我的观点是不同的。
不同的点可以参看以前Java版的《大话设计模式》。
观察者模式和发布订阅模式
观察者模式:
观察者和被观察者
代码后面奉上
发布订阅模式:
发布者 - 发布中心 - 订阅者
区别在于是否有发布中心作为二者的中介
单向数据绑定的更新操作
而下面代码实际上是利用观察者模式,没有发布中心的概念,别被命名误导。
发布者/被观察者
被 ,观察者,观察,所以需要绑定不同的观察者,也支持解绑,同时数据改变,通知所有观察者执行更新操作。
这里如果想做得更好,应该抽象出一个观察者的基类,观察者基类里面有更新视图的接口,各式各样的观察者去继承这个基类,实现这个接口。但js里很难去implement?反正我写起来有点困难
class Publish {
constructor() {
this.subList = [];
}
//绑定一个观察者,类型最好是基类,可以接受不同的类型的子类观察者
attachSub(subscribe) {
this.subList.push(subscribe);
}
//解绑
detachSub(sub) {
var index = this.subList.findIndex(sub);
this.subList.splice(index, 1);
}
//通知所有观察者执行更新操作。
notify() {
this.subList.forEach(sub => {
sub.update();
});
}
}
观察者,观察到数据改变做更新操作
class Subscribe {
//构造时,就传入一个回调的更新操作,达到接口实现的目的
constructor(fn) {
this.fn = fn;
}
// 去执行这个回调
update() {
this.fn();
}
}
let subscribe = new Subscribe(function() {
alert("接受到通知,做更新操作");
});
let pub = new Publish();
pub.attachSub(subscribe);
//点击某个按钮,去通知需要执行更新操纵了
document.querySelector("#btn").addEventListener("click", function() {
pub.notify();
});
讲完了观察者,就需要将观察者应用到单向数据更新上面.在双向绑定这个原理,需要被观察的是数据.
但现在又如何监控视图的改变,将改变的数据更新到真正的数据上?
假如目前是通过输入框来进行数据的改变,这就好办了,input输入的onchange事件就能随时拿到改变的值.然后监控到数据的改变,就可以去更新视图了.
set能监控数据的改变,这里就应该通知观察者更新
// 会在数据改变的时候直接设置
set(newVal) {
//数据并没有改变
if (newVal === val) {
return;
}
observe(newVal);
val = newVal;
publish.notify();
}
});
更新什么?更新视图啊,就是做之前写的模板编译的replace函数里,替换文本节点里的文字
// 创建一个订阅/观察者,调用更新视图操作
new Subscribe(vm, key, function(newVal) {
if (typeof newVal === "object") newVal = JSON.stringify(newVal);
// 如果不是对象就直接显示基本数据类型,是对象,需要把对象做一个Json类型的显示.如果不这样又会是
//[Object object]
node.textContent = text.replace(reg, newVal);
});
//当然别忘了,当前replace函数里还是要做替换文本节点
node.textContent = text.replace(reg, val);
那又是在哪里将观察者(执行更新数据的操作)绑定到被观察者(数据)上?
当然是一开始获取数据时,这个数据被获取,才证明他是被当前视图需要的,此时就可以添加到被观察者的队列中,做观察操作
// 一旦调用get说明值有被使用,就需要添加进被观察者的队列中,需要被观察
get() {
Publish.target && publish.attachSub(Publish.target);
return val;
},
每个数据都应该有一个观察者实例对不对?就像A检测B,C检测D ,一对一的关系.A如果既观察B又观察D,就会有更新操作的混乱.
所以每个数据都有一个观察者实例.更新操作需要当前vue实例vm,这样可以拿到data,也需要新的值newVal,还需要知道属性名,还有更新的回调函数.
抽象出来.
class Subscribe {
constructor(vm, key, fn) {
this.vm = vm;
this.key = key;
this.fn = fn;
Publish.target = this;
let val = this.vm; //从vm中去拿
let arr = this.key.split("."); // [name,firstName]
arr.forEach(function(k) {
val = val[k];
});
// Publish.target = null;
}
update() {
let val = this.vm; //从vm中去拿
let arr = this.key.split("."); // [name,firstName]
arr.forEach(function(k) {
val = val[k];
});
this.fn(val);
}
}
//这是针对对象是数据类型,如果data中的属性是对象,对象包裹对象,需要递归
// 发布 - 订阅模式
class Publish {
constructor() {
this.subList = [];
}
attachSub(subscribe) {
this.subList.push(subscribe);
}
detachSub(sub) {
var index = this.subList.findIndex(sub);
this.subList.splice(index, 1);
}
notify() {
this.subList.forEach(sub => {
sub.update();
});
}
说的很绕,望见谅
完整代码如下:
<!DOCTYPE html>
<html>
<head>
<title>数据改变,视图更新</title>
</head>
<body>
<div id="app">
<p>姓名是{{ name.firstName }}</p>
<div>年龄是{{ age }}</div>
{{ name }}
</div>
<script type="text/javascript">
function Vue(options = {}) {
this.$options = options; // 将所有属性挂载在vue实例$options上
var data = (this._data = this.$options.data);
observe(this._data);
// data挂载在vue上面
for (let key in data) {
Object.defineProperty(this, key, {
enumrable: true,
get() {
// 当访问this实例上的name时,this不是去它本身上找到这个属性,而是从this._data上去寻找
return this._data[key];
},
set(newVal) {
this._data[key] = newVal;
}
});
}
// 识别大括号,将文本节点替换
new Compile(this.$options.el, this);
}
function Compile(el, vm) {
vm.$el = document.querySelector(el);
// 这一段打印有问题
//documentFragment是文档碎片
let fragment = document.createDocumentFragment();
// 这一步是遍历app根节点下的所有节点,因为是DOM树的形式,将节点拆分,挂在新的空白文档上,也就是孩子节点的父节点指向改变了,让fragment作为他们的根节点
while ((child = vm.$el.firstChild)) {
fragment.appendChild(child);
}
// app下的孩子节点挂在fragment下,app下就没有节点,无法显示了了,所以需要显示回来
// var n = 1;
replace(fragment);
function replace(fragment) {
Array.from(fragment.childNodes).forEach(function(node) {
// console.log(n)
// n++;
// console.log(node.textContent)//标签与标签之间有text节点,注释也算一个标签{{name}}
let text = node.textContent;
text = text.trim();
let reg = /\{\{(.*)\}\}/;
// 是文本节点 又匹配双大括号语法
if (node.nodeType === 3 && reg.test(text)) {
//打印匹配到的第一个 name.firstName age name
let key = RegExp.$1;
key = key.trim();
// console.log(vm._data[key])
// node.textContent =text.replace(reg,vm._data[key]) //这样拿不到对象的对象,因为name.firstName 不可能通过字面量拿到
// 要去遍历key,先拿到对象name,再拿到firstName
let arr = key.split("."); // [name,firstName]
let val = vm; //从vm中去拿
arr.forEach(function(k) {
val = val[k];
});
console.log(val);
// 如果给的就是个对象,那么就序列化显示一下
if (typeof val === "object") val = JSON.stringify(val);
// 如果不是就直接显示基本数据类型
// 创建一个订阅/观察者,调用更新视图操作
new Subscribe(vm, key, function(newVal) {
if (typeof newVal === "object") newVal = JSON.stringify(newVal);
// 如果不是就直接显示基本数据类型
node.textContent = text.replace(reg, newVal);
});
node.textContent = text.replace(reg, val);
}
// 如果不是文本节点,是标签,则需要遍历标签下面的孩子节点是否有文本节点
if (node.childNodes) {
replace(node);
}
});
}
vm.$el.appendChild(fragment);
}
function Observe(data) {
// 创建观察者/发布中心 观察所有被观察者/订阅者
let publish = new Publish();
for (let key in data) {
let val = data[key];
// 如果data中包含属性是对象,则需要递归对象的中属性,进行数据劫持
// 如果data中的属性就是普通数据类型,递归退出 -- 递归出口
observe(val);
Object.defineProperty(data, key, {
enumrable: true,
// 一旦调用get说明值有被使用,就需要添加进被观察者的队列中,需要被观察
get() {
Publish.target && publish.attachSub(Publish.target);
return val;
},
// 会在数据改变的时候直接设置
set(newVal) {
//数据并没有改变
if (newVal === val) {
return;
}
observe(newVal);
val = newVal;
publish.notify();
}
});
}
}
/**
*观察对象变化,并且为data对象创建属性
*@{data} 被观察的对象或属性
*/
function observe(data) {
if (typeof data !== "object") return null;
return new Observe(data);
}
//这是针对对象是数据类型,如果data中的属性是对象,对象包裹对象,需要递归
// 发布 - 订阅模式
class Publish {
constructor() {
this.subList = [];
}
attachSub(subscribe) {
this.subList.push(subscribe);
}
detachSub(sub) {
var index = this.subList.findIndex(sub);
this.subList.splice(index, 1);
}
notify() {
this.subList.forEach(sub => {
sub.update();
});
}
}
class Subscribe {
constructor(vm, key, fn) {
this.vm = vm;
this.key = key;
this.fn = fn;
Publish.target = this;
let val = this.vm; //从vm中去拿
let arr = this.key.split("."); // [name,firstName]
arr.forEach(function(k) {
val = val[k];
});
// Publish.target = null;
}
update() {
let val = this.vm; //从vm中去拿
let arr = this.key.split("."); // [name,firstName]
arr.forEach(function(k) {
val = val[k];
});
this.fn(val);
}
}
</script>
<script type="text/javascript">
let vm = new Vue({
el: "#app",
data: {
name: {
firstName: "姓氏章",
lastName: "名字"
},
age: 12 //通过Obj.defineProperty实现()或者Obj.defineProperties()实现
}
});
</script>
</body>
</html>
可实现下图效果:
现在的功能可以做获取数据(从本地或服务端),更新视图。之后就是在标签内识别v-model指令,然后识别指令之后的变量,也就是data的key.就差不多了,唯一需要注意的是深度响应的问题,也就是对象的对象的对象或数组之类的问题,还有一些不是通过options配置对象的操作,导致无法…………响应。
我在干什么?我写这个博客的时候,忘了接约好的面试电话。阿西吧,完全忘了有这回事,打回去,打不通了,有些时候真的很讨厌自己。手动再见。