前言
先正式讲解之前先看一张来自Vue官网的实例生命周期图。
由图中可知,当我们实例化一个Vue对象并完成初始化后,Vue会检查el和template属性,以获取模板字符串。然后将得到的模板编译成render函数。
只有当template未指定时,vue才会以所制定的el元素的outerHTML作为模板。
但如果这时我们还指定了自定义的render函数,vue不会再通过前两者去获取模板了。
本文重点聊聊vue是如何将通过template或者el获得的模板最终编译成render函数的。
将HTML模板解析为AST节点树
如上所述,vue在获取到模板字符串后,通过正则表达式对HTML模板逐字符解析,分别解析出元素节点以及每个元素节点上所设置的指令、attribute、事件绑定等,最终构建成一个完整描述HTML节点信息的AST节点树。
先认识一下几个核心的正则表达式和AST对象。
html字符匹配核心正则表达式
// 1. 匹配开始标签(不包括结尾的>),如匹配<div const startTagOpen = /^<((?:[a-zA-Z_][\-\.0-9_a-zA-Za-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]*\:)?[a-zA-Z_][\-\.0-9_a-zA-Za-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]*)/ // 2. 匹配普通html属性,如id="app" const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ // 3. 匹配动态属性 如v-for="(item, index) in roles",v-bind:src="imageSrc" :[key]="value" const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ 4. 匹配开始标签的尾部,如>或者/> const startTagClose = /^\s*(\/?)>/ // 5. 匹配闭合标签,如</div> const endTag = /^<\\/((?:[a-zA-Z_][\\-\\.0-9_a-zA-Z((?:[a-zA-Z_][\-\.0-9_a-zA-Za-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]*\:)?[a-zA-Z_][\-\.0-9_a-zA-Za-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]*)]*\\:)?[a-zA-Z_][\\-\\.0-9_a-zA-Z((?:[a-zA-Z_][\-\.0-9_a-zA-Za-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]*\:)?[a-zA-Z_][\-\.0-9_a-zA-Za-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]*)]*)[^>]*>/ // 6. 匹配DOCTYPE const doctype = /^<!DOCTYPE [^>]+>/i // 7. 匹配HTML注释 const comment = /^<!\--/ // 8. 匹配HTML条件注释 const conditionalComment = /^<!\[/
AST节点对象
下面一段代码是AST节点的创建函数。通过代码,我们可以对AST节点有个基本了解。简单来说,
AST节点是对HTML节点信息的描述。例如AST将HTML标签属性解析出来的内容保存到attrsList、attrsMap和rawAttrsMap中,在parent中保持对父节点的引用,通过children来指向自己的子节点。
{ // 节点类型 type: 1, // 标签名,如div tag: "div", // 节点所包含的属性 attrsList: [], attrsMap: {}, rawAttrsMap: {}, // 父节点指针 parent: undefined, // 子节点指针 children: [] }
简单标签解析
假定有如下模板:
<div id="app">{{msg}}</div>
具体的解析过程是通过几个核心正则表达式分别捕获到标签名以及属性的名称和值,然后使用这些信息创建AST节点对象,主要代码如下:
// 1. 解析标签和属性 function parseStartTag () { // 匹配开始标签 var start = html.match(startTagOpen); if (start) { var match = { tagName: start[1], attrs: [], start: index }; // 将指针向前移动 advance(start[0].length); var end, attr; // 匹配标签中的属性 while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) { attr.start = index; advance(attr[0].length); attr.end = index; match.attrs.push(attr); } if (end) { match.unarySlash = end[1]; advance(end[0].length); match.end = index; return match } } } ...省略其他代码 // 2. 创建AST节点对象 var element = createASTElement(tag, attrs, currentParent); ...省略其他代码
经过以上解析,得到如下AST节点对象:
{ attrsList: [{name: "id", value: "app", start: 5, end: 13}], attrsMap: {id: "app"}, children: [], end: 14, parent: undefined, rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}}, start: 0, tag: "div", type: 1 }
接下来需要处理的是div的子节点{{msg}}。由于其子节点是文本节点,这里使用parseText来处理文本节点,然后使用这些信息创建AST节点对象,主要代码如下:
// 1. 解析文本节点中的字符 function parseText ( text: string, delimiters?: [string, string] ): TextParseResult | void { // ...省略其他代码 const tokens = [] const rawTokens = [] // tagRE默认值为/\{\{((?:.|\r?\n)+?)\}\}/g,识别出文本中通过{{value}}插入的值 let lastIndex = tagRE.lastIndex = 0 let match, index, tokenValue while ((match = tagRE.exec(text))) { index = match.index if (index > lastIndex) { rawTokens.push(tokenValue = text.slice(lastIndex, index)) tokens.push(JSON.stringify(tokenValue)) } // 如果模板插值中使用了过滤器,需要先解析过滤器 const exp = parseFilters(match[1].trim()) tokens.push(`_s(${exp})`) rawTokens.push({ '@binding': exp }) lastIndex = index + match[0].length } if (lastIndex < text.length) { rawTokens.push(tokenValue = text.slice(lastIndex)) tokens.push(JSON.stringify(tokenValue)) } // 解析结果为expression和tokens组成的对象 return { expression: tokens.join('+'), tokens: rawTokens } } //...省略其他代码 // 2. 创建文本AST对象 child = { type: 2, expression: res.expression, tokens: res.tokens, text: text } child.start = start; child.end = end; //...省略其他代码 // 添加到div的子节点数组里 children.push(child);
文本节点解析后的结果如下:
{ type: 2, expression: "_s(message)", tokens: [{@binding: "message"}], text: "{{message}}" }
解析完子节点后的结果根节点div的AST更新为:
{ attrsList: [{name: "id", value: "app", start: 5, end: 13}] attrsMap: {id: "app"} children: [{type: 2, expression: "_s(msg)", tokens: [{@binding: "msg"}], text: "{{msg}}"}] end: 14 parent: undefined rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}} start: 0 tag: "div" type: 1 }
最后是解析闭合标签</div>。 当解析器匹配到闭合标签后,意味着一个标签的匹配结束了。
因为在标签中除了会使用id="app",placeholder="edit me"等HTML attribute外,还有很多是使用了vue的指令属性,如v-for,v-on,v-model。解析器会对前面已经生成的AST节点对象,进一步处理。最终这部分信息会以directives、on、domProps等属性的形式添加到AST对象上。
// 节点的收尾处理 function closeElement (element) { // 去除元素的空子节点 trimEndingWhitespace(element) // 处理AST,添加额外属性 if (!inVPre && !element.processed) { element = processElement(element, options) } // ... 省略其他代码 } // 给AST添加额外属性 function processElement ( element: ASTElement, options: CompilerOptions ) { // 给AST添加key属性 processKey(element) // 给AST添加plain属性 element.plain = ( !element.key && !element.scopedSlots && !element.attrsList.length ) // 处理v-ref,给AST对象添加ref属性 processRef(element) // 处理传递给组件的slot,给AST添加slotScope属性 processSlotContent(element) // 处理slot标签,给AST添加slotName属性 processSlotOutlet(element) // 给AST添加component或inlineTemplate属性 processComponent(element) // 处理 for (let i = 0; i < transforms.length; i++) { element = transforms[i](element, options) || element } // 根据属性的不同,给AST对象添加directives、events、props等属性 processAttrs(element) return element }
了解了简单标签解析过程,我们再来看下对于v-for、v-if、v-model、v-on几个常见指令的解析。