第一步 编写正则表达式
在我们拿到模板之后,我们需要对标签、文本、属性、表达式、字符串等进行匹配,那就需要用到我们的正则表达式,如下:
// compiler/index.js
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`);
console.log(startTagOpen);
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
const startTagClose = /^\s*(\/?)>/;
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
输出一下,用正则可视化工具来看一下规则如下:
startTagOpen
:开始为<
,然后里面是匹配名字div
或者带命名空间的div:xxx
等这样的开始标签的名字
endTag
:结束标签</xxx>
,最终匹配到的分组就是结束标签的名字
attribute
:用来匹配属性的,属性前面可以有一些空白white space
,不能是None of
内的符号,中间是=
,左右于两边可以有空白字符,左右单引号中间不是单引号,左右双引号中间不是双引号,也就是说第一个分组就是属性的key,value是分组3/4/5
startTagClose
:匹配到的就是一个反斜杠/
,可能是</div>
或者<br/>
defaultTagRE
:匹配的是双大括号{{}}
,两边是大括号,中间是换行或者回车以及任何字符
第二步 将模板转换成ast语法树
有了上面的正则之后,我们就需要去对模板进行匹配,然后将匹配到的字符串转换成ast语法树。
// compiler/index.js
// 对模板进行编译处理
......
function parseHTML(html) {const ELEMENT_TYPE = 1const TEXT_TYPE = 3const stack = [] // 用于存放元素let currentParent; // 指向栈中最后一个let rootfunction createdASTElement(tag, attrs) {return {tag,type: ELEMENT_TYPE,children: [],attrs,parent: null}}// 开始标签function start(tag, attrs) {let node = createdASTElement(tag, attrs) // 创造一个ast节点if (!root) { // 看一下是否为空树root = node // 如果为空则表示当前是树的根节点}if (currentParent) {node.parent = currentParent // 只赋了parent属性currentParent.children.push(node) // 还需要让父亲记住自己}stack.push(node)currentParent = node//currentParent为栈中的最后一个}// 文本function chars(text) { // 文本直接放到当前指向的节点中text = text.replace(/\s/g,'')&& currentParent.children.push({type: TEXT_TYPE,text,parent: currentParent})}// 结束标签function end(tag) {stack.pop() // 弹出最后一个currentParent = stack[stack.length - 1]}function advance(n) {html = html.substring(n)}function parseStartTag() {const start = html.match(startTagOpen)if (start) {const match = {tagName: start[1], // 标签名attrs: []}advance(start[0].length)// 如果不是开始标签的结束就一直匹配下去let attr, endwhile (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {advance(attr[0].length)match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] || true })}if (end) {advance(end.length)}return match}return false // 不是开始标签}while (html) { // html最开始肯定是一个 <// 如果textEnd为0说明是一个开始标签或者结束标签// 如果textEnd大于0则说明是文本结束的位置let textEnd = html.indexOf('<') // 如果indexOf的索引是0则说明是个标签if (textEnd === 0) {const starttagMatch = parseStartTag()//开始标签的匹配if (starttagMatch) { // 解析到的开始标签start(starttagMatch.tagName, starttagMatch.attrs)continue}let endTagmatch = html.match(endTag)if (endTagmatch) {advance(endTagmatch[0].length)end(endTagmatch[1])continue}}if (textEnd > 0) {let text = html.substring(0, textEnd) // 文本内容if (text) {chars(text)advance(text.length) // 解析到的文本}}}console.log(root);return root
}
export function compileToFcuntion(template) {// 1.将template转换为ast语法树let ast = parseHTML(template)// 2.生成render方法 render方法执行后的结果就是虚拟DOMconsole.log(template);
}
思路:
1.通过 parseHTML(template)
方法传入模板,将模板转换成ast语法树,解析开始标签、结束标签等内容,通过这个方法就会返回一个ast语法树。
2.在parseHTML()
方法中,每解析一个标签则在模板的字符串中将其截取掉,直到为空,所以可以写一个while
循环来完成
3.html肯定是<
开头的,所以我们直接用indexOf
方法来进行判断:如果textEnd为0说明是一个开始标签或者结束标签,如果textEnd大于0则说明是文本结束的位置。
4.如果textEnd
等于0就认为他是开始标签,那就用parseStartTag
方法来解析开始标签。如下图接下来就可以塞到match
里面。
5.追加完成之后就需要去解析属性,那我们就需要先将已经匹配完成的标签进行删除,通过advance(start[0].length)
来进行截取,如下图就说明截取成功了,截取成功之后就可以去匹配属性
6.在匹配属性的过程中,只要不是开始标签的结束就可以一直进行匹配,所以可以通过while
,如果不是开始标签的结束就一直匹配下去,并且将每次拿到的属性进行保存,每次匹配完在将其截掉,最后还有一个>符号,所以在匹配结束以后end可能就会有值,所以再把end删掉
<img src=“https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/23912023e1eb43fe85c6dec78acfe6dc~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) 9. 整个过程就是遇到开始标签处理开始标签,遇到文本处理文本,遇到结束标签处理结束标签,当最后循环能够终止输出为空,就说明处理成功了 9. 现在还没有替换文本只是把字符串删掉了,那就还需要几个方法将它们暴露出去,也可以用htmlparser2去解析htm” style=“margin: auto” />
1.知道方法之后,就开始定义元素类型ELEMENT_TYPE
、文本类型TEXT_TYPE
,栈stack
,指针currentParent
,根节点root
2.当遇到开始标签的时候就创建一个AST元素,用createdASTElement
方法,然后还需要去判断产生的节点是否是根节点root,如果没有根节点这个节点就是根节点。然后将这个节点放进栈中,同时还需要当前这个指针指向栈中的最后一个。如果currentParent
有值就需要让当前节点的父亲等于currentParent
,并将node值赋给currentParent.children
3.当遇到文本的时候,这个文本就是当前元素的孩子,直接找到currentParent.children
放进去
4.当遇到结束标签的时候,直接弹出栈中最后一个,然后更新currentParent
指针,在end中也可以进行校验标签是否合法。
这个时候输出发现children是空的,可以在开始的时候chars
执行的时候将空格去掉text = text.replace(/\s/g,'')
最终
最后
为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。
有需要的小伙伴,可以点击下方卡片领取,无偿分享