这一节主要是如何对{{xxx}}表达式和指令进行编译
一.代码结构
二.主要流程
下面我们就按照上面的代码流程来解析。
三.dom元素转文档碎片
位于MyCompile的prototype中
四.编译文档碎片
不断递归所有的dom元素,每次到一个dom元素,判断是否是文本节点,如果是,进行表达式编译。
如果不是,则取出这个节点的所有属性,如果是事件绑定指令,进行事件板顶。如果是普通指令,则进行数据绑定。
这里的数据绑定还只是当向数据绑定。
下面看看对于文本标签怎么去做表达式解析:
用正则匹配出花括号里面对应的表达式,然后调用工具类的text方法。
分析一下调用联调:compileText -》 CompileUtril.text =》 bind =》 Updater.textUpdater
下面分析一下事件绑定指令
取出所有属性,对每个属性做处理,去掉属性前缀,判断是普通指令还是事件绑定指令。再做不同的处理。
事件绑定
普通指令绑定
调用链:compileAttrNoON => CompileUtril.text => bind => Updater.textUpdater
下面我们把整个的思路捋一捋:
1.取出内存碎片
2.递归遍历所有的内存碎片
3.判断当前的dom节点
3-1.如果是文本节点,取出表达式,解析,调用更新器进行更新。(CompileUtril.text =》 bind =》 Updater.textUpdater)
3-2.如果是普通节点,遍历所有的属性。
3-2-1.如果是事件绑定的指令,则调用事件绑定的方法。
3-2-2.如果是普通的指令(目前只支持html、text、model、class)。
(CompileUtril.text =》 bind =》 Updater.textUpdater)
4.循环第2步
代码:
/**
* @param el 需要解析dom元素
* @param vm Vue实例
*/
function MyCompile(el,vm){
this.$vm = vm; // 把Vue实例挂载到Compile实例上
// 获取到页面的dom元素
let ParenthEle;
el && document.querySelector(el) &&(ParenthEle = document.querySelector(el));
/**
* 1.取出目标元素下面的所有子元素,放入文档碎片
* 2.对文档碎片中的所有dom节点进行编译(包括{{xxx}}表达式编译,v-on:click="函数名"编译(事件绑定),)
* 3.取出文档碎片,渲染会原来的位置上
*/
if(ParenthEle){
this.$fragment = this.Node2Fragment(ParenthEle);
this.CompileAllEleInFragment(this.$fragment);
ParenthEle.appendChild(this.$fragment);
}
}
MyCompile.prototype = {
/**
* 1.创建一个文档碎片
* 2.把目标dom元素的子元素全部加入到碎片中
* 3.返回对应的碎片
* @param ParenthEle
* @returns {DocumentFragment}
* @constructor
*/
Node2Fragment:function(ParenthEle){
let Fragment = document.createDocumentFragment();
let ChildNode;
while(ChildNode = ParenthEle.firstChild){
Fragment.appendChild(ChildNode);
}
return Fragment;
},
/**
* 递归编译文档碎片里面的所有标签,包括指令和{{xxx}}表达式
* 编译文档碎片里面的所有文档
* 1.循环文档碎片,取出某个文档标签
* 2.判断是文本标签还是普通标签
* 普通标签:继续递归下去
* 文本标签:直接解析
* @param Fragment
* @constructor
*/
CompileAllEleInFragment:function (Fragment) {
let me = this;
let AllNode = Fragment.childNodes;
let data = this.$vm;
let methods = this.$vm.$option.methods;
// 利用slice返回一个全新的数组,并且循环它
Array.prototype.slice.call(AllNode).forEach(function (CurrentNode) {
// 如果是文本节点,如果文本有内容
if (CompileUtril.IsTextNode(CurrentNode) && CurrentNode.textContent) {
me.CompileText(CurrentNode, data);
}else if(CompileUtril.IsElementNode(CurrentNode)){
// 不是文本标签,那么它必然就是一个普通标签,普通标签的话,需要解析出该标签对应的所有cjf-属性
me.CompileEleAttr(CurrentNode,methods);// 当前dom元素 methods?
}
CurrentNode.childNodes && CurrentNode.childNodes.length && me.CompileAllEleInFragment(CurrentNode);
}
);
},
/**
* 解析该节点含有{{xxx}}的文本内容
* <span>{{xxx.xxx}}</sapn>
* @param Node 真实节点
* @param vm mvvm实例
* [目前只支持诸如:{{name}}的单标签,不支持{{name+name1}}这种相加的形式]
*/
CompileText:function (Node, vm) {
// 匹配出{{}}两个嵌套花括号中的数据
let reg = /\{\{(.*)\}\}/;
let CurrentText = Node.textContent;
// 如果文本为{{xxx}},则匹配成功
if(reg.test(CurrentText)){//如果当前的文本标签能够匹配上
CompileUtril.text(vm,RegExp.$1.trim(),Node);// Vue实例 花括号中的表达式 dom元素
}
},
/** 解析该节点的所有属性
* <input cjf-model="name.b.sex.boy" type="text"/>
* 1.遍历所有属性
* 2.提取出cjf-开头的属性name
* 3.判断属性的分类
* 3-1.事件指令:cjf-on:xxx=""
* 3-2.普通指令:cjf-text,cjf-html,cjf-class
* @param Node 当前节点
* @param methods mvvm实例中的methods集合
* @constructor
* [!]methods没有对应的事件
*/
CompileEleAttr:function (Node,methods) {
let CurrentNodeAllAttr = Node.attributes;// 取出所有属性
let me = this;// 保存Mycompile实例
// 利用slice返回一个全新数组的特性,对该标签的所有属性进行循环
Array.prototype.slice.call(CurrentNodeAllAttr).forEach(function (key) {
// 假设现在整个属性为 =》 1.cjf-model="xxx" 2.cjf-on:click="函数名"
let AttrName = key.name;
let tempAttrName = AttrName;// 临时保存属性名
// 1.cjf-model="xxx" 2.cjf-on:click="函数名" => 第一种能拿到为表达式,第二种为函数名
let AttrVal = key.value;
// 1.cjf-model="xxx" 2.cjf-on:click="函数名" => 该指令是否是以"cjf-"开头,只处理以 "cjf-"开头的指令
if(CompileUtril.IsCJFAttr(AttrName)){
/**
* 1.cjf-model="xxx" 2.cjf-on:click => 从第四个字符开始截取
* 第一种情况为:
* model 、html 、class、text
* 第二种情况为:
* on:事件名
* @type {string}
*/
AttrName = AttrName.substring(4);
// 到这里 AttrName = "on:事件名"
if(CompileUtril.IsEventAttr(AttrName)){
// 指令绑定方法
me.EvenHandler(Node,AttrName,AttrVal,methods);// 当前dom节点 on:事件名 函数名 methods
}else{
//到这里 AttrName = "html/class/text/model"
me.CompileAttrNoOn(me.$vm,AttrName,AttrVal,Node);// Vue实例 "html/class/text/model" 表达式 对应的dom元素
}
/**
* 如果是事件绑定指令,则该事件已经被绑定到对应的dom节点上
* 如果是普通指令,则对应的值会被加到dom节点上
* so,到这里解析完一个指令,则需要把这个指令从对应的dom元素上进行移除
*/
Node.removeAttribute(tempAttrName);
}
})
},
/**<input cjf-on:click="函数名" />
* 根据 属性名里面带的事件类型 和 vm 和 方法名,给node绑定上对应的事件
* @param Node 当前节点
* @param AttrName 属性名: on:click
* @param AttrVal 属性值: 方法名
* @param methods vm里面的methods对象
* @constructor
*/
EvenHandler:function (Node,AttrName,AttrVal,methods) {
// on:click => 提取出click
let EventType = AttrName.split(":")[1];// click
let Fn = methods[AttrVal];// 提取出对应的方法引用
let vm = this.$vm; // 取出Vue实例
// 如果methods中有对应的方法 并且指令没有写错 则给对应的dom节点绑定事件
// 这里有个细节,Fn.bind(vm) ,这样设置之后,运行Fn的this才能是当前的Vue实例
Fn && EventType && Node.addEventListener(EventType, Fn.bind(vm), false);
},
/**
* 做一个委托功能,
* @param vm Vue实例
* @param dir text、class、html、model
* @param exp 表达式
* @param Node 当前的dom节点
* @constructor
*/
CompileAttrNoOn:function (vm, dir, exp, Node) {
/**
* 不可以这样调用,这样调用的话,this是compile实例,而compile实例没有bind方法
* Fn = CompileUtril[dir];
Fn && Fn(vm,exp,Node);
1.先使用工具类找到对应的方法,用策略模式避免硬代码
2.
*/
CompileUtril[dir] && CompileUtril[dir](vm,exp,Node);
},
};
/**
* 基础工具类,判断是否为元素节点,是否为文本节点,解析表达式等工具类
*/
CompileUtril = {
/**
* 判断是否cjf开头的指令
* @param dir
* @returns {boolean}
* @constructor
*/
IsCJFAttr:function(dir){
return dir.indexOf('cjf-') == 0;
},
/**
* 判断属性是否是on开头
* @constructor
*/
IsEventAttr:function(dir){
return dir.indexOf('on') == 0;
},
IsElementNode: function(node) {
return node.nodeType == 1;
},
IsTextNode: function(node) {
return node.nodeType == 3;
},
/**
* 根据表达式解析data域对应的值,如果表达式与data域对应不上,则返回undefined或者null
* @param data Vue实例的data域
* @param exp 表达式 =》 假设为 =》 name.sex.a
* @returns {*}
* @constructor
*/
GetValInObjectByReg:function (data,exp) {
// name.sex.a 先把表达式切割 =》 name sex a
let keys = exp.split(".");
// 取出data域
let oldV = data;
// 遍历 name sex a
keys.forEach(function (key) {
if(!oldV)return oldV;// 如果是undefined或者null,则返回undefined
// 不断取出oldV新引用,赋值给旧引用
oldV = oldV[key];
});
return oldV;// 能遍历完并且不会中途被return,则说明这个值解析正确
},
/**
* 根据表达式解析data,一直到最后一个属性
* @constructor
*/
GetValInObjectByRegLastAndSet:function(data,exp,NewVal){
let keys = exp.split(".");
let oldV = data;
keys.forEach(function (key,index) {
if(index < keys.length-1){
oldV = oldV[key];
}else{
oldV[key] = NewVal;
}
});
},
/**
* 1.先进行数据解析
* 2.调用更新器进行更新
* @param vm Vue实例
* @param dir 对应的指令:html/model/text/class
* @param exp 表达式
* @param Node 当前的dom节点
*/
bind:function (vm, dir, exp, Node) {
UpdaterFn = Updater[dir + 'Updater'];// 获取对应的更新器
// 根据Vue实例保存的参数,解析表达式结果
let Result = CompileUtril.GetValInObjectByReg(vm.$option.data,exp);// Vue的data域 表达式
// 如果解析出来的值正确,并且更新器也能正确拿到,则对相应的dom节点进行渲染
Result && UpdaterFn && UpdaterFn(Node,Result);
//为当前的表达解析式new出一个Watcher
new MyWatcher(exp,vm, UpdaterFn, Node);
return Result;// 最后返回对应的解析结果
},
/**
* 下面4个方法 html、text、model、class,使用了策略模式来避免硬代码的产生
* 每个不同的方法处理不同的指令
* 这里的this指向CompileUtril,so,bind就是CompileUtril.bind
* @param vm
* @param exp
* @param Node
*/
text:function (vm, exp, Node) {
let dir = 'text';
this.bind(vm, dir, exp, Node);// Vue实例 ‘text’ 表达式 dom元素
},
html:function (vm, exp, Node) {
let dir = 'html';
this.bind(vm, dir, exp, Node);
},
class:function (vm, exp, Node) {
let dir = 'class';
this.bind(vm,dir, exp, Node);
},
// 在这里增加v-model事件
model:function (vm, exp, Node) {
let me = this;
let dir = 'model';
let val = this.bind(vm,dir, exp, Node);
// 给当前Node绑定input事件
Node.addEventListener('input',function (e) {
let NewVal = e.target.value;
if(NewVal === val)return;
// 更新到data中
me.GetValInObjectByRegLastAndSet(vm._MyData, exp, NewVal);
// 调用对应的这个节点的Dep的notify
});
}
}
/**
* dom节点的更新器
* 策略模式对应四种指令操作,做map映射,避免硬代码
*/
Updater = {
textUpdater:function (Node,value) {
Node.textContent = typeof value == 'undefined' ? '' : value;
},
htmlUpdater:function (Node,value) {
Node.innerHTML = typeof value == 'undefined' ? '' : value;
},
classUpdater:function (Node,value) {
let oldClass = Node.className;
Node.className = (Node.className === '' ? '':Node.className+" ")+value;
},
modelUpdater:function (Node,value) {
Node.value = typeof value == 'undefined' ? '' : value;
}
}