冲一杯美式 ☕️ ,读编译真经,岂不快哉?
本文的 🍪 (表示 例子,☕️ 和 🍪 更配哦!全文都会围绕这个 DEMO 做解析。⚠️ 因不能直接跳转到外链,注意有标注的地方,文末有对应的地址哦,跳转着看更容易理解哦!):
<div id="app">
<!-- 这是一个注释节点 -->
<Child name="yjc" :age="12" v-if="isShow"></Child>
<input type="text" v-model="inputValue" />
<div class="abc"></div>
</div>
const Child = Vue.extend({
name: 'Child',
props: {
name: String,
age: Number
},
render (h) {
return h('div', null, [
h('span', null, this.name),
h('span', null, this.age),
])
}
})
new Vue({
el: '#app',
components: {
Child
},
data () {
return {
isShow: true,
inputValue: '123123'
};
}
})
🍪 中包含模板编译处理的节点——注释节点、开始标签、props
属性、DOM
属性、自闭合标签。
抿一口☕️,让我们看看是从哪里开始执行模板编译的。回忆一下 [咖聊]Vue执行过程,其中有一个 options
是否存在 render
的判断。如果是自己手写 render
函数,例如 🍪 中的 Child
组件就属于这种情况则不需要走模板编译流程;如果是通过 SFC 或者写 template
的,那么会通过模板编译去生成 render
函数。
这部分代码在 src\platforms\web\entry-runtime-with-compiler.js
/**
* 挂载组件,带模板编译
*/
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean // 与服务端渲染有关,不考虑
): Component {
// 挂载dom,query对它做了一些判断,是dom直接返回,是字符串通过querySelector去获取dom
el = el && query(el)
// 配置信息
const options = this.$options
// resolve template/el and convert to render function
// 不存在render函数,处理template内容,转换为render函数
if (!options.render) {
// ... 省略一部分获取 template 字符串的过程
}
if (template) {
// ...
// 执行模板编译,最终结果返回 render 和 staticRenderFns
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
// ...
}
}
/*调用const mount = Vue.prototype.$mount保存下来的不带编译的mount*/
return mount.call(this, el, hydrating)
}
可以看到,模板编译最终得到的结果是 render
和 staticRenderFns
函数,这个 staticRenderFns
干嘛用的?😵不是只需要 render
吗?
为了得到编译函数 compileToFunctions
,需要绕大半个“地球”,最终进入到真正的编译:
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 编译生成AST
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
/**
* 将AST进行优化
* 优化的目标:生成模板AST,检测不需要进行DOM改变的静态子树。
* 一旦检测到这些静态树,我们就能做以下这些事情:
* 1.把它们变成常数,这样我们就再也不需要每次重新渲染时创建新的节点了。
* 2.在patch的过程中直接跳过。
*/
optimize(ast, options)
}
// 根据AST生成所需的code(内部包含render与staticRenderFns)
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
在执行编译之前,扩展 baseOptions
上的很多配置。同时在开始编译时,就决定了当前的编译环境,后面再更新用的还是这套编译环境,所以也做了编译器的缓存。
整装待发,就踏入了解析阶段。
这个阶段用一句话概括起来就是“用正则表达式去匹配字符串中的开始标签、标签属性、注释、闭合标签等,最终输出 AST
的过程”。
首先安利一个正则小工具:regex1011,页面中每一个板块都极其好用,太香啦 😋:
开始之前,先看一个不管任何匹配都会调用的函数 advance
:
function advance (n) {
index += n
html = html.substring(n)
}
清晰明了,就是将匹配到的结果从字符串中剔除,然后更新 html
。
这节我们就通过 🍪 中的模板,看 AST
是如何生成的:
<div id="app">
<!-- 这是一个注释节点 -->
<Child name="yjc" :age="12" v-if="isShow"></Child>
<input type="text" v-model="inputValue" />
<div class="abc"></div>
</div>
按照上面的模板,一步一步梳理匹配过程:
开始标签 <div id="app">
:
function parseStartTag () {
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
}
}
}
匹配开始标签名2,此时会创建一个 match
对象;匹配开始标签中的属性3,给 match
中的 attrs
添加属性 match
的结果;匹配开始标签的结尾 > 字符4,将匹配分组信息和结尾位置分别记录到match.unarySlash
和 match.end
中。
紧接着对 match 调用 handleStartTag
做处理:
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)
}
}
// 判断是不是一元标签,例子的中的 input 这里会是 true,后面再看
const unary = isUnaryTag(tagName) || !!unarySlash
// 遍历全部的 attrs
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
// 对属性值做编码处理,xss攻击
attrs[i] = {
name: args[1],
value: decodeAttr(value, shouldDecodeNewlines)
}
}
// 不是一元标签的情况下将标签名等信息推进 stack 中,并给 lastTag 赋值当前标签名,这个用于后面的标签栈匹配
if (!unary) {
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
lastTag = tagName
}
// 调用 start 生成 ASTElement
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
handleStartTag
先判断当前标签是不是一元标签,然后处理了 attrs
上的值,比如编码处理等。不是一元标签的话,把标签部分信息存到 stack
中,最后调用 start
函数生成 rootElement
:
start (tag, attrs, unary) {
// ...
// 创建 ASTElement
let element: ASTElement = createASTElement(tag, attrs, currentParent)
// ...
// apply pre-transforms
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
// ...
if (!root) {
root = element
// 校验检查,不要用slot、template做根节点,也不要用 v-for 属性,因为这些都可能产生多个根节点
checkRootConstraints(root)
} else {
// ...
}
// ...
// 不是一元标签,把当前的 ASTElement 推入到 stack 中
if (!unary) {
currentParent = element
stack.push(element)
} else {
closeElement(element)
}
},
对于 🍪 中的 rootElement
比较简单,没有其他逻辑分支处理,就直接贴上结果图:
到此开始标签
<div id="app">
就解析完了。此时的html
因为advance
的递进处理,变成了下面这般模样:
<!-- 这是一个注释节点 -->
<Child name="yjc" :age="12" v-if="isShow"></Child>
<input type="text" v-model="inputValue" />
<div class="abc"></div>
</div>
在解析注释节点之前,我们可以看到有一系列空格,这个处理也比较简单,就是看当前 textEnd
(🍪 中 <
的位置),然后判断是大于 0 的情况,将这些空白字符去掉就行了:
let text, rest, next
// demo 中这里是 4 ,是大于 0 的
if (textEnd >= 0) {
/**
* 直接走到这里,rest 是
* <!-- 这是一个注释节点 -->
<child name="yjc" :age="12" v-if="isShow"></child>
<input type="text" v-model="inputValue">
<div class="abc"></div>
</div>
*/
rest = html.slice(textEnd)
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// < in plain text, be forgiving and treat it as text
next = rest.indexOf('<', 1)
if (next < 0) break
textEnd += next
rest = html.slice(textEnd)
}
text = html.substring(0, textEnd)
advance(textEnd)
}
然后又会进入创建 AST
的过程,这次的回调函数是 options.chars
:
chars (text: string) {
// ...
const children = currentParent.children
text = inPre || text.trim()
? isTextTag(currentParent) ? text : decodeHTMLCached(text)
// only preserve whitespace if its not right after a starting tag
: preserveWhitespace && children.length ? ' ' : ''
if (text) {
let res
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
children.push({
type: 2,
expression: res.expression,
tokens: res.tokens,
text
})
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
children.push({
type: 3,
text
})
}
}
},
空格字符走进来兜了一圈,因为 trim
之后就啥都不剩了,所以兜了一圈又回到 parseHTML
主流程上啦。😅
接下来是一个注释节点 <!-- 这是一个注释节点 -->
:
if (comment.test(html))
// 计算注释节点结束位置
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
// 是否保存注释节点
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd))
}
// 递进,从 html 中剔除注释节点
advance(commentEnd + 3)
continue
}
}
匹配注释节点的开头5;判断是否需要保留注释节点(⚠️ 这个配置从配置中读取,你可以按照下面的方式配置),不需要的话接着处理 html
模板,否则 AST
会添加一个注释文本节点:
new Vue({
el: '#app',
components: {
Child
},
// 注意:这里可以配置保存注释信息
comments: true,
data () {
return {
isShow: true,
inputValue: ''
};
}
})
处理完了注释节点,模板变成了:
<Child name="yjc" :age="12" v-if="isShow"></Child>
<input type="text" v-model="inputValue" />
<div class="abc"></div>
</div>
处理空白字符,重复步骤2。
接下来是一个组件节点 <Child name="yjc" :age="12" v-if="isShow"></Child>
:
parseStartTag
跟前面 <div id="app">
没有区别,无非就是多循环了几遍 attrs
的处理过程。处理之后的 match
结果如下:
然后执行到 options.start
函数,跟上面 div
相同的逻辑这里就不叙述了。Child
跟 div
有几点不一样的是:Child
有 v-if
指令,getAndRemoveAttr
会把 attrsList
中的 v-if
属性删除,然后在 Child AST
上加上 if
和 ifCondition
字段;
function processIf (el) {
// 获取 v-if 指令的值,例子中是 isShow
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
el.if = exp
addIfCondition(el, {
exp: exp,
block: el
})
} else {
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
}
const elseif = getAndRemoveAttr(el, 'v-else-if')
if (elseif) {
el.elseif = elseif
}
}
}
属性的 AST
处理,在上面 <div id="app">
的时候略过了,现在来看看:
function processAttrs (el) {
// 获取属性列表
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
/*匹配v-、@以及:,处理el的特殊属性*/
if (dirRE.test(name)) {
// mark element as dynamic
/*标记该ele为动态的*/
el.hasBindings = true
// modifiers
/*解析表达式,比如a.b.c.d得到结果{b: true, c: true, d:true}*/
modifiers = parseModifiers(name)
if (modifiers) {
/*得到第一级,比如a.b.c.d得到a,也就是上面的操作把所有子级取出来,这个把第一级取出来*/
name = name.replace(modifierRE, '')
}
/*如果属性是v-bind的*/
if (bindRE.test(name)) { // v-bind
name = name.replace(bindRE, '')
value = parseFilters(value)
isProp = false
if (modifiers) {
/**
* https://cn.vuejs.org/v2/api/#v-bind
* 这里用来处理v-bind的修饰符
*/
/*.prop - 被用于绑定 DOM 属性。*/
if (modifiers.prop) {
isProp = true
/*将原本用-连接的字符串变成驼峰 aaa-bbb-ccc => aaaBbbCcc*/
name = camelize(name)
if (name === 'innerHtml') name = 'innerHTML'
}
/*.camel - (2.1.0+) 将 kebab-case 特性名转换为 camelCase. (从 2.1.0 开始支持)*/
if (modifiers.camel) {
name = camelize(name)
}
//.sync (2.3.0+) 语法糖,会扩展成一个更新父组件绑定值的 v-on 侦听器。
if (modifiers.sync) {
addHandler(
el,
`update:${camelize(name)}`,
genAssignmentCode(value, `$event`)
)
}
}
if (isProp || (
!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)
)) {
/*将属性放入el的props属性中*/
addProp(el, name, value)
} else {
/*将属性放入el的attr属性中*/
addAttr(el, name, value)
}
} else if (onRE.test(name)) { // v-on
/*将属性放入el的attr属性中*/
name = name.replace(onRE, '')
addHandler(el, name, value, modifiers, false, warn)
} else { // normal directives
/*去除@、:、v-*/
name = name.replace(dirRE, '')
// parse arg
const argMatch = name.match(argRE)
/*比如:fun="functionA"解析出fun="functionA"*/
const arg = argMatch && argMatch[1]
if (arg) {
name = name.slice(0, -(arg.length + 1))
}
/*将参数加入到el的directives中去*/
addDirective(el, name, rawName, value, arg, modifiers)
if (process.env.NODE_ENV !== 'production' && name === 'model') {
checkForAliasModel(el, value)
}
}
} else {
// ...
/*将属性放入el的attr属性中*/
addAttr(el, name, JSON.stringify(value))
// #6887 firefox doesn't update muted state if set via attribute
// even immediately after element creation
if (!el.component &&
name === 'muted' &&
platformMustUseProp(el.tag, el.attrsMap.type, name)) {
addProp(el, name, 'true')
}
}
}
}
parseAttrs
遍历 attrsList
,处理各种属性情况,例如:v-bind
、@
、值表达式、修饰符等各种场景,就不一个一个逻辑去执行了。只看我们 🍪 中name=“yjc”
和 :age="12"
。纯文本的比较简单,执行 addAttr(el, name, JSON.stringify(value))
在 AST
上加上 attrs
属性;后者通过 dirRE6 和 bindRE7 去掉 :
符号之后添加到 attrs
中。
编译 Child
时,root
节点是存在的,这时会构建 parent
和 children
的关系:
// 解析到 Child 时,currentParent 指向的是 div 节点
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else if (element.slotScope) { // scoped slot
currentParent.plain = false
const name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
} else {
// div AST 的 children 字段加入 Child AST
currentParent.children.push(element)
// Child AST 的 parent 赋值为 div AST
element.parent = currentParent
}
}
处理完 Child 节点后的结果:
</Child>
<input type="text" v-model="inputValue" />
<div class="abc"></div>
</div>
闭合标签 </Child>
的处理过程:先用闭合标签正则8惰性地匹配,这个正则就是在开始标签正则的基础上加了一个 /
;然后用 advance
剔除闭合标签;通过 parseEndTag
和 options.end
去更新标签和 AST
的 stack
;
function parseEndTag (tagName, start, end) {
let pos, lowerCasedTagName
if (start == null) start = index
if (end == null) end = index
if (tagName) {
lowerCasedTagName = tagName.toLowerCase()
}
// Find the closest opened tag of the same type
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
}
if (pos >= 0) {
// Close all the open elements, up the stack
for (let i = stack.length - 1; i >= pos; i--) {
// ...
if (options.end) {
options.end(stack[i].tag, start, end)
}
}
// 将数组长度设置成当前位置,提出栈中最后一个标签,并更新 lastTag
stack.length = pos
lastTag = pos && stack[pos - 1].tag
}
// ...
}
parseEndTag
将标签转成小写之后和栈中最上面的元素做比较,这就是为什么 <Child></child>
这样也不会报标签不匹配的原因。然后调用 options.end
去更新 AST stack
:
end () {
// 处理尾部空格的情况
const element = stack[stack.length - 1]
const lastNode = element.children[element.children.length - 1]
if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
element.children.pop()
}
// 最后一个AST信息弹出栈,并更新当前的currentParent节点
stack.length -= 1
currentParent = stack[stack.length - 1]
// 更新了 inVPre 和 inPrV 的状态, 🌰不需要了解
closeElement(element)
},
处理了 </Child>
之后的结果:
<input type="text" v-model="inputValue" />
<div class="abc"></div>
</div>
至此,开始标签、标签属性、闭合标签等都已经通过源码过了一遍,对于下一个 input
节点,我们就看 v-model
和自闭合标签的处理:parseStartTag
和之前的流程一样;执行到 handleStartTag
的 const unary = isUnaryTag(tagName) || !!unarySlash
时,这里返回的是 true
;自闭合标签因为不用匹配闭合标签,所以不需要入栈。直接执行 options.start
;生成 AST
时,90% 的流程都是一样的。v-model="inputValue"
会在执行 processElement
-> processAttrs
时调用 addDirective
:
export function addDirective (
el: ASTElement,
name: string,
rawName: string,
value: string,
arg: ?string,
modifiers: ?ASTModifiers
) {
(el.directives || (el.directives = [])).push({ name, rawName, value, arg, modifiers })
el.plain = false
}
会在 AST
节点上添加 directives
数组然后把 model
和 inputValue
都推进到该数组中。最终 input
生成的 AST
如下图所示:
解析完 input
节点,html
只剩下:
<div class="abc"></div>
</div>
最终剩下的模板就非常简单了,就是重复前面的过程处理即可。这里就不写了。(其实这个节点是为了后面的 optimize
做铺垫。😄😄)当 html
只剩下 ""
时,最终会再执行一次 parseEndTag
,用于栈中清理剩余的标签。
parse
过程就是将 template
字符串通过正则表达式(复杂的正则通过 regex101 工具协助分析,可以梳理匹配场景)去匹配出开始标签、闭合标签、注释节点、标签属性等。补充一个标签栈的动画过程:
然后在匹配过程中调用各自的回调函数去生成 AST
。每次解析完一个节点之后通过 advance
递进。最终解析完整个字符串,返回 AST
给下一个环节——optimize
。在开始分析 optimize
之前,生成 AST
有一个细节还没讲到,就是 AST
中的 type
字段。type
的含义(⚠️ 魔数慎用,降低理解成本):
本小节目标:
带着以上3个问题,开始取“优化”真经。在入口有一个判断:
if (options.optimize !== false) {
optimize(ast, options)
}
还有不进行优化的情况吗?对于 web
的情况,这个是 undefined
的,undefined !== false
成立,所以需要进行优化。对于 weex
的情况,options.optimize
是明确成 false
的。看到 optimize
:
/**
* Goal of the optimizer: walk the generated template AST tree
* and detect sub-trees that are purely static, i.e. parts of
* the DOM that never needs to change.
*
* Once we detect these sub-trees, we can:
*
* 1. Hoist them into constants, so that we no longer need to
* create fresh nodes for them on each re-render;
* 2. Completely skip them in the patching process.
*/
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// first pass: mark all non-static nodes.
markStatic(root)
// second pass: mark static roots.
markStaticRoots(root, false)
}
对于第一个问题,optimize
的注释已经给出了答案:
patch
过程中可以完全跳过它们;看到第一个主流程 markStatic(root)
:
function markStatic (node: ASTNode) {
node.static = isStatic(node)
if (node.type === 1) {
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
return
}
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
if (!child.static) {
node.static = false
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block
markStatic(block)
if (!block.static) {
node.static = false
}
}
}
}
}
function isStatic (node: ASTNode): boolean {
// 表达式一定不是静态节点
if (node.type === 2) { // expression
return false
}
// 纯文本节点一定是静态的
if (node.type === 3) { // text
return true
}
// vpre 或者 没有绑定值、没有v-if、没有v-for、不是slot、template节点、是html或svg保留的标签(非组件)
// 不是v-for的template的子节点
// 任何属性都满足静态的情况
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))
}
这里能够得到第二个问题(怎样的节点才算是静态节点)的答案:
node.pre
即 v-pre
指令的内容是静态节点;v-if
、没有 v-for
、不是 slot
、template
节点、是 html
或 svg
保留的标签(非组件),不是 v-for
的 template
子节点、任一属性都是静态的;回到 🍪 中:
<div id="app">
<!-- 这是一个注释节点 -->
<Child name="yjc" :age="12" v-if="isShow"></Child>
<input type="text" v-model="inputValue" />
<div class="abc"></div>
</div>
根据上面静态节点的范畴,那么静态节点有 3 个:
第二个主流程是标记静态根节点,什么是静态根节点呢?先看下函数逻辑:
function markStaticRoots (node, isInFor) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor;
}
// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true;
return
} else {
node.staticRoot = false;
}
if (node.children) {
for (var i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for);
}
}
if (node.ifConditions) {
for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
markStaticRoots(node.ifConditions[i$1].block, isInFor);
}
}
}
}
函数递归调用 markStaticRoots
,如果节点是静态节点并且是 node.once
(即 v-once
作用的节点),会加上标记 node.staticInFor = isInFor
。如果一个节点在满足自身是静态节点且是普通节点的情况下,如果它的孩子节点不全是文本节点(type === 3
)的情况下,那么它就是一个静态根节点。可以看到上述代码的注释,标记这种条件下的静态根节点会有重新更新性能。🍪 中没有这种节点。所以所有普通节点(type === 1
)都会被标记 staticRoot = false
。
optimize
通过递归的方式给每个节点标记 static
字段,对于满足静态判断条件的节点标记 static: true
。在静态节点的基础上,如果一个普通节点含有一个非纯文本的静态节点时,那么该节点就会标记为静态根节点,标记 staticRoot:true
。
万事俱备,只欠东风。参谋了很多网上编译的文章,到这一步时可能写累了,都草草地把生成的 render
代码贴上来就做总结了。generate
过程一句话概括起来就是“识别 AST
中的各个字段,经过一系列处理之后转成 render
函数。”这个过程条件判断非常多,这里我们按照 🍪 中的 AST
来一步一步走完 generate
过程。
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
入口先创建一个 CodegenState
的实例 state
,该实例的作用我们在后面用到的时候再分析。然后调用 genElement
去生成最终的 code
:
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.staticRoot && !el.staticProcessed) { // 静态根节点
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) { // v-once
return genOnce(el, state)
} else if (el.for && !el.forProcessed) { // v-for
return genFor(el, state)
} else if (el.if && !el.ifProcessed) { // v-if
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget) { // template
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') { // slot
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
// 生成根节点
const data = el.plain ? undefined : genData(el, state)
// 生成孩子节点
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
genElement
判断节点上各个字段,然后做不同的 genXXX
处理。🍪 生成的 AST
如下截图所示:
根节点的 AST
属性会执行到 const data = el.plain ? undefined : genData(el, state)
这行代码,进到 genData
里:
export function genData (el: ASTElement, state: CodegenState): string {
let data = '{'
// directives first.
// directives may mutate the el's other properties before they are generated.
const dirs = genDirectives(el, state)
if (dirs) data += dirs + ','
// ... 一堆 if,对于当前 AST 执行不到的逻辑先剔除
// attributes
if (el.attrs) {
data += `attrs:{${genProps(el.attrs)}},`
}
// ... 一堆 if,对于当前 AST 执行不到的逻辑先剔除
data = data.replace(/,$/, '') + '}'
// ...
return data
}
根节点 so easy
,就只有 id = app
这个 attrs
。最终 return "{attrs:{\"id\":\"app\"}}"
。下一步就是遍历 children
去生成子节点的 render
函数,会执行到
const children = el.inlineTemplate ? null : genChildren(el, state, true)
🍪 不是内联模板,所以执行到 genChildren(el, state, true)
:
export function genChildren (
el: ASTElement,
state: CodegenState,
checkSkip?: boolean,
altGenElement?: Function,
altGenNode?: Function
): string | void {
const children = el.children
if (children.length) {
const el: any = children[0]
// optimize single v-for
if (children.length === 1 &&
el.for &&
el.tag !== 'template' &&
el.tag !== 'slot'
) {
return (altGenElement || genElement)(el, state)
}
/**
* 获取规范化的类型
* 0 不需要规范化
* 1 简单的规范化即可(可能是一级的嵌套数组) --> 子节点 v-if 存在组件
* 2 完全的规范化 --> 子节点 v-if 并且有 v-for、或者 template 或者 tag 标签
*/
const normalizationType = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0
const gen = altGenNode || genNode
return `[${children.map(c => gen(c, state)).join(',')}]${
normalizationType ? `,${normalizationType}` : ''
}`
}
}
🍪 中有 child
组件,所以规划化类型是 1。这个有什么用呢?留作悬念!
然后每个子组件循环调用 genNode
函数,去生成各自的 render
函数。
function genNode (node: ASTNode, state: CodegenState): string {
// 普通节点
if (node.type === 1) {
return genElement(node, state)
// 注释节点
} if (node.type === 3 && node.isComment) {
return genComment(node)
// 文本节点
} else {
return genText(node)
}
}
第一个节点是 child
,这个节点有 v-if
指令,有点特色,老规矩我先把节点的 AST
截图丢上来:
下面就一起看看是怎么处理这个指令,genNode
-> genElement
:
// ...
// 存在 v-if,并且没有被标记过
else if (el.if && !el.ifProcessed) { // v-if
return genIf(el, state)
}
// ...
进入 genIf
:
export function genIf (
el: any,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
// 做标记,避免递归
el.ifProcessed = true // avoid recursion
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
进入 genIfConditions
:
function genIfConditions (
conditions: ASTIfConditions,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
if (!conditions.length) {
return altEmpty || '_e()'
}
const condition = conditions.shift()
if (condition.exp) {
return `(${condition.exp})?${
genTernaryExp(condition.block)
}:${
genIfConditions(conditions, state, altGen, altEmpty)
}`
} else {
return `${genTernaryExp(condition.block)}`
}
// v-if with v-once should generate code like (a)?_m(0):_m(1)
function genTernaryExp (el) {
return altGen
? altGen(el, state)
: el.once
? genOnce(el, state)
: genElement(el, state)
}
}
🍪 中的 condition.exp
是 isShow
,所以会进入 if 逻辑,调用 genTernaryExp
和 genIfConditions
。
先看 genTernaryExp
,会依次执行 genElement
(不同的是此时的 el.ifProcessed
已经是 true
了,所以流程跟上面的 div
节点一毛一样) -> genData
,最后生成的代码是:
"_c('child',{attrs:{"name":"yjc","age":12}})"
最后看 genIfConditions
,🍪 中的 condition
此时为 0。所以直接返回 _e()
。最终这个节点生成的代码:
isShow ? _c('Child', {
attrs: {
"name": "yjc",
"age": 12
}
}) : _e()
第二个孩子节点是空格节点:
{
text: " ",
type: 3,
static: true
}
执行到 genText
:
export function genText (text: ASTText | ASTExpression): string {
return `_v(${text.type === 2
? text.expression // no need for () because already wrapped in _s()
: transformSpecialNewlines(JSON.stringify(text.text))
})`
}
生成的代码:
"_v(\" \")"
第三个孩子节点也比较有特色,有 v-model
指令,这个处理起来可谓是非常复杂的了。事不宜迟,先看下 AST
:
genNode
-> genElement
-> genData
,前面两步都是一样的,到了 getData
时,因为有 directives
,所以会执行到 genDirectives
:
function genDirectives (el: ASTElement, state: CodegenState): string | void {
const dirs = el.directives
if (!dirs) return
let res = 'directives:['
let hasRuntime = false
let i, l, dir, needRuntime
for (i = 0, l = dirs.length; i < l; i++) {
dir = dirs[i]
needRuntime = true
// modal 定义,定义在 src\platforms\web\compiler\directives\model.js
const gen: DirectiveFunction = state.directives[dir.name]
if (gen) {
// compile-time directive that manipulates AST.
// returns true if it also needs a runtime counterpart.
needRuntime = !!gen(el, dir, state.warn)
}
if (needRuntime) {
hasRuntime = true
res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
}${
dir.arg ? `,arg:"${dir.arg}"` : ''
}${
dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
}},`
}
}
if (hasRuntime) {
return res.slice(0, -1) + ']'
}
}
看到 gen
函数的定义,也就是 modal
指令的函数定义:
export default function model (
el: ASTElement,
dir: ASTDirective,
_warn: Function
): ?boolean {
warn = _warn
const value = dir.value
const modifiers = dir.modifiers
const tag = el.tag
const type = el.attrsMap.type
// ...
} else if (tag === 'input' || tag === 'textarea') {
genDefaultModel(el, value, modifiers)
}
// ...
return true
}
省略掉判断是否组件 v-model
、是否 input
和 checkbox
、radio
、file
的组合、是否 select
的判断。看到我们 🍪 中的 input
,进入 genDefaultModel
:
function genDefaultModel (
el: ASTElement,
value: string,
modifiers: ?ASTModifiers
): ?boolean {
const type = el.attrsMap.type
// ...
const { lazy, number, trim } = modifiers || {}
const needCompositionGuard = !lazy && type !== 'range'
const event = lazy
? 'change'
: type === 'range'
? RANGE_TOKEN
: 'input'
let valueExpression = '$event.target.value'
// v-model.trim 处理去除空格修饰符
if (trim) {
valueExpression = `$event.target.value.trim()`
}
// v-model.number 数字化
if (number) {
valueExpression = `_n(${valueExpression})`
}
let code = genAssignmentCode(value, valueExpression)
if (needCompositionGuard) {
code = `if($event.target.composing)return;${code}`
}
addProp(el, 'value', `(${value})`)
addHandler(el, event, code, null, true)
if (trim || number) {
addHandler(el, 'blur', '$forceUpdate()')
}
}
先对 lazy
、number
、trim
3个修饰符做了处理,最后通过 addProp
和 addHandler
给 AST
加上 value
和 input
事件。v-model
是语法糖就是这么一个道理:
export function addProp (el: ASTElement, name: string, value: string) {
(el.props || (el.props = [])).push({ name, value })
el.plain = false
}
export function addHandler (
el: ASTElement,
name: string,
value: string,
modifiers: ?ASTModifiers,
important?: boolean,
warn?: Function
) {
modifiers = modifiers || emptyObject
// ...
let events
if (modifiers.native) {
delete modifiers.native
events = el.nativeEvents || (el.nativeEvents = {})
} else {
events = el.events || (el.events = {})
}
// ...
const handlers = events[name]
/* istanbul ignore if */
if (Array.isArray(handlers)) {
important ? handlers.unshift(newHandler) : handlers.push(newHandler)
} else if (handlers) {
events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
} else {
events[name] = newHandler
}
el.plain = false
}
去掉了不关键的修饰符逻辑跟日志,上面两个函数的逻辑就简单了。生成的 AST
如下:
AST
处理完了,回到 genDirectives
中,最终该函数返回的 res
是下面这样一个字符串:
"directives:[{name:\"model\",rawName:\"v-model\",value:(inputValue),expression:\"inputValue\"}]"
再往上回到 genData
,会处理 props
和 events
字段:
// DOM props
if (el.props) {
data += "domProps:{" + (genProps(el.props)) + "},";
}
// event handlers
if (el.events) {
data += (genHandlers(el.events, false, state.warn)) + ",";
}
props
跟上面 attrs
的处理一样,看一下 genHandlers
:
function genHandlers (
events,
isNative,
warn
) {
var res = isNative ? 'nativeOn:{' : 'on:{';
for (var name in events) {
res += "\"" + name + "\":" + (genHandler(name, events[name])) + ",";
}
return res.slice(0, -1) + '}'
}
把事件函数挂在 on
字段上,然后将事件逻辑用 genHandler
包起来,这个函数的逻辑有很多事件处理,比如键盘的 key
,事件修饰符等,因为 🍪 中不涉及,直接贴生成后的代码 :
"on:{"input":function($event){if($event.target.composing)return;inputValue=$event.target.value}}"
最终 input
节点生成的代码:
"_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(inputValue),expression:\"inputValue\"}],attrs:{\"type\":\"text\"},domProps:{\"value\":(inputValue)},on:{\"input\":function($event){if($event.target.composing)return;inputValue=$event.target.value}}})"
最后两个 AST
都比较简单,这里就不展开讲了,有兴趣的童鞋冲一杯 ☕️ 单步调试一下吧。至此,整个 generate
过程就结束了,生成的完整 render
如下:
"with(this){return _c('div',{attrs:{\"id\":\"app\"}},[(isShow)?_c('child',{attrs:{\"name\":\"yjc\",\"age\":12}}):_e(),_v(\" \"),_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(inputValue),expression:\"inputValue\"}],attrs:{\"type\":\"text\"},domProps:{\"value\":(inputValue)},on:{\"input\":function($event){if($event.target.composing)return;inputValue=$event.target.value}}}),_v(\" \"),_c('div',{staticClass:\"abc\"})],1)}"
generate
通过字段匹配、处理,将 optimize
之后的 AST
转换成 render code
。整个过程有太多的叉枝,没办法一次性全部讲到位。通过 🍪 分析了 v-if
、v-model
的生成过程,render
的过程肯定都能够有个大概印象。其他的细节在遇到具体问题时,在恰当的位置进行单步调试,相信很快就能解决问题咯。
整个模板编译过程能够分成 4 卷:
web
、weex
)有不一样的编译处理,所以将这种差异在入口处抹平;parse
阶段,通过正则匹配将 template
字符串转成 AST
,期间用到的 regex101 工具,结尾再次推荐一波,嘎嘎香;😆😆😆optimize
阶段,标记静态节点、静态根节点,在 AST
上加上 static
和 staticRoot
信息;generate
阶段,通过节点上的属性符号,将 AST
生成 render
代码。