前言
虚拟DOM是Moon重要的部分,也是现在很多前端框架中都存在的部分,本文主要介绍虚拟DOM的概念以及它在Moon中具体表现形式。
具体分析
还是采用简单实例开始来分析整个虚拟DOM的创建以及与实际DOM之间的处理,实例代码如下:
<div id="app">
<p>{{today}}</p>
<p>{{getToday}}</p>
<mn-test></mn-test>
</div>
<script src="./moon.js"></script>
<script>
debugger
Moon.component('mn-test', {
template: '<p>{{msg}}</p>',
data: {
msg: 'hello world'
}
});
new Moon({
el: '#app',
data: {
today: Date.now()
},
computed: {
getToday: {
get() {
var now = new Date(this.get('today'));
return now.toLocaleString();
}
}
}
});
</script>
上面的实例中涉及到三个部分:普通变量、计算属性、自定义模块,我们来看看Moon是如何处理的以及对应的虚拟DOM的生成。
之前通过对于Moon构造函数逻辑处理流程的分析得到如下几点:
- 在mount方法中获取this.$render方法,该方法就是用于产生虚拟DOM
- 在build方法中调用了render函数,产生了虚拟DOM
- 在patch方法中处理了虚拟DOM与浏览器DOM之间的转换
所以就下来就按照上面的逻辑处理来分析整个的处理流程。
构建this.$render方法
this.$render = Moon.compile(this.$template);
可见调用了Moon.compile方法,该方法顾名思义即编译的功能,具体看看该方法内部的具体处理流程。
Moon.compile = function (template) {
return compile(template);
};
调用了私有方法compile
var compile = function (template) {
var tokens = lex(template);
var ast = parse(tokens);
return generate(ast);
};
实际上lex函数是分析html中每一个标识符,parse是将所有的标识符构建成指定的树结构,generate是返回创建虚拟DOM的函数。
lex
上面是lex部分整体的处理流程,具体的处理代码我会详细注释,此处就不将代码展示出来。
lex实际上就是分别对应处理html中的节点、标签、注释,构建成自己想要的对象,就上面的实例而言,html部分如下:
<div id="app">
<p>{{today}}</p>
<p>{{getToday}}</p>
<mn-test></mn-test>
</div>
那么lex处理的输出结果是一个数组,数组中的元素都是对象,对象按照node类型分为标签对象、文本对象,具体组成属性如下:
// 标签对象: attributes是标签属性
// closeStart表示是否是闭合标签
// type表示类型、value表示标签名称
attributes: Object
closeStart: true
type: "tag"
value: "div"
// 文本对象
// type表示类型,value表示文本值
type: "text"
value: "↵ "
parse
var parse = function (tokens) {
var root = {
type: "ROOT",
children: []
};
var state = {
current: 0,
tokens: tokens
};
// 遍历处理tokens中的标签
while (state.current < tokens.length) {
// 主要的处理就是walk函数,该函数是用于构建成虚拟dom树
var child = walk(state);
if (child) {
root.children.push(child);
}
}
return root;
};
walk的处理代码会详细注释之后会上传到我的Github上,此处就不在粘贴出来,parse函数是将tokens中所有的node对象构建成树结构,该函数返回的结果结构为:
type: 'ROOT',
children: [
{
type: 标签名称
props: 属性以及指令集合
meta: 元数据
children:子标签对象
}
]
generate
generate函数是依据虚拟DOM树创建function,function如下:
(function(h) {
var instance = this;
return h("div", {attrs: {"id": "app"}}, {
"shouldRender": true,
"eventListeners": {}
}, [
h("#text", {"shouldRender": false,"eventListeners": {}}, "\n "),
h("p", {attrs: {}}, {"shouldRender": true, "eventListeners": {}}, [h("#text", {"shouldRender": true,"eventListeners": {}}, "" + instance.get("today") + "")]),
h("#text", {"shouldRender": false,"eventListeners": {}}, "\n "),
h("p", {attrs: {}}, {"shouldRender": true, "eventListeners": {}}, [h("#text", {"shouldRender": true,"eventListeners": {}}, "" + instance.get("getToday") + "")]),
h("#text", {"shouldRender": false, "eventListeners": {}}, "\n "),
h("mn-test", {attrs: {}}, {"shouldRender": false,"eventListeners": {}}, []),
h("#text", {"shouldRender": false,"eventListeners": {}}, "\n ")
])
})
由上面构建的形式是将每个虚拟DOM对象都传递给h函数来处理,传递给h函数的参数分为两类,标签和文本,标签的话(标签名称,属性集,meta元数据,children),文本的话(#text,meta,值)。
经过上面步骤的处理:this.$render = (function(h) {代码});
build中调用render
Moon.prototype.render = function () {
return this.$render(h);
};
核心的处理就是h函数,h函数是Moon内部的私有方法,具体处理逻辑如下:
var h = function (tag, attrs, meta, children) {
var component = null;
// #text的处理
if (tag === TEXT_TYPE) {
return createElement(TEXT_TYPE, meta, { attrs: {} }, attrs, []);
} else if ((component = components[tag]) !== undefined) {
// 自定义组件的处理
if (component.opts.functional === true) {
return createFunctionalComponent(attrs, children, components[tag]);
} else {
meta.component = component;
}
}
// 组件或者标签处理
return createElement(tag, "", attrs, meta, children);
};
createElement的实际处理如下:
var createElement = function (type, val, props, meta, children) {
return {
type: type,
val: val,
props: props,
children: children,
meta: meta || defaultMetadata()
};
};
可见build中render就是执行h函数,构建虚拟DOM树
patch
该方法是将虚拟DOM中的改变实际应用到浏览器DOM中,这部分的处理逻辑相对复杂些,整个的处理逻辑如下:
总结
Moon整个虚拟DOM的逻辑处理大致如此,更多细节会在源码中详细注释,通过分析Moon了解对入虚拟DOM有了直观的认知:所谓的虚拟DOM实际上就是模拟浏览器DOM的功能,减少实际修改DOM带来的浏览器重绘相关的代价,本质上它们还是一些具有DOM信息的对象。