本文主要介绍vue.js如何实现v-model和{{}}指令。边肖认为这很好。现在分享给大家,给大家一个参考。来和边肖一起看看吧。
上次我们分析了vue.js通过Object.defineProperty和发布订阅模式劫持和监控数据,并实现了一个简单的demo。今天,基于上一节中的代码,我们将实现一个MVVM类,将其与html结合,并实现v-model和{{}}语法。
温馨提示:本节新增代码(不含注释)约一百行。使用的观察器和观察器是上一节代码的延续,没有经过修改。
接下来,让我们一步一步地实现一个MVVM类。
构造函数
首先,一个MVVM的构造函数如下(与vue.js的构造函数相同):
MVVM级
构造函数({ data,el }) {
this.data=data
this.el=el
this . init();
this . initdom();
}
}
和vue.js一样,有它的数据属性和el元素。
初始化操作
Vue.js可以通过this.xxx方法直接访问this.data.xxx的属性。这是怎么做到的?其实答案很简单。它通过Object.defineProperty耍花招当你访问this.xxx时,它实际上返回this.data.xxx当你修改this.xxx的值时,你实际上修改了this.data.xxx的值详见以下代码:
MVVM级
构造函数({ data,el }) {
this.data=data
this.el=el
this . init();
this . initdom();
}
//初始化
init() {
//劫持this.data
新观察者(this . data);
//传入的el可以是选择器,也可以是元素,所以我们要在这里做一层处理,确保这个的值。$el是一个元素节点。
这个。$el=this.isElementNode(this.el)?this . El:document . query selector(this . El);
//将this.data的所有属性绑定到this,这样用户就可以通过this.xxx直接访问this.data.xxx的值
对于(输入此数据){
this . define reactive(key);
}
}
定义活动(键){
Object.defineProperty(this,key,{
get() {
返回this . data[key];
},
set(newVal) {
this . data[key]=new val;
}//前端全栈学习交流圈:866109386
})//针对1-3年的前端开发者
}//帮助突破技术瓶颈,提升思维能力。
//是属性节点吗?
isElementNode(node) {
return node . nodetype===1;
}
}
初始化之后,我们需要编译这个的节点。$el。目前我们要实现的语法有v-model和{{}}语法。属性v-model只能出现在元素节点的属性中,而语法{{}}出现在文本节点中。
fragment
在编译节点之前,我们先考虑一个实际问题:如果在编译过程中直接操作DOM节点,那么DOM的每一次修改都会导致DOM的回流或重绘,这部分性能损失是不必要的。因此,我们可以使用fragment将节点转换为片段,然后在片段中编译它们,并将其放回页面。
MVVM级
构造函数({ data,el }) {
this.data=data
this.el=el//前端全栈交流学习圈:866109386
this . init();//适合1-3年的前端开发者
this . initdom();//帮助突破技术瓶颈,提升思维能力。
}
initDom() {
const fragment=this . node 2 fragment();
this.compile(片段);
//将片段返回到页面
document.body.appendChild(片段);
}
//把节点变成片段,通过片段操作DOM,可以达到更高的效率。
//因为如果直接操作DOM节点,DOM的每一次修改都会导致DOM的回流或重绘,但是如果放在片段中,修改片段并不会导致DOM的回流或重绘。
//当片段被一次性修改后,直接放回DOM节点。
node2Fragment() {
const fragment=document . createdocumentfragment();
让第一胎;
while(firstChild=this。$el.firstChild) {
fragment . appendchild(first child);
}
返回片段;
}
}
实现v-model
节点node转换成fragment后,我们来编译v-model语法。
由于v-model语句只能出现在一个元素节点的属性中,我们会先判断该节点是否是元素节点,如果是,再判断是否是指示性的(目前只有v-model)。如果满意,我们将调用CompileUtils.compileModelAttr来编译节点。
使用v-model编译节点有两个主要步骤:
为元素节点注册输入事件,当输入事件被触发时,更新vm上对应的属性值(this.data)。
为v-model依赖的属性注册一个Watcher函数,当依赖的属性发生变化时,更新元素节点的值。
MVVM级
构造函数({ data,el }) {
this.data=data
this.el=el
this . init();
this . initdom();
}
initDom() {
const fragment=this . node 2 fragment();
this.compile(片段);
//将片段返回到页面
document.body.appendChild(片段);
}
编译(节点){
if (this.isElementNode(node)) {
//如果是元素节点,遍历其属性,编译其指令。
const attrs=node.attributes
array . prototype . foreach . call(attrs,(attr)={
if(this . is direct(attr)){
compileutils . compilemodelAttr(this . data,node,attr)
}
})
}
//如果节点有子节点,编译子节点。
if(node . child nodes node . child nodes . length 0){
array . prototype . foreach . call(node . child nodes,(child)={
this.compile(子);
})
}
}
//是属性节点吗?
isElementNode(node) {
return node . nodetype===1;
}
//检查属性是否为指令(vue的指令以v-)开头
is direct(attr){
return attr . nodename . index of( v-)=0;
}
}
const CompileUtils={
//编译v-model属性,为元素节点注册输入事件,触发输入事件时更新vm的对应值。
//同时注册一个Watcher函数,在依赖值发生变化时更新节点的值。
compileModelAttr(虚拟机,节点,属性){
const { value: keys,nodeName }=attr
node . value=this . getmodelvalue(VM,keys);
//从元素节点中移除v-model属性值
node.removeAttribute(节点名);
node.addEventListener(input ,(e)={
this.setModelValue(vm,keys,e . target . value);
});
新观察器(虚拟机,密钥,(旧值,新值)={
node.value=newVal
});
},
/*解析键,例如,用户可以传入
*输入v-model=obj.name /
*此时,当我们获取值时,需要将 obj.name 解析成data[obj][name]的形式,以获取目标值。
*/
解析(虚拟机,密钥){
keys=keys.split( . ));
设value=vm
keys.forEach(_key={
value=value[_ key];
});
返回值;
},
//根据vm和key,返回v-model对应属性的值
getModelValue(虚拟机,密钥){
返回this.parse(vm,keys);
},
//修改v-model对应属性的值
setModelValue(虚拟机,键,值){
keys=keys.split( . ));
设value=vm
for(设I=0;I keys . length-1;i ) {
value=value[keys[I]];
}
value[keys[keys . length-1]]=val;
},
}
实现{{}}语法
{{}}语法只能出现在文本节点中,所以我们只需要处理文本节点。如果语句{{key}}出现在文本节点中,我们将编译该节点。在这里,我们可以通过下面的正则表达式来处理文本节点,以确定它是否包含{{}}语法。
const text reg=/ { { s * w s * } }/gi;//检查{{name}}语法
console . log(textreg . test( SSS ));
console . log(textreg . test( AAA { { name } } ));
console . log(text reg . test( AAA { { name } } { { text } } );
如果包含{{}}语法,我们可以处理。由于文本节点可能有多种{{}}语法,因此使用{{}}语法编译文本节点有两个主要步骤:
找出文本节点中所有的依赖属性,保留原始文本信息,根据原始文本信息和属性值生成最终的文本信息。例如,如果原始文本信息是 test {{test}} {{name}} ,那么这个文本信息所依赖的属性就是this.data.test和this.data.name,那么我们就可以根据原始信息和属性值生成最终的文本。
为文本节点的所有依赖属性注册Watcher函数,当依赖属性发生变化时,更新文本节点的内容。
MVVM级
构造函数({ data,el }) {
this.data=data
this.el=el
this . init();
this . initdom();
}
initDom() {
const fragment=this . node 2 fragment();
this.compile(片段);
//将片段返回到页面
document.body.appendChild(片段);
}
编译(节点){
const text reg=/ { { s * w s * } }/gi;//检查{{name}}语法
if (this.isTextNode(node)) {
//如果是文本节点,判断是否有{{}}文法,如果有,编译{{}}文法。
设text content=node . text content;
if (textReg.test(textContent)) {
//对于 test{{test}} {{name}} 的文本,一个文本节点可能有多个匹配,所以必须统一处理。
//使用textReg匹配文本节点,可以得到两个匹配值[{{test}} , {{name}}]。
const matchs=text content . match(text reg);
compileutils . compile text Node(this . data,node,matches);
}
}
//如果节点有子节点,编译子节点。
if(node . child nodes node . child nodes . length 0){
array . prototype . foreach . call(node . child nodes,(child)={
this.compile(子);
})
}
}
//它是否是文本节点
isTextNode(node) {
return node . nodetype===3;
}
}
const CompileUtils={
Reg:/ { s * ( w) s * } }/,//匹配{{ key }}中的键
//编译文本节点,注册Watcher函数,当它所依赖的属性发生变化时,更新文本节点。
compileTextNode(虚拟机、节点、匹配项){
//原始文本信息
const raw text content=node . text content;
matches . foreach((match)={
const keys=match . match(this . reg)[1];
console . log(rawTextContent);
new Watcher(vm,keys,()=this.updateTextNode(vm,Node,matchs,rawTextContent));
});
this.updateTextNode(vm,Node,matchs,rawTextContent);
},
//更新文本节点信息
updateTextNode(虚拟机,节点,匹配,原始文本内容){
设new text content=raw text content;
matches . foreach((match)={
const keys=match . match(this . reg)[1];
const val=this.getModelValue(vm,keys);
new text content=new text content . replace(match,val);
})
node . text content=new text content;
}
}
结语
这样,一个具有v-model和{{}}函数的MVVM类就完成了。
这里也举个简单的例子(忽略风格)。
接下来,我们可以继续实现computed属性、v-bind方法,并支持将表达式放入{{}}。如果你觉得这篇文章对你有帮助,请给个赞,嘻嘻。
最后,粘贴所有代码:
课堂观察者{
构造函数(数据){
//如果不是对象,则返回
如果(!数据||数据类型!==object) {
返回;
}
this.data=data
this . walk();
}
//劫持传入的数据。
walk() {
对于(输入此数据){
this.defineReactive(this.data,key,this . data[key]);
}
}
//创建当前属性的发布实例,使用Object.defineProperty劫持当前属性的数据。
defineReactive(obj,key,val) {
//创建当前属性的发布者
const Dep=new Dep();
/*
*递归劫持子属性的值,例如下面的数据
* let data={
*名称: cjg ,
* obj: {
*名称: zht ,
*年龄:22岁,
* obj: {
*名称: cjg ,
*年龄:22岁,
* }
* },
* };
*我们先劫持数据最外层的name和obj,然后劫持obj对象的子属性obj.name、obj.age、obj.obj,逐层递归下去,直到所有数据都被劫持。
*/
新观察家(val);
Object.defineProperty(obj,key,{
get() {
//若当前有对该属性的依赖项,则将其加入到发布者的订阅者队列里
if (Dep.target) {
离开添加sub(dep。目标);
}
返回英国压力单位
},
set(newVal) {
if (val===newVal) {
返回;
}
val=newVal
新观察家(新val);
离开notify();
}
})
}
}
//发布者,将依赖该属性的看守人都加入潜水艇数组,当该属性改变的时候,则调用所有依赖该属性的看守人的更新函数,触发更新。
类Dep
构造函数(){
这个。subs=[];
}
addSub(sub) {
if (this.subs.indexOf(sub) 0) {
这个。潜艇。推(分);
}
}
通知(){
this.subs.forEach((sub)={
sub update();
})
}
}
Dep.target=null
//观察者
类监视器{
/**
*创建观察器的实例。
* @param {*}虚拟机
* @param {*}个键
* @param {*} updateCb
* @观察者的成员
*/
构造函数(虚拟机、密钥、更新Cb) {
this.vm=vm
this.keys=keys
这个。更新CB=更新CB;
这个值=空
这个。get();
}
//根据伏特计和键获取到最新的观察值
get() {
//将Dep的依赖项设置为当前的观察者,并且根据传入的键遍历获取到最新值。
//在这个过程中,由于会调用观察者对象属性的吸气剂方法,因此在遍历过程中这些对象属性的发布者就将看守人添加到订阅者队列里。
//因此,当这一过程中的某一对象属性发生变化的时候,则会触发看守人的更新方法
Dep.target=这
这个。值=compileutils。解析(这个。VM,这个。钥匙);
Dep.target=null
返回这个值
}
update() {
常量旧值=this.value
const新值=this。get();
if (oldValue!==新值){
this.updateCb(旧值,新值);
}
}
}
MVVM级
构造函数({ data,el }) {
this.data=数据
this.el=el
这个。init();
这个。initdom();
}
//初始化
init() {
//对这。数据进行数据劫持
新观察者(这个。数据);
//传入的埃尔可以是选择器,也可以是元素,因此我们要在这里做一层处理,保证这个10.95美元埃尔的值是一个元素节点
这个. this $ El=this . iselementnode(this . El)?这个。艾尔:文件。查询选择器(this。El);
//将这。数据的属性都绑定到这上,这样用户就可以直接通过this.xxx来访问this.data.xxx的值
对于(输入此数据){
这个。定义无功(关键);
}
}
initDom() {
常量片段=this。节点2 fragment();
this.compile(片段);
document.body.appendChild(片段);
}
//将节点转为片段,通过碎片来操作多姆,可以获得更高的效率
//因为如果直接操作数字正射影像图节点的话,每次修改数字正射影像图都会导致数字正射影像图的回流或重绘,而将其放在碎片里,修改碎片不会导致数字正射影像图回流和重绘
//当在碎片一次性修改完后,在直接放回到数字正射影像图节点中
node2Fragment() {
const fragment=文档。createdocumentfragment();
让第一胎;
while(第一个孩子=this .$el.firstChild) {
碎片。appendchild(第一个孩子);
}
返回片段;
}
定义活动(键){
Object.defineProperty(this,key,{
get() {
返回这个。数据[键];
},
set(newVal) {
这个。data[key]=新值;
}
})
}
编译(节点){
const text reg=/ { { s * w s * } }/gi;//检测{{name}}语法
if (this.isElementNode(node)) {
//若是元素节点,则遍历它的属性,编译其中的指令
const attrs=节点.属性
数组。原型。foreach。呼叫(属性,(属性)={
如果(这个。是直接的(attr)){
复合材料。compilemodelAttr(这个。数据,节点,属性)
}
})
} else if(这个。istextnode(node)){
//若是文本节点,则判断是否有{{}}语法,如果有的话,则编译{{}}语法
设文本内容=节点。文本内容;
if (textReg.test(textContent)) {
//对于测试{{test}} {{name}} 这种文本,可能在一个文本节点会出现多个匹配符,因此得对他们统一进行处理
//使用文本注册来对文本节点进行匹配,可以得到[{{test}} , {{name}}]两个匹配值
const matchs=文本内容。匹配(文本注册);
复合材料。编译文本节点(this。数据、节点、匹配);
}
}
//若节点有子节点的话,则对子节点进行编译。
如果(节点。子节点节点。子节点。长度0){
数组。原型。foreach。调用(节点。子节点,(子节点)={
this.compile(子);
})
}
}
//是否是属性节点
isElementNode(node) {
返回节点。nodetype===1;
}
//是否是文本节点
isTextNode(node) {
返回节点。nodetype===3;
}
isAttrs(节点){
返回节点。nodetype===2;
}
//检测属性是否是指令(vue的指令是五-开头)
是直接的(attr){
返回属性。节点名。( v-)=0的索引;
}
}
const CompileUtils={
reg: /{{s*(w )s*}}/,//匹配{{ key }}中的键
//编译文本节点,并注册看守人函数,当文本节点依赖的属性发生变化的时候,更新文本节点
compileTextNode(虚拟机、节点、匹配项){
//原始文本信息
const原始文本内容=节点。文本内容;
火柴。foreach((匹配)={
const keys=匹配。匹配(这个。reg)[1];
控制台。日志(rawTextContent);
new Watcher(vm,keys,()=this.updateTextNode(vm,Node,matchs,rawTextContent));
});
this.updateTextNode(vm,Node,matchs,rawTextContent);
},
//更新文本节点信息
updateTextNode(虚拟机,节点,匹配,原始文本内容){
设新文本内容=原始文本内容;
火柴。foreach((匹配)={
const keys=匹配。匹配(这个。reg)[1];
const val=this.getModelValue(vm,keys);
新文本内容=新文本内容。replace(匹配,val);
})
节点。文本内容=新的文本内容;
},
//编译v型车属性,为元素节点注册投入事件,在投入事件触发的时候,更新伏特计对应的值。
//同时也注册一个看守人函数,当所依赖的值发生变化的时候,更新节点的值
compileModelAttr(虚拟机,节点,属性){
const { value: keys,nodeName }=属性
节点。价值=这个。getmodelvalue(VM,keys);
//将v型车属性值从元素节点上去掉
node . remove属性(节点名);
新观察器(虚拟机,密钥,(旧值,新值)={
node.value=新值
});
node.addEventListener(input ,(e)={
this.setModelValue(vm,keys,e . target。值);
});
},
/* 解析钥匙,比如,用户可以传入
* let data={
*名称: cjg ,
* obj: {
*名称: zht ,
* },
* };
*新观察器(data, obj.name ,(oldValue,newValue)={
* console.log(旧值,新值);
* })
* 这个时候,我们需要将键解析为数据[对象][名称]的形式来获取目标值
*/
解析(虚拟机,密钥){
keys=keys.split( . )));
设值=虚拟机
keys.forEach(_key={
value=value[_ key];
});
返回值;
},
//根据伏特计和钥匙,返回v型车对应属性的值
获取模型值(虚拟机,密钥){
返回this.parse(vm,keys);
},
//修改v型车对应属性的值
setModelValue(虚拟机,键,值){
keys=keys.split( . )));
设值=虚拟机
对于(设I=0;我钥匙。长度-1;i ) {
value=value[keys[I]];
}
值[键]键。length-1]]=val;
},
}
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。