1. Vue2 Dom 解析流程概览
要想将数据渲染高页面上,首先需要将元素解析为抽象将语法树(AST),将语法树中的 data 属性替换为 data 中的值,再将抽象语法树渲染成真实的 DOM。
解析流程
- 查找选项中是否有 render 函数,如果有则使用 render 函数创建 VNode,没有再去查找 template 选项,如果有则使用 template 创建 VNode,最后去查找 el 选项,如果有则根据 el 选项创建 VNode。优先级:render > template > el
- 将元素解析为抽象语法树:
Vue 获取获取页面上的元素,使用正则去匹配页面中的所有标签,属性,文本,将他们一一对应到 AST 中的节点上
下面我们开始逐行实现解析流程。
2.获取页面元素
我们创建 $mount
函数,在 mount 函数中将元素解析为抽象语法树,去options
中查找相应的选项,优先级为:render > template > el:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
Vue.prototype.$mount = function (elementName) { const vm = this; const el = document.querySelector(elementName);
let ops = vm.$options; let template; if (!ops.render) { if (ops.template) { template = ops.template; } else if (el) { template = el.outerHTML; } else { console.log("没有 render 函数,也没有 template 属性"); } if (template) { ops.render = compile(template); } } };
|
3.元素编译
这步主要是将页面元素编译为抽象语法中,我们创建函数 complie 来实现这个功能。
3.1 解析开始标签
- 因为标签总是成对出现的,当结束标签出现,说明这个标签的内容已经结束了,我们可以循环匹配字符,匹配成功就将这个字符删除,直到所有字符匹配完成,首先我们匹配开始标签字符,也就是匹配
<
这歌字符, 当这个字符在开头那么这歌就是一个元素的开始标签<ele
,或者结束标签</ele
,下面是完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| function complie(html) { if (html) { parseHtml(html); } }
function subHtml(startIndex) { return html.substring(startIndex); }
function parseHtml(html) { function subHtml(startIndex) { return html.substring(startIndex); } const tagName = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; const qnameCapture = `((?:${tagName}\\:)?${tagName})`; const stratTag = new RegExp(`^<${qnameCapture}`); const attrReg = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
function parseStartTag() { const start = html.match(stratTag); if (start) { const match = { tagName: start[1], attrs: [], }; const startIndex = start[0].length; html = subHtml(startIndex);
let attr, end; while (!(end = html.match(stratTagClose)) && (attr = html.match(attrReg))) { match.attrs.push({ name: attr[1], value: attr[3], }); html = subHtml(attr[0].length); } if (end) { html = subHtml(end[0].length); }
return match; } }
while (html) { let isStartTag = html.indexOf("<") === 0;
if (isStartTag) { const startMatch = parseStartTag(html); } } }
|
parseStartTag
函数获取匹配开始标签内的内容:
- stratTag 正则匹配
<element-name
字符, 例如 <div>
匹配的结果为:['<div', 'div']
, 那么他的标签名为数组中的第二个元素,即div
,我们创建 match 对象,并将 tagName 属性设置为div
, 并将 attrs 属性设置为空数组:
1 2 3 4 5 6 7 8 9
| const start = html.match(stratTag); if (start) { const match = { tagName: start[1], attrs: [], }; }
|
- 我们需要将匹配过的字符删除,匹配结果:
['<div', 'div']
中的第一个元素就是我们需要删除的元素。创建一个函数subHtml
,使用substring
方法来删除匹配过的字符:
1 2 3 4 5 6 7 8 9
| function subHtml(startIndex) { return html.substring(startIndex); }
const start = html.match(stratTag);
const startIndex = start[0].length; html = subHtml(startIndex);
|
- 匹配属性,使用正则
attrTag
来匹配标签上的属性 例如:<div id="..." class="...">
, 匹配的结果为:['id="..."', 'id', '"...']
, 第一个元素为匹配的字符串,第二个元素为属性名,第三个元素为属性值;因为标签上可能存在多个属性,所以我们需要循环匹配属性,直到匹配到标签的结束符>
则退出循环,我们将匹配到的属性添加到 attrs
数组中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function subHtml(startIndex) { return html.substring(startIndex); }
let attr, end;
while (!(end = html.match(stratTagClose)) && (attr = html.match(attrReg))) { match.attrs.push({ name: attr[1], value: attr[3], }); html = subHtml(attr[0].length); }
|
- 匹配开始标签的结束标签
>
,则开始标签匹配完成,将匹配到的字符串 >
删除即可:
1 2 3 4
| if (end) { html = subHtml(end[0].length); }
|
- 匹配开始标签完成,返回匹配对象 match
3.2 匹配结束标签
如果接下来在我们匹配完一个元素的开始标签后,直接匹配到该元素的结束标签,则说明该元素没有内容:文本、子元素, 而且结束标签也不会有属性,而该元素的标签名我们在匹配开始标签时就已经知道了,所以在这里我们就直接删除该元素的结束标签,以便可以开始下一个元素标签的匹配:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| function parseHtml(html) { while (html) { let isStartTag = html.indexOf("<") === 0;
if (isStartTag) { const startMatch = parseStartTag(html); if (startMatch) { stratHandle(startMatch.tagName, startMatch.attrs); continue; } let endTag = html.match(endTagReg); if (endTag) { endHandle(endTag[1]); html = subHtml(endTag[0].length); continue; } } } }
|
3.3 匹配文本节点
如果我们在开始标签后,匹配的不是 <
字符(isStartTag
为 false),那么就说明匹配到了元素内的文本节点,我们就可以获取该节点,并删除匹配到的文本节点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| function parseHtml(html) { while (html) { let isStartTag = html.indexOf("<") === 0;
if (isStartTag) { const startMatch = parseStartTag(html); if (startMatch) { stratHandle(startMatch.tagName, startMatch.attrs); continue; } let endTag = html.match(endTagReg); if (endTag) { endHandle(endTag[1]); html = subHtml(endTag[0].length); continue; } }
function textHandle(text) { console.log("文本", text); text = text.trim(); text && currentParent.children.push({ type: TEST_TYPE, text, }); } } }
|
完整流程
至此,我们会循环匹配每个元素,直到元素文本为空,并将匹配到的文本转换为对象,那么如何将者一个个对象连接起来呢,这就是我们接下来要做的
4.创建 AST
我们已经循环了每一个元素,并将他们转换为一个个对象,下面我们就将他们连接起来,形成一个树状结构
4.1 开始标签处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| let root, currentParent, stack = []; const ELEMENT_TYPE = 1; const TEST_TYPE = 3; function createAST(tag, attrs) { return { tag, type: ELEMENT_TYPE, children: [], attrs, parent: null, }; } function stratHandle(tag, attrs) { let node = createAST(tag, attrs); }
|
- 判断有没有 root(根节点),没有则说明刚刚解析,那么当前 node 就是 root 节点
1 2 3 4
| if (!root) { root = node; }
|
- 如果有 root, 那说明当前 node 对象是上个节点的子节点,那么如何获取上个 node 呢,我们可以通过 stack 数组来获取,每次处理一次开始节点,我们就把该节点 push 到 stack 中,当处理结束节点的时候,说明该元素的所有内容匹配完成了,开始匹配下一元素了,我们就可以把该元素从 stack 中 pop 出来:
stack 前一位元素是后一位元素的父元素
我们还需要判断是否存在父元素,存在,则当前 node 节点对象的 parent 是该父元素,而该父元素的 children 是该 node 对象,我们相互赋值:
1 2 3 4 5
| if (currentParent) { node.parent = currentParent; currentParent.children.push(node); }
|
在最后我们将当前 node 对象赋值给 currentParent,这样下一次循环获取元素里面的节点时,我们就可以获取到该节点的父节点:
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| const tagName = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${tagName}\\:)?${tagName})`;
const stratTag = new RegExp(`^<${qnameCapture}`); const endTagReg = new RegExp(`^<\\/${qnameCapture}[^>]*>`); const attrReg = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
const stratTagClose = /^\s*(\/?)>/; const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
function parseHtml(html) { let root, currentParent, stack = []; const ELEMENT_TYPE = 1; const TEST_TYPE = 3;
function createAST(tag, attrs) { return { tag, type: ELEMENT_TYPE, children: [], attrs, parent: null, }; }
function stratHandle(tag, attrs) { console.log("开始标签", tag); let node = createAST(tag, attrs); if (!root) { root = node; } if (currentParent) { node.parent = currentParent; currentParent.children.push(node); } stack.push(node); currentParent = node; }
function subHtml(startIndex) { return html.substring(startIndex); }
function parseStartTag() { const start = html.match(stratTag); if (start) { const match = { tagName: start[1], attrs: [], }; const startIndex = start[0].length; html = subHtml(startIndex);
let attr, end; while (!(end = html.match(stratTagClose)) && (attr = html.match(attrReg))) { match.attrs.push({ name: attr[1], value: attr[3], }); html = subHtml(attr[0].length); } if (end) { html = subHtml(end[0].length); }
return match; } }
while (html) { let isStartTag = html.indexOf("<") === 0;
if (isStartTag) { const startMatch = parseStartTag(html); if (startMatch) { stratHandle(startMatch.tagName, startMatch.attrs); continue; }
} }
|
流程图:
4.2 结束标签的处理
当我们匹配到结束标签时,则说明当前标签已经匹配完成,我们将该标签从 stack 数组中删除,并将 currenParent 设置为 stack 数组中的上一个 node 节点:
1 2 3 4 5 6
| function endHandle(tag) { console.log("结束标签", tag); let node = stack.pop(); currentParent = stack[stack.length - 1]; }
|
4.3 文本节点的处理
文本节点一定是当前 node 节点的子节点,因为文本在标签内,那么它就是当前 node 对象 children 属性数组的元素,而在处理开始标签最后,我们将当前 node 赋值给 currenParent,那么在处理文本节点时,只需要将文本节点添加到当前 node 节点的 children 数组中即可:
1 2 3 4 5 6 7 8 9 10
| function textHandle(text) { console.log("文本", text); text = text.trim(); text && currentParent.children.push({ type: TEST_TYPE, text, }); }
|
现在所有一个个单独的 node 节点对象通过 parent 与 chidren 属性组成了抽象语法树,并赋值给了 root, complie 函数的完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
| const tagName = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${tagName}\\:)?${tagName})`;
const stratTag = new RegExp(`^<${qnameCapture}`); const endTagReg = new RegExp(`^<\\/${qnameCapture}[^>]*>`); const attrReg = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
const stratTagClose = /^\s*(\/?)>/; const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
function parseHtml(html) { let root, currentParent, stack = []; const ELEMENT_TYPE = 1; const TEST_TYPE = 3;
function createAST(tag, attrs) { return { tag, type: ELEMENT_TYPE, children: [], attrs, parent: null, }; }
function stratHandle(tag, attrs) { console.log("开始标签", tag); let node = createAST(tag, attrs); if (!root) { root = node; } if (currentParent) { node.parent = currentParent; currentParent.children.push(node); } stack.push(node); currentParent = node; }
function textHandle(text) { console.log("文本", text); text = text.trim(); text && currentParent.children.push({ type: TEST_TYPE, text, }); }
function endHandle(tag) { console.log("结束标签", tag); let node = stack.pop(); currentParent = stack[stack.length - 1]; }
function subHtml(startIndex) { return html.substring(startIndex); }
function parseStartTag() { const start = html.match(stratTag); if (start) { const match = { tagName: start[1], attrs: [], }; const startIndex = start[0].length; html = subHtml(startIndex);
let attr, end; while (!(end = html.match(stratTagClose)) && (attr = html.match(attrReg))) { match.attrs.push({ name: attr[1], value: attr[3], }); html = subHtml(attr[0].length); } if (end) { html = subHtml(end[0].length); }
return match; } }
while (html) { let isStartTag = html.indexOf("<") === 0;
if (isStartTag) { const startMatch = parseStartTag(html); if (startMatch) { stratHandle(startMatch.tagName, startMatch.attrs); continue; } let endTag = html.match(endTagReg); if (endTag) { endHandle(endTag[1]); html = subHtml(endTag[0].length); continue; } }
if (!isStartTag) { const textEnd = html.indexOf("<"); const text = html.substring(0, textEnd); if (text) { textHandle(text); html = subHtml(text.length); } } } console.log("root", root); }
export default function complie(html) { if (html) { parseHtml(html); } }
|