将模板编译成ast语法树
标签:匹配,AST,标签,html,源码,let,attrs,vue2,属性 From: https://www.cnblogs.com/dgqp/p/17320057.html
complileToFunction
方法
vue
数据渲染:template
模板->ast
语法树->render
函数,模板编译的最终结果结果就是render
函数。在
complileToFunction
方法中,生成render
函数,需要以下两个核心步骤:
- 通过
parserHTML
方法:将模板(template
或html
)内容编译成ast
语法树- 通过
codegen
方法:根据ast
语法树生成为render
函数export function complileToFunction(template) { // 1.就是将template转化为ast语法树 let ast = parseHTML(template) // 2.生成render方法(render方法执行后的返回结果就是虚拟DOM),使用ast生成render函数 // 模板引擎的实现原理就是with + new Function let code = codegen(ast) code = `with(this){return ${code}}` let render = new Function(code) return render }
parserHTML
方法
parserHTML
方法:将模板(template
或html
)编译成为ast
语法树注意:
parserHTML
方法的入参template
,指的是<template>
标签内部的内容并不包括<template>
标签本身。在
Vue
初始化时:
- 如果
options
选项选项中设置了template
,将优先使用template
内容作为模板- 如果
options
选项没有设置template
,将采用元素内容作为html
模板:<div id="app"></div>
主要使用的正则匹配。七个正则匹配。
- 匹配标签号
- 匹配命名空间标签名
- 匹配开始标签-开始部分
- 匹配结束标签
- 匹配属性
- 匹配开始标签-闭合部分
- 匹配插值表达式
export function parseHTML(html) { const ELEMENT_TYPE = 1 const TEXT_TYPE = 3 const stack = [] // 用于存放元素的 let currenrParent // 指向栈的最后一个 let root // 最终转化为一颗抽象语法树 function createASTElement(tag, attrs) { return { tag, type: ELEMENT_TYPE, children: [], attrs, parent: null, } } // 利用栈构造一棵树 function start(tag, attrs) { let node = createASTElement(tag, attrs) // 创造一个ast节点 // 看一下是否为空树 if (!root) { root = node // 如果为空则当前是树的根节点 } if (currenrParent) { node.parent = currenrParent // 只赋予了parent属性 currenrParent.children.push(node) // 还需要让父亲记住自己 } stack.push(node) currenrParent = node // currenrParent为栈中的最后一个 } // 文本 function chars(text) { text = text.replace(/\s/g, '') // 文本直接放到当前指向的节点中 text && currenrParent.children.push({ type: TEXT_TYPE, text, parent: currenrParent, }) } // 结束 function end(tag) { stack.pop() // 弹出最后一个,校验标签是否合法 currenrParent = stack[stack.length - 1] } function advance(n) { html = html.substring(n) } function parseStartTag() { // 1. 匹配开始标签 const start = html.match(startTagOpen) if (start) { // 2. 构造怕匹配结果对象:包含标签名和属性 const match = { tagName: start[1], // 标签名 attrs: [], // 属性 } // 3. 截取匹配到的结果 advance(start[0].length) // 如果不是开始标签的结束,就一直匹配下去 // 4. 开始解析标签的属性 id="app" a=1 b=2> let attr // 是否匹配开始标签的结束符号 > 或 /> let end // 存储属性匹配的结果 // 匹配并获取属性,放入 match.attrs 数组 while ( !(end = html.match(startTagClose)) && (attr = html.match(attribute)) ) { // 4.1 截取掉已匹配的结果 advance(attr[0].length) // 4.2 将匹配到的属性记录到数组 match.attrs(属性对象包含属性名和属性值) match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5], }) } // 4.3 将匹配到的属性记录到数组 match.attrs(属性对象包含属性名和属性值) if (end) { // <div id="app" 处理完成,需要连同关闭符号 > 一起截取掉,截取掉 advance(end[0].length) } // 4.4 开始标签处理完成后,返回匹配结果:tagName 标签名 + attrs 属性对象 return match } return false // 不是开始标签 } while (html) { // 如果textEnd为0,说明是一个开始标签或者结束标签 // 如果textEnd>0说明就是文本的结束位置 // 解析标签or文本,判断html的第一个字符,是否为 < 尖角号 let textEnd = html.indexOf('<') // 如果索引是0则说明是一个标签 if (textEnd === 0) { // 解析开始标签,返回匹配结果,即标签名。 const startTagMatch = parseStartTag() if (startTagMatch) { // 解析到的开始标签,传递标签名和属性 start(startTagMatch.tagName, startTagMatch.attrs) continue } // 如果开始标签没有匹配到,有可能是结束标签 </div> let endTageMatch = html.match(endTag) if (endTageMatch) { // 删除已匹配完成的部分 advance(endTageMatch[0].length) // 匹配到开始标签,向外传递标签名和属性 end(endTageMatch[1]) continue } } // 如果是文本:将文本内容取出来并发射出去,并从html片段中截取掉 if (textEnd > 0) { let text = html.substring(0, textEnd) // 文本内容 if (text) { // 向外传递文本 chars(text) advance(text.length) // 解析到的文本 } } } return root }