前言
Vue 编译器主要处理内容
- 将组件的 html 模版解析成 AST 对象
- 优化
- 通过遍历 AST 对象,为每个节点做 静态标记,通过标记其是否为静态节点,然后进一步标记出 静态根节点,方便在后续更新过程中跳过这些静态节点
- 标记静态根用于生成渲染函数阶段,生成静态根节点的渲染函数
- 从 AST 生成运行渲染函数
- render 函数
- staticRenderFns 数组,里面保存了所有的 静态节点的渲染函数
编译器的解析过程是如何将 html 字符串模版变成 AST 对象?
- 遍历
HTML
模版字符串,通过正则表达式匹配"<"
- 跳过某些不需要处理的标签,比如:注释标签
<!-- xxx -->
、条件注释标签<!--[if IE]>
、<!DOCTYPE html>
- 解析开始标签
- 解析得到一个对象,包括标签名(
tagName
)、所有的属性(attrs
)、标签在html
模版字符串中的索引位置 - 接着处理上一步的
attrs
属性,将其变成[{ name: attrName, value: attrVal, start: xx, end: xx }, ...]
的形式 - 通过标签名、属性对象和当前元素的父元素生成
AST
对象(普通的JS
对象),通过key、value
的形式记录了该元素的一些信息 - 接下来进一步处理开始标签上的一些指令,比如
v-pre、v-for、v-if、v-once
,并将处理结果放到AST
对象上 - 步骤(2、3、4)处理结束后将
ast
对象保存到stack
数组中 - 之前的所有处理完成后,会截断
html
字符串,将已经处理掉的字符串截掉
- 解析闭合标签
- 如果匹配到结束标签,就从
stack
数组中拿出最后一个元素,它和当前匹配到的结束标签是一对 - 再次处理开始标签上的属性,这些属性和前面处理的不一样,比如:
key、ref、scopedSlot、样式
等,并将处理结果放到元素的AST
对象 - 然后将当前元素和父元素产生关联,给当前元素的
ast
对象设置parent
属性,然后将自己放到父元素的ast
对象的children
数组中
- 最后遍历完整个
html
模版字符串以后,返回ast
对象
深入源码
编译器入口 —— Vue.prototype.$mount
文件位置:src\platforms\web\entry-runtime-with-compiler.js
这里重点在于获取动态渲染函数 render 函数和静态渲染函数 staticRenderFns 的 compileToFunctions 方法.
// 保存原来的 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 为 string if (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 = 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') } } } // 通过调用前面保存 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 和 静态渲染函数 staticRenderFns 5、缓存编译结果 */ 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 || baseWarn delete options.warn /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production') { // detect possible CSP restriction try { 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.' ) } } } // 定义缓存对应的 key const 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 functions const 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 和 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) } } // 合并自定义 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 = errors compiled.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-model modules, // 指令 directives, // pre 标签 isPreTag, // 是否是一元标签 isUnaryTag, // 必须用于 props 的属性 mustUseProp, // 只有开始标签的标签 canBeLeftOpenTag, // 保留标签 isReservedTag, // 命名空间 getTagNamespace, // 静态 key staticKeys: genStaticKeys(modules) } 复制代码
baseCompile() 方法
文件位置:src\compiler\index.js
这里的重点就是通过 parse 方法将 html 模版字符串解析成 ast.
/* 在这之前做的所有的事情,只是为了构建平台特有的编译选项(options),比如 web 平台 1、将 html 模版字符串解析成 ast 2、对 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 } })