分析准备
[ ].slice.call(lis):将伪数组转换为真数组
const lis = document.getElementsByTagName('li')
const lis2 = Array.prototype.slice.call(lis)
console.log(lis2 instanceof Array) //true
console.log(lis2[1].innerHTML) //'test2'
console.log(lis2.forEach) //'function'
Node.nodeType:节点类型
const elementNode = document.getElementById('app')
const attrNode = elementNode.getAttributeNode('id')
const textNode = elementNode.firstChild
console.log(elementNode.nodeType) // 1
console.log(attrNode.nodeType) // 2
console.log(textNode.nodeType) // 3
- Document:文档
- Element:元素
- Attr:属性
- Text:文本
DocumentFragment
DocumentFragment:文档片段(高效批量更新多个节点)
document:对应显示的页面,包含n个element,一旦更新document内部的某个元素,界面更新。
documentFragment:内存中保存n个element的容器对象(不与界面关联),如果更新fragment中的某个element,界面不变。
与document相比,最大的区别是DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。
<ul id="fragment_test">
<li>test1</li>
<li>test2</li>
<li>test3</li>
</ul>
const ul = document.getElementById('fragment_test')
// 1. 创建fragment
const fragment = document.createDocumentFragment()
// 2. 取出ul中所有子节点保存到fragment
let child
// 一个节点只能有一个父亲
while (child = ul.firstChild) {
// 先将child从ul中移除,添加为fragment子节点
fragment.appendChild(child)
}
// 3. 更新fragment中所有li的文本
Array.prototype.slice.call(fragment.childNodes).forEach(node => {
if (node.nodeType === 1) {
node.textContent = 'kk'
}
})
// 4. 将fragment插入ul
ul.appendChild(fragment)
大括号表达式
<div id="app">
<p>{{name}}</p>
</div>
<script src="./mvvm/compile.js"></script>
<script src="./mvvm/mvvm.js"></script>
<script src="./mvvm/observer.js"></script>
<script src="./mvvm/watcher.js"></script>
<script>
new MVVM({
el: "#app",
data: {
name: 'kk'
}
})
</script>
源码分析
以下代码用的是:https://github.com/DMQ/mvvm.git,此版进行简化改造主要说明原理与实现
function Compile(el, vm) {
// 保存vm到compile对象
this.$vm = vm;
// 将el对应的元素对象保存到compile对象中
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
// 1. 取出el元素中所有子节点保存到一个fragment对象中
this.$fragment = this.node2Fragment(this.$el);
// 2. 编译fragment中所有层次的子节点
this.init();
// 3. 将编译好的fragment添加页面的el元素中
this.$el.appendChild(this.$fragment);
}
}
上述代码所示为上图step into后进入到的compile.js中代码中,如注释所示步骤123即可实现模板解析中的大括号表达式。在第九行打上断点,进入到node2Fragment中
node2Fragment: function (el) {
var fragment = document.createDocumentFragment(),
child;
// 将原生节点拷贝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
},
如上所示该部分首先创建一个空的fragment,然后将el中所有子节点转移到fragment,返回fragment。
接下来编译fragment中所有层次的子节点
init: function () {
this.compileElement(this.$fragment);
},
compileElement函数如下所示。
compileElement: function (el) {
// 取出最外层所有子节点
var childNodes = el.childNodes,
// 保存compile对象
me = this;
// 遍历所有子节点(text/element)
[].slice.call(childNodes).forEach(function (node) {
//得到节点的文本内容
var text = node.textContent;
// 创建正则对象(匹配大括号表达式)
var reg = /\{\{(.*)\}\}/;
//判断节点是否是一个元素节点
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) {
me.compileElement(node);
}
});
},
开始时取出的childNodes = NodeList(3) [text, p, text],通过遍历所有子节点,根据正则对象得到匹配出的表达式字符串,从data中取出表达式对应的属性值,将属性值设置为文本节点的textContent。{{name}}符合大括号格式的文本节点,进入compileText中
compileText: function (node, exp) {
compileUtil.text(node, this.$vm, exp);
},
var compileUtil = {
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);
});
},
_getVMVal: function (vm, exp) {
var val = vm;
exp = exp.split('.');
exp.forEach(function (k) {
val = val[k];
});
return val;
},
};
通过bind函数得到需要更新节点的函数,调用函数。通过_getVMVal从vm得到表达式所对应的值更新节点。
最后将编译好的fragment添加页面的el元素中。
事件指令
<div id="app">
<p>{{name}}</p>
<button v-on:click="show">tips</button>
</div>
<script>
new MVVM({
el: "#app",
data: {
name: 'kk'
},
methods: {
show() {
alert(this.name)
}
}
})
</script>
源码分析
以下代码用的是:https://github.com/DMQ/mvvm.git,此版进行简化改造主要说明原理与实现
步骤一,取出el元素中所有子节点保存到一个fragment对象中。与前面大致一样
步骤二中对元素节点的事件指令属性进行解析。
compile函数如下
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);
}
});
},
首先得到标签的所有属性nodeAttrs,遍历所有属性nodeAttrs。得到属性名attrName = “v-on:click”。判断是否是指令属性。如果是则继续执行,得到属性值exp = “show”,从属性名中得到指令名dir = “on:click”。判断是否为事件指令,进行解析处理事件指令。指令解析完后移除此指令属性。其中事件处理过程的事件处理函数eventHandler如下
// 事件处理
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);
}
},
得到事件名(类型)eventType = “click”,从methods中得到表达式所对应的函数(事件回调函数)fn = ƒ show()。如果事件名和时间回调函数都存在,则给节点绑定事件名和回调函数(强制绑定this为vm)的DOM事件监听。
一般指令
<style>
<div id="app">
<p v-text="msg"></p>
<p v-html="msg"></p>
<p class="kk" v-class="myclass">hello world</p>
</div>
new MVVM({
el: "#app",
data: {
msg: '<a href="http://www.baidu.com">百度</a>',
myclass: 'xx'
}
})
源码分析
以下代码用的是:https://github.com/DMQ/mvvm.git,此版进行简化改造主要说明原理与实现
步骤一取出el元素中所有子节点保存到一个fragment对象中。与前面大致一样,步骤二中对元素节点的一般指令属性进行解析。得到指令名和指令值,从data中根据表达式得到对应的值。
// 指令处理集合
var compileUtil = {
// 解析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-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);
});
},
};
根据指令名确定需要操作元素节点的属性
var 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, oldValue) {
var className = node.className;
className = className.replace(oldValue, '').replace(/\s$/, '');
var space = className && String(value) ? ' ' : '';
node.className = className + space + value;
},
};
该篇为学习过程的学习笔记,若有不足和错误,欢迎指正!