我们从Vue.prototype._init此入口开始
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// ...已经讲过的各种initXXX初始化
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
可见在各种initXXX之后会判断vm.$options.el,然后调用vm.$mount(vm.$options.el)来挂载实例,这时候已经完成了所有的初始化工作
我们知道Vue有运行时版和完整版,入口分别是src/platforms/web/entry-runtime.js、src/platforms/web/entry-runtime-with-compiler.js,它们的区别就是是否带有编译器,而这个$mount实例方法就在这俩个入口文件赋的值
我们要看的自然就是带编译器的
我们从$mount开始步步进入
Vue.prototype.$mount(完整版)
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
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
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
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 = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
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)
}
可见其实最终调用的是缓存好的运行时$mount,它做的是生成render方法赋值给this.$options.render
el = el && query(el)
/* istanbul ignore if */
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
}
首先判断el是否存在且给转换成DOM对象也就是挂载点(实例要挂载的真实DOM所在),然后判断下挂载点是不是body、html,是的话得报错,因为挂载点只是个占位,它会被替换掉,很显然body、html是不能被替换的
接着来到了正戏,判断下options.render不存在的话就开始生成,我们进入if语句内部
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
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 = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
我们知道render编译需要模板,所以先判断下template什么情况,我们先看template不存在的情况
它会判断el是否存在,若是存在的话就把el的outerHTML作为模板
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
}
}
它调用getOuterHTML方法获取,该方法也很简单,主要是做了el.outerHTML不存在的兼容(比如IE9的svg就没有)
然后看template存在的情况,它分为三种情况:
template是字符串template以#开头,那么就将其作为id去寻找该DOM,并将其innerHTML作为template- 不以
#开头就什么都不做
template是DOM节点就将其innerHTML作为template- 俩者都不是就提示无效的
template选项
这时候template应该是有效的,当然也可能是空
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
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')
}
}
判断下template不为空的话就开始编译render,我们进入if
首先是非生产环境下的的查看编译器性能的,就是编译前后打俩个标记compile、compile end
然后就是调用compileToFunctions编译获取到render、staticRenderFns赋值给options
render是在完整版(entry-runtime-with-compiler.js)的$mount方法里由compileToFunctions生成
如何生成compileToFunctions
我们知道render是compileToFunctions创建的,所以我们找下这个函数什么情况
import { compileToFunctions } from './compiler/index'
首先找到src/platforms/web/compiler/index.js
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
可见compileToFunctions是src/compiler/index.js的createCompiler生成
import { createCompilerCreator } from './create-compiler'
export const createCompiler = createCompilerCreator(function baseCompile(
template: string,
options: CompilerOptions
): CompiledResult {
// ...
})
而createCompiler又是src/compiler/create-compiler.js的createCompilerCreator生成
import { createCompileToFunctionFn } from './to-function'
export function createCompilerCreator(baseCompile: Function): Function {
return function createCompiler(baseOptions: CompilerOptions) {
function compile(
template: string,
options?: CompilerOptions
): CompiledResult {
const finalOptions = Object.create(baseOptions)
// ...
const compiled = baseCompile(template, finalOptions)
// ...
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
最终这里看见了compileToFunctions,它又是src/compiler/to-function.js的createCompileToFunctionFn所生
export function createCompileToFunctionFn(compile: Function): Function {
return function compileToFunctions(
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
// ...
const compiled = compile(template, options)
// ...
const res = {}
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
// ...
return (cache[key] = res)
}
}
现在我们捋一下
就是我用compileToFunctions来生成render,compileToFunctions是createCompiler所生,createCompiler是createCompilerCreator所生,而createCompiler的compileToFunctions又是createCompileToFunctionFn所生
也就是如下这么个执行顺序流程
createCompilerCreator --> createCompiler --> createCompileToFunctionFn --> 得到compileToFunctions
compileToFunctions --> compile --> baseCompile
下面简单的看下各个方法干了些什么
createCompilerCreator
我们先从入口看起
import { createCompileToFunctionFn } from './to-function'
export function createCompilerCreator(baseCompile: Function): Function {
return function createCompiler(baseOptions: CompilerOptions) {
// ...
}
}
可见它就是直接返回了createCompiler函数,它接收了baseCompile参数,至于它在哪里调用暂且不管
createCompiler
import { createCompilerCreator } from './create-compiler'
export const createCompiler = createCompilerCreator(function baseCompile(
template: string,
options: CompilerOptions
): CompiledResult {
// ...
})
import { createCompileToFunctionFn } from './to-function'
export function createCompilerCreator(baseCompile: Function): Function {
return function createCompiler(baseOptions: CompilerOptions) {
function compile(
template: string,
options?: CompilerOptions
): CompiledResult {
const finalOptions = Object.create(baseOptions)
// ...
const compiled = baseCompile(template, finalOptions)
// ...
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
我们知道它其实就是createCompilerCreator返回的函数
首先它定义了一个compile函数,然后返回了一个对象包含了compile、compileToFunctions俩个属性
这就是
compileToFunctions的来源
compileToFunctions
看一个函数先看其调用
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
template自然是模板字符串,主要是第二参数options
首先是delimiters、comments这俩个在官网就有说明delimiters
余下俩个是工具方法里的所以之后统一成篇
接下来进入src/compiler/to-function.js找到compileToFunctions所在
进入compileToFunctions
options = extend({}, options)
const warn = options.warn || baseWarn
delete options.warn
这个就是简单的把options创建了个副本,然后缓存了warn,最后delete了options.warn,因为这个options会传入compile函数,而其内部会丢弃warn
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.'
)
}
}
}
非生产环境下会判断下new Function是否可以使用,在CSP(内容安全策略)严格的话new Function会报错,错误信息包含unsafe-eval、CSP。有俩办法解决:预编译、放宽CSP
// check cache
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// ...
return (cache[key] = res)
这段其实是优化性能逻辑,它会把编译过得结果放入cache对象,这样子可以先检查缓存,若在的话直接返回即可
这里的key在delimiters存在时将其转为字符串再加上template作为key,这是因为同样的template若是delimiters不一样的话编译的结果就不一样
// compile
const compiled = compile(template, options)
这句其实是关键,这个compile就是通过闭包引用create-compiler.js里定义的compile,具体逻辑看下文
// 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))
}
}
在编译之后返回的结果compiled对象会包括errors、tips(看下文的compile),它其实是编译过程中的错误、提示信息
这段代码就是在非生产环境下打印错误、提示信息的
// turn code into functions
const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
如注释所示就是将code转为函数,而这个code就是render字符串
首先定义了res变量,它就是返回值,然后定义了变量fnGenErrors,它是为了接收createFunction异常信息
res有俩个属性render、staticRenderFns,前者就是render函数字符串了,后者用于diff算法优化,之后展开
function createFunction(code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err, code })
return noop
}
}
可见createFunction只是个简单的函数,它主要是用于生成函数和收集错误
// 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
)
}
}
这个类似上文的编译错误打印,这个是打印生成渲染函数时产生的错误,注释也说明了大多是编译器自身错误
compileToFunctions就是做缓存编译结果、将render字符串转为Function、打印编译和代码生成时的错误、调用compile将template转为render字符串
compile
首先我们找到调用处,即src/compiler/to-function.js
const compiled = compile(template, options)
// options
{
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}
值得注意的是这里的options是没有warn属性的
我们找到src/compiler/create-compiler.js且进入compile
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
finalOptions.warn = (msg, tip) => {
(tip ? tips : errors).push(msg)
}
首先我们创建了finalOptions变量,它通过Object.create以baseOptions为原型对象创建的,而且它才是最终的options参数,继续看下去会发现它就是baseOptions、options的结合体
然后定义了俩个参数errors、tips用于存储编译过程中的tip、errors,这就是compileToFunctions里编译过程中错误和提示的来源
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]
}
}
}
这段代码就是结合baseOptions、options。前者可以理解为编译器的默认选项,后者则是定制选项
首先是merge options.modules,这个是直接合并数组
然后是merge options. directives,这个是直接混入
最后若是有其他非modules 、directives属性就直接覆盖
const compiled = baseCompile(template, finalOptions)
这句也很关键,这是真正进行词法分析、语法分析生成ast的地方,它是通过闭包取到执行createCompilerCreator时传入的参数
export const createCompiler = createCompilerCreator(function baseCompile(
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
可见它就行了代码优化以及生成了ast、render、staticRenderFns,具体parse、optimize等逻辑之后章节详述
回到compile
if (process.env.NODE_ENV !== 'production') {
errors.push.apply(errors, detectErrors(compiled.ast))
}
compiled.errors = errors
compiled.tips = tips
return compiled
首先根据compiled.ast解析出错误push到errors
然后将errors、tips赋值给compiled且返回
compile主要是合并编译器选项、收集编译错误、调用baseCompile编译模板
error-detector.js
这就是根据编译得到的ast对象解析出问题的一个方法集,主要是三大规则:
- 表达式得是有效的,可以如下来检测
new Function(`return ${exp}`)
- 表达式剥离掉不相干的字符串之后剩下的属性必须不能是一些保留关键词
- 变量标识符的话就得是符合常规标识符,可以如下检测
new Function(`var ${ident}=_`)
checkExpression、checkIdentifier这俩者检测逻辑不同,分别对应第三、第一规则,前者是用于检测变量、具体数值,后者只是用于判断变量
export function detectErrors(ast: ?ASTNode): Array<string> {
const errors: Array<string> = []
if (ast) {
checkNode(ast, errors)
}
return errors
}
首先导出detectErrors函数,它接收ast参数,返回errors作为错误集,实际上检测错误是在checkNode
checkNode
这个就是递归遍历节点,根据不同的节点进入不同的分析方法
/*
{
"type": 1, // 1是元素节点,2是文本节点,3是注释节点
"tag": "div", // 标签
"attrsList": [{
"name": "id",
"value": "app"
}], // 属性列表
"attrsMap": {
"id": "app"
}, // 属性map
"children": [{
"type": 2,
"expression": "_s(flag)", // 表达式字符串
"tokens": [{
"@binding": "flag"
}],
"text": "{{flag}}", // 文本字符串
"static": false
}] // 子节点
}
*/
function checkNode(node: ASTNode, errors: Array<string>) {
if (node.type === 1) {
for (const name in node.attrsMap) {
if (dirRE.test(name)) {
const value = node.attrsMap[name]
if (value) {
if (name === 'v-for') {
checkFor(node, `v-for="${value}"`, errors)
} else if (onRE.test(name)) {
checkEvent(value, `${name}="${value}"`, errors)
} else {
checkExpression(value, `${name}="${value}"`, errors)
}
}
}
}
// 若是有子节点就递归遍历
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
checkNode(node.children[i], errors)
}
}
} else if (node.type === 2) {
checkExpression(node.expression, node.text, errors)
}
}
这里首先得回顾下ast这个对象的几个重要属性,以下的操作都是根据这几个属性来进行
进入函数内部,首先是判断下当前元素节点类型,我们先看若是node.type === 2即带插值的文本节点,它调用checkExpression函数来处理,具体如下文
我们再看元素节点,首先遍历node.attrsMap拿到具体属性名与值
// /^v-|^@|^:/
dirRE.test(name)
这句就是判断该属性是不是v-、@、:开头,也就是元素节点主要是得处理三处:
v-for:这个就是(val, name, index) in list类型,val、name、index、list这四个都得符合具体规则
// node: { for: 'list', alias: 'val', iterator1: 'name', iterator1: 'index' }
// text: v-for="(val, name, index) in list"
function checkFor(node: ASTElement, text: string, errors: Array<string>) {
checkExpression(node.for || '', text, errors)
checkIdentifier(node.alias, 'v-for alias', text, errors)
checkIdentifier(node.iterator1, 'v-for iterator', text, errors)
checkIdentifier(node.iterator2, 'v-for iterator', text, errors)
}
可见node.for对应checkExpression,因为它是个表达式(可以是变量名、也可以是具体数值),alias、iterator1、iterator2对应checkIdentifier,因为它们三是变量
@click、v-on::这个就是到了事件绑定,使用checkEvent检测:,其他属性,这个就是变量名或者具体数值(:flag="true"、:flag="flag"),所以走checkExpression
checkEvent
用于检测事件名,具体表现为一元操作符不能作为方法名
// these unary operators should not be used as property/method names
// /\bdelete\s*\([^\)]*\)|\btypeof\s*\([^\)]*\)|\bvoid\s*\([^\)]*\)/
const unaryOperatorsRE = new RegExp('\\b' + (
'delete,typeof,void'
).split(',').join('\\s*\\([^\\)]*\\)|\\b') + '\\s*\\([^\\)]*\\)')
/**检查事件是否有问题
*
* @param {*} exp delete('Delete')
* @param {*} text @click="delete('Delete')"
* @param {*} errors
*/
function checkEvent(exp: string, text: string, errors: Array<string>) {
const stipped = exp.replace(stripStringRE, '')
const keywordMatch: any = stipped.match(unaryOperatorsRE)
if (keywordMatch && stipped.charAt(keywordMatch.index - 1) !== '$') {
errors.push(
`avoid using JavaScript unary operator as property name: ` +
`"${keywordMatch[0]}" in expression ${text.trim()}`
)
}
checkExpression(exp, text, errors)
}
首先看下unaryOperatorsRE这个正则,它很简单就是匹配delete ( )这样子的字符串,我们进入函数内部
首先剥离无关的字符串,这样子就可以把传入的参数字符串去除,就像注释所示,传入的'Delete'去除,这样子得到是delete()
然后就是匹配是否是一元操作符,也就是delete、typeof、void这三个,这时判断下是否匹配上了且匹配上的字符串前一个字符不是$,若是俩者皆符合那么那么就说明是delete()且不是$delete()($xxx是合法的变量名)的字符串。这是不允许传入作为函数名的,其实没有这个检测接下来的checkExpression也会报错,因为它也不是合法的表达式
这里调用checkExpression是因为事件可以直接传入方法体@click="function() {}"
检查事件名的时候先检测了是否匹配一元操作符,这样子进一步细化了错误提示
checkIdentifier
用于检测变量名是否规范
function checkIdentifier(
ident: ?string,
type: string,
text: string,
errors: Array<string>
) {
if (typeof ident === 'string') {
try {
// 作为变量是可以声明
new Function(`var ${ident}=_`)
} catch (e) {
errors.push(`invalid ${type} "${ident}" in expression: ${text.trim()}`)
}
}
}
先判断标识符是否是字符串,然后使用try/catch捕获new Function(var ${ident}=_),这样子就可以知道这个是否可以是规范的变量名,要是报错的话就说明这个变量名是有问题的
checkExpression
它就是用于检查表达式(变量名、具体数值)是否有问题,在该表达式无效的情况下分为俩种错误:表达式使用了关键词、表达式本身错误
假设我们模板是<div>{{flag}}</div>,那么该node为
{
expression: "_s(flag)",
text: "{{flag}}"
}
也就是exp是_s(flag),text是{{flag}}
// strip strings in expressions
const stripStringRE = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`/g
function checkExpression(exp: string, text: string, errors: Array<string>) {
try {
// 作为插值是可以return
new Function(`return ${exp}`)
} catch (e) {
const keywordMatch = exp.replace(stripStringRE, '').match(prohibitedKeywordRE)
if (keywordMatch) {
// 若是发现有禁止的关键词就将错误push到errors
errors.push(
`avoid using JavaScript keyword as property name: ` +
`"${keywordMatch[0]}"\n Raw expression: ${text.trim()}`
)
} else {
// 若是无效的表达式也是不成的
errors.push(
`invalid expression: ${e.message} in\n\n` +
` ${exp}\n\n` +
` Raw expression: ${text.trim()}\n`
)
}
}
}
首先我们解释下stripStringRE这个正则表达式,如注释所示,就是删除表达式上无关的字符串,它很长所以我们分几部分来看:
-
单引号包裹的字符串,字符串分俩种: 可适配:'\dflag'
- 非
'非\,单引号包裹的此种字符串也是需要剔除,因为这就是纯字符串非插值 \.,也就是\之后可加任何非换行字符,这时因为表达式是不可能有\x这种样子的,得剔除
'(?:[^'\\]|\\.)*'| - 非
-
类似第一条,可适配:"\dflag"
"(?:[^"\\]|\\.)*"| -
类似第一条,只是右边加了
${`xxx${aim}xxx`这个就是剔除模板字符串左右俩边无效的字符,只留下中间目标字符
aim,这才是需要判断的字符串// 可适配:`\dflag${ `(?:[^`\\]|\\.)*\$\{| -
类似第一条,只是左边加了
},可适配:}\dflag`\}(?:[^`\\]|\\.)*`| -
类似第一条,可适配:
\dflag`(?:[^`\\]|\\.)*`进入函数体,首先使用
try/catch捕获new Function(return ${exp}),这是因为exp是插值,所以将其转为函数且将return,这样子可以判断该插值是否有语法错误
它是个返回值,这是和
checkIdentifier的区别。它可以是具体值如_s(1)也可以是_s(flag),checkIdentifier的只能是变量
- 若是报错的话就先使用
stripStringRE将exp里匹配到的字符串给替成空。这是因为需要剔除干扰的字符串从而留下需要判断的插值表达式,然后判断下这个表达式是否能匹配上关键词,若能匹配上那么说明使用了JS关键词,提示它用了哪个具体关键词即可 - 若不是因为关键词的话那就是表达式本身就无效的,将
catch提供的e.message作为错误信息提供即可
代码组织思路
function baseCompile(
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
我们知道这个baseCompile就是编译的核心。Vue在不同的平台都有编译,这个生成ast的parse方法是一样的,生成代码的generate方法却是不同且这个options也是不一样的,而我们在项目运行过程中可能会多次编译,所以我们封装编译器比较直观的做法就有以下几种:
- 将已经适配好环境的
generate以参数的形式传入编译器,缺点就是每次都要传入 - 创建多个编译器方法,里面的
generate等都是对应的环境,缺点就是冗余代码很多 其实可以从第一点来想,我们可以在初始化的时候通过这几个不确定的以参数形式传入某个函数然后生成一个符合所处环境的compile函数,其实就是应用高阶函数来动态生成
export function createCompiler(baseOptions) {
return function compile(template, options) {
const ast = parse(template.trim(), baseOptions)
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
}
const webCompile = createCompiler(baseOptions)
const weexCompile = createCompiler(baseOptions)
首先我们创建createCompiler函数来返回一个compile函数,这就可以通过传入不同的baseOptions参数来返回不同的compile,但是generate还是写死的的
export function createCompilerCreator(baseCompile) {
return function createCompiler(baseOptions) {
return function compile(template, options) {
// 这里可以干很多别的事,比如编译错误处理等
return baseCompile(template, options)
}
}
}
const createCompiler = createCompilerCreator(function baseCompile(template, options) {
const ast = parse(template.trim(), options)
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
const compile = createCompiler(baseOptions)
// <======>
// 这样子通过俩个参数baseOptions、baseCompile可以动态生成compile
const compile = function compile(template, options) {
// 这里可以干很多别的事,比如编译错误处理等
const ast = parse(template.trim(), options)
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
这里再创建一个createCompilerCreator函数用于生成createCompiler函数,它接收一个compile参数,这样子就可以通过传入不同的compile来返回不同的createCompiler