一、数据劫持
1. 使用setter getter 方式实现数据劫持
- getter 是一种获得属性值的方法,负责查询值,它不带任何参数
- setter是一种设置属性值的方法,setter负责设置键值,值是以参数的形式传递
let obj = {
$name: "kkb",
get name(){
// 获取 name 属性时触发
//console.log("想要获取name属性");
return this.$name;// 想要让 obj 在获取name 属性时,拿到的值
},
set name(newVal){
// 设置 name 属性时 触发
console.log(newVal); // 给 name 设置的新值
this.$name = newVal;
}
};
//console.log(obj.name);
obj.name = "开课吧集团";
// console.log(obj.name);
// console.log(obj);
//通俗的讲 get 如果想要在获取对象的属性的时候做一些别的操作,set 如果想要在设置对象的属性的时候做一些别的操作
2.利用 defineProperty 实现数据劫持
语法:
Object.defineProperty(obj, prop, descriptor)
参数:
- obj 要在其上定义属性的对象。
- prop 要定义或修改的属性的名称。
- descriptor 将被定义或修改的属性描述符。
数据描述符
- configurable 当且仅当该属性的 configurable 为 true时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 true。
- enumerable 当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 true。。
- value 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
- writable 当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 true。
存取描述符
- get 一个给属性提供 getter 的方法,如果没有 getter 则为undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。默认为 undefined。
- set 一个给属性提供 setter 的方法,如果没有 setter 则为undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为 undefined。
let obj = {
name: "kkb",
age: 8
};
Object.defineProperty(obj,"name",{
configurable: true, //配置是否允许被删除 true(默认值) 可以被删除,false 不能被删除
enumerable: true, //配置是否允许被枚举 true(默认值) 可以被枚举,false 不能被枚举
set(newVal){
console.log("这个想要设置的新值",newVal);
},
get(){
return "你猜名字是啥";
}
});
// for(let s in obj){
// console.log(s);
// }
obj.name = "开课吧";
console.log(obj.name);
//console.log(obj);
2、MVVM框架中编译数据到视图
MVC 架构
MVC:前端的MVC与后端类似,具备着View,Controller和Model.
Model :负责保存应用数据,与后端数据同步
Controller:负责业务逻辑,根据用户行为对Model数据进行修改
View:负责视图展示,将model中的数据可视化出来。
MVVM架构
ViewModel通过实现一套数据响应式机制自动响应Model中的数据变化;
同时ViewModel会实现一套更新策略自动将数据变化转换为视图更新;
通过事件监听响应View中用户交互修改Model中数据。核心是双向时间绑定。
这样在ViewModel中就减少了大量的DOM操作代码。MVVM在保持View和Model低耦合的同时,还减少了维护它们关系的代码,是用户专注于业务逻辑,兼顾开发效率。
- MVC,MVP和MVVM都是框架模式,它们设计的目标都是为了解决Model和View的耦合问题。
- MVC模式出现较早主要应用在后端,如Spring MVC、ASP.NET MVC等,在前端领域的早期也有应用,如Backbone.js。它的优点是分层清晰,缺点是数据就混乱,灵活性带来的维护性问题。
- MVP 模式是MVC的进化形式,Presenter作为中间层负责MV通信,解决了两者耦合的问题,但P层过于臃肿会导致维护问题。
- MVVM 模式在前端领域有广泛的应用,它不仅解决MV耦合问题,还同时解决了维护两者映射关系的大量繁杂代码和DOM操作代码,在提高开发效率、可读性同时还保持了优越的性能表现。
数据响应式
数据响应式是指当数据改变后,会通知到使用该数据的代码。例如,视图渲染中使用了数据,数据改变后,视图也会自动更新。
数据响应式实现原理:
- 利用数据劫持 (defineProperty, Proxy) 监听对应数据的修改设置
- 利用观察者模式(或事件机制),给每一项数据添加相应的事件监听,当数据修改时触发该事件,然后驱动视图进行修改
双向绑定: - 利用数据响应式,完成数据对视图修改
- 添加 change 或 input 等事件,监听视图的修改,当视图改变时修改数据
模拟vue
<div id="app">
{
{
message }}
<div>
<p>姓名:{
{
name }},年龄: {
{
age}}</p>
<p v-html="htmlData"></p>
<input type="text" v-model="modelData" />
<p>{
{
modelData}}</p>
</div>
就是一段纯文本
</div>
<script type="text/javascript">
class KVue {
constructor(option){
this.$option = option;
let el = document.querySelector(option.el);
this.compileNode(el);
this.observe(this.$option.data);
}
// 给数据添加数据劫持
observe(data){
let keys = Object.keys(data);
keys.forEach(key=>{
this.dataProxy(data,key,data[key]);
});
}
// 完成数据劫持,在数据修改时去触发视图的变化
dataProxy(data,key,value){
let _this = this;
Object.defineProperty(data,key,{
configurable: true,
enumerable: true,
get(){
return value;
},
set(newVal){
value = newVal;
let event = new Event(key);
event.value = newVal;
_this.dispatchEvent(event);
}
});
}
// 根据当前元素的结构,将我们的数据编译进去
compileNode(el){
let child = el.childNodes; // 找到元素下的所有节点
child.forEach(node => {
if(node.nodeType == 1){
// 如果该节点是元素节点
let attrs = node.attributes;
[...attrs].forEach(attr=>{
//attrs 不是数组没有forEach方法,它是一个可迭代对象,通过结构成一个数组
let attrName = attr.name;
if(attrName.indexOf("v-") == 0){
let attrVal = attr.value;
//console.log(attrName,attrVal);
if(attrName === "v-html" ){
// 这是一个v-html指令,我们应该用数据替换该元素的内容
node.innerHTML = this.$option.data[attrVal];
} else if(attrName == "v-model"){
// 这是一个双向绑定指令
node.value = this.$option.data[attrVal];
// 监听视图发生了变化,同步修改我们的数据
node.addEventListener("input",({
target})=>{
this.$option.data[attrVal] = target.value;
});
}
}
});
if(node.childNodes.length > 0){
// 如果该元素还有子元素继续想要查找
this.compileNode(node);
}
} else if(node.nodeType == 3){
// 如果该节点是文本节点
// console.log(node);
//console.dir(node);
let startContent = node.textContent;
let reg = /\{\{\s*(\S+)\s*\}\}/g;
//console.log(reg.test(startContent),startContent);
if(reg.test(startContent)){
node.textContent = startContent.replace(reg,(...arg)=>{
//console.log(arg[1]);
return this.$option.data[arg[1]];
});
}
}
});
}
}
let kvue = new KVue({
el: "#app",
data: {
message: "Hello KKB",
modelData: "呵呵",
name: "kkb",
age: 8,
htmlData: "<strong>圣诞节要陪我一起过吗</strong>"
}
});
</script>
3、实现数据驱动视图更新
class Event {
events = {
} // 事件池记录所有的相关事件及处理函数
on(eventName,fn){
if(!this.events[eventName]){
this.events[eventName] = [];
}
this.events[eventName].push(fn);
}
off(eventName,fn){
// 删除一个事件处理 eventName 事件名称 fn 对应的处理函数
if(!this.events[eventName]){
return ;
}
this.events[eventName] = this.events[eventName].filter(item=>item!=fn);
}
/*
dispatch 负责把触发到的事件给执行了
*/
dispatch(eventName){
if(!this.events[eventName]){
return ;
}
this.events[eventName].forEach(item => {
item.call(this);
});
}
}
class KVue extends Event {
constructor(option){
super();
this.$option = option;
let el = document.querySelector(option.el);
this.compileNode(el);
this.observe(option.data);
}
// 给数据添加数据劫持
observe(data){
let keys = Object.keys(data);
keys.forEach(key=>{
this.dataProxy(data,key,data[key]);
});
}
// 完成数据劫持,在数据修改时去触发视图的变化
dataProxy(data,key,value){
let _this = this;
Object.defineProperty(data,key,{
configurable: true,
enumerable: true,
set(newVal){
value = newVal;
_this.dispatch(key);
//console.log("数据已经修改了该触发视图的修改了",key);
},
get(){
return value;
}
});
}
// 根据当前元素的结构,将我们的数据编译进去
compileNode(el){
let child = el.childNodes; // 找到元素下的所有节点
child.forEach(node => {
if(node.nodeType == 1){
// 如果该节点是元素节点
let attrs = node.attributes;
[...attrs].forEach(attr=>{
let attrName = attr.name;
if(attrName.indexOf("v-") == 0){
let attrVal = attr.value;
//console.log(attrName,attrVal);
if(attrName === "v-html" ){
// 这是一个v-html指令,我们应该用数据替换该元素的内容
node.innerHTML = this.$option.data[attrVal];
this.on(attrVal,()=>{
// console.log(attrVal,"进行了修改");
node.innerHTML = this.$option.data[attrVal];
})
} else if(attrName == "v-model"){
// 这是一个双向绑定指令
node.value = this.$option.data[attrVal];
this.on(attrVal,()=>{
// console.log(attrVal,"进行了修改");
node.value = this.$option.data[attrVal];
})
// 监听视图发生了变化,同步修改我们的数据
node.addEventListener("input",({
target})=>{
this.$option.data[attrVal] = target.value;
});
}
}
});
if(node.childNodes.length > 0){
// 如果该元素还有子元素继续想要查找
this.compileNode(node);
}
} else if(node.nodeType == 3){
// 如果该节点是文本节点
// console.log(node);
//console.dir(node);
let startContent = node.textContent;
let reg = /\{\{\s*(\S+)\s*\}\}/g;
//console.log(reg.test(startContent),startContent);
if(reg.test(startContent)){
node.textContent = startContent.replace(reg,(...arg)=>{
//console.log(arg[1]);
this.on(arg[1],()=>{
node.textContent = startContent.replace(reg,(...arg)=>{
return this.$option.data[arg[1]];
});
})
return this.$option.data[arg[1]];
});
}
}
});
}
}
let kvue = new KVue({
el: "#app",
data: {
message: "Hello KKB",
modelData: "呵呵",
name: "kkb",
age: 8,
htmlData: "<strong>圣诞节要陪我一起过吗</strong>"
}
});
setTimeout(()=>{
kvue.$option.data.htmlData = "Hello 开课吧";
},1000);
setInterval(()=>{
kvue.$option.data.age++;
},1000);
使用js自带的事件机制 EventTarget来实现数据相应
class KVue extends EventTarget {
constructor(option){
super();
this.$option = option;
let el = document.querySelector(option.el);
this.compileNode(el);
this.observe(option.data);
}
// 给数据添加数据劫持
observe(data){
let keys = Object.keys(data);
keys.forEach(key=>{
this.dataProxy(data,key,data[key]);
});
}
// 完成数据劫持,在数据修改时去触发视图的变化
dataProxy(data,key,value){
let _this = this;
Object.defineProperty(data,key,{
configurable: true,
enumerable: true,
set(newVal){
value = newVal;
let event = new Event(key);
event.name = key;
// new Event(事件名称) // 新建一个事件对象
_this.dispatchEvent(event);
//_this.dispatch(key);
//console.log("数据已经修改了该触发视图的修改了",key);
},
get(){
return value;
}
});
}
// 根据当前元素的结构,将我们的数据编译进去
compileNode(el){
let child = el.childNodes; // 找到元素下的所有节点
child.forEach(node => {
if(node.nodeType == 1){
// 如果该节点是元素节点
let attrs = node.attributes;
[...attrs].forEach(attr=>{
let attrName = attr.name;
if(attrName.indexOf("v-") == 0){
let attrVal = attr.value;
//console.log(attrName,attrVal);
if(attrName === "v-html" ){
// 这是一个v-html指令,我们应该用数据替换该元素的内容
node.innerHTML = this.$option.data[attrVal];
node.addEventListener(attrVal,()=>{
node.innerHTML = this.$option.data[attrVal];
});
} else if(attrName == "v-model"){
// 这是一个双向绑定指令
node.value = this.$option.data[attrVal];
this.addEventListener(attrVal,(e)=>{
console.log(e);
node.value = this.$option.data[attrVal];
});
// 监听视图发生了变化,同步修改我们的数据
node.addEventListener("input",({
target})=>{
this.$option.data[attrVal] = target.value;
});
}
}
});
if(node.childNodes.length > 0){
// 如果该元素还有子元素继续想要查找
this.compileNode(node);
}
} else if(node.nodeType == 3){
// 如果该节点是文本节点
// console.log(node);
//console.dir(node);
let startContent = node.textContent;
let reg = /\{\{\s*(\S+)\s*\}\}/g;
//console.log(reg.test(startContent),startContent);
if(reg.test(startContent)){
node.textContent = startContent.replace(reg,(...arg)=>{
this.addEventListener(arg[1],()=>{
node.textContent = startContent.replace(reg,(...arg)=>{
return this.$option.data[arg[1]];
});
});
return this.$option.data[arg[1]];
});
}
}
});
}
}
let kvue = new KVue({
el: "#app",
data: {
message: "Hello KKB",
modelData: "呵呵",
name: "kkb",
age: 8,
htmlData: "<strong>圣诞节要陪我一起过吗</strong>"
}
});
setTimeout(()=>{
kvue.$option.data.htmlData = "Hello 开课吧";
},1000);
setInterval(()=>{
kvue.$option.data.age++;
},1000);
</script>