本文的 ? (表示 例子,☕️ 和 ? 更配哦!全文都会围绕这个 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
属性、自闭合标签。
拿起 :coffee: ,让我们看看是从哪里开始执行模板编译的。回忆一下 [咖聊]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
, 需要执行以下5步:
-
src\platforms\web\compiler\index.js
的createCompiler(baseOptions)
; -
src\compiler\create-compiler.js
的createCompilerCreator
; -
src\compiler\to-function.js
的createCompileToFunctionFn (*compile*: Function)
; -
const compiled = baseCompile(template, finalOptions)
-
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
上的很多配置。同时在开始编译时,就决定了当前的编译环境,后面再更新用的还是这套编译环境,所以也做了编译器的缓存。
整装待发,踏入了解析阶段。
parse
这个阶段用一句话概括起来就是“用各种正则表达式去匹配字符串中的开始标签、属性、注、闭合标签等,最终产出 AST
的过程”。
首先安利一个正则小工具:regex101 ,页面中每一个板块都极其好用,太香啦 :yum::
- 有详细的正则解释;
- 可以实时输入查看匹配结果;
- 如果忘记正则基础知识,还有快速参考模块;
- 能够输出匹配到的全部分组结果;
- 保留测试结果,通过链接就能同步给其他小伙伴,(⚠️ 后文中看到的正则都可以点击查看详情)。
开始之前,先看一个不管任何匹配都会调用的函数 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 } } }
-
匹配开始标签名,此时会创建一个
match
对象; -
匹配开始标签中的属性,给
match
中的attrs
添加属性match
的结果; -
匹配开始标签的结尾 > 字符,将匹配分组信息和结尾位置分别记录到
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
比较简单,没有其他逻辑分支处理,就直接贴上结果图:
<!-- 这是一个注释节点 --> <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
主流程上啦。:sunglasses: -
接下来是一个注释节点
<!-- 这是一个注释节点 -->
: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 } }
-
匹配注释节点的开头;
-
判断是否需要保留注释节点( ⚠️ 这个配置从配置中读取,你可以按照下面的方式配置),不需要的话接着处理
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
属性;后者通过 dirRE 和 bindRE 去掉:
符号之后添加到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> <input type="text" v-model="inputValue" /> <div class="abc"></div> </div>
-
-
闭合标签
</Child>
的处理过程:-
先用闭合标签正则惰性地匹配,这个正则就是在开始标签正则的基础上加了一个
/
; -
然后用
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
的含义(⚠️ 魔数慎用,降低理解成本):
-
1 表示的是普通元素;
-
2 表示表达式;
-
3 表示纯文本。
optimize
本小节目标:
- 优化的目的是什么?
- 怎样的节点才算是静态节点?
- 满足什么条件的节点才能是静态根节点?
带着以上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
看到第一个主流程 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 个:
markStaticRoots
第二个主流程是标记静态根节点,什么是静态根节点呢?先看下函数逻辑:
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
。
generate
万事俱备,只欠东风。参谋了很多网上编译的文章,到这一步时可能写累了,都草草地把生成的 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
都比较简单,这里就不展开讲了,有兴趣的童鞋冲一杯 :coffee: 单步调试一下吧。至此,整个 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
代码。
能读到这里,相信你一定对模板编译的过程有比较清晰地了解。 有问题及时指出哈! ? 红着脸及时纠错。动动小手点个赞吧 ??
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!