为什么实现数据响应式
为什么实现数据响应式
当前vue、react等框架流行。无论是vue、还是react框架大家最初的设计思路都是类似的。都是以数据驱动视图,数据优先。希望能够通过框架减少开发人员直接操作节点,让开发人员能够把的精力放在业务上而不是过多的放在操作节点上。另一方面,框架会通过虚拟dom及diff算法提高页面性能。这其中需要数据优先最根本的思路就是实现数据响应式。so,本次来看下如何基于原生实现数据响应式。vue中的数据响应vue中的数据响应vue中会根据数据将数据通过大胡子语法及指令渲染到视图上,这里我们以大胡子语法为例。如下:


{{message}}




{{message}}


let vm = new Vue({

el:"#app",

data:{

message:"测试数据"

}
})
setTimeout(()=>{

vm.message = "修改的数据";
},1000)

let vm = new Vue({

el:"#app",

data:{

message:"测试数据"

}
})
setTimeout(()=>{

vm.message = "修改的数据";
},1000)
如上代码,很简单 。vue做了两件事情。一、把message数据初次渲染到视图。二、当message数据改变的时候视图上渲染的message数据同时也会做出响应。以最简单的案例。带着问题来看,通过原生js如何实现??这里为了简化操作便于理解,这里就不去使用虚拟dom。直接操作dom结构。实现数据初次渲染实现数据初次渲染根据vue调用方式。定义Vue类来实现各种功能。将初次渲染过程定义成编译compile函数渲染视图。通过传入的配置以及操作dom来实现渲染。大概思路是通过正则查找html 里 #app 作用域内的表达式,然后查找数据做对应的替换即可。具体实现如下:
class Vue {

constructor(options) {

this.opts = options;

this.compile();

}

compile() {

let ele = document.querySelector(this.opts.el);

// 获取所有子节点

let childNodes = ele.childNodes;

childNodes.forEach(node => {

if (node.nodeType === 3) {

// 找到所有的文本节点

let nodeContent = node.textContent;

// 匹配“{{}}”

let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;

if (reg.test(nodeContent)) {

let $1 = RegExp.$1;

// 查找数据替换 “{{}}”

node.textContent = node.textContent.replace(reg, this.opts.data[$1]);

}

}

})

}
}

class Vue {

constructor(options) {

this.opts = options;

this.compile();

}

compile() {

let ele = document.querySelector(this.opts.el);

// 获取所有子节点

let childNodes = ele.childNodes;

childNodes.forEach(node => {

if (node.nodeType === 3) {

// 找到所有的文本节点

let nodeContent = node.textContent;

// 匹配“{{}}”

let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;

if (reg.test(nodeContent)) {

let $1 = RegExp.$1;

// 查找数据替换 “{{}}”

node.textContent = node.textContent.replace(reg, this.opts.data[$1]);

}

}

})

}
}
如上完成了初次渲染,将message数据渲染到了视图上。但是会返现并没对深层次的dom结构做处理也就是如下情况:message


1{{ message }}2



hello , {{ message }}







1{{ message }}2



hello , {{ message }}




渲染结果如上发现结果并没有达到预期。so,需要改下代码,让节点可以深层次查找就可以了。代码如下:

compile() {

let ele = document.querySelector(this.opts.el);

this.compileNodes(ele);

}

compileNodes(ele) {

// 获取所有子节点

let childNodes = ele.childNodes;

childNodes.forEach(node => {

if (node.nodeType === 3) {

// 找到所有的文本节点

let nodeContent = node.textContent;

// 匹配“{{}}”

let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;

if (reg.test(nodeContent)) {

let $1 = RegExp.$1;

// 查找数据替换 “{{}}”

node.textContent = node.textContent.replace(reg, this.opts.data[$1]);

}

} else if (node.nodeType === 1) {

if (node.childNodes.length > 0) {

this.compileNodes(node);

}

}

})

}


compile() {

let ele = document.querySelector(this.opts.el);

this.compileNodes(ele);

}

compileNodes(ele) {

// 获取所有子节点

let childNodes = ele.childNodes;

childNodes.forEach(node => {

if (node.nodeType === 3) {

// 找到所有的文本节点

let nodeContent = node.textContent;

// 匹配“{{}}”

let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;

if (reg.test(nodeContent)) {

let $1 = RegExp.$1;

// 查找数据替换 “{{}}”

node.textContent = node.textContent.replace(reg, this.opts.data[$1]);

}

} else if (node.nodeType === 1) {

if (node.childNodes.length > 0) {

this.compileNodes(node);

}

}

})

}
上述代码通过递归查找节点 实现深层次节点的渲染工作。如此,就实现了视图的初次渲染。数据劫持数据劫持回过头来看下上面说的第二个问题:当message数据改变的时候视图上渲染的message数据同时也会做出响应。如何实现数据响应式?简而言之就是数据变动影响视图变动?再将问题拆分下 1. 如何知道数据变动了? 2.如何根据数据变动来更改视图?

如何知道数据变动了? 这里就需要用到数据拦截了,或者叫数据观察。把会变动的data数据观察起来。当他变动的时候我们可以做后续的渲染事情。如何拦截数据呢 ?vue2里采取的是definePrototype。
如何知道数据变动了? 这里就需要用到数据拦截了,或者叫数据观察。把会变动的data数据观察起来。当他变动的时候我们可以做后续的渲染事情。如何拦截数据呢 ?vue2里采取的是definePrototype。
let obj = {

myname:"张三"
}
Object.defineProperty(obj,'myname',{

configurable:true,

enumerable:true,

get(){

console.log("get.")

return "张三";

},

set(newValue){

console.log("set")

console.log(newValue);

}
})
console.log(obj);


let obj = {

myname:"张三"
}
Object.defineProperty(obj,'myname',{

configurable:true,

enumerable:true,

get(){

console.log("get.")

return "张三";

},

set(newValue){

console.log("set")

console.log(newValue);

}
})
console.log(obj);

上述代码会发现,通过defineProperty劫持的对象属性下都会有get及set方法。那么当我们获取或者设置数据的时候就能出发对应的get及set 。这样就能拦截数据做后续操作。还有没有其他方式达到数据劫持的效果呢?ES6中出现了Proxy 代理对象同样也可以达到类似劫持数据的功能。如下代码:
let obj = {

myname:"张三"
}
let newObj = new Proxy(obj,{

get(target,key){

console.log("get...")

return "张三"

},

set(target,name,newValue){

console.log("set...");

return Reflect.set(target,name,newValue);

}
})

let obj = {

myname:"张三"
}
let newObj = new Proxy(obj,{

get(target,key){

console.log("get...")

return "张三"

},

set(target,name,newValue){

console.log("set...");

return Reflect.set(target,name,newValue);

}
})
两种方式都可以实现数据劫持。proxy功能更加强大,很多方法是defineProperty所不具备的。且proxy直接拦截的是对象而defineProperty拦截的是对象属性。so,可以利用上述方式将data数据做劫持,代码如下:
observe(data){

let keys = Object.keys(data);

keys.forEach(key=>{

let value = data[key];

Object.defineProperty(data,key,{

configurable:true,

enumerable:true,

get(){

return value;

},

set(newValue){

value = newValue;

}

});

})
}

observe(data){

let keys = Object.keys(data);

keys.forEach(key=>{

let value = data[key];

Object.defineProperty(data,key,{

configurable:true,

enumerable:true,

get(){

return value;

},

set(newValue){

value = newValue;

}

});

})
}
观察者模式实现数据响应
观察者模式实现数据响应
有了劫持数据方式后,接下来需要实现的就是当修改数据的时候将新数据渲染到视图。如何办到呢?会发现,需要在data设置的时候触发视图的compile编译。二者之间互相影响,此时可以想到利用观察者模式,通过观察者模式让二者产生关联,如下:图略小,代码也贴上吧。
class Vue extends EventTarget {

constructor(options) {

super();

this.opts = options;

this.observe(this.opts.data);

this.compile();

}

observe(data){

let keys = Object.keys(data);

let _this = this;

keys.forEach(key=>{

let value = data[key];

Object.defineProperty(data,key,{

configurable:true,

enumerable:true,

get(){

return value;

},

set(newValue){

_this.dispatchEvent(new CustomEvent(key,{

detail:newValue

}));

value = newValue;

}

});

})

}

compile() {

let ele = document.querySelector(this.opts.el);

this.compileNodes(ele);

}

compileNodes(ele) {

// 获取所有子节点

let childNodes = ele.childNodes;

childNodes.forEach(node => {

if (node.nodeType === 3) {

// 找到所有的文本节点

let nodeContent = node.textContent;

// 匹配“{{}}”

let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;

if (reg.test(nodeContent)) {

let $1 = RegExp.$1;

// 查找数据替换 “{{}}”

node.textContent = node.textContent.replace(reg, this.opts.data[$1]);

this.addEventListener($1,e=>{

let oldValue = this.opts.data[$1];

let newValue = e.detail;

let reg = new RegExp(oldValue);

node.textContent = node.textContent.replace(reg,newValue);

})

}

} else if (node.nodeType === 1) {

if (node.childNodes.length > 0) {

this.compileNodes(node);

}

}

})

}
}

class Vue extends EventTarget {

constructor(options) {

super();

this.opts = options;

this.observe(this.opts.data);

this.compile();

}

observe(data){

let keys = Object.keys(data);

let _this = this;

keys.forEach(key=>{

let value = data[key];

Object.defineProperty(data,key,{

configurable:true,

enumerable:true,

get(){

return value;

},

set(newValue){

_this.dispatchEvent(new CustomEvent(key,{

detail:newValue

}));

value = newValue;

}

});

})

}

compile() {

let ele = document.querySelector(this.opts.el);

this.compileNodes(ele);

}

compileNodes(ele) {

// 获取所有子节点

let childNodes = ele.childNodes;

childNodes.forEach(node => {

if (node.nodeType === 3) {

// 找到所有的文本节点

let nodeContent = node.textContent;

// 匹配“{{}}”

let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;

if (reg.test(nodeContent)) {

let $1 = RegExp.$1;

// 查找数据替换 “{{}}”

node.textContent = node.textContent.replace(reg, this.opts.data[$1]);

this.addEventListener($1,e=>{

let oldValue = this.opts.data[$1];

let newValue = e.detail;

let reg = new RegExp(oldValue);

node.textContent = node.textContent.replace(reg,newValue);

})

}

} else if (node.nodeType === 1) {

if (node.childNodes.length > 0) {

this.compileNodes(node);

}

}

})

}
}
如上,成功的通过观察者模式实现了数据的响应。但是会发现data与compile之间需要通过键名来进行关联。如果data数据结构嵌套关系复杂后面会比较难处理。有没有一种方式让二者松解耦呢?这时候可以用发布订阅模式来进行改造。发布订阅模式改造响应式发布订阅模式改造响应式还是略小,也还是贴上代码:
class Vue {

constructor(options) {

this.opts = options;

this.observe(this.opts.data);

this.compile();

}

observe(data){

let keys = Object.keys(data);

let _this = this;

keys.forEach(key=>{

let value = data[key];

let dep = new Dep();

Object.defineProperty(data,key,{

configurable:true,

enumerable:true,

get(){

if(Dep.target){

dep.addSub(Dep.target);

}

return value;

},

set(newValue){

dep.notify(newValue);

value = newValue;

}

});

})

}

compile() {

let ele = document.querySelector(this.opts.el);

this.compileNodes(ele);

}

compileNodes(ele) {

// 获取所有子节点

let childNodes = ele.childNodes;

childNodes.forEach(node => {

if (node.nodeType === 3) {

// 找到所有的文本节点

let nodeContent = node.textContent;

// 匹配“{{}}”

let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;

if (reg.test(nodeContent)) {

let $1 = RegExp.$1;

// 查找数据替换 “{{}}”

node.textContent = node.textContent.replace(reg, this.opts.data[$1]);

new Watcher(this.opts.data,$1,(newValue)=>{

let oldValue = this.opts.data[$1];

let reg = new RegExp(oldValue);

node.textContent = node.textContent.replace(reg,newValue);

})

}

} else if (node.nodeType === 1) {

if (node.childNodes.length > 0) {

this.compileNodes(node);

}

}

})

}
}

class Dep{

constructor(){

this.subs = [];

}

addSub(sub){

this.subs.push(sub);

}

notify(newValue){

this.subs.forEach(sub=>{

sub.update(newValue);

})

}
}

class Watcher{

constructor(data,key,cb){

Dep.target = this;

data[key];

this.cb = cb;

Dep.target = null;

}

update(newValue){

this.cb(newValue);

}
}

class Vue {

constructor(options) {

this.opts = options;

this.observe(this.opts.data);

this.compile();

}

observe(data){

let keys = Object.keys(data);

let _this = this;

keys.forEach(key=>{

let value = data[key];

let dep = new Dep();

Object.defineProperty(data,key,{

configurable:true,

enumerable:true,

get(){

if(Dep.target){

dep.addSub(Dep.target);

}

return value;

},

set(newValue){

dep.notify(newValue);

value = newValue;

}

});

})

}

compile() {

let ele = document.querySelector(this.opts.el);

this.compileNodes(ele);

}

compileNodes(ele) {

// 获取所有子节点

let childNodes = ele.childNodes;

childNodes.forEach(node => {

if (node.nodeType === 3) {

// 找到所有的文本节点

let nodeContent = node.textContent;

// 匹配“{{}}”

let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;

if (reg.test(nodeContent)) {

let $1 = RegExp.$1;

// 查找数据替换 “{{}}”

node.textContent = node.textContent.replace(reg, this.opts.data[$1]);

new Watcher(this.opts.data,$1,(newValue)=>{

let oldValue = this.opts.data[$1];

let reg = new RegExp(oldValue);

node.textContent = node.textContent.replace(reg,newValue);

})

}

} else if (node.nodeType === 1) {

if (node.childNodes.length > 0) {

this.compileNodes(node);

}

}

})

}
}

class Dep{

constructor(){

this.subs = [];

}

addSub(sub){

this.subs.push(sub);

}

notify(newValue){

this.subs.forEach(sub=>{

sub.update(newValue);

})

}
}

class Watcher{

constructor(data,key,cb){

Dep.target = this;

data[key];

this.cb = cb;

Dep.target = null;

}

update(newValue){

this.cb(newValue);

}
}
如上代码思路是 针对每个数据会生成一个dep(依赖收集器)在数据get的时候收集watcher,将watcher 添加到dep里保存。数据一旦有改变触发notify发布消息从而影响compile编译更新视图。这个流程也可以参看下图:如上就完成了视图响应。通过上述代码,我们可以看出实现数据响应两个核心点1.数据劫持。2.观察者和发布订阅。在这我们可以思考一个问题,2个设计模式都是可以实现的但是有什么区别呢?观察者与发布订阅观察者与发布订阅这里需要从概念来看

观察者模式:定义一个对象与其他对象之间的一种依赖关系,当对象发生某种变化的时候,依赖它的其它对象都会得到更新,一对多的关系。

发布订阅模式:是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。
观察者模式:定义一个对象与其他对象之间的一种依赖关系,当对象发生某种变化的时候,依赖它的其它对象都会得到更新,一对多的关系。发布订阅模式:是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。两者之间关系,发布订阅是三者之间关系。发布订阅会多了一个关系器来组织主题和观察者之间的关系。这样做的好处就是松解耦。看上面响应式例子可以看出观察者需要通过事件名称来进行关联。发布订阅定义dep管理器之后data和compile彻底解耦,让二者松散解耦。在处理多层数据结构上发布订阅会更清晰。松解耦能够应对变化,把模块之间依赖降到最低。发布订阅广义上是观察者模式。好了 暂时先over 。 如果觉得有收获的话可以点个赞,赠人玫瑰,手有余香!!!!以上就是vue mvvm数据响应实现的详细内容,关于vue mvvm数据响应的资料请关注其它相关文章!