vue模版编译
模板编译的作用
主要目的是将template转换为渲染函数(render)
<div>
<h1 @click="handler">title</h1>
<p>some content</p>
</div>
渲染函数 render
render(h) {
return h('div', [
h('h1', { on: { click: this.handler } }, 'title'),
h('p', 'some content')
])
}
Vue2.x使用VNode来描述视图及交互,自己手写比较麻烦。通过编译器可以将模板转换为返回 VNode 的 render 函数,要注意的是.vue 文件会被 webpack 在构建的过程中(vue-loader)转换成 render 函数
体验模板编译的结果
<div id="app">
<h1>Vue<span>模板编译过程</span></h1>
<p>{{ msg }}</p>
<comp @myclick="handler"></comp>
</div>
<script src="../../dist/vue.js"></script>
<script>
Vue.component('comp', {
template: '<div>I am a comp</div>'
})
const vm = new Vue({
el: '#app',
data: {
msg: 'Hello compiler'
},
methods: {
handler() {
console.log('test')
}
}
})
// 打印编译生成的render函数
console.log(vm.$options.render)
</script>
编译后 render 输出的结果
(function anonymous() {
with (this) {
return _c(
"div",
{ attrs: { id: "app" } },
[
_m(0),
_v(" "),
_c("p", [_v(_s(msg))]),
_v(" "),
_c("comp", { on: { myclick: handler } }),
],
1
);
}
});
- _c 是 createElement() 方法,定义的位置 instance/render.js 中
- 相关的渲染函数(_开头的方法定义),在 instance/render-helps/index.js 中
- 对比vue2和vue3的模版编译结果,vue2不会过滤掉多余的空格,所以要少写空格以优化代码
编译的入口
src\platforms\web\entry-runtime-with-compiler.js
- 拓展了$mount方法
- 解析了el,template等选项,根据模版生成了render
// 可以看到在这里引入了Vue
import Vue from './runtime/index'
// 拓展了$mount方法,说明除了它本来要做的一些事情还要做
// 一些其他的事情,在web平台上
const mount = Vue.prototype.$mount
// 定义了$mount方法
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
const options = this.$options
// resolve template/el and convert to render function
// 先判断有没有render函数(用户传入的)
// 说明render优先级最高,其次template
if (!options.render) {
let template = options.template
// 没有传递的化继续判断是否有template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
// 如果有就是我们的模版
template = idToTemplate(template)
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
return this
}
} else if (el) {
// 如果没有template,el的OuterHTML作为模版
// 也就是说我们的模版就是template或者el
template = getOuterHTML(el)
}
// 最终获取模版之后编译它
if (template) {
// 可以看到编译最终目标就是获取render,和一个静态renderr方法
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
// 还会赋值给options
options.render = render
options.staticRenderFns = staticRenderFns
}
}
// 执行默认的挂载功能
return mount.call(this, el, hydrating)
}
function getOuterHTML (el: Element): string {
if (el.outerHTML) {
return el.outerHTML
} else {
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
Vue.compile = compileToFunctions
export default Vue
compileToFunctions
const { compile, compileToFunctions } = createCompiler(baseOptions)
// baseOptions是跟web平台相关的选项
baseOptions
export const baseOptions: CompilerOptions = {
expectHTML: true, // 期望的是html的内容
modules, // 模块和指令
directives,
isPreTag, // 是否是pre标签和自闭和标签
isUnaryTag,
mustUseProp,
canBeLeftOpenTag,
isReservedTag, // 是否是html的保留标签
getTagNamespace,
staticKeys: genStaticKeys(modules)
}
createCompiler
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 把模版编译成ast抽象语法树
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 优化抽象语法树
optimize(ast, options)
}
// 把抽象语法树转换成字符串形式的js代码
const code = generate(ast, options)
return {
ast,
// 这里的render不是最终的,是字符串形式的,还要通过toFunction转换成函数形式
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
createCompilerCreator
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
function compile ( // 定义了compile函数
template: string,
options?: CompilerOptions
): CompiledResult {}
...
return {
compile,
// 返回了这个入口函数🍂
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
createCompileToFunctionFn
就是入口的compileToFunctions
export function createCompileToFunctionFn (compile: Function): Function {
// 先定义一个cache缓存编译后的结果
const cache = Object.create(null)
return function compileToFunctions (// 🍂
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
// 克隆了一份options防止污染
options = extend({}, options)
// check cache
// 判断缓存中是否有编译的结果,把模版的内容作为key
const key = options.delimiters// 改变差值表达式使用的符号,完整版才有
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// compile
// 这个编译后的对象有render和staticRenderFns
// 此时的render中存储的是字符串形式的js代码
const compiled = compile(template, options)
// turn code into functions
const res = {}
const fnGenErrors = []
// 把字符串形式的代码转换成函数
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
// 通过new Function(code)
return createFunction(code, fnGenErrors)
})
// 缓存并返回res对象(render和staticRenderFns方法)
return (cache[key] = res)
}
}
到这里编译的入口函数就完了,核心就是缓存编译后的结果,有就直接返回,没有就开始编译,并且把编译后的字符串形式的代码转换成函数形式缓存并返回
模版编译过程
compile
定义在createCompilerCreator里的createCompiler,核心作用就是合并选项,调用baseCompile进行编译
function compile (
template: string,
options?: CompilerOptions // 用户传入的选项
): CompiledResult {
// 用来最后合并参数的options和baseOptions
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
// 开始合并
if (options) {
// merge custom modules
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules)
}
// merge custom directives
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.directives
)
}
// copy other options
for (const key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key]
}
}
}
// 调用模版编译的核心函数开始编译
const compiled = baseCompile(template.trim(), finalOptions)
return compiled
}
baseCompile
编译的核心函数,生成AST,分为三步:解析,优化和生成
// 把模版编译成ast抽象语法树(现在模版是字符串)
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 优化抽象语法树
optimize(ast, options)
}
// 把抽象语法树转换成字符串形式的js代码
const code = generate(ast, options)
return {
ast,
// 这里的render不是最终的,是字符串形式的,还要通过toFunction转换成函数形式
render: code.render,
staticRenderFns: code.staticRenderFns
}
parse - 解析
parse内部过于复杂,且借鉴了某个开源库,有三个文件:html-parser / text-parser(差值表达式文本) / filter-parser
export function parse(
template: string,
options: CompilerOptions
): ASTElement | void {
// 先解析options中的成员
// ...
// 解析过程
// <div><span></span></div>
// 这边是入栈的操作,找到一个开始标签就插入栈中,找到结束标签就出栈
// 如果栈清空就说明是合法的模版
const stack = []
const preserveWhitespace = options.preserveWhitespace !== false
const whitespaceOption = options.whitespace
let root
// 核心的解析算法
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
// 开始标签,创建一个ast对象,同时把这个开始标签tag插入到刚才那个栈中
start(tag, attrs, unary, start) {
// 创建一个ast对象
let element: ASTElement = createASTElement(tag, attrs, currentParent)
// ...
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
// 结构型指令处理,v-if,v-for,v-once
processFor(element)
processIf(element)
processOnce(element)
}
},
// 结束标签
end(tag, start, end) {
// 出栈
},
// 文本标签
chars(text: string, start: number, end: number) {
},
// 注释内容
comment(text: string, start, end) {
}
})
// root就是存储好的ast对象🍂
return root
}
parseHTML
// Regular Expressions for parsing tags and attributes
// 很多正则表达式用来匹配html模版中的内容
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--
...
// 遍历html模版字符串
// 会把处理完的文本截取掉,继续处理剩余的部分
while (html) {
let textEnd = html.indexOf('<')
if (textEnd === 0) {
...
}
...
}
总结:parse函数会依次遍历html模版字符串,转换成AST对象,html的属性和指令都会记录在AST对象的相应属性上
接下来会对AST进行优化,optimise,优化实际就是标记静态语法树,将来可以跳过不用再比对,ast中最终会被标记static和staticRoot
$
抽象语法树
- 简称AST
- 使用对象的形式描述树形的代码结构
- 此处的抽象语法树是用来描述树形结构的HTML字符串
优点:
- 转成AST后,可以通过AST对模版做优化处理
- 标记模版中的静态内容,在patch的时候直接跳过静态内容(静态内容不需要对比和重新渲染)
通过children和parent记录父子节点可以生成树的形式
浙公网安备 33010602011771号