一、模板解析的基本流程:
(1)将el的所有子节点取出,添加到一个新建的文档fragment对象中。
(2)对fragment中所有层次子节点递归进行编译解析处理。
-
对大括号表达式文本节点进行解析。
-
对元素节点的指令属性进行解析。
事件指令解析。
一般指令解析。
二、大括号表达式文本节点解析流程:
1.HTML中的代码:
<div id="test">
<p>{
{
name}}</p>
</div>
2.引入的JavaScript文件:
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script src="js/compile.js"></script>
<script src="js/observer.js"></script>
<script src="js/mvvm.js"></script>
<script src="js/watcher.js"></script>
3.JavaScript中的代码:
new Vue({
el:"#test",
data:{
name:'小花'
}
});
4.编译:
这里new了一个Compile实例传入的是options.el || document.body,this
这里this
指的是vm,
其实就是我们引入的compile.js向外暴露了一个Compile函数,此时我们进入compile.js文件里面。
compile.js文件代码分步解析:
function Compile(el, vm) {
//保存vm到Compile对象
this.$vm = vm;
// console.log(this);
//判断el是不是元素节点(看nodeType是不是1),将el对应的元素对象保存到Compile对象
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
//判断是否存在 如果有el元素则...
if (this.$el) {
//模板解析的三条语句,取出el元素中所有的子节点,保存到$fragment对象中
this.$fragment = this.node2Fragment(this.$el);
//用来实现初始化显示(重点语句),编译fragment中所有层次的子节点
this.init();
//将编译好的fragment添加到页面el元素中
this.$el.appendChild(this.$fragment);
}
}
点来node2Fragment
函数,这个函数是写在Compile的原型里面。(这样添加可以优化DOM操作)
node2Fragment: function(el) {
//创建空的fragment对象
var fragment = document.createDocumentFragment(),
child;
//将el中所有子节点转移到fragment中
while (child = el.firstChild) {
fragment.appendChild(child);
}
//返回fragment
return fragment;
},
点开init
函数,此函数也是存在于Compile的原型里面,init
函数里面很简短。
init: function() {
//编译fragment里面所有的子节点
this.compileElement(this.$fragment);
},
接着点开compileElement
函数(显示的是部分代码)。
compileElement: function(el) {
var childNodes = el.childNodes,//获得所有的子节点
me = this;//this指compile的实例
//遍历每一个子节点(text/element)
[].slice.call(childNodes).forEach(function(node) {
var text = node.textContent;//返回文本内容
var reg = /\{\{(.*)\}\}/;//用来匹配{
{name}}表达式,多了一个(),用来子匹配
if (me.isElementNode(node)) {
//是否是元素节点
//编译它(解析指令)
me.compile(node);
} else if (me.isTextNode(node) && reg.test(text)) {
//是文本节点并且文本和正则是否匹配
//编译大括号表达式文本节点
me.compileText(node, RegExp.$1.trim());
}
//如果当前节点还有子节点,通过递归调用实现所有层次节点的编译。递归是把所有层次都递归调用的最好方法
if (node.childNodes && node.childNodes.length) {
//问子节点是否有子子节点?并且子子节点长度是否大于0
me.compileElement(node);
}
});
}
打开isElementNode,isTextNode,compileText
函数:
compileText: function(node, exp) {
compileUtil.text(node, this.$vm, exp);
},
isDirective: function(attr) {
return attr.indexOf('v-') == 0;
},
isEventDirective: function(dir) {
return dir.indexOf('on') === 0;
},
//判断是否是元素节点
isElementNode: function(node) {
return node.nodeType == 1;
},
//判断是否是文本节点
isTextNode: function(node) {
return node.nodeType == 3;
}
打开compile
函数,此函数位于compileElement
函数中:
//用来编译元素节点里面所有的指令(指令以标签形式写上去的)
compile: function(node) {
var nodeAttrs = node.attributes,
me = this;
[].slice.call(nodeAttrs).forEach(function(attr) {
var attrName = attr.name;
if (me.isDirective(attrName)) {
var exp = attr.value;
var dir = attrName.substring(2);
// 事件指令
if (me.isEventDirective(dir)) {
compileUtil.eventHandler(node, me.$vm, exp, dir);
// 普通指令
} else {
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
}
node.removeAttribute(attrName);
}
});
},
compile.js文件夹里面有包含解析指令方法的工具:
var compileUtil = {
//用来编译v-text,v-text和编译{}用的同一个函数
text: function(node, vm, exp) {
this.bind(node, vm, exp, 'text');
},
//解析v-html
html: function(node, vm, exp) {
this.bind(node, vm, exp, 'html');
},
//解析v-model
model: function(node, vm, exp) {
this.bind(node, vm, exp, 'model');
var me = this,
val = this._getVMVal(vm, exp);
node.addEventListener('input', function(e) {
var newValue = e.target.value;
if (val === newValue) {
return;
}
me._setVMVal(vm, exp, newValue);
val = newValue;
});
},
//解析v-class
class: function(node, vm, exp) {
this.bind(node, vm, exp, 'class');
},
bind: function(node, vm, exp, dir) {
//得到更新节点的函数
var updaterFn = updater[dir + 'Updater'];//函数
//调用函数更新节点
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
new Watcher(vm, exp, function(value, oldValue) {
updaterFn && updaterFn(node, value, oldValue);
});
},
// 事件处理
eventHandler: function(node, vm, exp, dir) {
var eventType = dir.split(':')[1],
fn = vm.$options.methods && vm.$options.methods[exp];
if (eventType && fn) {
node.addEventListener(eventType, fn.bind(vm), false);
}
},
//从vm得到表达式所对应的值(表达式可能是多层)
_getVMVal: function(vm, exp) {
var val = vm;
exp = exp.split('.');
exp.forEach(function(k) {
val = val[k];
});
return val;
},
_setVMVal: function(vm, exp, value) {
var val = vm;
exp = exp.split('.');
exp.forEach(function(k, i) {
// 非最后一个key,更新val的值
if (i < exp.length - 1) {
val = val[k];
} else {
val[k] = value;
}
});
}
};
compile.js文件夹里面有包含更新节点方法的工具:
//包含多个更新节点的方法的工具对象
var updater = {
//更新节点的textContent属性值
textUpdater: function(node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
},
//更新节点的innerHTML属性值
htmlUpdater: function(node, value) {
node.innerHTML = typeof value == 'undefined' ? '' : value;
},
//更新节点的className属性值
classUpdater: function(node, value, oldValue) {
var className = node.className;
className = className.replace(oldValue, '').replace(/\s$/, '');
var space = className && String(value) ? ' ' : '';
node.className = className + space + value;
},
//更新节点的value属性值
modelUpdater: function(node, value, oldValue) {
node.value = typeof value == 'undefined' ? '' : value;
}
};
三、大括号表达式文本节点解析核心:
- 第一步:取出name。
- 第二步:根据name得到value。
- 第三步:把得到的值放到节点的textContent属性里面。
取出name的代码:
//模板解析的三条语句,取出el元素中所有的子节点,保存到$fragment对象中
this.$fragment = this.node2Fragment(this.$el);
//用来实现初始化显示(重点语句),编译fragment中所有层次的子节点
this.init();
//创建DocumentFragment减少DOM的回流
node2Fragment: function(el) {
//创建空的fragment对象
var fragment = document.createDocumentFragment(),
child;
//将el中所有子节点转移到fragment中
while (child = el.firstChild) {
fragment.appendChild(child);
}
//返回fragment
return fragment;
},
init: function() {
//编译fragment里面所有的子节点
this.compileElement(this.$fragment);
},
compileElement: function(el) {
var childNodes = el.childNodes,//获得所有的子节点
me = this;//this指compile的实例
//遍历每一个子节点(text/element)
[].slice.call(childNodes).forEach(function(node) {
var text = node.textContent;//返回文本内容
var reg = /\{\{(.*)\}\}/;//用来匹配{
{name}}表达式,多了一个(),用来子匹配
if (me.isElementNode(node)) {
//是否是元素节点
//编译它(解析指令)
me.compile(node);
} else if (me.isTextNode(node) && reg.test(text)) {
//是文本节点并且文本和正则是否匹配
//编译大括号表达式文本节点
me.compileText(node, RegExp.$1.trim());
}
//如果当前节点还有子节点,通过递归调用实现所有层次节点的编译。递归是把所有层次都递归调用的最好方法
if (node.childNodes && node.childNodes.length) {
//问子节点是否有子子节点?并且子子节点长度是否大于0
me.compileElement(node);
}
});
},
根据name得到value。
else if (me.isTextNode(node) && reg.test(text)) {
//是文本节点并且文本和正则是否匹配
//编译大括号表达式文本节点
me.compileText(node, RegExp.$1.trim());
}
compileText: function(node, exp) {
compileUtil.text(node, this.$vm, exp);
},
text: function(node, vm, exp) {
this.bind(node, vm, exp, 'text');
},
bind: function(node, vm, exp, dir) {
//得到更新节点的函数
var updaterFn = updater[dir + 'Updater'];//函数
//调用函数更新节点
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
new Watcher(vm, exp, function(value, oldValue) {
updaterFn && updaterFn(node, value, oldValue);
});
},
//从vm得到表达式所对应的值(表达式可能是多层)
_getVMVal: function(vm, exp) {
var val = vm;
exp = exp.split('.');
exp.forEach(function(k) {
val = val[k];
});
return val;
},
把得到的值放到节点的textContent属性里面:
//将编译好的fragment添加到页面el元素中
this.$el.appendChild(this.$fragment);
四:compile.js文件全部代码:
function Compile(el, vm) {
//保存vm到Compile对象
this.$vm = vm;
// console.log(this);
//判断el是不是元素节点(看nodeType是不是1),将el对应的元素对象保存到Compile对象
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
//判断是否存在 如果有el元素则...
if (this.$el) {
//模板解析的三条语句,取出el元素中所有的子节点,保存到$fragment对象中
this.$fragment = this.node2Fragment(this.$el);
//用来实现初始化显示(重点语句),编译fragment中所有层次的子节点
this.init();
//将编译好的fragment添加到页面el元素中
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype = {
constructor: Compile,
//创建DocumentFragment减少DOM的回流
node2Fragment: function(el) {
//创建空的fragment对象
var fragment = document.createDocumentFragment(),
child;
//将el中所有子节点转移到fragment中
while (child = el.firstChild) {
fragment.appendChild(child);
}
//返回fragment
return fragment;
},
init: function() {
//编译fragment里面所有的子节点
this.compileElement(this.$fragment);
},
compileElement: function(el) {
var childNodes = el.childNodes,//获得所有的子节点
me = this;//this指compile的实例
//遍历每一个子节点(text/element)
[].slice.call(childNodes).forEach(function(node) {
var text = node.textContent;//返回文本内容
var reg = /\{\{(.*)\}\}/;//用来匹配{
{name}}表达式,多了一个(),用来子匹配
if (me.isElementNode(node)) {
//是否是元素节点
//编译它(解析指令)
me.compile(node);
} else if (me.isTextNode(node) && reg.test(text)) {
//是文本节点并且文本和正则是否匹配
//编译大括号表达式文本节点
me.compileText(node, RegExp.$1.trim());
}
//如果当前节点还有子节点,通过递归调用实现所有层次节点的编译。递归是把所有层次都递归调用的最好方法
if (node.childNodes && node.childNodes.length) {
//问子节点是否有子子节点?并且子子节点长度是否大于0
me.compileElement(node);
}
});
},
//用来编译元素节点里面所有的指令(指令以标签形式写上去的)
compile: function(node) {
var nodeAttrs = node.attributes,
me = this;
[].slice.call(nodeAttrs).forEach(function(attr) {
var attrName = attr.name;
if (me.isDirective(attrName)) {
var exp = attr.value;
var dir = attrName.substring(2);
// 事件指令
if (me.isEventDirective(dir)) {
compileUtil.eventHandler(node, me.$vm, exp, dir);
// 普通指令
} else {
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
}
node.removeAttribute(attrName);
}
});
},
//递归是一种特别的嵌套调用
compileText: function(node, exp) {
compileUtil.text(node, this.$vm, exp);
},
isDirective: function(attr) {
return attr.indexOf('v-') == 0;
},
isEventDirective: function(dir) {
return dir.indexOf('on') === 0;
},
//判断是否是元素节点
isElementNode: function(node) {
return node.nodeType == 1;
},
//判断是否是文本节点
isTextNode: function(node) {
return node.nodeType == 3;
}
};
// 包含多个解析指令方法的工具对象
var compileUtil = {
//用来编译v-text,v-text和编译{}用的同一个函数
text: function(node, vm, exp) {
this.bind(node, vm, exp, 'text');
},
//解析v-html
html: function(node, vm, exp) {
this.bind(node, vm, exp, 'html');
},
//解析v-model
model: function(node, vm, exp) {
this.bind(node, vm, exp, 'model');
var me = this, val = this._getVMVal(vm, exp);
node.addEventListener('input', function(e) {
var newValue = e.target.value;
if (val === newValue) {
return;
}
me._setVMVal(vm, exp, newValue);
val = newValue;
});
},
//解析v-class
class: function(node, vm, exp) {
this.bind(node, vm, exp, 'class');
},
bind: function(node, vm, exp, dir) {
//得到更新节点的函数
var updaterFn = updater[dir + 'Updater'];//函数
//调用函数更新节点
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
new Watcher(vm, exp, function(value, oldValue) {
updaterFn && updaterFn(node, value, oldValue);
});
},
// 事件处理
eventHandler: function(node, vm, exp, dir) {
var eventType = dir.split(':')[1],
fn = vm.$options.methods && vm.$options.methods[exp];
if (eventType && fn) {
node.addEventListener(eventType, fn.bind(vm), false);
}
},
//从vm得到表达式所对应的值(表达式可能是多层)
_getVMVal: function(vm, exp) {
var val = vm;
exp = exp.split('.');
exp.forEach(function(k) {
val = val[k];
});
return val;
},
_setVMVal: function(vm, exp, value) {
var val = vm;
exp = exp.split('.');
exp.forEach(function(k, i) {
// 非最后一个key,更新val的值
if (i < exp.length - 1) {
val = val[k];
} else {
val[k] = value;
}
});
}
};
//包含多个更新节点的方法的工具对象
var updater = {
//更新节点的textContent属性值
textUpdater: function(node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
},
//更新节点的innerHTML属性值
htmlUpdater: function(node, value) {
node.innerHTML = typeof value == 'undefined' ? '' : value;
},
//更新节点的className属性值
classUpdater: function(node, value, oldValue) {
var className = node.className;
className = className.replace(oldValue, '').replace(/\s$/, '');
var space = className && String(value) ? ' ' : '';
node.className = className + space + value;
},
//更新节点的value属性值
modelUpdater: function(node, value, oldValue) {
node.value = typeof value == 'undefined' ? '' : value;
}
};