「这是我参与2022首次更文挑战的第15天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
上一篇小作文介绍讲解了一下 $mount 方法的注册以及模板编译函数—— compileToFunctions。
编译模板的函数 compieToFunctions 是个套娃,他是 createCompiler 方法的返回值,而 createCompiler 方法又是 createCompilerCreator 函数的返回值,createCompileToFunction 作为 createCompiler 返回值的一个属性,值是 createCompileToFunction(compile)。
本篇小作文的重点在于创建模板编译函数 compileToFunctions 的过程中都做了什么,在这个过程中我们可以看到模板编译的全过程。
这个过程堪称整个 Vue 源码中最复杂的部分,如果一遍搞不定,坚持在来一遍。说句实在话,我也是看了两遍之后才开始动笔写这篇小作文,写的过程中还需要重新梳理这个过程,既是复习也是验证自己的理解是否正确。
二、createCompileToFunctions
方法位置:src/compiler/to-function.js -> createCompileToFunctionFn
方法参数:compile 方法,这个方法是在 createCompiler 方法中声明,最后在调用 createCompileToFunctions 时传入的
方法作用:
创建 compileToFunctions 并缓存,对,就是前面那个 $mount 方法中用于编译模板的方法。
export function createCompileToFunctionFn (compile: Function): Function {
const cache = Object.create(null)
return function compileToFunctions (
template: string, // template 模板,就是 $mount 处理的
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
}
}
三、compileToFunctions
createCompileToFunctionFn的返回值
方法位置:src/compiler/to-function.js -> createCompileToFunctionFn -> return function compileToFunctions
方法参数:
template:需要编译的模板字符串options:传递给compile方法的编译选项;这个参数是在$mount方法中调用传入的;
// $mount 代码片段
compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production', // 在非生产环境下,编译生成 ast 时记录 html 标签属性在模板字符串中开始和结束的位置索引
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters, // 界定符:默认 mustache 语法的 {{}}
comments: options.comments // 是否保留注释
})
vm:Vue实例
方法作用:
- 处理
options,extend到一个空对象,就是个复制操作; - 开发环境监测
CSP限制,CSP是内容安全策略; - 检查缓存,如果本次编译命中缓存就使用缓存;
- 执行
createCompileToFunctionFn在createCompiler方法传入的compile函数; - 处理编译报错并输出;
- 把编译得到的
render和staticRenderFns通过new Function()变成函数; - 缓存编译结果;
export function createCompileToFunctionFn (compile: Function): Function {
const cache = Object.create(null)
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
// 上面参数 options 有说是调用 compileToFunctions 时传入的
options = extend({}, options)
const warn = options.warn || baseWarn
delete options.warn
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production') {
// 检测 CSP 限制, CSP 会影响编译器工作,提前检测
// detect possible CSP restriction
try {
new Function('return 1')
} catch (e) {
if (e.toString().match(/unsafe-eval|CSP/)) {
}
}
}
// 如果命中缓存,直接从缓存中获取上次编译结果
// key 是 delimiters + template, +是字符串拼接
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// 执行编译方法,编译结果赋值给 compiled
// 这个 compile 是 createCompilerCreator 调用 createCompileToFunctionsFn 方法时传入的回调,在 src/compiler/create-compiler.js
const compiled = compile(template, options)
// 检查编译期间产生的 error 和 tip,分别输出到 console
if (process.env.NODE_ENV !== 'production') {
}
// 通过 new Function(code),把编译得到的字符串代码变成函数
const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
// 处理上面代码转换过程中的错误
if (process.env.NODE_ENV !== 'production') {
}
// 缓存编译结果
return (cache[key] = res)
}
}
3.1 createCompiler 中的 compile
说 compile 之前先看下 createCompiler 的代码,看下 compile 的声明位置
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
// 这个就是你朝思暮想的 compile 了
function compile (template, options) {}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
方法位置:compile 是 createCompileToFunctions 方法在 createCompiler 方法调用时传入的方法,compile 本身也是在 createCompiler 声明;
方法参数:
template: 模板字符串options: 创建编译器所需参数,指定诸如delimiters等
方法作用:
合并 baseOptions,调用 baseCompile 编译模板,返回编译结果;
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
// 创建一个以 baseOptions 为原型的对象,相当于继承 baseOptions
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
// 日志,负责记录将 error 和 tip
let warn = (msg, range, tip) => {
(tip ? tips : errors).push(msg)
}
// 传了 options 就合并 options 和 baseOptions
if (options) {
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
const leadingSpaceLength = template.match(/^\s*/)[0].length
// 抛出劲警告的方法
warn = (msg, range, tip) => {}
}
// 合并自 module
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules)
}
// 合并指令
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.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)
}
}
}
在这里,我们暂时不管 options.modules/directives,在我们的 demo 中,初次渲染 options 中的 modules 和 directives 都是 undefined;
紧接着这里又调用了 baseCompile,那么 baseCompile 又是哪里来的呢?
3.2 createCompileCreator 接收到的 baseCompile
方法位置:baseCompile 是作为参数传递给 createCompilerCreator 方法的:
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 这个函数就是 baseCompile
})
前面我们说过 createCompilerCreator 返回的函数就是 createCompiler 函数,createCompiler 调用就会返回 { compile, compileToFunctions };
方法参数:
template,需要编译的模板字符串- 编译过程中所需要的配置项目
接下来我们看看 baseCompile 都做了什么工作:
- 调用 parse 方法把编译模板编译成
AST节点。每个AST节点包含了一个模板标记的所有信息,比如标签名字、属性、插槽、父节点、子节点 - 当
options.optimize不为false则调用parse方法进行静态节点的优化工作; - 调用
generate方法将AST变成render/staticRenderFns的字符串形式代码,这就是render/staticRenderFns函数的前身了;
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)
}
// generate 代码生成,将 ast 转换成可执行的 render 函数的字符串形式:
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
3.2.1 ast 对象
ast 对象就是对我们模板的一种抽象描述,用一个对象描述节点上的所有信息,比如标签、标签的行内属性、子节点、父节点;比如我们 test.html 中的模板文件长这样:
<div id="app">
{{ msg }}
<some-com :some-key="forProp"></some-com>
<div>someComputed = {{someComputed}}</div>
<div class="static-div">静态节点</div>
</div>
编译出来的 ast 长这样:
3.2.2 把 ast 转换成 code
在 baseCompile 的 parse 和 generate 后得到的 code 是一种字符串形式的 js 函数体:
为了让大家感受的更直观一些,下面这个是经过手动格式化的 code.render
code.render = with (this) {
return _c( // <div id=app>
'div',
{ attrs: { 'id': 'app' } }, // div 上的属性 id = app
[ // 这个数组就是 div#app 的子元素了
_v('\n\t' + _s(msg) + '\n\t'), // 第一个子元素
_c( // 第二个子元素 <some-com />
'some-com',
{ attrs: { 'some-key': forProp } } // some-com 的 属性
),
_v(' '), // 第三个子元素
_c( // 第四个子元素
'div',
[_v('someComputed = ' + _s(someComputed))]
),
_v(' '), // 第五个
_c( // 第六个
'div',
{ staticClass: 'static-div' },
[_v('静态节点')]
)
],
1
)}
})
参照上面的 html 中的模板,可以看到先到 ast 再将 ast 变成一个 render 函数的代码体。无论是 ast 还是 render 函数都是在描述 html 模板。
四、总结
本文细致的梳理 compileToFunctions 方法的获取过程,另外简单的交代了 test.html 模板生成的 ast 和 generate ast 后得到的 code.render 即 render 函数体,注意我这里说的是函数体并不是 render 函数 这是因为现在它还是一坨字符串,经过后面的 new Function 才是函数哦;
很多人写 Vue 源码的时候都没有写这个套娃部分,我试着写这部分是为了理解到这么设计的好处,但是很显然没有悟道,这个坑先不填了,等我再看看,懂了在补上吧;所以如果你觉得这部分套娃复杂就不用管了,先看看 compile 和 baseCompile 做了什么就可以了。
当然,如果你发现这个妙处,也请不吝赐教评论区告诉我,万分感谢~