| 
AST 结构 AST 即抽象语法树,剥版编在 虚拟dom、丝抽生成eslint、茧模babel 都有接触过了,剥版编简单来说就是丝抽生成一种描述 dom 的数据结构。通过 AST 可以还原 dom ,茧模也可以把 dom 转为 AST 。剥版编 因为是丝抽生成树的结构,所以肯定有一个 children 字段来保存子节点,茧模同时有 parent 来保存父节点。剥版编其他的丝抽生成话还有 tag 名,节点类型,茧模type = 1 代表普通节点类型,剥版编type=3 表示普通文本类型,丝抽生成type=2 代表有插值的茧模的文本类型。 提供一个函数 createASTElement 来生成 dom 节点,后续会用到。 export function createASTElement(tag, parent) { return { type: 1, tag, parent, children: [], }; }栈因为 dom 是一对一对出现的,一对中可能又包含多对,因此和括号匹配一样,这里就缺少不了栈了。 遇到开始标签就入栈,遇到结束标签就出栈,这样就可以保证栈顶元素始终是后续节点的免费信息发布网父节点。 举个例子,对于 <div><span>3<5吗</span><span>?</span></div> 。 1、 div span 3<5吗 /span span ? /span /div ^ stack:[div],当前栈顶 div,后续节点为 div 的子节点 2、 div span 3<5吗 /span span ? /span /div ^ stack:[div, span],当前栈顶 span,后续节点为 span 的子节点 3、 div span 3<5吗 /span span ? /span /div ^ stack:[div, span],当前栈顶 span,3<5吗 属于 span 4、 div span 3<5吗 /span span ? /span /div ^ stack:[div],遇到结束节点 span,栈顶的 span 去掉,后续节点为 div 的子节点 5、 div span 3<5吗 /span span ? /span /div ^ stack:[div, span],当前栈顶 span,后续节点为 span 的子节点 6、 div span 3<5吗 /span span ? /span /div ^ stack:[div, span],当前栈顶 span,? 属于 span 7、 div span 3<5吗 /span span ? /span /div ^ stack:[div],遇到结束节点 span,栈顶的 span 去掉,后续节点为 div 的站群服务器子节点 8、 div span 3<5吗 /span span ? /span /div ^ stack:[],遇到结束节点 div,栈顶的 div 去掉,遍历结束整体算法添加 stack 变量、添加 currentParent 保存当前的父节点、添加 root 保存根节点。 let root; let currentParent; let stack = [];接下来完善 模版编译之分词 中遗留的 start 、end、chars 三个回调函数。 parseHTML(template, { start: (tagName, unary, start, end) => { console.log("开始标签:", tagName, unary, start, end); }, end: (tagName, start, end) => { console.log("结束标签:", tagName, start, end); }, chars: (text, start, end) => { console.log("文本:", text, start, end); }, });start 函数 start: (tagName, unary, start, end) => { let element = createASTElement(tagName, currentParent); if (!root) { root = element; } if (!unary) { currentParent = element; stack.push(element); } else { closeElement(element); } },先调用 createASTElement 生成一个 AST 节点,如果当前是第一个开始节点,就将 root 赋值,接下来判断是否是出一元节点。 如果是一元节点直接调用 closeElement ,将当前节点加入到父节点中。 function closeElement(element) { if (currentParent) { currentParent.children.push(element); element.parent = currentParent; } }end 函数 end: (tagName, start, end) => { const element = stack[stack.length - 1]; // pop stack stack.length -= 1; currentParent = stack[stack.length - 1]; closeElement(element); },出栈,更新当前父节点,并且将出栈的元素加入到父节点中。 chars 函数 chars: (text, start, end) => { if (!currentParent) { return; } const children = currentParent.children; if (text) { let child = { type: 3, text, }; children.push(child); } },这里只考虑了 type 为 3 的普通文本节点。 整体代码结合 模版编译之分词 中实现的分词,整体代码如下: const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/; const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`; const qnameCapture = `((?:${ncname}\\:)?${ncname})`; const startTagOpen = new RegExp(`^<${qnameCapture}`); const startTagClose = /^\s*(\/?)>/; const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); export function createASTElement(tag, parent) { return { type: 1, tag, parent, children: [], }; } export function parseHTML(html, options) { let index = 0; while (html) { let textEnd = html.indexOf("<"); if (textEnd === 0) { // Start tag: const startTagMatch = parseStartTag(); if (startTagMatch) { handleStartTag(startTagMatch); continue; } // End tag: var endTagMatch = html.match(endTag); if (endTagMatch) { var curIndex = index; advance(endTagMatch[0].length); parseEndTag(endTagMatch[1], curIndex, index); continue; } } let text, rest, next; if (textEnd >= 0) { rest = html.slice(textEnd); while (!endTag.test(rest) && !startTagOpen.test(rest)) { // < in plain text, be forgiving and treat it as text next = rest.indexOf("<", 1); if (next < 0) break; textEnd += next; rest = html.slice(textEnd); } text = html.substring(0, textEnd); } if (textEnd < 0) { text = html; } if (text) { advance(text.length); } if (options.chars && text) { options.chars(text, index - text.length, index); } } function advance(n) { index += n; html = html.substring(n); } function parseStartTag() { const start = html.match(startTagOpen); if (start) { const match = { tagName: start[1], attrs: [], start: index, }; advance(start[0].length); let end = html.match(startTagClose); if (end) { match.unarySlash = end[1]; advance(end[0].length); match.end = index; return match; } } } function handleStartTag(match) { const tagName = match.tagName; const unarySlash = match.unarySlash; const unary = !!unarySlash; options.start(tagName, unary, match.start, match.end); } function parseEndTag(tagName, start, end) { options.end(tagName, start, end); } } const template = " 3<5吗?";console.log(template); function parse(template) { let root; let currentParent; let stack = []; function closeElement(element) { if (currentParent) { currentParent.children.push(element); element.parent = currentParent; } } parseHTML(template, { start: (tagName, unary, start, end) => { let element = createASTElement(tagName, currentParent); if (!root) { root = element; } if (!unary) { currentParent = element; stack.push(element); } else { closeElement(element); } }, end: (tagName, start, end) => { const element = stack[stack.length - 1]; // pop stack stack.length -= 1; currentParent = stack[stack.length - 1]; closeElement(element); }, chars: (text, start, end) => { if (!currentParent) { return; } const children = currentParent.children; if (text) { let child = { type: 3, text, }; children.push(child); } }, }); return root; } const ast = parse(template); console.log(ast);输入 <div><span>3<5吗</span><span>?</span></div> 。 最终生成的服务器租用 AST 结构如下:   总结这篇文章实现了最简单情况的 AST 生成,了解了整个的结构,下一篇文章会通过 AST 生成 render 函数。  |