来源 刘涛
Vue的核心可以分为三个大块:数据处理和双向绑定、模板编译、虚拟dom。
前面我们对第一部分的主要内容双向绑定做了一个分析讲解,接下来我们说一说模板编译。
这一部分的内容比较多,也比较复杂。由于所涉及的情况太多了,我也不可能把每一种情况都覆盖到。
尽量做到既不啰嗦,又能分享更多的内容。前面我们也提到过,模板编译分为三个阶段:
生成ast、优化静态内容、生成render。
官方文档中提到render比template更加底层,许多人对render函数都不是很明白,
相信通过这一部分的分析,你可以对render基本做到了如指掌。
如果你在创建对象时直接传入render函数,模板编译这一步就可以直接跳过,这样效率肯定更高,
(react就是不用模板编译的, 如果vue比react效率高,那就是因为需要编译jsx?)
但同时我们编写代码的难度会增加很多。实际开发过程中,根据需要,恰当选择。
Vue对象上有一个全局函数compile,在src/entries/web-runtime-with-compiler.js中。
import { compileToFunctions } from 'web/compiler/index' ... ... ... Vue.compile = compileToFunctions
该方法来自src/platforms/web/compiler/index文件。
import { isUnaryTag, canBeLeftOpenTag } from './util' import { genStaticKeys } from 'shared/util' import { createCompiler } from 'compiler/index' import modules from './modules/index' import directives from './directives/index' import { isPreTag, mustUseProp, isReservedTag, getTagNamespace } from '../util/index' export const baseOptions: CompilerOptions = { expectHTML: true, modules, directives, isPreTag, isUnaryTag, mustUseProp, canBeLeftOpenTag, isReservedTag, getTagNamespace, staticKeys: genStaticKeys(modules) } const { compile, compileToFunctions } = createCompiler(baseOptions) export { compile, compileToFunctions }
该文件中主要定义了一个baseOptions,它主要保存了解析模板时和平台相关的一些配置。
对应的src/platforms/weex/compiler/index中也有一份名称一样的配置。
这里我们简单说说web相关的这些配置都是什么意思。
-
expectHTML。目前具体还不是很明白,weex中没有改项,从字面意思来看,应该是是否期望HTML。 -
modules。包括klass和style,对模板中类和样式的解析。 -
directives。这里包括model(v-model)、html(v-html)、text(v-text)三个指令。 -
isPreTag。是否是pre标签。 -
isUnaryTag。是否是单标签,比如img、input、iframe等。 -
mustUseProp。需要使用props绑定的属性,比如value、selected等。 -
canBeLeftOpenTag。可以不闭合的标签,比如tr、td等。 -
isReservedTag。是否是保留标签,html标签和SVG标签。 -
getTagNamespace。获取命名空间,svg和math。 -
staticKeys。静态关键词,包括staticClass,staticStyle。
上面这些方法或者属性,在编译模板时会用到。
这里知识简单的列出来它们的用途,方便看一眼。
我们来看createCompiler函数的实现。
export function createCompiler (baseOptions: CompilerOptions) { const functionCompileCache: { [key: string]: CompiledFunctionResult; } = Object.create(null) // compile 函数的实现 // compileToFunctions 函数的实现 return { compile, compileToFunctions } }
该函数只是compile和compileToFunctions的简单封装,开始定义了functionCompileCache,
它用来缓存编译之后的模板,方便之后复用。
因为compileToFunctions里面调用了compile,所以我们先看一下compile。
function compile ( template: string, options?: CompilerOptions ): CompiledResult { const finalOptions = Object.create(baseOptions) const errors = [] const tips = [] finalOptions.warn = (msg, tip) => { (tip ? tips : errors).push(msg) } 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), options.directives ) } // copy other options for (const key in options) { if (key !== 'modules' && key !== 'directives') { finalOptions[key] = options[key] } } } const compiled = baseCompile(template, finalOptions) if (process.env.NODE_ENV !== 'production') { errors.push.apply(errors, detectErrors(compiled.ast)) } compiled.errors = errors compiled.tips = tips return compiled }
我们从上往下,依次看看它都做了哪些事儿。
它接收两个参数template和options,template不用过多解释,
options在内部主要是用户自己定义的delimiters。
finalOptions继承自我们上面提到的baseOptions,
并添加了一个搜集错误的warn方法,然后合并了options传入的各种配置选项。
modules和directives合并方法不同是因为modules是数组,而directives是一个对象。
baseCompile中执行的就是模板编译的三个重要步骤,后面我们会详细讲解。
最终,返回编译之后的对象。
function compileToFunctions ( template: string, options?: CompilerOptions, vm?: Component ): CompiledFunctionResult { options = options || {} ... // check cache const key = options.delimiters ? String(options.delimiters) + template : template if (functionCompileCache[key]) { return functionCompileCache[key] } // compile const compiled = compile(template, options) // check compilation errors/tips if (process.env.NODE_ENV !== 'production') { if (compiled.errors && compiled.errors.length) { warn( `Error compiling template:\n\n${template}\n\n` + compiled.errors.map(e => `- ${e}`).join('\n') + '\n', vm ) } if (compiled.tips && compiled.tips.length) { compiled.tips.forEach(msg => tip(msg, vm)) } } // turn code into functions const res = {} const fnGenErrors = [] res.render = makeFunction(compiled.render, fnGenErrors) const l = compiled.staticRenderFns.length res.staticRenderFns = new Array(l)
for (let i = 0; i < l; i++) { res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i], fnGenErrors) } 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 (functionCompileCache[key] = res) }
compileToFunctions函数中,首先从缓存中获取编译结果,没有则调用compile函数来编译。
在开发环境,我们在这里会抛出编译过程中产生的错误,
最终返回一个含有render函数,和staticRenderFns数组的对象,并把它放在缓存中。
这里我们使用makeFunction来创建函数。
function makeFunction (code, errors) { try { return new Function(code) } catch (err) { errors.push({ err, code }) return noop } }
很简单,就是利用了我们new Function,并搜集了错误。
compile和compileToFunctions两个方法的不同之处有以下几点。
1、 compile返回的结果中render是字符串,staticRenderFns是字符串组成的数组,而compileToFunctions中把它们变成了函数。
2、 compile返回的结果中,有模板生成的ast和搜集到的错误。而compileToFunctions对其结果进行了一些处理。
