这一章咱们开始讲模板解析编译:总结来说就是通过compile
函数把tamplate
解析成render Function
模式的字符串compiler/index.js
import { parse } from './parser/index'import { optimize } from './optimizer'import { generate } from './codegen/index'import { createCompilerCreator } from './create-compiler'// `createCompilerCreator` allows creating compilers that use alternative// parser/optimizer/codegen, e.g the SSR optimizing compiler.// Here we just export a default compiler using the default parts.export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions): CompiledResult { const ast = parse(template.trim(), options) if (options.optimize !== false) { optimize(ast, options) } const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns }})
咱们能够看出createCompiler
函数外部运行的是parse
、optimize
、generate
三个函数,而生成的是ast
,render
,staticRenderFns
三个对象
parse
export function parse ( template: string, options: CompilerOptions): ASTElement | void { /** * 有自定义warn用自定义没有用根底: console.error(`[Vue compiler]: ${msg}`) */ warn = options.warn || baseWarn // 查看标签是否须要保留空格 platformIsPreTag = options.isPreTag || no // 查看属性是否应被绑定 platformMustUseProp = options.mustUseProp || no // 查看标记的名称空间 platformGetTagNamespace = options.getTagNamespace || no /** * 获取modules中的值 */ transforms = pluckModuleFunction(options.modules, 'transformNode') preTransforms = pluckModuleFunction(options.modules, 'preTransformNode') postTransforms = pluckModuleFunction(options.modules, 'postTransformNode') delimiters = options.delimiters const stack = [] // 是否保留elements间接的空白 const preserveWhitespace = options.preserveWhitespace !== false let root //return 进来的AST let currentParent //以后父节点 let inVPre = false let inPre = false let warned = false /** * 单次正告 */ function warnOnce (msg) { if (!warned) { warned = true warn(msg) } } function closeElement (element) { // check pre state if (element.pre) { inVPre = false } if (platformIsPreTag(element.tag)) { inPre = false } // apply post-transforms for (let i = 0; i < postTransforms.length; i++) { postTransforms[i](element, options) } } parseHTML(template, { warn, expectHTML: options.expectHTML, isUnaryTag: options.isUnaryTag, canBeLeftOpenTag: options.canBeLeftOpenTag, shouldDecodeNewlines: options.shouldDecodeNewlines, shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref, shouldKeepComment: options.comments, start (tag, attrs, unary) { // check namespace. // inherit parent ns if there is one /** * 查看命名空间。如果有父nanmespace,则继承父nanmespace */ const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag) // handle IE svg bug /* istanbul ignore if */ // IE的另类bug if (isIE && ns === 'svg') { attrs = guardIESVGBug(attrs) } // 返回应答的AST let element: ASTElement = createASTElement(tag, attrs, currentParent) if (ns) { element.ns = ns } /** * 不是服务段渲染的时候,template 应该只负责渲染UI局部 * 不应该蕴含syle, script 的标签 */ if (isForbiddenTag(element) && !isServerRendering()) { element.forbidden = true process.env.NODE_ENV !== 'production' && warn( 'Templates should only be responsible for mapping the state to the ' + 'UI. Avoid placing tags with side-effects in your templates, such as ' + `<${tag}>` + ', as they will not be parsed.' ) } // apply pre-transforms // 预处理 for (let i = 0; i < preTransforms.length; i++) { element = preTransforms[i](element, options) || element } if (!inVPre) { processPre(element) if (element.pre) { inVPre = true } } // 检测该标签是否须要保留空格 if (platformIsPreTag(element.tag)) { inPre = true } if (inVPre) { // 当不须要转译时 processRawAttrs(element) } else if (!element.processed) { // structural directives // 给AST加上v-for响应属性 processFor(element) // 给AST加上v-if v-else v-else-if相应属性 processIf(element) // 判断是否含有v-once processOnce(element) // element-scope stuff processElement(element, options) } function checkRootConstraints (el) { if (process.env.NODE_ENV !== 'production') { // 根标签不应该是slot和template if (el.tag === 'slot' || el.tag === 'template') { warnOnce( `Cannot use <${el.tag}> as component root element because it may ` + 'contain multiple nodes.' ) } // 根标签不应该含有v-for if (el.attrsMap.hasOwnProperty('v-for')) { warnOnce( 'Cannot use v-for on stateful component root element because ' + 'it renders multiple elements.' ) } } } // tree management // 赋值给跟标签 if (!root) { root = element // 用于查看根标签 checkRootConstraints(root) // 缓存中是否有值 } else if (!stack.length) { // allow root elements with v-if, v-else-if and v-else // 如果根元素有v-if, v-else-if and v-else 则打上响应记号 if (root.if && (element.elseif || element.else)) { checkRootConstraints(element) addIfCondition(root, { exp: element.elseif, block: element }) } else if (process.env.NODE_ENV !== 'production') { warnOnce( `Component template should contain exactly one root element. ` + `If you are using v-if on multiple elements, ` + `use v-else-if to chain them instead.` ) } } if (currentParent && !element.forbidden) { if (element.elseif || element.else) { processIfConditions(element, currentParent) } else if (element.slotScope) { // scoped slot // 解决slot, scoped传值 currentParent.plain = false const name = element.slotTarget || '"default"' ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element } else { currentParent.children.push(element) element.parent = currentParent } } // 解决是否是自闭标签 if (!unary) { currentParent = element stack.push(element) } else { closeElement(element) } }, end () { // remove trailing whitespace const element = stack[stack.length - 1] const lastNode = element.children[element.children.length - 1] if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) { element.children.pop() } // pop stack stack.length -= 1 currentParent = stack[stack.length - 1] closeElement(element) }, chars (text: string) { if (!currentParent) { if (process.env.NODE_ENV !== 'production') { /** * 当文本没有跟标签的时候 */ if (text === template) { warnOnce( 'Component template requires a root element, rather than just text.' ) } else if ((text = text.trim())) { /** * 须要跟标签的时候 */ warnOnce( `text "${text}" outside root element will be ignored.` ) } } return } // IE textarea placeholder bug /* istanbul ignore if */ /** * IE的神奇bug * 如果textarea具备占位符,则IE会触发输出事件 */ if (isIE && currentParent.tag === 'textarea' && currentParent.attrsMap.placeholder === text ) { return } const children = currentParent.children // 之前设置的是否须要保留空格 text = inPre || text.trim() // 当为true时是不是文本标签 ? isTextTag(currentParent) ? text : decodeHTMLCached(text) // only preserve whitespace if its not right after a starting tag : preserveWhitespace && children.length ? ' ' : '' if (text) { let res /** * 当不是原内容输入时 * 并且text不是空内容 * 且AST解析时有内容返回 */ if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) { children.push({ type: 2, expression: res.expression, tokens: res.tokens, text }) } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') { children.push({ type: 3, text }) } } }, comment (text: string) { currentParent.children.push({ type: 3, text, isComment: true }) } }) return root}
当咱们把代码折叠起来的话会看到parse
函数外面外围就是parseHTML
函数,他通过正则文法
和start
,end
,chars
,comment
四个钩子函数
来解析模板标签
的:
// Regular Expressions for parsing tags and attributes// 匹配attributesconst attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/// could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName// but for Vue templates we can enforce a simple charsetconst ncname = '[a-zA-Z_][\\w\\-\\.]*'const qnameCapture = `((?:${ncname}\\:)?${ncname})`/** * 匹配开始标签 * 例子:<XXXXXX */const startTagOpen = new RegExp(`^<${qnameCapture}`)/** * 匹配完结标签 * 例如(有多个空格的): /> or XXX> */const startTagClose = /^\s*(\/?)>//** * 很奇妙的匹配闭合标签的办法 * 例子 <ssss/>>>>>>> <aw/>>>>> */const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)const doctype = /^<!DOCTYPE [^>]+>/i// #7298: escape - to avoid being pased as HTML comment when inlined in pageconst comment = /^<!\--/const conditionalComment = /^<!\[/
这些正则文法
都是用来Vue
中匹配开始标签
,完结标签
,属性
,标签名
,正文
,文本
等
咱们晓得了parseHTML(html,options){}
承受俩个参数,咱们再来看一下parseHTML
中是如何去匹配的:
export function parseHTML (html, options) { const stack = [] const expectHTML = options.expectHTML const isUnaryTag = options.isUnaryTag || no const canBeLeftOpenTag = options.canBeLeftOpenTag || no let index = 0 let last, lastTag while (html) { last = html // Make sure we're not in a plaintext content element like script/style // 如果没有lastTag,并确保咱们不是在一个纯文本内容元素中:script、style、textarea if (!lastTag || !isPlainTextElement(lastTag)) { // 查找<的地位 let textEnd = html.indexOf('<') // 当是第一个的时候 if (textEnd === 0) { // Comment: // 匹配正文文本 if (comment.test(html)) { const commentEnd = html.indexOf('-->') if (commentEnd >= 0) { // 当要贮存正文时 if (options.shouldKeepComment) { options.comment(html.substring(4, commentEnd)) } advance(commentEnd + 3) continue } } // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment // 兼容另类正文 例子:<![if!IE]> if (conditionalComment.test(html)) { const conditionalEnd = html.indexOf(']>') if (conditionalEnd >= 0) { advance(conditionalEnd + 2) continue } } // Doctype: // <!doctype> 这类结尾 const doctypeMatch = html.match(doctype) if (doctypeMatch) { advance(doctypeMatch[0].length) continue } // End tag: // 匹配完结标签 const endTagMatch = html.match(endTag) if (endTagMatch) { const curIndex = index advance(endTagMatch[0].length) parseEndTag(endTagMatch[1], curIndex, index) continue } // Start tag: /** * 获取标签里的match对象 */ const startTagMatch = parseStartTag() if (startTagMatch) { handleStartTag(startTagMatch) // 是否须要须要新的一行 if (shouldIgnoreFirstNewline(lastTag, html)) { advance(1) } continue } } let text, rest, next if (textEnd >= 0) { /** * 接下来判断 textEnd 是否大于等于 0 的,满足则阐明到从以后地位到 textEnd 地位都是文本 * 并且如果 < 是纯文本中的字符,就持续找到真正的文本完结的地位,而后后退到完结的地位。 */ rest = html.slice(textEnd) while ( !endTag.test(rest) && !startTagOpen.test(rest) && !comment.test(rest) && !conditionalComment.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) advance(textEnd) } // html解析完结了 if (textEnd < 0) { text = html html = '' } if (options.chars && text) { options.chars(text) } } else { let endTagLength = 0 const stackedTag = lastTag.toLowerCase() const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i')) const rest = html.replace(reStackedTag, function (all, text, endTag) { endTagLength = endTag.length if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') { text = text .replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298 .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1') } if (shouldIgnoreFirstNewline(stackedTag, text)) { text = text.slice(1) } if (options.chars) { options.chars(text) } return '' }) index += html.length - rest.length html = rest parseEndTag(stackedTag, index - endTagLength, index) } if (html === last) { options.chars && options.chars(html) if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) { options.warn(`Mal-formatted tag at end of template: "${html}"`) } break } } // Clean up any remaining tags parseEndTag() /** * 截取html * 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, attr /** * 当不是完结标签时 * 并记录attribute * 例如:<div @click="test"></div> 中的@click="test" * tip: match */ while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) { advance(attr[0].length) match.attrs.push(attr) } /** * 当匹配到完结标签时 * 返回存进去的match对象 */ 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 /** * 是否是对于web的构建 */ if (expectHTML) { /** * 如果以后的tag不能被p标签蕴含的的时候就先完结p标签 */ if (lastTag === 'p' && isNonPhrasingTag(tagName)) { parseEndTag(lastTag) } /** * 是不是不闭合的标签 * 例子: tr td */ if (canBeLeftOpenTag(tagName) && lastTag === tagName) { parseEndTag(tagName) } } /** * 是不是自闭和标签的时候 * 例子: <img> */ const unary = isUnaryTag(tagName) || !!unarySlash // 获取属性长度属性 const l = match.attrs.length const attrs = new Array(l) // 属性解决 for (let i = 0; i < l; i++) { const args = match.attrs[i] // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778 // FF上的很奇怪的bug if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) { if (args[3] === '') { delete args[3] } if (args[4] === '') { delete args[4] } if (args[5] === '') { delete args[5] } } const value = args[3] || args[4] || args[5] || '' // a标签是否须要解码 !import const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href' ? options.shouldDecodeNewlinesForHref : options.shouldDecodeNewlines attrs[i] = { name: args[1], // 解码 value: decodeAttr(value, shouldDecodeNewlines) } } /** * 当不是闭合标签的时候缓存该标签用于之后的循环 */ if (!unary) { stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs }) lastTag = tagName } /** * 当有start函数时 * 次要是对v-for,v-if, v-else-if,v-else,slot,scoped的解决 * 检测根标签 */ if (options.start) { options.start(tagName, attrs, unary, match.start, match.end) } } function parseEndTag (tagName, start, end) { let pos, lowerCasedTagName if (start == null) start = index if (end == null) end = index if (tagName) { lowerCasedTagName = tagName.toLowerCase() } // Find the closest opened tag of the same type if (tagName) { for (pos = stack.length - 1; pos >= 0; pos--) { if (stack[pos].lowerCasedTag === lowerCasedTagName) { break } } } else { // If no tag name is provided, clean shop pos = 0 } if (pos >= 0) { // Close all the open elements, up the stack for (let i = stack.length - 1; i >= pos; i--) { if (process.env.NODE_ENV !== 'production' && (i > pos || !tagName) && options.warn ) { options.warn( `tag <${stack[i].tag}> has no matching end tag.` ) } if (options.end) { options.end(stack[i].tag, start, end) } } // Remove the open elements from the stack stack.length = pos lastTag = pos && stack[pos - 1].tag } else if (lowerCasedTagName === 'br') { if (options.start) { options.start(tagName, [], true, start, end) } } else if (lowerCasedTagName === 'p') { if (options.start) { options.start(tagName, [], false, start, end) } if (options.end) { options.end(tagName, start, end) } } }}
参考 前端进阶面试题具体解答
所以整个parseHTML中的流程总结为:
- 首先通过
while (html)
去循环判断html
内容是否存在。 - 再判断文本内容是否在
script/style
标签中 - 上述条件都满足的话,开始解析
html
字符串纸上得来终觉浅,绝知此事要躬行
,那我么来实操一下如何解析一段字符串吧:
//此为测试所用节点信息<div id="app"> <!-- Hello 正文 --> <div v-if="show" class="message">{{message}}</div></div>
开始解析:
// Start tag://获取标签里的match对象const startTagMatch = parseStartTag()if (startTagMatch) { handleStartTag(startTagMatch)// 是否须要须要新的一行 if (shouldIgnoreFirstNewline(lastTag, html)) { advance(1) } continue}
那么咱们持续来看一下parseStartTag
,handleStartTag
两个函数别离实现了啥性能:
function parseStartTag () { //判断html中是否存在开始标签 const start = html.match(startTagOpen); // 定义 match 构造 if (start) { const match = { tagName: start[1], // 标签名 attrs: [], // 属性 start: index // 开始地位 } // 去除标签名 advance(start[0].length) let end, attr /** * 当不是完结标签时 * 并记录attribute * 例如:<div @click="test"></div> 中的@click="test" * tip: match */ while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) { advance(attr[0].length) match.attrs.push(attr) } /** * 当匹配到完结标签时 * 返回存进去的match对象 */ if (end) { match.unarySlash = end[1] advance(end[0].length) match.end = index return match } } }
咱们再来看看解析过程中是如何一个字符一个字符的匹配html
字符串的:
/** * 截取html * index记录多少个 */function advance (n) { index += n html = html.substring(n)}
//通过传入变量n
来截取字符串
,这也是Vue
解析的重要办法,通过一直地宰割html
字符串,一步步实现对他的解析过程。
那么咱们再回到parseStartTag
上,首先开始匹配开始标签那入栈的是
{ attrs: [ { 0: " id="app"", 1: "id", 2: "=", 3: "app", 4: undefined, 5: undefined, end: 13, groups: undefined, index: 0, input: " id="app">↵ <!-- 正文 -->↵ <div v-if="show" class="message">{{message}}</div>↵ </div>", start: 4, } ], end: 14, start: 0, tagName: "div", unarySlash: "",}
//目前代码<!-- 正文 --> <div v-if="show" class="message">{{message}}</div></div>
再者匹配到正文:
// 匹配正文文本if (comment.test(html)) { const commentEnd = html.indexOf('-->') if (commentEnd >= 0) { // 当要贮存正文时 if (options.shouldKeepComment) { options.comment(html.substring(4, commentEnd)) } advance(commentEnd + 3) continue }}
解决成:
//目前代码 <div v-if="show" class="message">{{message}}</div></div>
而后持续解决标签节点<div v-if="show" class="message">
,再解决{{message}}
之后模板变成
//目前代码 </div></div>
看tamplate
曾经是只剩下完结标签了,那么毫无疑问就会走到parseEndTag
函数:
// End tag:// 匹配完结标签const endTagMatch = html.match(endTag)if (endTagMatch) { const curIndex = index advance(endTagMatch[0].length) parseEndTag(endTagMatch[1], curIndex, index) continue}
那么在handStartTag
与handEndTag
中别离调用了options.start
options.end
钩子函数,而在start钩子函数中间接调用createASTElement
函数(语法分析阶段):
export function createASTElement ( tag: string, attrs: Array<Attr>, parent: ASTElement | void): ASTElement { return { type: 1, tag, attrsList: attrs, attrsMap: makeAttrsMap(attrs), parent, children: [] }}......start(){ ...... //创立ast根底对象 let element: ASTElement = createASTElement(tag, attrs, currentParent); ...... 解决服务端渲染 预处理一些动静类型:v-model 对vue的指令进行解决v-pre、v-if、v-for、v-once、slot、key、ref 限度解决根节点不能是slot,template,v-for这类标签 解决是否是自闭标签}
那么就解析完了整个tamplate
变成了AST:
{ "type": 0, "children": [ { "type": 1, "ns": 0, "tag": "div", "tagType": 0, "props": [ { "type": 6, "name": "id", "value": { "type": 2, "content": "app", "loc": { "start": { "column": 9, "line": 1, "offset": 8 }, "end": { "column": 14, "line": 1, "offset": 13 }, "source": "\"app\"" } }, "loc": { "start": { "column": 6, "line": 1, "offset": 5 }, "end": { "column": 14, "line": 1, "offset": 13 }, "source": "id=\"app\"" } } ], "isSelfClosing": false, "children": [ { "type": 1, "ns": 0, "tag": "div", "tagType": 0, "props": [ { "type": 7, "name": "if", "exp": { "type": 4, "content": "show", "isStatic": false, "isConstant": false, "loc": { "start": { "column": 16, "line": 3, "offset": 52 }, "end": { "column": 20, "line": 3, "offset": 56 }, "source": "show" } }, "modifiers": [], "loc": { "start": { "column": 10, "line": 3, "offset": 46 }, "end": { "column": 21, "line": 3, "offset": 57 }, "source": "v-if=\"show\"" } }, { "type": 6, "name": "class", "value": { "type": 2, "content": "message", "loc": { "start": { "column": 28, "line": 3, "offset": 64 }, "end": { "column": 37, "line": 3, "offset": 73 }, "source": "\"message\"" } }, "loc": { "start": { "column": 22, "line": 3, "offset": 58 }, "end": { "column": 37, "line": 3, "offset": 73 }, "source": "class=\"message\"" } } ], "isSelfClosing": false, "children": [ { "type": 5, "content": { "type": 4, "isStatic": false, "isConstant": false, "content": "message", "loc": { "start": { "column": 40, "line": 3, "offset": 76 }, "end": { "column": 47, "line": 3, "offset": 83 }, "source": "message" } }, "loc": { "start": { "column": 38, "line": 3, "offset": 74 }, "end": { "column": 49, "line": 3, "offset": 85 }, "source": "{{message}}" } } ], "loc": { "start": { "column": 5, "line": 3, "offset": 41 }, "end": { "column": 55, "line": 3, "offset": 91 }, "source": "<div v-if=\"show\" class=\"message\">{{message}}</div>" } } ], "loc": { "start": { "column": 1, "line": 1, "offset": 0 }, "end": { "column": 7, "line": 4, "offset": 98 }, "source": "<div id=\"app\">\n <!-- Hello 正文 -->\n <div v-if=\"show\" class=\"message\">{{message}}</div>\n</div>" } } ], "helpers": [], "components": [], "directives": [], "hoists": [], "imports": [], "cached": 0, "temps": 0, "loc": { "start": { "column": 1, "line": 1, "offset": 0 }, "end": { "column": 7, "line": 4, "offset": 98 }, "source": "<div id=\"app\">\n <!-- Hello 正文 -->\n <div v-if=\"show\" class=\"message\">{{message}}</div>\n</div>" }}
咱们也能够去AST Explorer下面去尝试
这是tamplate
通过解析的第一步,生成了一个AST
对象,那么此章节到这里就完了