在上一篇里,我们主要聊了下Vue数据绑定简析,明白了其观察者模式的基本原理。我们知道在观察者中有一种属于渲染函数观察者(vm._watcher
),通过对渲染函数的求值计算来触发依赖收集,进而进行响应式的数据绑定,但是对于渲染函数如何编译却不曾了解。 这一篇我们将通过compiler
编译模板字符串template
,生成AST
语法树这一过程来看看Vue
的编译原理。
compilerOption
在我们编写的Vue
组件中,要渲染一条文本的UI
可能会是如下写法:
<template>
<p class="foo">{{text}}</p>
</template>
编译后变成:
var render = function() {
var _vm = this
_vm._c("p", {
staticClass: "foo"
}, [
_vm._v(_vm._s(_vm.text))
])
}
经过编译,将一段template
的html
代码变成一个js
函数,通过对该函数的调用来收集到依赖_vm.text
,实现数据变化时实时更新
UI
。那么它是如何识别class
这些attr
的,又是如何编译成一个完整的渲染函数的呢?
为了探究Compiler
的原理,我们找到完整版的入口文件entry-runtime-with-compiler.js
,发现其重写了$mount
函数:如果没有render
函数,则转换template
为字符串,再调用compileToFunctions
进行编译,最终将render
和staticRenderFns
挂到$options
下并调用原始的$mount
。通过对compileToFunctions
的层层剥离,我们可以得到最终的结果:
const { compile, compileToFunctions } = createCompilerCreator(function baseCompile() {
...
})(baseOptions)
可以看到,其利用闭包将baseCompile
函数和baseOptions
对象依次传入createCompilerCreator
函数并保存,相当于对工厂函数进行柯理化以提高模块解耦下的代码复用性,最终返回compile
和compileToFunctions
两个编译函数。
baseOptions
属于编译时的配置项,与具体的目标平台有关,web
平台下的代码如下:
// src/platforms/web/compiler/options.js
export const baseOptions: CompilerOptions = {
expectHTML: true,
modules,
directives,
isPreTag,
isUnaryTag,
mustUseProp,
canBeLeftOpenTag,
isReservedTag,
getTagNamespace,
staticKeys: genStaticKeys(modules)
}
modules
:主要包含了一些编译时做的转换和前置转换的操作,包括对class
和style
做一些staticAttr
和bindingAttr
相关的处理,对<input />
的bindingType
做了动态匹配type
的处理等。directives
:包含了v-html
、v-text
及v-model
的指令编译。staticKeys
:将modules
里的staticKeys
拼接成字符串,这里是staticClass,staticStyle
。- 剩下的主要是一些对
tag
的判断函数,待用到时再来具体分析。
了解了baseOptions
,我们再来看看baseCompile
:
// src/compiler/index.js
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
}
})
parse
:根据字符串template
和options
生成AST
语法树。optimize
:用来给root
递归添加各种static
属性,配合VNode
和patch
使用,避免不必要的re-render
,提高性能。generate
:根据ast
语法树,生成String
类型的render
和Array<String>
类型的staticRenderFns
,供后续的new Function
构造生成最终的render
和staticRenderFns
。
以上只是对compiler
有个粗略的了解,明白了其编译的主要工作流程,现在我们需要了解第一步(parse
生成AST
),加深对其编译原理的理解。首先我们回到createCompilerCreator
,为了避免无关代码干扰阅读,这里去掉了一些和开发调试相关的代码,来看具体的compile
函数:
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
let warn = (msg, range, 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 || null),
options.directives
)
}
// copy other options
for (const key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key]
}
}
}
finalOptions.warn = warn
const compiled = baseCompile(template.trim(), finalOptions)
if (process.env.NODE_ENV !== 'production') {
detectErrors(compiled.ast, warn)
}
compiled.errors = errors
compiled.tips = tips
return compiled
}
显而易见,这里主要通过继承、拷贝等生成finalOptions
,并执行baseCompile
方法。我们以web
下的编译为例,在$mount
方法中编译传入的options
如下:
// src/platforms/web/entry-runtime-with-compiler.js
{
outputSourceRange: process.env.NODE_ENV !== 'production', // 开发模式下标记start和end,便于定位编译出错信息
shouldDecodeNewlines, // 是否对换行符进行转码
shouldDecodeNewlinesForHref, // 是否对href内的换行符进行转码
delimiters: options.delimiters, // 改变纯文本插入分隔符,用来自定义<p>{{a}}</p>中的'{{}}'
comments: options.comments // 是否保留模板HTML中的注释,默认false
}
这里我们以一张流程图来简要梳理下编译过程:
parse
了解了编译参数的生成过程后,我们再来看parse
的工作原理。现在回到之前的baseCompile
,其中template
在经过$mount
函数转换后,统一转为字符串:
const ast = parse(template.trim(), options)
进入parse
方法体,首先是:
// src/compiler/parser/index.js
transforms = pluckModuleFunction(options.modules, 'transformNode')
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
pluckModuleFunction
方法用以将modules
数组中的各元素target
对应的target[key]
组成新的数组,用以后续进行遍历操作,详见src/compiler/helper.js
,在后续调用时再做具体说明。
而后声明一个变量root
,并调用parseHTML
(传入template
和option
,包括start
、end
、chars
和comment
方法),最终返回root
,而这就是我们所需要的AST
。
parseHTML
parseHTML
,如其命名,用于解析我们的HTML
字符串。首先是初始化stack
,用以维护非自闭合的element
栈,之后利用while(html)
对html
进行标签正则匹配、循环解析,这里我们用一张导图来更加直观地展示其解析逻辑:
通过调用advance
来截断更新html
字符串,供下一次循环解析,并调用option
中的start
、end
、chars
和comment
方法将其添加到root
对象中,最终完成AST
语法树的输出。在parseHTML
中,最核心的当属解析标签的开始,因此我们以parseStartTag
为入口,来进行代码阅读。
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1],
attrs: [],
start: index
}
// 正则匹配标签,并截断正在解析的html字符串
advance(start[0].length)
let end, attr
while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
// 正则获取标签上的attrs,并存入match的attrs数组
attr.start = index
advance(attr[0].length)
attr.end = index
match.attrs.push(attr)
}
// 解析到'>'字符
if (end) {
match.unarySlash = end[1] // '/>'时为自闭合标签,否则为false
advance(end[0].length)
match.end = index
return match
}
}
以如下template
为例:
<my-component :foo.sync="foo" :key="index" @barTap="barTap" v-for="(item, index) in barList">
<input v-model.number="value" />
<template #bodySlot="{link, text}">
<a v-if="link" :href="link">链接地址为:{{text | textFilter | linkFilter}}!</a>
<p v-else>{{text | filter}}</p>
</template>
</my-component>
可以得到如下match
:
{
tagName: 'my-component',
attrs: [
[ ' :foo.sync="foo"',
':foo.sync',
'=',
'foo',
undefined,
undefined,
index: 0,
input: ' :foo.sync="foo" :key="index" @barTap="barTap" v-for="(item, index) in barList">'],
[ ' :key="index"',
':key',
'=',
'index',
undefined,
undefined,
index: 0,
input: ' :key="index" @barTap="barTap" v-for="(item, index) in barList">'],
[ ' @barTap="barTap"',
'@barTap',
'=',
'barTap',
undefined,
undefined,
index: 0,
input: ' @barTap="barTap" v-for="(item, index) in barList">'],
[ ' v-for="(item, index) in barList"',
'v-for',
'=',
'(item, index) in barList',
undefined,
undefined,
index: 0,
input: ' v-for="(item, index) in barList">']
],
}
获取到match
后,接着调用handleStartTag
方法:
- 判断
if (expectHTML)
,主要是对于编写自闭合标签时做自动纠错的功能,确保lastTag
的正确解析。 const unary = isUnaryTag(tagName) || !!unarySlash
来获取当前标签的闭合属性,其中isUnaryTag
为当前编译的web
平台下自闭合的标签,可见于src/platforms/web/compiler/util.js
。- 解析
match
到的attrs
数组,其中需要对属性的值做反转义处理,确保值能正确运算执行。 - 判断
if (!unary)
,如果非自闭合,则将当前标签的所有信息存入stack
栈里,同时将lastTag
替换为当前tagName
,确保后续parseEndTag
的正常解析。 - 最后通过传入的
options.start
方法,调用options.start(tagName, attrs, unary, match.start, match.end)
。
回到parseHTML
时的start
方法,首先创建了一个ASTElement
:
let element: ASTElement = createASTElement(tag, attrs, currentParent)
得到element
如下:
// 去掉了与调试相关的start和end属性
{
type: 1,
tag: 'my-component',
attrsList: [
{name: ':foo.sync', value: 'foo'},
{name: ':key', value: 'index'},
{name: '@barTap', value: 'barTap'},
{name: 'v-for', value: '(item, index) in barList'},
],
attrsMap: {
':foo.sync': 'foo',
':key': 'index',
'@barTap': 'barTap',
'v-for': '(item, index) in barList',
},
rawAttrsMap: {},
parent, // parent为其父element
children: []
}
接着是
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
在parseHTML
开头提到preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
(用以执行options.modules
数组中每个元素的preTransformNode
方法),通过查看编译时的finalOptions
,我们得到options.modules
为baseOptions.modules
(详见src/platforms/web/compiler/options.js
)。在这里主要是执行input
标签里v-model
指令的前置转换,具体等解析input
标签时我们再来看。接着是一连串的判断:
if (!inVPre)
,用来判断其某个parent
是否包含v-pre
指令,我们知道包含v-pre
指令,则跳过这个元素和它的子元素的编译过程。如果没有,则调用processPre
来判断当前元素是否包含v-pre
,若是则设置inVPre
。platformIsPreTag(element.tag)
判断element
是否为pre
标签。- 根据
inVPre
执行process
编译,若inVPre
为true
,则将el.attrsList
的各元素value
调用JSON.stringify
为字符串。 - 若
inVPre
为false
,则根据element.processed
判断当前element
是否已编译完成,依次调用processFor(element)
、processIf(element)
、processOnce(element)
。
processFor
在利用processFor
解析v-for
指令时:
- 首先是获取
v-for
的attr
值,exp = getAndRemoveAttr(el, 'v-for')
,根据上面得到的element
和getAndRemoveAttr
方法,我们可以得到exp
为'(item, index) in barList'
,同时在element
的attrsList
移除当前元素 - 之后调用
const res = parseFor(exp)
解析for
语句:
export function parseFor (exp: string): ?ForParseResult {
const inMatch = exp.match(forAliasRE)
// 简化后 inMatch = ['(item, index) in barList', '(item, index)', 'barList']
if (!inMatch) return
const res = {}
res.for = inMatch[2].trim()
const alias = inMatch[1].trim().replace(stripParensRE, '') // 取括号内的alias如:item, index
const iteratorMatch = alias.match(forIteratorRE)
if (iteratorMatch) {
res.alias = alias.replace(forIteratorRE, '').trim()
// iterator1和iterator2,依次取for的后两项迭代属性
res.iterator1 = iteratorMatch[1].trim()
if (iteratorMatch[2]) {
res.iterator2 = iteratorMatch[2].trim()
}
} else {
res.alias = alias
}
return res
}
在我们的例子中,可以得到res
为{for: 'barList', alias: 'item', iterator1: 'index'}
- 得到
res
后,我们调用extend(el, res)
将其合并到element
里,得到此时的element
为:
{
type: 1,
tag: 'my-component',
attrsList: [
{name: ':foo.sync', value: 'foo'},
{name: '@barTap', value: 'barTap'},
],
attrsMap: {
':foo.sync': 'foo',
'@barTap': 'barTap',
'v-for': '(item, index) in barList',
},
rawAttrsMap: {},
parent, // parent为其父element
children: [],
key: 'index',
for: 'barList',
alias: 'item',
iterator1: 'index'
}
接着是processIf
和processOnce
,processOnce
用来给element
添加once
标志,processIf
这里未涉及到,将在解析a
标签那里去展开。
之后便是判断是否为根节点,如果之前未解析过标签,则将当前element
赋值给root
,同时,由于<my-component></my-component>
非自闭合标签,因此暂存至currentParent
变量,同时将其推入stack
栈,至此,我们简单的解析完了一个标签的开始部分。
preTransformNode
此时的html
字符串变成了:
<input v-model.number="value" />
<template #bodySlot="{link, text}">
<a v-if="link" :href="link">链接地址为:{{text | textFilter | linkFilter}}!</a>
<p v-else>{{text | filter}}</p>
</template>
</my-component>
接着我们重复parseHTML
的while
方法,此时解析到的是一个input
的自闭合标签,因此不需要进入stack
,其他与上一个标签类似,这里主要来看下之前提到的preTransforms
:
// src/platforms/web/compiler/modules/model.js
function preTransformNode (el: ASTElement, options: CompilerOptions) {
if (el.tag === 'input') {
const map = el.attrsMap
if (!map['v-model']) {
return
}
let typeBinding
if (map[':type'] || map['v-bind:type']) {
typeBinding = getBindingAttr(el, 'type')
}
if (!map.type && !typeBinding && map['v-bind']) {
// 取v-bind的对象中type值
typeBinding = `(${map['v-bind']}).type`
}
if (typeBinding) {
// 判断是否有v-if指令
const ifCondition = getAndRemoveAttr(el, 'v-if', true)
const ifConditionExtra = ifCondition ? `&&(${ifCondition})` : ``
const hasElse = getAndRemoveAttr(el, 'v-else', true) != null
const elseIfCondition = getAndRemoveAttr(el, 'v-else-if', true)
// 1. checkbox
const branch0 = cloneASTElement(el)
// process for on the main node
processFor(branch0)
addRawAttr(branch0, 'type', 'checkbox')
processElement(branch0, options)
branch0.processed = true // prevent it from double-processed
branch0.if = `(${typeBinding})==='checkbox'` + ifConditionExtra
addIfCondition(branch0, {
exp: branch0.if,
block: branch0
})
// 2. add radio else-if condition
const branch1 = cloneASTElement(el)
getAndRemoveAttr(branch1, 'v-for', true)
addRawAttr(branch1, 'type', 'radio')
processElement(branch1, options)
addIfCondition(branch0, {
exp: `(${typeBinding})==='radio'` + ifConditionExtra,
block: branch1
})
// 3. other
const branch2 = cloneASTElement(el)
getAndRemoveAttr(branch2, 'v-for', true)
addRawAttr(branch2, ':type', typeBinding)
processElement(branch2, options)
addIfCondition(branch0, {
exp: ifCondition,
block: branch2
})
if (hasElse) {
branch0.else = true
} else if (elseIfCondition) {
branch0.elseif = elseIfCondition
}
return branch0
}
}
}
- 判断
ASTElement
的tag
及attrsMap
,当包含v-model
的input
标签时才继续执行,并根据多种绑定方式,获取typeBinding
。 - 生成
checkbox
情况下的ASTElement
代码块,并添加到branch0
。 - 生成
radio
下的ASTElement
代码块,并添加到branch1
。 - 其他
type
情况,保留:type
,并添加到branch2
。
在preTransformNode
方法里,通过cloneASTElement
克隆各种type
的branch
,用processFor
来解析branch0
根节点的v-for
指令,并调用getAndRemoveAttr
删除其余branch
的v-for
属性和addRawAttr
添加type
属性。再调用processElement
来编译解析各个branch0
,并添加到ifConditions
数组(元素对象有两个属性:exp
和block
,用以后续genIf
循环遍历,并根据exp
是否成立而返回对应block
),最后根据v-if
、v-else
和v-else-if
来设置branch0
对应的属性,并返回branch0
。
input
是自闭合标签,因而不需要放入stack
,直接执行closeElement
。在preTransformNode
已设置element.processed
为true
,只需将my-component
和input
设置children
和parent
互相引用即可。最后执行postTransforms
,完成当前element
的编译。
接着解析template
标签<template #bodySlot="{link, text}">
,将对应的attr
放入attrsList
和attrsMap
,过程与上述类似,在此不再赘述。
parseText
接着是一组a
标签和p
标签,首先是一组v-if
和v-else
:
<a v-if="link" :href="link">链接地址为:{{text | textFilter | linkFilter}}!</a>
<p v-else>{{text | filter}}</p>
可以看到,processIf
方法里通过对v-if
的判断来调用addIfCondition
,而v-else-if
没有调用,是因为在closeElement
里会通过processIfConditions
向前查找最近一个v-if
表达式的element
并对其调用addIfCondition
方法。之后通过传入的option.char
方法来解析字符串。在方法里,我们可以看到根据(res = parseText(text, delimiters))
用来生成不同的child
,下面我们来看下parseText
是如何编译的。
首先会根据compilerOption
里传入的delimiters
来确定text
的匹配规则,默认为{{}}
,接着通过while
循环匹配"链接地址为:{{text | textFilter | linkFilter}}!"
字符串,并将结果依次存入rawTokens
和tokens
,最终返回expression
和tokens
。
增加
index > lastIndex
及循环结束时的lastIndex < text.length
,是为了将未包含在delimiters
之内的字符串也一起解析保存。
在while
循环体内,还有一个parseFilter
用来解析我们的过滤器,利用for
循环去识别各个i
位置的char
这里增加对 ` ' " / 等的判断,是为了避免把字符串和正则里的 | 当做过滤器操作符
首先是取出需要操作的表达式expression
,之后将匹配到的过滤器依次存入filters
数组,最后调用wrapFilter
方法,根据过滤器有无其他参数, 返回真正的表达式。据此我们可以得到最终的expression
为_f("linkFilter")(_f("textFilter")(text))
,可以看到当同一表达式存在多个过滤器时,其将按照从左往右的顺序执行。_f
是挂载在Vue
原型链上的方法,用以运行时根据vm.$options
来获取到对应的过滤器函数,后续类似的_s, _c
等方法,可从src/core/instance/render-helpers/index.js
进行查看,具体不做展开。
在parseText
方法最后,我们可以得到:
{
expression: '"链接地址为:" + _s(_f("linkFilter")(_f("textFilter")(text))) + "!"'
tokens: ["链接地址为:", {@binding: '_f("linkFilter")(_f("textFilter")(text))'}, "!"]
}
closeElement
之后解析</template>
的闭合标签:
</template>
</my-component>
我们先取到之前的element
:
{
type: 1,
tag: 'template',
attrsList: [
{name: '#bodySlot', value: '{link, text}'},
],
attrsMap: {
'#bodySlot': '{link, text}',
},
parent, // parent为其父my-component
children: [],
}
之后先执行processElement
方法,processElement
函数在src/compiler/parser/index.js
,主要负责element
本身的编译解析。
processSlotContent
调用核心方法
processSlotContent
用来编译slot
。
-
先是兼容
template
的scope
属性,并解析slot-scope
至el.slotScope
,用来保存作用域插槽需要传递的props
。 -
解析
slot
用来处理默认插槽和具名插槽,插槽名设置到el.slotTarget
,el.slotTargetDynamic
用以判断是否为动态解析的插槽,支持:slot="dynamicSlot"
这种动态写法。 -
v-slot
在tag
为template
的标签上时,等效于将slot
和slot-scope
作合并处理。 -
v-slot
在component
上时,此时的slotScope
为component
自身的插槽上所提供。the scope variable provided by a component is also declared on that component itself.getAndRemoveAttrByRegex(el, slotRE)
根据正则取到slotBinding
,并将其从attrsList
里移除。getSlotName(slotBinding)
得到{name: "bodySlot", dynamic: false}
。slotContainer = el.scopedSlots.bodySlot
调用createASTElement('template', [], el)
增加一层新的ASTElement
,用以传递slotScope
至slotContainer
,使得与template
解析方式保持一致。- 将
el.children
过滤带有slotScope
的element
,并添加至slotContainer.children
,因为重新梳理了el.children
中element
的父子关系,故而需要移除el.children
。
如果
component
的el.children
中有element
存在slotScope
,如下示例,此时在my-component
和template
上均有slotScope
,然而事实上这些域变量均来自于my-component
,因而为避免scope ambiguity
,在开发模式下会给出错误提示!
<my-component v-slot="{msg}">
<template v-slot="{foo}"></template>
</my-component>
至此完成了processSlotContent
对当前element
及其children
的插槽语法编译。
processAttrs
for (let i = 0; i < transforms.length; i++) {
element = transforms[i](element, options) || element
}
继续执行processElement
,便是对element
依次调用transforms
,通过查看对应文件src/platforms/web/compiler/modules/index.js
,该方法主要处理class
和style
的静态属性和动态绑定,并设置到el
的staticClass
、classBinding
、staticStyle
、styleBinding
四个属性。
最后通过processAttrs
方法来处理attrs
各个属性:
modifiers = parseModifiers(name.replace(dirRE, ''))
将各个attr
的修饰符依次存入对应的modifiers
,并设置为true
。如:foo.sync
返回{sync: true}
。
syncGen = genAssignmentCode(value, `$event`)
返回一个foo=$event
字符串(也支持foo.bar
、foo['bar'].tab
等这种属性绑定)。
之后调用addHandler
方法,根据有无.native
修饰符将update:foo
方法添加到el.events
或el.nativeEvents
。
根据是否为.prop
修饰符或非动态component
、DOM
保留的Property
,分别调用addProp
添加到el.props
和addAttr
添加至el.attrs
、el.dynamicAttrs
。最后将未匹配上的v-
指令调用addDirective
添加到el.directives
,至此完成了一个完整element
包括其子element
的编译工作。
通过parse
我们就能根据一段字符串html
解析得到一个root
节点及其children
和子孙后代,生成一棵完整的AST
语法树,最终得到如下结构的root
:
{
alias: 'item',
attrs: [{name: 'foo', value: 'foo', dynamic: false}],
attrsList: [{name: ':foo.sync', value: 'foo'}, {name: '@barTap', value: 'barTap'}],
attrsMap: {
':foo.sync': 'foo',
'@barTap': 'barTap',
'v-for': '(item, index) in barList',
},
children: [{
attrsList: [{name: 'v-model.number', value: 'value'}],
attrsMap: {'v-model.number': 'value'},
directives: [{
arg: null,
isDynamicArg: false,
modifiers: {number: true},
name: 'model',
rawName: "v-model.number",
value: "value",
}],
plain: false,
prop: [{name: 'value', value: '(value)', dynamic: undefined}],
tag: 'input',
type: 1,
}],
events: {
barTap: {value: 'barTap', dynamic: false},
'update:foo': {value: 'foo=$event', dynamic: undefined},
},
for: 'barList',
forProcessed: true,
hasBindings: true,
iterator1: 'index',
key: 'index',
plain: false,
scopedSlots: {
'bodySlot': {
...,
children: [{
...,
if: 'link',
// aElement和pElement分别为<a>和<p>创建的两个ASTElement
ifConditions: [{exp: 'link', block: aElement}, {exp: undefined, block: pElement}],
ifProcessed: true,
}],
slotScope: '{link, text}',
slotTarget: '"bodySlot"',
slotTargetDynamic: false,
tag: "template",
type: 1,
}
}
},
tag: 'my-component',
type: 1,
}
最后用一张导图来梳理下
parse
生成AST
过程中的主要知识