Vue|Vue 源码解读(8)—— 编译器 之 解析(上)

当学习成为了习惯,知识也就变成了常识。 感谢各位的 关注、点赞、收藏和评论。
新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn
文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。
特殊说明 由于文章篇幅限制,所以将 Vue 源码解读(8)—— 编译器 之 解析 拆成了上下两篇,所以在阅读本篇文章时请同时打开 Vue 源码解读(8)—— 编译器 之 解析(下)一起阅读。
前言 Vue 源码解读(4)—— 异步更新 最后说到刷新 watcher 队列,执行每个 watcher.run 方法,由 watcher.run 调用 watcher.get,从而执行 watcher.getter 方法,进入实际的更新阶段。这个流程如果不熟悉,建议大家再去读一下这篇文章。
当更新一个渲染 watcher 时,执行的是 updateComponent 方法:

// /src/core/instance/lifecycle.js const updateComponent = () => { // 执行 vm._render() 函数,得到 虚拟 DOM,并将 vnode 传递给 _update 方法,接下来就该到 patch 阶段了 vm._update(vm._render(), hydrating) }

可以看到每次更新前都需要先执行一下 vm._render() 方法,vm._render 就是大家经常听到的 render 函数,由两种方式得到:
  • 用户自己提供,在编写组件时,用 render 选项代替模版
  • 由编译器编译组件模版生成 render 选项
今天我们就来深入研究编译器,看看它是怎么将我们平时编写的类 html 模版编译成 render 函数的。
编译器的核心由三部分组成:
  • 解析,将类 html 模版转换为 AST 对象
  • 优化,也叫静态标记,遍历 AST 对象,标记每个节点是否为静态节点,以及标记出静态根节点
  • 生成渲染函数,将 AST 对象生成渲染函数
由于编译器这块儿的代码量太大,所以,将这部分知识拆成三部分来讲,第一部分就是:解析。
目标 深入理解 Vue 编译器的解析过程,理解如何将类 html 模版字符串转换成 AST 对象。
源码解读 接下来我们去源码中找答案。
阅读建议
由于解析过程代码量巨大,所以建议大家抓住主线:“解析类 HTML 字符串模版,生成 AST 对象”,而这个 AST 对象就是我们最终要得到的结果,所以大家在阅读的过程中,要动手记录这个 AST 对象,这样有助于理解,也让你不那么容易迷失。
也可以先阅读 下篇 的 帮助 部分,有个提前的准备和心理预期。
入口 - $mount
编译器的入口位置在 /src/platforms/web/entry-runtime-with-compiler.js,有两种方式找到这个入口
  • 断点调试,Vue 源码解读(2)—— Vue 初始化过程 中讲到,初始化的最后一步是执行 $mount 进行挂载,在全量的 Vue 包中这一步就会进入编译阶段。
  • 通过 rollup 的配置文件一步步的去找
/src/platforms/web/entry-runtime-with-compiler.js
/** * 编译器的入口 * 运行时的 Vue.js 包就没有这部分的代码,通过 打包器 结合 vue-loader + vue-compiler-utils 进行预编译,将模版编译成 render 函数 * * 就做了一件事情,得到组件的渲染函数,将其设置到 this.$options 上 */ const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { // 挂载点 el = el && query(el)// 挂载点不能是 body 或者 html /* istanbul ignore if */ if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue toor - mount to normal elements instead.` ) return this }// 配置项 const options = this.$options // resolve template/el and convert to render function /** * 如果用户提供了 render 配置项,则直接跳过编译阶段,否则进入编译阶段 *解析 template 和 el,并转换为 render 函数 *优先级:render > template > el */ if (!options.render) { let template = options.template if (template) { // 处理 template 选项 if (typeof template === 'string') { if (template.charAt(0) === '#') { // { template: '#app' },template 是一个 id 选择器,则获取该元素的 innerHtml 作为模版 template = idToTemplate(template) /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !template) { warn( `Template element not found or is empty: ${options.template}`, this ) } } } else if (template.nodeType) { // template 是一个正常的元素,获取其 innerHtml 作为模版 template = template.innerHTML } else { if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } return this } } else if (el) { // 设置了 el 选项,获取 el 选择器的 outerHtml 作为模版 template = getOuterHTML(el) } if (template) { // 模版就绪,进入编译阶段 /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile') }// 编译模版,得到 动态渲染函数和静态渲染函数 const { render, staticRenderFns } = compileToFunctions(template, { // 在非生产环境下,编译时记录标签属性在模版字符串中开始和结束的位置索引 outputSourceRange: process.env.NODE_ENV !== 'production', shouldDecodeNewlines, shouldDecodeNewlinesForHref, // 界定符,默认 {{}} delimiters: options.delimiters, // 是否保留注释 comments: options.comments }, this) // 将两个渲染函数放到 this.$options 上 options.render = render options.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') } } } // 执行挂载 return mount.call(this, el, hydrating) }

compileToFunctions
/src/compiler/to-function.js
/** * 1、执行编译函数,得到编译结果 -> compiled * 2、处理编译期间产生的 error 和 tip,分别输出到控制台 * 3、将编译得到的字符串代码通过 new Function(codeStr) 转换成可执行的函数 * 4、缓存编译结果 * @param { string } template 字符串模版 * @param { CompilerOptions } options 编译选项 * @param { Component } vm 组件实例 * @return { render, staticRenderFns } */ return function compileToFunctions( template: string, options?: CompilerOptions, vm?: Component ): CompiledFunctionResult { // 传递进来的编译选项 options = extend({}, options) // 日志 const warn = options.warn || baseWarn delete options.warn/* istanbul ignore if */ if (process.env.NODE_ENV !== 'production') { // 检测可能的 CSP 限制 try { new Function('return 1') } catch (e) { if (e.toString().match(/unsafe-eval|CSP/)) { // 看起来你在一个 CSP 不安全的环境中使用完整版的 Vue.js,模版编译器不能工作在这样的环境中。 // 考虑放宽策略限制或者预编译你的 template 为 render 函数 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.' ) } } }// 如果有缓存,则跳过编译,直接从缓存中获取上次编译的结果 const key = options.delimiters ? String(options.delimiters) + template : template if (cache[key]) { return cache[key] }// 执行编译函数,得到编译结果 const compiled = compile(template, options)// 检查编译期间产生的 error 和 tip,分别输出到控制台 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)) } } }// 转换编译得到的字符串代码为函数,通过 new Function(code) 实现 // turn code into functions const res = {} const fnGenErrors = [] res.render = createFunction(compiled.render, fnGenErrors) res.staticRenderFns = compiled.staticRenderFns.map(code => { return createFunction(code, fnGenErrors) })// 处理上面代码转换过程中出现的错误,这一步一般不会报错,除非编译器本身出错了 /* 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
/** * 编译函数,做了两件事: *1、选项合并,将 options 配置项 合并到 finalOptions(baseOptions) 中,得到最终的编译配置对象 *2、调用核心编译器 baseCompile 得到编译结果 *3、将编译期间产生的 error 和 tip 挂载到编译结果上,返回编译结果 * @param {*} template 模版 * @param {*} options 配置项 * @returns */ function compile( template: string, options?: CompilerOptions ): CompiledResult { // 以平台特有的编译配置为原型创建编译选项对象 const finalOptions = Object.create(baseOptions) const errors = [] const tips = []// 日志,负责记录将 error 和 tip let warn = (msg, range, tip) => { (tip ? tips : errors).push(msg) }// 如果存在编译选项,合并 options 和 baseOptions if (options) { // 开发环境走 if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { // $flow-disable-line const 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) } }/** * 将 options 中的配置项合并到 finalOptions */// 合并自定义 module if (options.modules) { finalOptions.modules = (baseOptions.modules || []).concat(options.modules) } // 合并自定义指令 if (options.directives) { finalOptions.directives = extend( Object.create(baseOptions.directives || null), options.directives ) } // 拷贝其它配置项 for (const key in options) { if (key !== 'modules' && key !== 'directives') { finalOptions[key] = options[key] } } }// 日志 finalOptions.warn = warn// 到这里为止终于到重点了,调用核心编译函数,传递模版字符串和最终的编译选项,得到编译结果 // 前面做的所有事情都是为了构建平台最终的编译选项 const compiled = baseCompile(template.trim(), finalOptions) if (process.env.NODE_ENV !== 'production') { detectErrors(compiled.ast, warn) } // 将编译期间产生的错误和提示挂载到编译结果上 compiled.errors = errors compiled.tips = tips return compiled }

baseOptions
/src/platforms/web/compiler/options.js
export const baseOptions: CompilerOptions = { expectHTML: true, // 处理 class、style、v-model modules, // 处理指令 // 是否是 pre 标签 isPreTag, // 是否是自闭合标签 isUnaryTag, // 规定了一些应该使用 props 进行绑定的属性 mustUseProp, // 可以只写开始标签的标签,结束标签浏览器会自动补全 canBeLeftOpenTag, // 是否是保留标签(html + svg) isReservedTag, // 获取标签的命名空间 getTagNamespace, staticKeys: genStaticKeys(modules) }

baseCompile
/src/compiler/index.js
/** * 在这之前做的所有的事情,只有一个目的,就是为了构建平台特有的编译选项(options),比如 web 平台 * * 1、将 html 模版解析成 ast * 2、对 ast 树进行静态标记 * 3、将 ast 生成渲染函数 *静态渲染函数放到code.staticRenderFns 数组中 *code.render 为动态渲染函数 *在将来渲染时执行渲染函数得到 vnode */ function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { // 将模版解析为 AST,每个节点的 ast 对象上都设置了元素的所有信息,比如,标签信息、属性信息、插槽信息、父节点、子节点等。 // 具体有那些属性,查看 start 和 end 这两个处理开始和结束标签的方法 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
/** * * 将 HTML 字符串转换为 AST * @param {*} template HTML 模版 * @param {*} options 平台特有的编译选项 * @returns root */ export function parse( template: s tring, 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 || !isReservedTag(el.tag)// 分别获取 options.modules 下的 class、model、style 三个模块中的 transformNode、preTransformNode、postTransformNode 方法 // 负责处理元素节点上的 class、style、v-model transforms = pluckModuleFunction(options.modules, 'transformNode') preTransforms = pluckModuleFunction(options.modules, 'preTransformNode') postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')// 界定符,比如: {{}} delimiters = options.delimitersconst stack = [] // 空格选项 const preserveWhitespace = options.preserveWhitespace !== false const whitespaceOption = options.whitespace // 根节点,以 root 为根,处理后的节点都会按照层级挂载到 root 下,最后 return 的就是 root,一个 ast 语法树 let root // 当前元素的父元素 let currentParent let inVPre = false let inPre = false let warned = false// 解析 html 模版字符串,处理所有标签以及标签上的属性 // 这里的 parseHTMLOptions 在后面处理过程中用到,再进一步解析 // 提前解析的话容易让大家岔开思路 parseHTML(template, parseHtmlOptions)// 返回生成的 ast 对象 return root

parseHTML
/src/compiler/parser/html-parser.js
/** * 通过循环遍历 html 模版字符串,依次处理其中的各个标签,以及标签上的属性 * @param {*} html html 模版 * @param {*} options 配置项 */ export function parseHTML(html, options) { const stack = [] const expectHTML = options.expectHTML // 是否是自闭合标签 const isUnaryTag = options.isUnaryTag || no // 是否可以只有开始标签 const canBeLeftOpenTag = options.canBeLeftOpenTag || no // 记录当前在原始 html 字符串中的开始位置 let index = 0 let last, lastTag while (html) { last = html // 确保不是在 script、style、textarea 这样的纯文本元素中 if (!lastTag || !isPlainTextElement(lastTag)) { // 找第一个 < 字符 let textEnd = html.indexOf('<') // textEnd === 0 说明在开头找到了 // 分别处理可能找到的注释标签、条件注释标签、Doctype、开始标签、结束标签 // 每处理完一种情况,就会截断(continue)循环,并且重置 html 字符串,将处理过的标签截掉,下一次循环处理剩余的 html 字符串模版 if (textEnd === 0) { // 处理注释标签 if (comment.test(html)) { // 注释标签的结束索引 const commentEnd = html.indexOf('-->')if (commentEnd >= 0) { // 是否应该保留 注释 if (options.shouldKeepComment) { // 得到:注释内容、注释的开始索引、结束索引 options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3) } // 调整 html 和 index 变量 advance(commentEnd + 3) continue } }// 处理条件注释标签:/g, '$1') // #7298 .replace(//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) }// 到这里就处理结束,如果 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 tags parseEndTag() }

advance
/src/compiler/parser/html-parser.js
/** * 重置 html,html = 从索引 n 位置开始的向后的所有字符 * index 为 html 在 原始的 模版字符串 中的的开始索引,也是下一次该处理的字符的开始位置 * @param {*} n 索引 */ function advance(n) { index += n html = html.substring(n) }

parseStartTag
/src/compiler/parser/html-parser.js
/** * 解析开始标签,比如: * @returns { tagName: 'div', attrs: [[xx], ...], start: index } */ function parseStartTag() { const start = html.match(startTagOpen) if (start) { // 处理结果 const match = { // 标签名 tagName: start[1], // 属性,占位符 attrs: [], // 标签的开始位置 start: index } /** * 调整 html 和 index,比如: *html = ' id="app">' *index = 此时的索引 *start[0] = '' 或 end = ' />' if (end) { match.unarySlash = end[1] advance(end[0].length) match.end = index return match } } }

handleStartTag
/src/compiler/parser/html-parser.js
/** * 进一步处理开始标签的解析结果 ——— match 对象 *处理属性 match.attrs,如果不是自闭合标签,则将标签信息放到 stack 数组,待将来处理到它的闭合标签时再将其弹出 stack,表示该标签处理完毕,这时标签的所有信息都在 element ast 对象上了 *接下来调用 options.start 方法处理标签,并根据标签信息生成 element ast, *以及处理开始标签上的属性和指令,最后将 element ast 放入 stack 数组 * * @param {*} 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) } }// 一元标签,比如 const unary = isUnaryTag(tagName) || !!unarySlash// 处理 match.attrs,得到 attrs = [{ name: attrName, value: attrVal, start: xx, end: xx }, ...] // 比如 attrs = [{ name: 'id', value: 'app', start: xx, end: xx }, ...] const l = match.attrs.length const attrs = new Array(l) for (let i = 0; i < l; i++) { const args = match.attrs[i] // 比如:args[3] => 'id',args[4] => '=',args[5] => 'app' const value = https://www.it610.com/article/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*/).length attrs[i].end = args.end } }// 如果不是自闭合标签,则将标签信息放到 stack 数组中,待将来处理到它的闭合标签时再将其弹出 stack // 如果是自闭合标签,则标签信息就没必要进入 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 }) // 标识当前标签的结束标签为 tagName lastTag = tagName }/** * 调用 start 方法,主要做了以下 6 件事情: *1、创建 AST 对象 *2、处理存在 v-model 指令的 input 标签,分别处理 input 为 checkbox、radio、其它的情况 *3、处理标签上的众多指令,比如 v-pre、v-for、v-if、v-once *4、如果根节点 root 不存在则设置当前元素为根节点 *5、如果当前元素为非自闭合标签则将自己 push 到 stack 数组,并记录 currentParent,在接下来处理子元素时用来告诉子元素自己的父节点是谁 *6、如果当前元素为自闭合标签,则表示该标签要处理结束了,让自己和父元素产生关系,以及设置自己的子元素 */ if (options.start) { options.start(tagName, attrs, unary, match.start, match.end) } }

parseEndTag
/src/compiler/parser/html-parser.js
/** * 解析结束标签,比如: * 最主要的事就是: *1、处理 stack 数组,从 stack 数组中找到当前结束标签对应的开始标签,然后调用 options.end 方法 *2、处理完结束标签之后调整 stack 数组,保证在正常情况下 stack 数组中的最后一个元素就是下一个结束标签对应的开始标签 *3、处理一些异常情况,比如 stack 数组最后一个元素不是当前结束标签对应的开始标签,还有就是 *br 和 p 标签单独处理 * @param {*} tagName 标签名,比如 div * @param {*} start 结束标签的开始索引 * @param {*} end 结束标签的结束索引 */ function parseEndTag(tagName, start, end) { let pos, lowerCasedTagName if (start == null) start = index if (end == null) end = index// 倒序遍历 stack 数组,找到第一个和当前结束标签相同的标签,该标签就是结束标签对应的开始标签的描述对象 // 理论上,不出异常,stack 数组中的最后一个元素就是当前结束标签的开始标签的描述对象 // Find the closest opened tag of the same type 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 shop pos = 0 }// 如果在 stack 中一直没有找到相同的标签名,则 pos 就会 < 0,进行后面的 else 分支if (pos >= 0) { // 这个 for 循环负责关闭 stack 数组中索引 >= pos 的所有标签 // 为什么要用一个循环,上面说到正常情况下 stack 数组的最后一个元素就是我们要找的开始标签, // 但是有些异常情况,就是有些元素没有给提供结束标签,比如: // stack = ['span', 'div', 'span', 'h1'],当前处理的结束标签 tagName = div // 匹配到 div,pos = 1,那索引为 2 和 3 的两个标签(span、h1)说明就没提供结束标签 // 这个 for 循环就负责关闭 div、span 和 h1 这三个标签, // 并在开发环境为 span 和 h1 这两个标签给出 ”未匹配到结束标签的提示” // 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.`, { start: stack[i].start, end: stack[i].end } ) } if (options.end) { // 走到这里,说明上面的异常情况都处理完了,调用 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') { // 当前处理的标签为
标签 if (options.start) { options.start(tagName, [], true, start, end) } } else if (lowerCasedTagName === 'p') { // p 标签 if (options.start) { // 处理 标签 options.start(tagName, [], false, start, end) } if (options.end) { // 处理
标签 options.end(tagName, start, end) } } }

parseHtmlOptions
src/compiler/parser/index.js
定义如何处理开始标签、结束标签、文本节点和注释节点。
start
/** * 主要做了以下 6 件事情: *1、创建 AST 对象 *2、处理存在 v-model 指令的 input 标签,分别处理 input 为 checkbox、radio、其它的情况 *3、处理标签上的众多指令,比如 v-pre、v-for、v-if、v-once *4、如果根节点 root 不存在则设置当前元素为根节点 *5、如果当前元素为非自闭合标签则将自己 push 到 stack 数组,并记录 currentParent,在接下来处理子元素时用来告诉子元素自己的父节点是谁 *6、如果当前元素为自闭合标签,则表示该标签要处理结束了,让自己和父元素产生关系,以及设置自己的子元素 * @param {*} tag 标签名 * @param {*} attrs [{ name: attrName, value: attrVal, start, end }, ...] 形式的属性数组 * @param {*} unary 自闭合标签 * @param {*} start 标签在 html 字符串中的开始索引 * @param {*} end 标签在 html 字符串中的结束索引 */ function 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、end if (process.env.NODE_ENV !== 'production') { if (options.outputSourceRange) { element.start = start element.end = end // 将属性数组解析成 { attrName: { name: attrName, value: attrVal, start, end }, ... } 形式的对象 element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => { cumulated[attr.name] = attr return 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 标签 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.', { start: element.start } ) }/** * 为 element 对象分别执行 class、style、model 模块中的 preTransforms 方法 * 不过 web 平台只有 model 模块有 preTransforms 方法 * 用来处理存在 v-model 的 input 标签,但没处理 v-model 属性 * 分别处理了 input 为 checkbox、radio 和 其它的情况 * input 具体是哪种情况由 el.ifConditions 中的条件来判断 * */ // apply pre-transforms for (let i = 0; i < preTransforms.length; i++) { element = preTransforms[i](element, options) || element }if (!inVPre) { // 表示 element 是否存在 v-pre 指令,存在则设置 element.pre = true processPre(element) if (element.pre) { // 存在 v-pre 指令,则设置 inVPre 为 true inVPre = true } } // 如果 pre 标签,则设置 inPre 为 true if (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 = true * v-if 属性会额外在 element.ifConditions 数组中添加 { exp, block } 对象 */ processIf(element) // 处理 v-once 指令,得到 element.once = true processOnce(element) }// 如果 root 不存在,则表示当前处理的元素为第一个元素,即组件的 根 元素 if (!root) { root = element if (process.env.NODE_ENV !== 'production') { // 检查根元素,对根元素有一些限制,比如:不能使用 slot 和 template 作为根元素,也不能在有状态组件的根元素上使用 v-for 指令 checkRootConstraints(root) } }if (!unary) { // 非自闭合标签,通过 currentParent 记录当前元素,下一个元素在处理的时候,就知道自己的父元素是谁 currentParent = element // 然后将 element push 到 stack 数组,将来处理到当前元素的闭合标签时再拿出来 // 将当前标签的 ast 对象 push 到 stack 数组中,这里需要注意,在调用 options.start 方法 // 之前也发生过一次 push 操作,那个 push 进来的是当前标签的一个基本配置信息 stack.push(element) } else { /** * 说明当前元素为自闭合标签,主要做了 3 件事: *1、如果元素没有被处理过,即 el.processed 为 false,则调用 processElement 方法处理节点上的众多属性 *2、让自己和父元素产生关系,将自己放到父元素的 children 数组中,并设置自己的 parent 属性为 currentParent *3、设置自己的子元素,将自己所有非插槽的子元素放到自己的 children 数组中 */ closeElement(element) } }

end
/** * 处理结束标签 * @param {*} tag 结束标签的名称 * @param {*} start 结束标签的开始索引 * @param {*} end 结束标签的结束索引 */ function end(tag, start, end) { // 结束标签对应的开始标签的 ast 对象 const element = stack[stack.length - 1] // pop stack stack.length -= 1 // 这块儿有点不太理解,因为上一个元素有可能是当前元素的兄弟节点 currentParent = stack[stack.length - 1] if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { element.end = end } /** * 主要做了 3 件事: *1、如果元素没有被处理过,即 el.processed 为 false,则调用 processElement 方法处理节点上的众多属性 *2、让自己和父元素产生关系,将自己放到父元素的 children 数组中,并设置自己的 parent 属性为 currentParent *3、设置自己的子元素,将自己所有非插槽的子元素放到自己的 children 数组中 */ closeElement(element) }

chars
/** * 处理文本,基于文本生成 ast 对象,然后将该 ast 放到它的父元素的肚子里,即 currentParent.children 数组中 */ function 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) { // 说明文本不在 pre 标签内而且 text.trim() 为空,而且当前父元素也没有孩子节点, // 则将 text 置为空 // remove the whitespace-only node right after an opening tag text = '' } else if (whitespaceOption) { // 压缩处理 if (whitespaceOption === 'condense') { // in condense mode, remove the whitespace node if it contains // line break, otherwise condense to a single space text = lineBreakRE.test(text) ? '' : ' ' } else { text = ' ' } } else { text = preserveWhitespace ? ' ' : '' } // 如果经过处理后 text 还存在 if (text) { if (!inPre && whitespaceOption === 'condense') { // 不在 pre 节点中,并且配置选项中存在压缩选项,则将多个连续空格压缩为单个 // condense consecutive whitespaces into single space text = text.replace(whitespaceRE, ' ') } let res // 基于 text 生成 AST 对象 let child: ?ASTNode if (!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 放到父元素的肚子里,即 currentParent.children 数组中 if (child) { if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { child.start = start child.end = end } children.push(child) } } },

comment
/** * 处理注释节点 */ function 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) { // 注释节点的 ast const child: ASTText = { // 节点类型 type: 3, // 注释内容 text, // 是否为注释 isComment: true } if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { // 记录节点的开始索引和结束索引 child.start = start child.end = end } // 将当前注释节点放到父元素的 children 属性中 currentParent.children.push(child) } }

createASTElement
/src/compiler/parser/index.js
/** * 为指定元素创建 AST 对象 * @param {*} tag 标签名 * @param {*} attrs 属性数组,[{ name: attrName, value: attrVal, start, end }, ...] * @param {*} parent 父元素 * @returns { type: 1, tag, attrsList, attrsMap: makeAttrsMap(attrs), rawAttrsMap: {}, parent, children: []} */ export function createASTElement( tag: string, attrs: Array, parent: ASTElement | void ): ASTElement { return { // 节点类型 type: 1, // 标签名 tag, // 标签的属性数组 attrsList: attrs, // 标签的属性对象 { attrName: attrVal, ... } attrsMap: makeAttrsMap(attrs), // 原始属性对象 rawAttrsMap: {}, // 父节点 parent, // 孩子节点 children: [] } }

preTransformNode
/src/platforms/web/compiler/modules/model.js
/** * 处理存在 v-model 的 input 标签,但没处理 v-model 属性 * 分别处理了 input 为 checkbox、radio 和 其它的情况 * input 具体是哪种情况由 el.ifConditions 中的条件来判断 * * @param {*} el * @param {*} options * @returns branch0 */ function preTransformNode (el: ASTElement, options: CompilerOptions) { if (el.tag === 'input') { const map = el.attrsMap // 不存在 v-model 属性,直接结束 if (!map['v-model']) { return }// 获取 :type 的值 let typeBinding if (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 的值,比如: const ifCondition = getAndRemoveAttr(el, 'v-if', true) // &&test const ifConditionExtra = ifCondition ? `&&(${ifCondition})` : `` // 是否存在 v-else 属性, const hasElse = getAndRemoveAttr(el, 'v-else', true) != null // 获取 v-else-if 属性的值 const elseIfCondition = getAndRemoveAttr(el, 'v-else-if', true) // 克隆一个新的 el 对象,分别处理 input 为 chekbox、radio 或 其它的情况 // 具体是哪种情况,通过 el.ifConditins 条件来判断 // 1. checkbox const branch0 = cloneASTElement(el) // process for on the main node // // 处理 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&&test or false&&test,标记当前 input 是否为 checkbox branch0.if = `(${typeBinding})==='checkbox'` + ifConditionExtra // 在 branch0.ifConfitions 数组中放入 { exp, block } 对象 addIfCondition(branch0, { exp: branch0.if, block: branch0 }) // 克隆一个新的 ast 对象 // 2. add radio else-if condition const 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) // 在 branch0.ifConfitions 数组中放入 { exp, block } 对象 addIfCondition(branch0, { // 标记当前 input 是否为 radio exp: `(${typeBinding})==='radio'` + ifConditionExtra, block: branch1 }) // 3. other,input 为其它的情况 const branch2 = cloneASTElement(el) 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
/src/compiler/helpers.js
/** * 获取 el 对象上执行属性 name 的值 */ export function getBindingAttr ( el: ASTElement, name: string, getStatic?: boolean ): ?string { // 获取指定属性的值 const dynamicValue = https://www.it610.com/article/getAndRemoveAttr(el,':' + name) || getAndRemoveAttr(el, 'v-bind:' + name) if (dynamicValue != null) { return parseFilters(dynamicValue) } else if (getStatic !== false) { const staticValue = https://www.it610.com/article/getAndRemoveAttr(el, name) if (staticValue != null) { return JSON.stringify(staticValue) } } }

getAndRemoveAttr
/src/compiler/helpers.js
/** * 从 el.attrsList 中删除指定的属性 name * 如果 removeFromMap 为 true,则同样删除 el.attrsMap 对象中的该属性, *比如 v-if、v-else-if、v-else 等属性就会被移除, *不过一般不会删除该对象上的属性,因为从 ast 生成 代码 期间还需要使用该对象 * 返回指定属性的值 */ // note: this only removes the attr from the Array (attrsList) so that it // doesn't get processed by processAttrs. // By default it does NOT remove it from the map (attrsMap) because the map is // needed during codegen. export function getAndRemoveAttr ( el: ASTElement, name: string, removeFromMap?: boolean ): ?string { let val // 将执行属性 name 从 el.attrsList 中移除 if ((val = el.attrsMap[name]) != null) { const list = el.attrsList for (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 }

processFor
/src/compiler/parser/index.js
/** * 处理 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'] ) } } }

addRawAttr
/src/compiler/helpers.js
// 在 el.attrsMap 和 el.attrsList 中添加指定属性 name // add a raw attr (use this in preTransforms) export function addRawAttr (el: ASTElement, name: string, value: any, range?: Range) { el.attrsMap[name] = value el.attrsList.push(rangeSetItem({ name, value }, range)) }

processElement
/src/compiler/parser/index.js
/** * 分别处理元素节点的 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 ) { // el.key = val processKey(element)// 确定 element 是否为一个普通元素 // determine whether this is a plain element after // removing structural attributes element.plain = ( !element.key && !element.scopedSlots && !element.attrsList.length )// el.ref = val, el.refInFor = boolean processRef(element) // 处理作为插槽传递给组件的内容,得到插槽名称、是否为动态插槽、作用域插槽的值,以及插槽中的所有子元素,子元素放到插槽对象的 children 属性中 processSlotContent(element) // 处理自闭合的 slot 标签,得到插槽名称 => el.slotName = xx processSlotOutlet(element) // 处理动态组件,得到 el.component = compName, // 以及标记是否存在内联模版,el.inlineTemplate = true of false processComponent(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 } /** * 处理元素上的所有属性: * v-bind 指令变成:el.attrs 或 el.dynamicAttrs = [{ name, value, start, end, dynamic }, ...], *或者是必须使用 props 的属性,变成了 el.props = [{ name, value, start, end, dynamic }, ...] * v-on 指令变成:el.events 或 el.nativeEvents = { name: [{ value, start, end, modifiers, dynamic }, ...] } * 其它指令:el.directives = [{name, rawName, value, arg, isDynamicArg, modifier, start, end }, ...] * 原生属性:el.attrs = [{ name, value, start, end }],或者一些必须使用 props 的属性,变成了: *el.props = [{ name, value: true, start, end, dynamic }] */ processAttrs(element) return element }

processKey
/src/compiler/parser/index.js
/** * 处理元素上的 key 属性,设置 el.key = val * @param {*} el */ function processKey(el) { // 拿到 key 的属性值 const exp = getBindingAttr(el, 'key') if (exp) { // 关于 key 使用上的异常处理 if (process.env.NODE_ENV !== 'production') { // template 标签不允许设置 key if (el.tag === 'template') { warn( `