前言
大家看源码是不是都有一种感觉,就是很多东西都是看的时候当时记住了,过段时间不去使用或者回顾又忘记了,所以现在开始以博客的方式写一下自己的理解,也方便后续自己的回顾。
这一篇主要讲的是Vue源码解析parse的相关内容,主要分成三大块
- 从编译入口到parse函数(封装思想,柯里化)
- parse中的词法分析(把template模板解析成js对象)
- parse中的语法分析(处理解析出的js对象生成ast)
从编译入口到parse函数
编译入口
让我们开始直入主题吧,在vue源码开始入口的entry-runtime-with-compiler.js
文件中,我们可以看到$mount
的定义,下面是精简后的源码,这边可以看到先是判断我们传入的options
上面是不是有render
方法,没有的话才会走template生成render函数的过程,最后都是执行render函数。
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
const options = this.$options
// 是否有render方法,目前是没有
if (!options.render) {
let template = options.template
if (template) {
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
// 拿到temp后,编译成render函数
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
return mount.call(this, el, hydrating)
}
compileToFunctions
这边可以看到主要生成render的函数就是compileToFunctions
,这个函数定义在src/platforms/web/compiler/index.js
中,这个文件就四行代码
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
createCompiler
这时又发现compileToFunctions
实际上是createCompiler
这个地方解构出来的,那我们还是继续向上找,这个时候已经找到了src/compiler/index.js
这个文件下,这个就是编译三步走的主文件了
import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 模版解释器,功能为从HTML模版转换为AST
const ast = parse(template.trim(), options)
// AST优化,处理静态不参与重复渲染的模版片段
if (options.optimize !== false) {
optimize(ast, options)
}
// 代码生成器。基于AST,生成js函数,延迟到运行时运行,生成纯HTML。
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
createCompilerCreator
这里可以看到createCompiler
是createCompilerCreator
这个函数接收了baseCompile
作为参数,真正的编译过程都在这个baseCompile
函数中执行的,所有需要继续看createCompilerCreator
这个函数里是在哪调用baseCompile
的,下面的伪代码一看不是有点眼熟,返回的compileToFunctions
这个就是和入口的函数相对应,还是继续深入下去吧,马上就要到头了
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
funtion compile {
//省略
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
createCompileToFunctionFn
看到createCompileToFunctionFn
这个函数里面可以看到先判断了下缓存然后执行了我们传入的compile
函数,入参为template
,options
。
export function createCompileToFunctionFn (compile: Function): Function {
const cache = Object.create(null)
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
options = extend({}, options)
// check cache
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// compile
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 => {
return createFunction(code, fnGenErrors)
})
return (cache[key] = res)
}
}
终点compile
终于要执行到compile
了,我自己都快要晕了,这边主要处理了些options
后,终于执行了baseCompile
,这个函数就是在上面说三步走的主要函数了,到这个地方可以松一口气了,不用继续深入下去了。
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
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)
compiled.errors = errors
compiled.tips = tips
return compiled
}
这边从入口到真正执行parse
已经经历了四五个步骤,这么做主要是利用柯里化的思想,把多平台的baseOptions
,缓存处理
,编译配置处理
等进行了拆分封装。这边从编译入口到parse函数就已经结束了,接下来让我们来看看parse
中做了些什么。
parse中的词法分析
parser简介
首先让我们简单的了解一下parser,简单来说就是把源代码转换为目标代码的工具。引用下基维百科对于parser的解释。
语法分析器(parser)通常是作为编译器或解释器的组件出现的,它的作用是进行语法检查、并构建由输入的单词组成的数据结构(一般是语法分析树、抽象语法树等层次化的数据结构)。语法分析器通常使用一个独立的词法分析器从输入字符流中分离出一个个的“单词”,并将单词流作为其输入。
vue其实也是使用解析器来对模板代码进行解析。
/*!
* HTML Parser By John Resig (ejohn.org)
* Modified by Juriy "kangax" Zaytsev
* Original code by Erik Arvidsson, Mozilla Public License
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
*/
它的源代码上有这样一段注释,是Vue fork
自 John Resig
所写的一个开源项目:http://erik.eae.net/simplehtmlparser/simplehtmlparser.js 然后在这个解析器上面做了一些扩展。
流程总览
大概了解了解析器后让我们来看看parse的整体流程,这边通过精简后的代码可以看到parse实际上就是先处理了一些传入的options,然后执行了parseHTML
函数,传入了template
,options
和相关钩子
。
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
// 处理传入的options合并的实例vm的options上
dealOptions(options)
// 模板和相关的配置和钩子传入到parseHTML中
parseHTML(template, {
someOptions,
start (tag, attrs, unary, start) {...},
end (tag, start, end) {...},
chars (text: string, start: number, end: number) {...},
comment (text: string, start, end) {}...,
})
}
这边我们继续看parseHTML
函数里面做了什么?
parseHTML
是定义在src/compiler/parser/html-parser.js
这个文件中,首先文件头部是定义了一些后续需要使用的正则,不太懂正则的可以先看看正则先关的知识。
// Regular Expressions for parsing tags and attributes
// 匹配标签的属性
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeLetters}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^]+>/i
// #7298: escape - to avoid being pased as HTML comment when inlined in page
const comment = /^
然后先看看parseHTML
的伪代码,大概流程就是先定义需要的变量,然后循环遍历template,通过正则匹配到对应的标签后,通过进行处理通过传入的钩子函数把处理后的对象转换成ast。
export function parseHTML (html, options) {
const stack = []
const expectHTML = options.expectHTML
const isUnaryTag = options.isUnaryTag || no
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
let index = 0
let last, lastTag
while (html) {
if (!lastTag || !isPlainTextElement(lastTag)){
let textEnd = html.indexOf('<')
if (textEnd === 0) {
if(matchComment) {
advance(commentLength)
continue
}
if(matchDoctype) {
advance(doctypeLength)
continue
}
if(matchEndTag) {
advance(endTagLength)
parseEndTag()
continue
}
if(matchStartTag) {
parseStartTag()
handleStartTag()
continue
}
}
handleText()
advance(textLength)
} else {
handlePlainTextElement()
parseEndTag()
}
}
}
辅助函数分析
让我们分别看看parseHTML
中的四个辅助函数他们的实现。
首先advance主要是把处理过后的html给截取掉,直到在上面代码while (html)
中判断为空,代表所有的模板已经处理完成。
// 为计数index加上n,同时,使html到n个字符以后到位置作为起始位
function advance (n) {
index += n
html = html.substring(n)
}
首先是通过正则匹配开始标签,简单处理成对象后,把开始标签存储到数组中,后续在匹配到闭合标签的时候就会把这个数组中的数据取出。
// 解析开始标签
function parseStartTag () {
//正则匹配获取HTML开始标签
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1],
attrs: [],
start: index
}
advance(start[0].length)
let end, attr
// 开始标签中的属性都保存到一个数组中
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length)
match.attrs.push(attr)
}
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}
把上面parseStartTag
这个函数简单处理的对象再进行一次处理
// 处理开始标签,将开始标签中的属性提取出来。
function handleStartTag (match) {
const tagName = match.tagName
const unarySlash = match.unarySlash
// 解析结束标签
if (expectHTML) {
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag)
}
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag(tagName)
}
}
const unary = isUnaryTag(tagName) || !!unarySlash
// 解析开始标签的属性名和属性值
const l = match.attrs.length
const attrs = new Array(l)
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
// hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
if (args[3] === '') { delete args[3] }
if (args[4] === '') { delete args[4] }
if (args[5] === '') { delete args[5] }
}
const value = args[3] || args[4] || args[5] || ''
const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
? options.shouldDecodeNewlinesForHref
: options.shouldDecodeNewlines
attrs[i] = {
name: args[1],
value: decodeAttr(value, shouldDecodeNewlines)
}
}
// 将标签及其属性推如堆栈中
if (!unary) {
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
lastTag = tagName
}
// 触发 options.start 方法。
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
匹配结束标签,找到刚刚开始标签存放的数组取出。
// 解析结束TAG
function parseEndTag (tagName, start, end) {
let pos, lowerCasedTagName
if (start == null) start = index
if (end == null) end = index
if (tagName) {
lowerCasedTagName = tagName.toLowerCase()
}
// 找到同类的开始 TAG 在堆栈中的位置
if (tagName) {
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {
// If no tag name is provided, clean shop
pos = 0
}
// 对堆栈中的大于等于 pos 的开始标签使用 options.end 方法。
if (pos >= 0) {
// Close all the open elements, up the stack
for (let i = stack.length - 1; i >= pos; i--) {
if (process.env.NODE_ENV !== 'production' &&
(i > pos || !tagName) &&
options.warn
) {
options.warn(
`tag <${stack[i].tag}> has no matching end tag.`
)
}
if (options.end) {
options.end(stack[i].tag, start, end)
}
}
// Remove the open elements from the stack
// 从栈中移除元素,并标记为 lastTag
stack.length = pos
lastTag = pos && stack[pos - 1].tag
} else if (lowerCasedTagName === 'br') {
// 回车标签
if (options.start) {
options.start(tagName, [], true, start, end)
}
} else if (lowerCasedTagName === 'p') {
// 段落标签
if (options.start) {
options.start(tagName, [], false, start, end)
}
if (options.end) {
options.end(tagName, start, end)
}
}
}
html解析详解
这边详细讲解html解析,下面简单的template模板就是作为这次详解的例子,有可能不能覆盖全部场景。
上面这一段是作为字符串来处理,首先我们的开头是parseStartTag
这个函数中
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(lastTag, html)) {
advance(1)
}
continue
}
}
返回值就是一个这样的简单匹配出来的js对象,然后再通过handleStartTag
这个函数把这里面一些无用的和需要添加的处理的数据处理后,执行最开始的start
钩子函数,这个在后面的ast生成中描述。
{
attrs: [
{
0: " id="app"",
1: "id",
2: "=",
3: "app",
4: undefined,
5: undefined,
end: 13,
groups: undefined,
index: 0,
input: " id="app">↵ ↵
↵