从 vue 源码看问题 — vue 编译器的解析

前言

Vue 编译器主要处理内容

  • 将组件的 html 模版解析成 AST 对象
  • 优化
    • 通过遍历 AST 对象,为每个节点做 静态标记,通过标记其是否为静态节点,然后进一步标记出 静态根节点,方便在后续更新过程中跳过这些静态节点
    • 标记静态根用于生成渲染函数阶段,生成静态根节点的渲染函数
  • AST 生成运行渲染函数
    • render 函数
    • staticRenderFns 数组,里面保存了所有的 静态节点的渲染函数

编译器的解析过程是如何将 html 字符串模版变成 AST 对象?

  • 遍历 HTML 模版字符串,通过正则表达式匹配 "<"
  • 跳过某些不需要处理的标签,比如:注释标签 <!-- xxx -->、条件注释标签 <!--[if IE]><!DOCTYPE html>
  • 解析开始标签
      1. 解析得到一个对象,包括标签名(tagName)、所有的属性(attrs)、标签在 html 模版字符串中的索引位置
      1. 接着处理上一步的 attrs 属性,将其变成 [{ name: attrName, value: attrVal, start: xx, end: xx }, ...] 的形式
      1. 通过标签名、属性对象和当前元素的父元素生成 AST 对象(普通的 JS 对象),通过 key、value 的形式记录了该元素的一些信息
      1. 接下来进一步处理开始标签上的一些指令,比如 v-pre、v-for、v-if、v-once,并将处理结果放到 AST 对象上
      1. 步骤(2、3、4)处理结束后将 ast 对象保存到 stack 数组中
      1. 之前的所有处理完成后,会截断 html 字符串,将已经处理掉的字符串截掉
  • 解析闭合标签
    • 如果匹配到结束标签,就从 stack 数组中拿出最后一个元素,它和当前匹配到的结束标签是一对
    • 再次处理开始标签上的属性,这些属性和前面处理的不一样,比如:key、ref、scopedSlot、样式 等,并将处理结果放到元素的 AST 对象
    • 然后将当前元素和父元素产生关联,给当前元素的 ast 对象设置 parent 属性,然后将自己放到父元素的 ast 对象的 children 数组中
  • 最后遍历完整个 html 模版字符串以后,返回 ast 对象

深入源码

编译器入口 —— Vue.prototype.$mount

文件位置:src\platforms\web\entry-runtime-with-compiler.js

这里重点在于获取动态渲染函数 render 函数和静态渲染函数 staticRenderFnscompileToFunctions 方法.

// 保存原来的 Vue.prototype.$mount 方法
const mount = Vue.prototype.$mount/*重写 Vue.prototype.$mount问题:当一个配置项中存在 el、template、render 选项时,它们的优先级是怎样的?回答:源码中从上到下的处理顺序,决定了它们的优先级为:render > template > el
*/ 
Vue.prototype.$mount = function (el?: string | Element,hydrating?: boolean
): Component {/*el 有值,则通过 query 方法获取对应的 dom 元素1. el 是 string,则通过 document.querySelector(el) 获取 dom 元素- 获取到 dom 元素就直接返回 dom- 无法获取到 dom 元素就进行警告提示,并返回 document.createElement('div') 2. el 不是 string,则直接返回 el 本身*/ el = el && query(el)/* istanbul ignore if */// el 不能是 body 元素 和 html 元素if (el === document.body || el === document.documentElement) {process.env.NODE_ENV !== 'production' && warn(`Do not mount Vue to <html> or <body> - mount to normal elements instead.`)return this}//  获取配置选项const options = this.$options// resolve template/el and convert to render function// 当前配置选项中不存在 render 选项if (!options.render) {// 获取 template 模板let template = options.template// template 存在if (template) {// template 为 stringif (typeof template === 'string') {// 字符串以 # 开头,代表是 id 选择器if (template.charAt(0) === '#') {// 获取 dom 元素对应的 innerHtml 字符内容template = idToTemplate(template)/* istanbul ignore if */// template 选项不能为空字符串if (process.env.NODE_ENV !== 'production' && !template) {warn(`Template element not found or is empty: ${options.template}`,this)}}} else if (template.nodeType) {// 代表是一个 dom 元素,取出 dom 元素的 innerHTML 内容template = template.innerHTML} else {// 其他类型则不属于有效的 template 选项if (process.env.NODE_ENV !== 'production') {warn('invalid template option:' + template, this)}return this}} else if (el) {// template 不存在,直接使用 el 对应的 dom 元素作为 template 模板 template = getOuterHTML(el)}if (template) {/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {mark('compile')}// 获取对应的动态渲染函数 render 函数和静态渲染函数 staticRenderFns const { render, staticRenderFns } = compileToFunctions(template, {// 在非生产环境下,编译时记录标签属性在模版字符串中开始和结束的位置索引outputSourceRange: process.env.NODE_ENV !== 'production',shouldDecodeNewlines,shouldDecodeNewlinesForHref,// 界定符,默认 {{}}delimiters: options.delimiters,// 是否保留注释comments: options.comments}, this)// 将 render 和 staticRenderFns 分别保存到配置选项上options.render = renderoptions.staticRenderFns = staticRenderFns/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {mark('compile end')measure(`vue ${this._name} compile`, 'compile', 'compile end')}}}// 通过调用前面保存 mount 方法return mount.call(this, el, hydrating)
}

compileToFunctions() 方法

文件位置:src\compiler\to-function.js

这里的重点是 createCompileToFunctionFn 方法的入参 compile 函数.

/* 1、如果缓存中有编译结果,直接返回缓存的编译内容2、执行编译函数 compile,得到编译结果 compiled 3、处理编译期间出现的所有 error 和 tip,分别输出到控制台 4、将编译得到的字符串代码通过 new Function(codeStr) 转换成可执行的函数即 动态渲染函数 render 和 静态渲染函数 staticRenderFns5、缓存编译结果
*/
export function createCompileToFunctionFn (compile: Function): Function {const cache = Object.create(null)return function compileToFunctions (// 模板字符串template: string,// 编译选项options?: CompilerOptions,// 组件实例vm?: Component): CompiledFunctionResult {// 复制配置选项options = extend({}, options)// 日志const warn = options.warn || baseWarndelete options.warn/* istanbul ignore if */if (process.env.NODE_ENV !== 'production') {// detect possible CSP restrictiontry {new Function('return 1')} catch (e) {if (e.toString().match(/unsafe-eval|CSP/)) {warn('It seems you are using the standalone build of Vue.js in an ' +'environment with Content Security Policy that prohibits unsafe-eval. ' +'The template compiler cannot work in this environment. Consider ' +'relaxing the policy to allow unsafe-eval or pre-compiling your ' +'templates into render functions.')}}}// 定义缓存对应的 keyconst key = options.delimiters? String(options.delimiters) + template: template// 如果缓存中有编译结果,直接获取缓存的内容if (cache[key]) {return cache[key]}// 通过执行 compile 编译函数,得到编译结果const compiled = compile(template, options)// 检查编译结果中所有的 errors 和 tips,并输出到控制台if (process.env.NODE_ENV !== 'production') {if (compiled.errors && compiled.errors.length) {if (options.outputSourceRange) {compiled.errors.forEach(e => {warn(`Error compiling template:\n\n${e.msg}\n\n` +generateCodeFrame(template, e.start, e.end),vm)})} else {warn(`Error compiling template:\n\n${template}\n\n` +compiled.errors.map(e => `- ${e}`).join('\n') + '\n',vm)}}if (compiled.tips && compiled.tips.length) {if (options.outputSourceRange) {compiled.tips.forEach(e => tip(e.msg, vm))} else {compiled.tips.forEach(msg => tip(msg, vm))}}}// turn code into functionsconst res = {}const fnGenErrors = []/* 编译结果中 compiled.render 是一个可执行函数的字符串形式需要通过 createFunction 方法将 compiled.render 字符串变成一个真正可执行的函数本质就是通过 new Function(code) 的形式将字符串转换成函数*/// 动态渲染函数res.render = createFunction(compiled.render, fnGenErrors)// 静态渲染函数res.staticRenderFns = compiled.staticRenderFns.map(code => {return createFunction(code, fnGenErrors)})// check function generation errors.// this should only happen if there is a bug in the compiler itself.// mostly for codegen development use/* istanbul ignore if */if (process.env.NODE_ENV !== 'production') {if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {warn(`Failed to generate render function:\n\n` +fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),vm)}}// 缓存编译结果return (cache[key] = res)}
}

compile() 方法

文件位置:src\compiler\create-compiler.js

这里的中调就是调用核心编译函数 baseCompile,传递模版字符串和最终的编译选项,得到编译结果.

export function createCompilerCreator (baseCompile: Function): Function {return function createCompiler (baseOptions: CompilerOptions) {/* 编译函数:1、选项合并,将 options 配置项合并到 finalOptions(baseOptions) 中,得到最终的编译配置对象2、调用核心编译器 baseCompile 得到编译结果3、将编译期间产生的 error 和 tip 挂载到编译结果上4、返回编译结果*/function compile (// 模板字符串template: string,// 编译选项options?: CompilerOptions): CompiledResult {// 以平台特有的编译配置为原型,创建编译选项对象const finalOptions = Object.create(baseOptions)const errors = []const tips = []// 日志,负责记录 error 和 tiplet warn = (msg, range, tip) => {(tip ? tips : errors).push(msg)}// 如果存在编译选项,合并 options 和 baseOptionsif (options) {if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {// $flow-disable-lineconst leadingSpaceLength = template.match(/^\s*/)[0].length// 增强 日志 方法warn = (msg, range, tip) => {const data: WarningMessage = { msg }if (range) {if (range.start != null) {data.start = range.start + leadingSpaceLength}if (range.end != null) {data.end = range.end + leadingSpaceLength}}(tip ? tips : errors).push(data)}}// 合并自定义 modules 到 finalOptions 中if (options.modules) {finalOptions.modules =(baseOptions.modules || []).concat(options.modules)}// 合并自定义 directives 到 finalOptions 中if (options.directives) {finalOptions.directives = extend(Object.create(baseOptions.directives || null),options.directives)}// 除了 modules 和 directives,将其它配置项拷贝到 finalOptions 中for (const key in options) {if (key !== 'modules' && key !== 'directives') {finalOptions[key] = options[key]}}}finalOptions.warn = warn// 调用核心编译函数 baseCompile,传递模版字符串和最终的编译选项,得到编译结果const compiled = baseCompile(template.trim(), finalOptions)if (process.env.NODE_ENV !== 'production') {detectErrors(compiled.ast, warn)}// 将编译期间产生的错误和提示挂载到编译结果上compiled.errors = errorscompiled.tips = tips// 返回编译结果return compiled}return {compile,compileToFunctions: createCompileToFunctionFn(compile)}}
}

baseOptions 配置
文件位置:src\platforms\web\compiler\options.js

export const baseOptions: CompilerOptions = {expectHTML: true,// 负责 class、style、v-modelmodules,// 指令directives,// pre 标签isPreTag,// 是否是一元标签isUnaryTag,// 必须用于 props 的属性mustUseProp,// 只有开始标签的标签canBeLeftOpenTag,// 保留标签isReservedTag,// 命名空间getTagNamespace,// 静态 keystaticKeys: genStaticKeys(modules)
}

baseCompile() 方法

文件位置:src\compiler\index.js

这里的重点就是通过 parse 方法将 html 模版字符串解析成 ast.

/*在这之前做的所有的事情,只是为了构建平台特有的编译选项(options),比如 web 平台1、将 html 模版字符串解析成 ast2、对 ast 树进行静态标记3、将 ast 生成渲染函数- 静态渲染函数放到 code.staticRenderFns 数组中- 动态渲染函数 code.render- 在将来渲染时执行渲染函数能够得到 vnode*/
export const createCompiler = createCompilerCreator(function baseCompile(template: string,options: CompilerOptions
): CompiledResult {/* 将模版字符串解析为 AST 语法树每个节点的 ast 对象上都设置了元素的所有信息,如,标签信息、属性信息、插槽信息、父节点、子节点等*/const ast = parse(template.trim(), options)/*优化,遍历 AST,为每个节点做静态标记- 标记每个节点是否为静态节点,,保证在后续更新中跳过这些静态节点- 标记出静态根节点,用于生成渲染函数阶段,生成静态根节点的渲染函数*/if (options.optimize !== false) {optimize(ast, options)}/*从 AST 语法树生成渲染函数如:code.render = "_c('div',{attrs:{"id":"app"}},_l((arr),function(item){return _c('div',{key:item},[_v(_s(item))])}),0)"*/const code = generate(ast, options)return {ast,render: code.render,staticRenderFns: code.staticRenderFns}
})

parse() 方法

文件位置:src\compiler\parser\index.js

这里 parse 方法中定义了很多方法,这些方法是在parseHTMLOptions 中有使用到,因此在这里不提前做解读,同时也意味着这里的重点就在于 parseHTML(template, parseHTMLOptions) 方法.

/*** 将 HTML 字符串转换为 AST*/
export function parse (// 模板字符串template: string,// 编译选项options: CompilerOptions
): ASTElement | void {// 日志 warn = options.warn || baseWarn// 是否为 pre 标签platformIsPreTag = options.isPreTag || no// 必须使用 props 进行绑定的属性platformMustUseProp = options.mustUseProp || no// 是否为命名空间platformGetTagNamespace = options.getTagNamespace || no// 是否为保留标签(html + svg)const isReservedTag = options.isReservedTag || no// 判断一个元素是否为一个组件maybeComponent = (el: ASTElement) => !!(el.component ||el.attrsMap[':is'] ||el.attrsMap['v-bind:is'] ||!(el.attrsMap.is ? isReservedTag(el.attrsMap.is) : isReservedTag(el.tag)))/*分别获取 options.modules 中的 transformNode、preTransformNode、postTransformNode 方法负责处理元素节点上的 class、style、v-model*/ transforms = pluckModuleFunction(options.modules, 'transformNode')preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')// 界定符,比如: {{}}delimiters = options.delimiters// 存放解析的中间结果const stack = []// 是否保留空白const preserveWhitespace = options.preserveWhitespace !== false// 获取空白选项const whitespaceOption = options.whitespace// 根节点 root,之后处理的节点都会按照层级挂载到 root 下,最后 return 得到的就是 root,也就是 ast 语法树let root// 当前元素的父元素let currentParentlet inVPre = falselet inPre = falselet warned = falsefunction warnOnce (msg, range){xxx}function closeElement (element){xxx}function trimEndingWhitespace (el){xxx}function checkRootConstraints (el){xxx}// 解析 html 模版字符串,处理所有标签以及标签上的属性,// 这里 parseHTMLOptions 在后面处理过程中用到,再进一步解析parseHTML(template, parseHTMLOptions);// 返回生成的 ast 对象 return root}

parseHTML() 方法

文件位置:src\compiler\parser\html-parser.js

parseHTML 方法中主要涉及到了以下几个方法:

  • advance
  • parseStartTag
  • handleStartTag
  • parseEndTag

export function parseHTML(html, options) {const stack = []const expectHTML = options.expectHTML// 是否是自闭合标签const isUnaryTag = options.isUnaryTag || no// 是否可以只有开始标签const canBeLeftOpenTag = options.canBeLeftOpenTag || no// 记录当前在原始 html 字符串中的开始位置let index = 0let last, lastTagwhile (html) {last = html// 确保不会在 script、style、textarea 这样的纯文本内容元素中if (!lastTag || !isPlainTextElement(lastTag)) {// 找第一个 < 字符let textEnd = html.indexOf('<')/* textEnd === 0 说明在开头找到了分别处理可能找到的注释标签、条件注释标签、Doctype、开始标签、结束标签每处理完一种情况,就会截断(continue)循环,并且重置 html 字符串,将处理过的标签截掉,下一次循环处理剩余的 html 字符串模版*/if (textEnd === 0) {// 处理注释标签,如:<!-- xxx -->if (comment.test(html)) {// 找到注释节点结束标签的索引const commentEnd = html.indexOf('-->')if (commentEnd >= 0) {// 是否保留注释节点if (options.shouldKeepComment) {// options.comment(注释内容, 注释的开始索引, 结束索引)options.comment(html.substring(4, commentEnd),index,index + commentEnd + 3,)}// 调整 html 和 index 变量advance(commentEnd + 3)continue}}/*处理条件注释标签:<!--[if IE]>http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment*/if (conditionalComment.test(html)) {// 获取条件注释的结束索引const conditionalEnd = html.indexOf(']>')if (conditionalEnd >= 0) {// 调整 html 和 index 变量advance(conditionalEnd + 2)continue}}// 处理 Doctype ,规则为 /^<!DOCTYPE [^>]+>/i 如:<!DOCTYPE html>const doctypeMatch = html.match(doctype)if (doctypeMatch) {advance(doctypeMatch[0].length)continue}// End tag: 处理结束标签,如 </div>const endTagMatch = html.match(endTag)if (endTagMatch) {const curIndex = indexadvance(endTagMatch[0].length)// 处理结束标签parseEndTag(endTagMatch[1], curIndex, index)continue}// Start tag: 处理开始标签,比如处理 <div></div>// startTagMatch = { tagName: 'div', attrs: [[xx], ...], start: index }const startTagMatch = parseStartTag()if (startTagMatch) {// 进一步处理上一步得到结果,并最后调用 options.start 方法// 真正的解析工作都是在这个 start 方法中做的handleStartTag(startTagMatch)if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {advance(1)}continue}}let text, rest, nextif (textEnd >= 0) {/* 能走到这儿,说明虽然在 html 中匹配到到了 <xx,但是又不属于上述的几种情况,它就只是一段普通文本,如:<文本内容于是从 html 中找到下一个 <,直到 <xxx 是上述几种情况的标签,则结束在这整个过程中会一直调整 textEnd 的值,作为 html 中下一个有效标签的开始位置截取 html 模版字符串中 textEnd 之后的内容,rest = <xx*/rest = html.slice(textEnd)// 当前 while 循环就是处理 <xx 之后的纯文本情况// 截取文本内容,并找到有效标签的开始位置(textEnd)while (!endTag.test(rest) &&!startTagOpen.test(rest) &&!comment.test(rest) &&!conditionalComment.test(rest)) {// 认为 < 后面的内容为纯文本,然后在这些纯文本中再次找 <next = rest.indexOf('<', 1)// 如果没找到 <,则直接结束循环if (next < 0) break// 走到这儿说明在后续的字符串中找到了 <,索引位置为 textEndtextEnd += next// 截取 html 字符串模版 textEnd 之后的内容赋值给 rest,继续判断之后的字符串是否存在标签rest = html.slice(textEnd)}text = html.substring(0, textEnd)}// 如果 textEnd < 0,说明 html 中就没找到 <,那说明 html 就是一段文本if (textEnd < 0) {text = html}// 将文本内容从 html 模版字符串上截取掉if (text) {advance(text.length)}/* 处理文本基于文本生成 ast 对象,然后将该 ast 放到它的父元素中,即 currentParent.children 数组中*/if (options.chars && text) {options.chars(text, index - text.length, index)}} else {let endTagLength = 0// 处理 script、style、textarea 标签的闭合标签const stackedTag = lastTag.toLowerCase()// 开始标签的小写形式const reStackedTag =reCache[stackedTag] ||(reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)','i',))// 匹配并处理开始标签和结束标签之间的所有文本,比如 <script>xx</script>const rest = html.replace(reStackedTag, function (all, text, endTag) {endTagLength = endTag.lengthif (!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.lengthhtml = restparseEndTag(stackedTag, index - endTagLength, index)}// 正常要处理结束,如果 stack 数组中还有内容,则说明有标签没有被闭合,给出提示信息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}"`, {start: index + html.length,})}break}}// Clean up any remaining tagsparseEndTag()/*重置 html,html = 从索引 n 位置开始的向后的所有字符index 为 html 在原始的模版字符串中的结束索引,也是下一次该处理的字符的开始位置*/function advance(n) {index += nhtml = html.substring(n)}// 处理开始标签function parseStartTag() {// 匹配开始标签,规则为 /^<${qnameCapture}/const start = html.match(startTagOpen)if (start) {const match = {tagName: start[1], // 标签名attrs: [], // 属性,占位符start: index, // 标签的开始位置}/*调整 html 和 index,比如当前匹配到的开始标签内容为 start[0] = '<div',则:1. html = ' id="app">'2. index = start[0].length*/advance(start[0].length)let end, attr// 处理开始标签上的各个属性,并将这些属性放到 match.attrs 数组中while (!(end = html.match(startTagClose)) &&(attr = html.match(dynamicArgAttribute) || html.match(attribute))) {attr.start = indexadvance(attr[0].length)attr.end = indexmatch.attrs.push(attr)}// 开始标签的结束,end = '>' 或 end = ' />'if (end) {match.unarySlash = end[1]advance(end[0].length)match.end = indexreturn match}}}/*进一步处理开始标签的解析结果 ——— match 对象1. 处理属性 match.attrs,如果不是自闭合标签,则将标签信息放到 stack 数组,待将来处理到它的闭合标签时再将其弹出 stack,表示该标签处理完毕,此时标签的所有信息都在 element ast 对象上2. 接下来调用 options.start 方法处理标签,并根据标签信息生成 element ast,以及处理开始标签上的属性和指令,最后将 element ast 放入 stack 数组match = { tagName: 'div', attrs: [[xx], ...], start: index }*/function handleStartTag(match) {// 获取标签名const tagName = match.tagName// 一元斜线,如:/>const unarySlash = match.unarySlashif (expectHTML) {if (lastTag === 'p' && isNonPhrasingTag(tagName)) {parseEndTag(lastTag)}if (canBeLeftOpenTag(tagName) && lastTag === tagName) {parseEndTag(tagName)}}// 根据 标签名 或 一元斜线 unarySlash 判断是否是自闭合标签,比如 <hr />const unary = isUnaryTag(tagName) || !!unarySlashconst l = match.attrs.lengthconst attrs = new Array(l)// 处理 match.attrs,得到 attrs = [{ name: attrName, value: attrVal, start: xx, end: xx }, ...]// 比如处理 <div id="app"></div> ,则 attrs = [{ name: 'id', value: 'app', start: xx, end: xx }, ...]for (let i = 0; i < l; i++) {const args = match.attrs[i]// 比如:args[3] => 'id',args[4] => '=',args[5] => 'app'const value = args[3] || args[4] || args[5] || ''const shouldDecodeNewlines =tagName === 'a' && args[1] === 'href'? options.shouldDecodeNewlinesForHref: options.shouldDecodeNewlines// attrs[i] = { id: 'app' }attrs[i] = {name: args[1],value: decodeAttr(value, shouldDecodeNewlines),}// 非生产环境,记录属性的开始和结束索引if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {attrs[i].start = args.start + args[0].match(/^\s*/).lengthattrs[i].end = args.end}}/*1. 如果不是自闭合标签,则将标签信息放到 stack 数组中,待将来处理到它的闭合标签时再将其弹出 stack2. 如果是自闭合标签,则标签信息就没必要进入 stack 了,直接处理众多属性,将它们都设置到 element ast 对象上,就可以跳过处理结束标签的过程,这一步在处理开始标签的过程中就会进行*/if (!unary) {// 将标签信息放到 stack 数组中,{ tag, lowerCasedTag, attrs, start, end }stack.push({tag: tagName,lowerCasedTag: tagName.toLowerCase(),attrs: attrs,start: match.start,end: match.end,})// 标识当前标签的结束标签为 tagNamelastTag = tagName}/*调用 start 方法,主要内容为:1、创建 AST 对象2、处理存在 v-model 指令的 input 标签,分别处理 input 为 checkbox、radio、其它的情况3、处理标签上的众多指令,比如 v-pre、v-for、v-if、v-once4、如果根节点 root 不存在则设置当前元素为根节点5、如果当前元素为非自闭合标签则将自己 push 到 stack 数组,并记录 currentParent,在接下来处理子元素时用来告诉子元素自己的父节点是谁6、如果当前元素为自闭合标签,则表示该标签要处理结束了,让自己和父元素产生关系,以及设置自己的子元素*/if (options.start) {options.start(tagName, attrs, unary, match.start, match.end)}}/*解析结束标签,比如:</div>最主要的事就是:1、处理 stack 数组,从 stack 数组中找到当前结束标签对应的开始标签,然后调用 options.end 方法2、处理完结束标签之后调整 stack 数组,保证在正常情况下 stack 数组中的最后一个元素就是下一个结束标签对应的开始标签3、处理一些异常情况,比如 stack 数组最后一个元素不是当前结束标签对应的开始标签,还有就是 br 和 p 标签单独处理tagName —— 标签名,比如 divstart —— 结束标签的开始索引end —— 结束标签的结束索引*/function parseEndTag(tagName, start, end) {let pos, lowerCasedTagNameif (start == null) start = indexif (end == null) end = index// Find the closest opened tag of the same type/* 倒序遍历 stack 数组,找到第一个和当前结束标签相同的标签,该标签就是结束标签对应的开始标签的描述对象正常情况下,stack 数组中的最后一个元素就是当前结束标签的开始标签的描述对象*/ if (tagName) {lowerCasedTagName = tagName.toLowerCase()for (pos = stack.length - 1; pos >= 0; pos--) {if (stack[pos].lowerCasedTag === lowerCasedTagName) {break}}} else {// If no tag name is provided, clean shoppos = 0}// 如果在 stack 中一直没有找到相同的标签名,则 pos 就会 < 0,进行后面的 else 分支if (pos >= 0) {// Close all the open elements, up the stack/* 这个 for 循环负责关闭 stack 数组中索引 >= pos 的所有标签为什么要用一个循环,上面说到正常情况下 stack 数组的最后一个元素就是需要找到的开始标签,但是有些异常情况,就是有些元素没有给提供结束标签,比如:stack = ['span', 'div', 'span', 'h1'],当前处理的结束标签 tagName = div匹配到 div,pos = 1,那索引为 2 和 3 的两个标签(span、h1)说明就没提供结束标签这个 for 循环就负责关闭 div、span 和 h1 这三个标签,并在开发环境为 span 和 h1 这两个标签给出 ”未匹配到结束标签的提示*/ 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.`, {start: stack[i].start,end: stack[i].end,})}// 走到这里,说明上面的异常情况都处理完了,调用 options.end 处理正常的结束标签if (options.end) {options.end(stack[i].tag, start, end)}}// Remove the open elements from the stack// 将刚才处理的那些标签从数组中移除,保证数组的最后一个元素就是下一个结束标签对应的开始标签stack.length = pos// lastTag 记录 stack 数组中未处理的最后一个开始标签lastTag = pos && stack[pos - 1].tag} else if (lowerCasedTagName === 'br') {// 当前处理的标签为 <br /> 标签if (options.start) {options.start(tagName, [], true, start, end)}} else if (lowerCasedTagName === 'p') {// 当前处理的标签为 <p></p> 标签if (options.start) {// 处理 <p> 标签options.start(tagName, [], false, start, end)}if (options.end) {// 处理 </p> 标签options.end(tagName, start, end)}}}
}

parseHtmlOptions — parseHTML(template, options)

文件位置:src\compiler\parser\index.js

这里主要解读 start、end、chars、comment 这 4 个方法.

 /*start 方法主要内容:1、创建 AST 对象2、处理存在 v-model 指令的 input 标签,分别处理 input 为 checkbox、radio、其它的情况3、处理标签上的众多指令,比如 v-pre、v-for、v-if、v-once4、如果根节点 root 不存在则设置当前元素为根节点5、如果当前元素为非自闭合标签则将自己 push 到 stack 数组,并记录 currentParent,在接下来处理子元素时用来告诉子元素自己的父节点是谁6、如果当前元素为自闭合标签,则表示该标签要处理结束了,让自己和父元素产生关系,以及设置自己的子元素tag —— 标签名attrs —— [{ name: attrName, value: attrVal, start, end }, ...] 形式的属性数组unary —— 自闭合标签start —— 标签在 html 字符串中的开始索引end —— 标签在 html 字符串中的结束索引*/start (tag, attrs, unary, start, end) {// check namespace.// inherit parent ns if there is one// 检查命名空间,如果存在父元素存在命名空间,则继承父命名空间const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)// handle IE svg bug/* istanbul ignore if */if (isIE && ns === 'svg') {attrs = guardIESVGBug(attrs)}// 创建当前标签的 AST 对象let element: ASTElement = createASTElement(tag, attrs, currentParent)// 设置元素的命名空间if (ns) {element.ns = ns}// 非生产环境下,在 ast 对象上添加一些属性,比如 start、endif (process.env.NODE_ENV !== 'production') {if (options.outputSourceRange) {element.start = startelement.end = end/* 将属性数组解析成下面形式的对象:{ attrName: { name: attrName, value: attrVal, start, end }, ... }*/element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {cumulated[attr.name] = attrreturn cumulated}, {})}// 验证属性是否有效,比如属性名不能包含: spaces, quotes, <, >, / or =.attrs.forEach(attr => {if (invalidAttributeRE.test(attr.name)) {warn(`Invalid dynamic argument expression: attribute names cannot contain ` +`spaces, quotes, <, >, / or =.`,{start: attr.start + attr.name.indexOf(`[`),end: attr.start + attr.name.length})}})}/* 非服务端渲染,模版中不应该出现 style、script 标签: 可以通过 .vue 文件理解为:template、style、script 已经分离,因此正常情况下,template 模板中不应该包含 <style>、<script> 标签*/if (isForbiddenTag(element) && !isServerRendering()) {element.forbidden = trueprocess.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.',{ start: element.start })}// apply pre-transforms/*为 element 对象分别执行 class、style、model 模块中的 preTransforms 方法但是 web 平台只有 model 模块有 preTransforms 方法用来处理存在 v-model 的 input 标签,但没处理 v-model 属性分别处理了 input 为 checkbox、radio 和 其它的情况input 具体是哪种情况由 el.ifConditions 中的条件来判断<input v-mode="test" :type="checkbox || radio || other(如 text)" />*/for (let i = 0; i < preTransforms.length; i++) {element = preTransforms[i](element, options) || element}if (!inVPre) {processPre(element)// 判断 element 是否存在 v-pre 指令,存在则设置 element.pre = trueif (element.pre) {inVPre = true}}// 如果是 pre 标签,则设置 inPre 为 trueif (platformIsPreTag(element.tag)) {inPre = true}if (inVPre) {/* 说明标签上存在 v-pre 指令,这个节点只会渲染一次,将节点上的属性都设置到el.attrs 数组对象中,作为静态属性,数据更新时不会渲染这部分内容设置 el.attrs 数组对象,每个元素都是一个属性对象如下: { name: attrName, value: attrVal, start, end }*/ processRawAttrs(element)} else if (!element.processed) {// structural directives// 处理 v-for 属性,得到 element.for = 可迭代对象 element.alias = 别名processFor(element)/*处理 v-if、v-else-if、v-else得到 element.if = "exp",element.elseif = exp, element.else = truev-if 属性会额外在 element.ifConditions 数组中添加 { exp, block } 对象*/processIf(element)// 处理 v-once 指令,得到 element.once = trueprocessOnce(element)}// 如果 root 不存在,则表示当前处理的元素为第一个元素,即组件的根元素if (!root) {root = elementif (process.env.NODE_ENV !== 'production') {/* 检查根元素,对根元素有一些限制,比如:不能使用 slot 和 template 作为根元素,也不能在有状态组件的根元素上使用 v-for 指令*/checkRootConstraints(root)}}// 非自闭合标签,通过 currentParent 记录当前元素,// 下一个元素在处理的时候,就知道自己的父元素是谁if (!unary) {currentParent = element/*然后将 element push 到 stack 数组,将来处理到当前元素的闭合标签时再拿出来将当前标签的 ast 对象 push 到 stack 数组中注意:在调用 options.start 方法之前也进行过 push 操作,那个 push 进来的是当前标签的一个基本配置信息*/stack.push(element)} else {/*说明当前元素为自闭合标签,主要内容:1、如果元素没有被处理过,即 el.processed 为 false,则调用 processElement 方法处理节点上的众多属性2、让自己和父元素产生关系,将自己放到父元素的 children 数组中,并设置自己的 parent 属性为 currentParent3、设置自己的子元素,将自己所有非插槽的子元素放到自己的 children 数组中*/closeElement(element)}},/* 处理结束标签 */end (tag, start, end) {// 结束标签对应的开始标签的 ast 对象const element = stack[stack.length - 1]// pop stackstack.length -= 1currentParent = stack[stack.length - 1]if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {element.end = end}/*主要内容:1、如果元素没有被处理过,即 el.processed 为 false,则调用 processElement 方法处理节点上的众多属性2、让自己和父元素产生关系,将自己放到父元素的 children 数组中,并设置自己的 parent 属性为 currentParent3、设置自己的子元素,将自己所有非插槽的子元素放到自己的 children 数组中*/closeElement(element)},/* 处理文本基于文本生成 ast 对象,然后将该 ast 放到它的父元素的肚子里,即 currentParent.children 数组中  */chars (text: string, start: number, end: number) {// 异常处理,currentParent 不存在说明这段文本没有父元素if (!currentParent) {if (process.env.NODE_ENV !== 'production') {// 文本不能作为组件的根元素if (text === template) {warnOnce('Component template requires a root element, rather than just text.',{ start })} else if ((text = text.trim())) {warnOnce(`text "${text}" outside root element will be ignored.`,{ start })}}return}// IE textarea placeholder bug/* istanbul ignore if */if (isIE &&currentParent.tag === 'textarea' &&currentParent.attrsMap.placeholder === text) {return}// 当前父元素的所有孩子节点const children = currentParent.children// 对 text 进行一系列的处理,比如删除空白字符,// 或者存在 whitespaceOptions 选项,则 text 直接置为空或者空格if (inPre || text.trim()) {// 文本在 pre 标签内 或者 text.trim() 不为空text = isTextTag(currentParent) ? text : decodeHTMLCached(text)} else if (!children.length) {// remove the whitespace-only node right after an opening tag/* children 长度为 0 ,则说明文本不在 pre 标签内而且 text.trim() 为空,而且当前父元素也没有孩子节点,则将 text 置为空*/text = ''} else if (whitespaceOption) {// 压缩处理if (whitespaceOption === 'condense') {// in condense mode, remove the whitespace node if it contains// line break, otherwise condense to a single spacetext = lineBreakRE.test(text) ? '' : ' '} else {text = ' '}} else {text = preserveWhitespace ? ' ' : ''}// 如果经过处理后 text 还存在if (text) {// 不在 pre 节点中,并且配置选项中存在压缩选项,则将多个连续空格压缩为单个if (!inPre && whitespaceOption === 'condense') {// condense consecutive whitespaces into single spacetext = text.replace(whitespaceRE, ' ')}// 基于 text 生成 AST 对象let reslet child: ?ASTNodeif (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {// 文本中存在表达式(即有界定符)child = {type: 2,expression: res.expression,// 表达式tokens: res.tokens,text // 文本}} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {// 纯文本节点child = {type: 3,text}}// child 存在,则将 child 放到父元素的 children 中,即 currentParent.children 数组中if (child) {if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {child.start = startchild.end = end}children.push(child)}}},/* 注释内容-text, 注释开始索引-start, 结束索引-end */comment (text: string, start, end) {// adding anything as a sibling to the root node is forbidden// comments should still be allowed, but ignored// 禁止将任何内容作为 root 节点的同级进行添加,注释应该被允许,但是会被忽略// 如果 currentParent 不存在,说明注释和 root 为同级,则进行忽略if (currentParent) {// 注释节点的 astconst child: ASTText = {type: 3,text,isComment: true}if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {// 分别将注释节点的开始索引和结束索引保存到注释节点 child 中child.start = startchild.end = end}// 向父元素的 children 属性中添加当前注释节点currentParent.children.push(child)}}

preTransformNode() 方法

文件位置:src\platforms\web\compiler\modules\model.js

这里涉及到了下面几个方法:

  • getBindingAttr
  • getAndRemoveAttr
  • processFor
  • addRawAttr
  • processElement
/*
* 处理存在 v-model 的 input 标签,但没处理 v-model 属性
* 分别处理了 input 为 checkbox、radio 和 其它的情况
* input 具体是哪种情况由 el.ifConditions 中的条件来判断
* <input v-mode="test" :type="checkbox || radio || other(比如 text)" />
* @param {*} el 
* @param {*} options 
* @returns branch0
*/
function preTransformNode (el: ASTElement, options: CompilerOptions) {// 属于 input 标签if (el.tag === 'input') {const map = el.attrsMap// 不存在 v-model 属性,直接结束if (!map['v-model']) {return}// 获取 :type 的值let typeBindingif (map[':type'] || map['v-bind:type']) {typeBinding = getBindingAttr(el, 'type')}if (!map.type && !typeBinding && map['v-bind']) {typeBinding = `(${map['v-bind']}).type`}// type 类型存在if (typeBinding) {// 获取 v-if 的值,比如: <input v-model="test" :type="checkbox" v-if="isShow" />const ifCondition = getAndRemoveAttr(el, 'v-if', true)// 得到 &&isShowconst ifConditionExtra = ifCondition ? `&&(${ifCondition})` : ``// 是否存在 v-else 属性,<input v-else />const hasElse = getAndRemoveAttr(el, 'v-else', true) != null// 获取 v-else-if 属性的值 <inpu v-else-if="isShow" />const elseIfCondition = getAndRemoveAttr(el, 'v-else-if', true)/*克隆一个新的 el 对象,分别处理 input 为 chekbox、radio 或 其它的情况具体是哪种情况,通过 el.ifConditins 条件来判断*/ // 1. checkboxconst branch0 = cloneASTElement(el)// process for on the main node/*<input v-for="item in arr" :key="item" />处理 v-for 表达式,得到:branch0.for = arr;branch0.alias = item;*/processFor(branch0)// 在 branch0.attrsMap 和 branch0.attrsList 对象中添加 type 属性addRawAttr(branch0, 'type', 'checkbox')/* 分别处理元素节点:key、ref、插槽、自闭合的 slot 标签、动态组件、class、style、v-bind、v-on、其它指令和一些原生属性*/   processElement(branch0, options)// 标记当前对象已经被处理过了branch0.processed = true // prevent it from double-processed// 得到 true&&isShow || false&&isShow,标记当前 input 是否为 checkboxbranch0.if = `(${typeBinding})==='checkbox'` + ifConditionExtra// 在 branch0.ifConfitions 数组中放入 { exp, block } 对象addIfCondition(branch0, {exp: branch0.if,block: branch0})// 克隆一个新的 ast 对象// 2. add radio else-if conditionconst branch1 = cloneASTElement(el)// 获取 v-for 属性值getAndRemoveAttr(branch1, 'v-for', true)// 在 branch1.attrsMap 和 branch1.attrsList 对象中添加 type 属性addRawAttr(branch1, 'type', 'radio')/* 分别处理元素节点:key、ref、插槽、自闭合的 slot 标签、动态组件、class、style、v-bind、v-on、其它指令和一些原生属性*/   processElement(branch1, options)addIfCondition(branch0, {exp: `(${typeBinding})==='radio'` + ifConditionExtra,block: branch1})// 3. other input 为其它的情况const branch2 = cloneASTElement(el)// 获取 v-for 属性getAndRemoveAttr(branch2, 'v-for', true)addRawAttr(branch2, ':type', typeBinding)  processElement(branch2, options)addIfCondition(branch0, {exp: ifCondition,block: branch2})// 给 branch0 设置 else 或 elseif 条件if (hasElse) {branch0.else = true} else if (elseIfCondition) {branch0.elseif = elseIfCondition}// 返回return branch0}}
}

getBindingAttr、getAndRemoveAttr、addRawAttr 方法

文件位置:src\compiler\helpers.js

getBindingAttr

// 获取 el 对象上执行属性 name 的值 
export function getBindingAttr (el: ASTElement,name: string,getStatic?: boolean
): ?string {// 获取指定属性的值const dynamicValue =getAndRemoveAttr(el, ':' + name) ||getAndRemoveAttr(el, 'v-bind:' + name)if (dynamicValue != null) {return parseFilters(dynamicValue)} else if (getStatic !== false) {const staticValue = getAndRemoveAttr(el, name)if (staticValue != null) {return JSON.stringify(staticValue)}}
}

getAndRemoveAttr

/**从 el.attrsList 中删除指定的属性 name如果 removeFromMap 为 true,则同样删除 el.attrsMap 对象中的该属性,比如 v-if、v-else-if、v-else 等属性就会被移除,不过一般不会删除该对象上的属性,因为从 ast 生成 代码期间还需要使用该对象,返回指定属性的值*/
export function getAndRemoveAttr (el: ASTElement,name: string,removeFromMap?: boolean
): ?string {let val// 将执行属性 name 从 el.attrsList 中移除if ((val = el.attrsMap[name]) != null) {const list = el.attrsListfor (let i = 0, l = list.length; i < l; i++) {if (list[i].name === name) {list.splice(i, 1)break}}}// 如果 removeFromMap 为 true,则从 el.attrsMap 中移除指定的属性 name// 不过一般不会移除 el.attsMap 中的数据,因为从 ast 生成 代码期间还需要使用该对象if (removeFromMap) {delete el.attrsMap[name]}// 返回执行属性的值return val
}

addRawAttr

// 获取 el 对象上执行属性 name 的值 
export function getBindingAttr (el: ASTElement,name: string,getStatic?: boolean
): ?string {// 获取指定属性的值const dynamicValue =getAndRemoveAttr(el, ':' + name) ||getAndRemoveAttr(el, 'v-bind:' + name)if (dynamicValue != null) {return parseFilters(dynamicValue)} else if (getStatic !== false) {const staticValue = getAndRemoveAttr(el, name)if (staticValue != null) {return JSON.stringify(staticValue)}}
}

processFor、processRef、processKey、processElement

文件位置:/src/compiler/parser/index.js

processFor

/*** 处理 v-for,将结果设置到 el 对象上,得到:*   el.for = 可迭代对象,比如 arr*   el.alias = 别名,比如 item* @param {*} el 元素的 ast 对象*/
export function processFor (el: ASTElement) {let exp// 获取 el 上的 v-for 属性的值if ((exp = getAndRemoveAttr(el, 'v-for'))) {// 解析 v-for 的表达式,得到 { for: 可迭代对象, alias: 别名 }// 比如 { for: arr, alias: item }const res = parseFor(exp)if (res) {// 将 res 对象上的属性拷贝到 el 对象上extend(el, res)} else if (process.env.NODE_ENV !== 'production') {warn(`Invalid v-for expression: ${exp}`,el.rawAttrsMap['v-for'])}}
}

processRef

/*** 处理元素上的 ref 属性*  el.ref = refVal*  el.refInFor = boolean*/
function processRef (el) {const ref = getBindingAttr(el, 'ref')if (ref) {el.ref = ref// 判断包含 ref 属性的元素是否包含在具有 v-for 指令的元素内或后代元素中// 如果是,则 ref 指向的则是包含 DOM 节点或组件实例的数组el.refInFor = checkInFor(el)}
}

processKey

// 处理元素上的 key 属性,设置 el.key = val
function processKey (el) {// 拿到 key 的属性值const exp = getBindingAttr(el, 'key')// 关于 key 使用上的异常处理if (exp) {// template 标签不允许设置 keyif (process.env.NODE_ENV !== 'production') {if (el.tag === 'template') {warn(`<template> cannot be keyed. Place the key on real elements instead.`,getRawBindingAttr(el, 'key'))}// 不要在 <transition=group> 的子元素上使用 v-for 的 index 作为 key// 否则等价与没有使用 keyif (el.for) {const iterator = el.iterator2 || el.iterator1const parent = el.parentif (iterator && iterator === exp && parent && parent.tag === 'transition-group') {warn(`Do not use v-for index as key on <transition-group> children, ` +`this is the same as not using keys.`,getRawBindingAttr(el, 'key'),true /* tip */)}}}// 设置 el.key = expel.key = exp}
}

processElement

/*** 分别处理元素节点的 key、ref、插槽、自闭合的 slot 标签、动态组件、class、style、v-bind、v-on、其它指令和一些原生属性 * 然后在 el 对象上添加如下属性:* el.key、ref、refInFor、scopedSlot、slotName、component、inlineTemplate、staticClass* el.bindingClass、staticStyle、bindingStyle、attrs* @param {*} element 被处理元素的 ast 对象* @param {*} options 配置项* @returns */
export function processElement (element: ASTElement,options: CompilerOptions
) {processKey(element)// determine whether this is a plain element after// removing structural attributes// 确定 element 是否为一个普通元素element.plain = (!element.key &&!element.scopedSlots &&!element.attrsList.length)// el.ref = val, el.refInFor = booleanprocessRef(element)// 处理作为插槽传递给组件的内容,得到 插槽名称、是否为动态插槽、作用域插槽的值,// 以及插槽中的所有子元素,子元素放到插槽对象的 children 属性中processSlotContent(element)// 处理自闭合的 slot 标签,得到插槽名称 => el.slotName = xxprocessSlotOutlet(element)// 处理动态组件,<component :is="compoName"></component>得到 el.component = compName,// 以及标记是否存在内联模版,el.inlineTemplate = true of falseprocessComponent(element)/*为 element 对象分别执行 class、style、model 模块中的 transformNode 方法不过 web 平台只有 class、style 模块有 transformNode 方法,分别用来处理 class 属性和 style 属性得到 el.staticStyle、 el.styleBinding、el.staticClass、el.classBinding分别存放静态 style 属性的值、动态 style 属性的值,以及静态 class 属性的值和动态 class 属性的值*/for (let i = 0; i < transforms.length; i++) {element = transforms[i](element, options) || element}/**处理元素上的所有属性:1. v-bind 指令变成:el.attrs 或 el.dynamicAttrs = [{ name, value, start, end, dynamic }, ...],或者是必须使用 props 的属性,变成了 el.props = [{ name, value, start, end, dynamic }, ...]2. v-on 指令变成:el.events 或 el.nativeEvents = { name: [{ value, start, end, modifiers, dynamic }, ...] }3. 其它指令:el.directives = [{name, rawName, value, arg, isDynamicArg, modifier, start, end }, ...]4. 原生属性:el.attrs = [{ name, value, start, end }],或者一些必须使用 props 的属性,变成了:el.props = [{ name, value: true, start, end, dynamic }]*/processAttrs(element)return element
}

processSlotContent() 方法

文件位置:/src/compiler/parser/index.js

/*处理作为插槽传递给组件的内容,得到:slotTarget => 插槽名slotTargetDynamic => 是否为动态插槽slotScope => 作用域插槽的值直接在 <comp> 标签上使用 v-slot 语法时,将上述属性放到 el.scopedSlots 对象上,其它情况直接放到 el 对象上handle content being passed to a component as slot,e.g. <template slot="xxx">, <div slot-scope="xxx">
*/
function processSlotContent (el) {let slotScopeif (el.tag === 'template') {/* template 标签上使用 scope 属性的提示scope 已经弃用,并在 2.5 之后使用 slot-scope 代替slot-scope 即可以用在 template 标签也可以用在普通标签上*/slotScope = getAndRemoveAttr(el, 'scope')/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && slotScope) {warn(`the "scope" attribute for scoped slots have been deprecated and ` +`replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +`can also be used on plain elements in addition to <template> to ` +`denote scoped slots.`,el.rawAttrsMap['scope'],true)}// el.slotScope = valel.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')} else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && el.attrsMap['v-for']) {warn(`Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +`(v-for takes higher priority). Use a wrapper <template> for the ` +`scoped slot to make it clearer.`,el.rawAttrsMap['slot-scope'],true)}el.slotScope = slotScope}// slot="xxx"// 获取 slot 属性的值// slot="xxx",旧的具名插槽的写法const slotTarget = getBindingAttr(el, 'slot')if (slotTarget) {// el.slotTarget = 插槽名(具名插槽)el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget// 动态插槽名el.slotTargetDynamic = !!(el.attrsMap[':slot'] || el.attrsMap['v-bind:slot'])// preserve slot as an attribute for native shadow DOM compat// only for non-scoped slots.if (el.tag !== 'template' && !el.slotScope) {addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))}}// 2.6 v-slot syntaxif (process.env.NEW_SLOT_SYNTAX) {if (el.tag === 'template') {// v-slot on <template>// v-slot 在 tempalte 标签上,得到 v-slot 的值// v-slot on <template>const slotBinding = getAndRemoveAttrByRegex(el, slotRE)if (slotBinding) {if (process.env.NODE_ENV !== 'production') {if (el.slotTarget || el.slotScope) {// 不同插槽语法禁止混合使用warn(`Unexpected mixed usage of different slot syntaxes.`,el)}if (el.parent && !maybeComponent(el.parent)) {/* <template v-slot> 只能出现在组件的根位置,比如:<comp><template v-slot>xx</template></comp>而不能是<comp><div><template v-slot>xxx</template></div></comp>*/warn(`<template v-slot> can only appear at the root level inside ` +`the receiving component`,el)}}// 得到插槽名称const { name, dynamic } = getSlotName(slotBinding)// 将插槽名称保存到 el.slotTarget 上el.slotTarget = name// 是否为动态插槽el.slotTargetDynamic = dynamic// 作用域插槽的值el.slotScope = slotBinding.value || emptySlotScopeToken // force it into a scoped slot for perf}} else {/* 处理组件上的 v-slot,<comp v-slot:header />slotBinding = { name: "v-slot:header", value: "", start, end}v-slot on component, denotes default slot*/const slotBinding = getAndRemoveAttrByRegex(el, slotRE)if (slotBinding) {if (process.env.NODE_ENV !== 'production') {// el 不是组件的话,提示,v-slot 只能出现在组件上或 template 标签上if (!maybeComponent(el)) {warn(`v-slot can only be used on components or <template>.`,slotBinding)}// 语法混用if (el.slotScope || el.slotTarget) {warn(`Unexpected mixed usage of different slot syntaxes.`,el)}// 为了避免作用域歧义,当存在其他命名槽时,默认槽也应该使用<template>语法if (el.scopedSlots) {warn(`To avoid scope ambiguity, the default slot should also use ` +`<template> syntax when there are other named slots.`,slotBinding)}}// 将组件的孩子添加到它的默认插槽内// add the component's children to its default slotconst slots = el.scopedSlots || (el.scopedSlots = {})// 获取插槽名称以及是否为动态插槽const { name, dynamic } = getSlotName(slotBinding)// 创建一个 template 标签的 ast 对象,用于容纳插槽内容,父级是 elconst slotContainer = slots[name] = createASTElement('template', [], el)// 插槽名slotContainer.slotTarget = name// 是否为动态插槽slotContainer.slotTargetDynamic = dynamic// 所有的孩子,将每一个孩子的 parent 属性都设置为 slotContainerslotContainer.children = el.children.filter((c: any) => {if (!c.slotScope) {// 给插槽内元素设置 parent 属性为 slotContainer,也就是 template 元素c.parent = slotContainerreturn true}})slotContainer.slotScope = slotBinding.value || emptySlotScopeToken// remove children as they are returned from scopedSlots nowel.children = []// mark el non-plain so data gets generatedel.plain = false}}}
}

getSlotName、processSlotOutlet、processComponent

文件位置:/src/compiler/parser/index.js

getSlotName

/*** 解析 binding,得到插槽名称以及是否为动态插槽* @returns { name: 插槽名称, dynamic: 是否为动态插槽 }*/
function getSlotName (binding) {let name = binding.name.replace(slotRE, '')if (!name) {if (binding.name[0] !== '#') {name = 'default'} else if (process.env.NODE_ENV !== 'production') {warn(`v-slot shorthand syntax requires a slot name.`,binding)}}return dynamicArgRE.test(name)// dynamic [name]? { name: name.slice(1, -1), dynamic: true }// static name: { name: `"${name}"`, dynamic: false }
}

processSlotOutlet

// handle <slot/> outlets,处理自闭合 slot 标签
// 得到插槽名称,el.slotName
function processSlotOutlet (el) {if (el.tag === 'slot') {// 得到插槽名称el.slotName = getBindingAttr(el, 'name')// 不允许在 slot 标签上使用 key 属性if (process.env.NODE_ENV !== 'production' && el.key) {warn(`\`key\` does not work on <slot> because slots are abstract outlets ` +`and can possibly expand into multiple elements. ` +`Use the key on a wrapping element instead.`,getRawBindingAttr(el, 'key'))}}
}

processComponent

/*** 处理动态组件,<component :is="compName"></component>* 得到 el.component = compName*/
function processComponent (el) {let binding// 解析 is 属性,得到属性值,即组件名称,el.component = compNameif ((binding = getBindingAttr(el, 'is'))) {el.component = binding}/* <component :is="compName" inline-template>xx</component>组件上存在 inline-template 属性,进行标记:el.inlineTemplate = true表示组件开始和结束标签内的内容作为组件模版出现,而不是作为插槽别分发,方便定义组件模版*/if (getAndRemoveAttr(el, 'inline-template') != null) {el.inlineTemplate = true}
}

transformNode() —— class 模块

文件位置:/src/platforms/web/compiler/modules/class.js

/*** 处理元素上的 class 属性* 静态的 class 属性值赋值给 el.staticClass 属性* 动态的 class 属性值赋值给 el.classBinding 属性*/
function transformNode (el: ASTElement, options: CompilerOptions) {const warn = options.warn || baseWarn// 获取元素上静态 class 属性的值 xx,<div class="xx"></div>const staticClass = getAndRemoveAttr(el, 'class')if (process.env.NODE_ENV !== 'production' && staticClass) {const res = parseText(staticClass, options.delimiters)// 警告提示,同 style 的提示一样,不能使用 <div class="{{ val}}"></div>,请用// <div :class="val"></div> 代替if (res) {warn(`class="${staticClass}": ` +'Interpolation inside attributes has been removed. ' +'Use v-bind or the colon shorthand instead. For example, ' +'instead of <div class="{{ val }}">, use <div :class="val">.',el.rawAttrsMap['class'])}}// 静态 class 属性值赋值给 el.staticClassif (staticClass) {el.staticClass = JSON.stringify(staticClass.replace(/\s+/g, ' ').trim())}// 获取动态绑定的 class 属性值,并赋值给 el.classBindingconst classBinding = getBindingAttr(el, 'class', false /* getStatic */)if (classBinding) {el.classBinding = classBinding}
}

transformNode() —— style 模块

文件位置:/src/platforms/web/compiler/modules/style.js

/*** 从 el 上解析出静态的 style 属性和动态绑定的 style 属性,分别赋值给:* el.staticStyle 和 el.styleBinding*/
function transformNode (el: ASTElement, options: CompilerOptions) {const warn = options.warn || baseWarn// <div style="xx"></div>// 获取 style 属性const staticStyle = getAndRemoveAttr(el, 'style')if (staticStyle) {/*istanbul ignore if 提示,如果从 xx 中解析到了界定符,说明是一个动态的 style,比如 <div style="{{ val }}"></div>则给出提示:动态的 style 请使用 <div :style="val"></div>*/if (process.env.NODE_ENV !== 'production') {const res = parseText(staticStyle, options.delimiters)if (res) {warn(`style="${staticStyle}": ` +'Interpolation inside attributes has been removed. ' +'Use v-bind or the colon shorthand instead. For example, ' +'instead of <div style="{{ val }}">, use <div :style="val">.',el.rawAttrsMap['style'])}}// 将静态的 style 样式赋值给 el.staticStyleel.staticStyle = JSON.stringify(parseStyleText(staticStyle))}// 获取动态绑定的 style 属性,比如 <div :style="styleVariable"></div>const styleBinding = getBindingAttr(el, 'style', false /* getStatic */)if (styleBinding) {// 赋值给 el.styleBindingel.styleBinding = styleBinding}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/4389.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

电脑软件:推荐四款非常好用的电脑磁盘分析工具

一、WizTree WizTree 是一款Windows下磁盘空间分析工具。它可以快速扫描并分析你的电脑硬盘驱动器中文件和文件夹&#xff0c;并以可视化块状的方式展示哪些文件和文件夹使用的磁盘空间最多。这样你就可以很方便找到占用磁盘空间大的文件。 WizTree的特点 ● 磁盘空间利…

6. STM32之TIM实验--编码器接口()--(实验5:PWM驱动直流电机)

这篇文章是通用定时器的最后一章节&#xff0c;也就是编码器接口&#xff0c;主要是用来进行对精确测量旋转角度或速度的负载进行精确控制。 STM32 编码器模式详解-CSDN博客 STM32——编码器测速原理及STM32编码器模式_龙邱512编码器stm32历程-CSDN博客 代码可根据这个进行编…

如何批量创建文件夹并命名?6个一键批量创建的方法

如何批量创建文件夹并命名&#xff1f;在快节奏的现代工作环境中&#xff0c;时间成为了最宝贵的资源。面对海量的数据与文件&#xff0c;如何高效地组织与管理&#xff0c;成为了提升工作效率的关键。为了节省时间&#xff0c;批量创建文件夹并命名&#xff0c;成为了一项至关…

人工智能之人脸识别(face_recognition)

文章目录 face_recognition 介绍主要功能**与opencv联系联系检测人脸切割人脸提取人物关键特征计算人脸的欧几里得距离计算人脸匹配程度总结 face_recognition 介绍 face_recognition 介绍 face_recognition 是一个非常流行的 Python 库&#xff0c;专门用于人脸识别任务。它基…

Java学习路线:JUL日志系统(二)使用Properties配置文件

目录 认识properties 使用properties编写日志配置文件 认识properties 之前的学习中&#xff0c;我们学习了使用XML配置文件&#xff0c;但是XML的读取实在有些麻烦。那有没有更简单的方式来配置文件呢&#xff1f; 答案是&#xff1a;使用Properties配置文件 在这里了解pro…

【数据库】elasticsearch

1、架构 es会为每个索引创建一定数量的主分片和副本分片。 分片&#xff08;Shard&#xff09;&#xff1a; 将索引数据分割成多个部分&#xff0c;每个部分都是一个独立的索引。 主要目的是实现数据的分布式存储和并行处理&#xff0c;从而提高系统的扩展性和性能。 在创建索…

Qt——常用控件

前言&#xff1a;本篇文章&#xff0c;将分享Qt中常用的&#xff0c;具有代表性的一些控件。 一.按钮类控件 在前边的文章中我们也多次分享过PushButton按钮&#xff0c;但Qt中并非只提供这一种按钮。 在Qt中&#xff0c;QPushButton并非直接继承自QWidget&#xff0c;QAbstr…

硬件基础06 滤波器——无源、有源(含Filter Solutions、Filter Pro、MATLAB Fdatool)

推荐设计RC无源滤波器使用&#xff0c;数字滤波器可以使用MATLAB&#xff0c;有源滤波器使用Filter Pro。 一、Filter Solutions 1、软件资源及安装教程如下 FilterSolutions14.1.rar资源 &#xff08;1&#xff09;、双击FS14_1_0.exe进行安装&#xff1b;不要安装到中文路…

js WebAPI黑马笔记(万字速通)

此笔记来自于黑马程序员&#xff0c;pink老师yyds 复习&#xff1a; splice() 方法用于添加或删除数组中的元素。 注意&#xff1a; 这种方法会改变原始数组。 删除数组&#xff1a; splice(起始位置&#xff0c; 删除的个数) 比如&#xff1a;1 let arr [red, green, b…

docker安装低版本的jenkins-2.346.3,在线安装对应版本插件失败的解决方法

提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、网上最多的默认解决方法1、jenkins界面配置清华源2、替换default.json文件 二、解决低版本Jenkins在线安装插件问题1.手动下载插件并导入2.低版本jenkins在…

spark-on-k8s 介绍

spark-on-k8s 介绍 摘要 最近一段时间都在做与spark相关的项目&#xff0c;主要是与最近今年比较火的隐私计算相结合&#xff0c;主要是在机密计算领域使用spark做大数据分析、SQL等业务&#xff0c;从中也了解到了一些spark的知识&#xff0c;现在做一个简单的总结&#xff…

React教程(详细版)

React教程&#xff08;详细版&#xff09; 1&#xff0c;简介 1.1 概念 react是一个渲染html界面的一个js库&#xff0c;类似于vue&#xff0c;但是更加灵活&#xff0c;写法也比较像原生js&#xff0c;之前我们写出一个完成的是分为html&#xff0c;js&#xff0c;css&…

鸿蒙开发:自定义一个车牌省份简称键盘

前言 之前针对车牌省份简称键盘&#xff0c;在Android系统中搞过一个&#xff0c;当时使用的是组合View的形式&#xff0c;考虑到最后一个删除按钮单独占两个格子&#xff0c;做了特殊处理&#xff0c;单独设置了权重weight和单独设置了宽度width&#xff0c;既然鸿蒙系统的应…

电脑蓝屏不要慌,一分钟教你如何解决蓝屏问题

目录 一、检查硬件连接 二、更新驱动程序 三、修复操作系统错误 四、使用系统还原 电脑蓝屏是许多计算机用户经常遇到的问题之一。它可能由硬件故障、驱动程序问题、操作系统错误等多种原因引起。当电脑出现蓝屏时,很多人会感到困惑和焦虑。本文将向您介绍一些常见的解决方…

推荐!一些好用的VSCode插件

那些好用的VSCode插件 前言1、Auto Close Tag(自动补全标签)⭐2、Auto Rename Tag(自动更新标签)⭐3、Chinese(简体中文)⭐4、Git History (查看 Git 提交历史)⭐5、GitLens (增强 Git )6、open in browser (快速预览 )⭐7、Vetur ( Vue相关 )⭐8、Beautify ( 美化代码 )9、bac…

任务调度实现

我的后端学习大纲 XXL-JOB大纲 1、什么是任务调度 1.以下面业务场景就需要任务调度来解决问题: 某电商平台需要每天上午10点&#xff0c;下午3点&#xff0c;晚上8点发放一批优惠券某银行系统需要在信用卡到期还款日的前三天进行短信提醒某财务系统需要在每天凌晨0:10分结算前…

【SQL50】day 1

目录 1.可回收且低脂的产品 2.寻找用户推荐人 3.使用唯一标识码替换员工ID 4.产品销售分析 I 5.有趣的电影 6.平均售价 7.每位教师所教授的科目种类的数量 8.平均售价 1.可回收且低脂的产品 # Write your MySQL query statement below select product_id from Products w…

【数据结构与算法】第9课—数据结构之二叉树(链式结构)

文章目录 1. 二叉树的性质2. 链式结构二叉树3. 二叉树链式结构的4种遍历方式4. 二叉树节点个数5. 二叉树的叶子节点个数6. 二叉树第k层节点个数7. 二叉树的高度/深度8. 二叉树查找值为x的节点9. 二叉树的销毁10. 判断是否为完全二叉树11. 二叉树练习题11.1 单值二叉树11.2 相同…

ONLYOFFICE 8.2深度体验:高效协作与卓越性能的完美融合

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀ONLYOFFICE 8.2 &#x1f50d;引言&#x1f4d2;1. ONLYOFFICE 产品简介&#x1f4da;2. 功能与特点&#x1f341;协作编辑 PDF&#x1f342;…

一文带你了解,全国职业院校技能大赛老年护理与保健赛项如何备赛

老年护理与保健&#xff0c;作为2023年全国职业院校技能大赛的新增赛项&#xff0c;紧密贴合党的二十大精神&#xff0c;致力于加速健康与养老产业的蓬勃发展&#xff0c;并深化医养康养结合的服务模式。此赛项不仅承载着立德树人的教育使命&#xff0c;更通过竞赛的引领作用&a…