Java教程

编译器(一)概览

本文主要是介绍编译器(一)概览,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

我们从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是否存在,若是存在的话就把elouterHTML作为模板

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
    • 不以#开头就什么都不做
  • templateDOM节点就将其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

我们知道rendercompileToFunctions创建的,所以我们找下这个函数什么情况

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 }
复制代码

可见compileToFunctionssrc/compiler/index.jscreateCompiler生成

import { createCompilerCreator } from './create-compiler'
export const createCompiler = createCompilerCreator(function baseCompile(
    template: string,
    options: CompilerOptions
): CompiledResult {
   // ...
})
复制代码

createCompiler又是src/compiler/create-compiler.jscreateCompilerCreator生成

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.jscreateCompileToFunctionFn所生

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来生成rendercompileToFunctionscreateCompiler所生,createCompilercreateCompilerCreator所生,而createCompilercompileToFunctions又是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,最后deleteoptions.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对象,这样子可以先检查缓存,若在的话直接返回即可 这里的keydelimiters存在时将其转为字符串再加上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、打印编译和代码生成时的错误、调用compiletemplate转为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.createbaseOptions为原型对象创建的,而且它才是最终的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解析出错误pusherrors 然后将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的只能是变量

  • 若是报错的话就先使用stripStringREexp里匹配到的字符串给替成空。这是因为需要剔除干扰的字符串从而留下需要判断的插值表达式,然后判断下这个表达式是否能匹配上关键词,若能匹配上那么说明使用了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在不同的平台都有编译,这个生成astparse方法是一样的,生成代码的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

这篇关于编译器(一)概览的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!