「.vue文件的编译」3. 模板编译之AST生成
<div id="app" class="container">
<slot-test>
<template v-slot:header="slotProps">
<div>name: {{ slotProps.user.name }}</div>
<div>sex: {{ slotProps.user.sex }}</div>
</template>
</slot-test>
<div>static node</div>
<span v-if="showSpan" @click="clickHandler" :class="{ active: showSpan }"> show Span</span>
<span v-else @click="clickHandler">hide Span</span>
<div v-for="(item, index) in items">
<span> {{ item }}</span>
</div>
<input v-model="searchText" />
<!--
<input
:value="searchText"
@input="searchText = $event.target.value"
/>
-->
</div>
/* global Vue */
Vue.component('slot-test', {
template: `
<div style="border: 1px solid red">
<slot name="header" v-bind:user="user"></slot>
</div>
`,
data() {
return {
user: {
name: 'songyu',
sex: "box"
}
}
}
})
new Vue({
el: '#app',
data: {
showSpan: true,
searchText: 'init search value',
items: ['a', 'b', 'c']
},
watch: {
searchText() {
console.log('searchText', this.searchText)
}
},
methods: {
clickHandler() {
this.showSpan = !this.showSpan
}
}
})
下面的分析可以参考这个案例的用法
下面parseHTML
方法是用来遍历html
字符串的并解析出标签(当然包含标签中的属性)、文本等信息,详细分析参考这里。
下面看vue
是如何基于parseHTML
暴露的几个钩子来定制化自己的能力(主要是指令v-for
,v-if
等)的
整体的结构如下
// src/compiler/parser/index.js
import { parseHTML } from './html-parser' // 就是上一小节分析的simple-html-parser.js
/**
* Convert HTML string to AST.
*/
export function parse(template: string, options: CompilerOptions): ASTElement | void {
let root
//...
parseHTML(template, { // ...省略部分options
start(tag, attrs, unary, start, end) {
//...
},
end(tag, start, end) {
//...
},
chars(text: string, start: number, end: number) {
// 这里的逻辑是将文本节点作为存储到currentParent.children中,后面不再展开
if (!currentParent) {
return
}
const children = currentParent.children
// ... child = { type, text } 构造
children.push(child)
},
comment(text: string, start, end) {
// 注释相关,暂忽略
}
})
}
- start:开始标签解析完成后,会调用,如
<div id='app' v-if='showFlag' >
- end:遇到一个结束标签是会调用
</div>
- chars:解析到文本时会调用
start
为了保证整体逻辑的清晰性,删掉了以下部分特性
<pre>
标签以及v-pre
中的相关逻辑- v-pre :Skip compilation for this element and all its children.
<pre>
元素可定义预格式化的文本。被包围在 pre 元素中的文本通常会保留空格和换行符。而文本也会呈现为等宽字体。<pre>
标签的一个常见应用就是用来表示计算机的源代码。
- 忽略
forbiddenTag
(style
、script#type=text/javascript
)处理的逻辑
let element: ASTElement = createASTElement(tag, attrs, currentParent)
// apply pre-transforms
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
// structural directives
processFor(element)
processIf(element)
processOnce(element)
if (!root) {
root = element
}
if (!unary) {
currentParent = element
stack.push(element)
} else {
closeElement(element)
}
流程如下
createASTElement
:创建一个AST节点,就是个js对象,存了些属性而已,最为关键的是:tagName、attrs、父子关系export function createASTElement ( tag: string, attrs: Array<ASTAttr>, parent: ASTElement | void): ASTElement { return { type: 1, tag, attrsList: attrs, attrsMap: makeAttrsMap(attrs), rawAttrsMap: {}, parent, children: [] } }
preTransforms
钩子的调用- 处理部分指令:
v-for
、v-if
、v-once
,将相应的指令的信息解析并存储到AST节点上- 尝试获取
v-for
的值,并存储到AST节点上
{ alias: "item" for: "items" iterator1: "index" }
- 尝试获取
v-if
、v-else
、v-else-if
的值
// 有 v-if 时 el.if = exp, el.ifConditions.push({ exp: exp, block: el }) // 有 v-else 时 el.else = true // 值就应该是true啊 // 有 v-else-if 时 el.elseif = elseif // elseif的值
v-once
,
el.once = true
- 尝试获取
- 将第一个元素设置AST根节点
- 是否是一元标签
- 如果不是(如
<div></div>
),则设置为父元素,显然目的是为了建立父子关系啊;并push到stack中 - 如果是(如
<img />
),则调用closeElement
,稍后单独说一下这个方法(同样是涉及一些指令的处理、postTransforms
的执行)
- 如果不是(如
end
const element = stack[stack.length - 1]
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
closeElement(element)
当前元素可以正确关闭了,然后将栈中的上一个元素设置为currentParent
,比如此时要关闭的元素是id='2'(此时这个元素当然是栈顶元素),然后将上一个元素id='1'设置为currentParent
,显然是合理的。注意,在start中的一元标签和这里的情况有些区别,一元标签压根不会入栈,因此直接closeElement
,没有这里重新设置currentParent
的过程。
<div id='1'>
<span id='2'>second</span>
<span id='3'>second</span>
</div>
下面重点看看closeElement
方法的逻辑,当一个元素关闭时需要做哪些事情。
closeElement
function closeElement(element) {
element = processElement(element, options)
// tree management
if (!stack.length && element !== root) {
// allow root elements with v-if, v-else-if and v-else
if (root.if && (element.elseif || element.else)) {
addIfCondition(root, {
exp: element.elseif,
block: element
})
}
}
if (currentParent) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else {
if (element.slotScope) {
//... 特殊场景,暂忽略 ❎
}
// 建立父子关系,一对多啊
currentParent.children.push(element)
element.parent = currentParent
}
}
// final children cleanup
// filter out scoped slots
element.children = element.children.filter(c => !(c: any).slotScope)
// apply post-transforms
for (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options)
}
}
processElement
:处理部分指令如:key
、:ref
、:is
、<template slot="xxx">, <div slot-scope="xxx">
、<slot></slot>
等场景,详见processElement
方法的分析- 处理下面场景,允许根节点使用
v-if/else/else-if
来变更,此时rootElement.ifConditions
就会有多个可能得根节点<div v-if='flag_1'>1</div> <div v-else-if='flag_2'>2</div> <div v-else>3</div>
- 如有此时有父亲则
- 当前元素有
else
,else-if
:则找到上一个标签节点(非文本,非注释),如果有这样的节点(即pre.if存在),在preElement.ifConditions
添加当前el的信息。(因为if-else-else-if是一组信息,将这些信息全部保存到第一个节点上,当解析到第一个节点的时候去除所有的条件信息进行判断决定渲染哪一个。看起来是这样)function processIfConditions (el, parent) { const prev = findPrevElement(parent.children) // 找到上一个标签节点(非文本,非注释) if (prev && prev.if) { // 如果有if,在preElement.ifConditions添加这个信息 addIfCondition(prev, { exp: el.elseif, block: el }) } } function findPrevElement (children: Array<any>): ASTElement | void { let i = children.length while (i--) { if (children[i].type === 1) { // 非文本,非注释,即常规DOM标签 return children[i] } else { children.pop() } } }
- 否则:建立父子关系
- 当前元素有
- 过滤掉
scoped slot
(占位的<template v-slot>
标签不需要执行postTransforms
),触发postTransforms
执行。
processElement:指令等相关信息的收集
export function processElement (element: ASTElement, options: CompilerOptions) {
processKey(element)
// determine whether this is a plain element after
// removing structural attributes
element.plain = (
!element.key &&
!element.scopedSlots &&
// attrsList 在处理v-for/v-if/v-once等时会从attrsList将相应属性删除。
!element.attrsList.length
)
processRef(element)
processSlotContent(element)
processSlotOutlet(element)
processComponent(element)
for (let i = 0; i < transforms.length; i++) {
element = transforms[i](element, options) || element
}
processAttrs(element)
return element
}
- transforms 的触发
动态绑定之 :key
function processKey (el) {
// 获取:key的值,你看哈,下面的变量是exp,是expressin的缩写,
// 也就说这里会返回一个表达式(什么是表达式呢,读者)。
const exp = getBindingAttr(el, 'key')
if (exp) {
el.key = exp // 保存到节点上
}
}
getBindingAttr:
- 尝试获取动态绑定(
:
、v-bind
)的信息, - 如果没有动态绑定,则默认(
getStatic
默认值是undefined
,显然undefined !== false
是真值)会去获取静态值并返回;部分场景下如class/style
的获取会显示传递false
,即不进行静态值获取(待探索为啥,暂不影响主流程)❎- vue/src/platforms/web/compiler/modules/class.js -> transformNode
- vue/src/platforms/web/compiler/modules/style.js -> transformNode
export function getBindingAttr (el: ASTElement, name: string, getStatic?: boolean): ?string {
const dynamicValue = getAndRemoveAttr(el, ':' + name) || getAndRemoveAttr(el, 'v-bind:' + name)
if (dynamicValue != null) {
return parseFilters(dynamicValue)
} else if (getStatic !== false) {
const staticValue = getAndRemoveAttr(el, name)
if (staticValue != null) {
return JSON.stringify(staticValue)
}
}
}
动态绑定之 :ref
function processRef (el) {
const ref = getBindingAttr(el, 'ref')
if (ref) {
el.ref = ref
el.refInFor = checkInFor(el)
}
}}
还记得parseFor
方法吗,如果该元素设置了v-for
则会添加for
属性。注意 refInFor,看起来是针对父元素有v-for
的场景。
- checkInFor:判断祖先元素中是否有
v-for
function checkInFor (el: ASTElement): boolean {
let parent = el
while (parent) {
if (parent.for !== undefined) {
return true
}
parent = parent.parent
}
return false
}
动态组件 :is
function processComponent (el) {
let binding
if ((binding = getBindingAttr(el, 'is'))) {
el.component = binding
}
if (getAndRemoveAttr(el, 'inline-template') != null) {
el.inlineTemplate = true
}
}
- :is、动态组件
- 内联模板
当
inline-template
这个特殊的 attribute 出现在一个子组件上时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容。这使得模板的撰写工作更加灵活。
内联模板需要定义在 Vue 所属的 DOM 元素内。 不过,<my-component inline-template> <div> <p>These are compiled as the component's own template.</p> <p>Not parent's transclusion content.</p> </div> </my-component>
inline-template
会让模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择template
选项或.vue
文件里的一个<template>
元素来定义模板。
插槽相关
下面只关注2.6之后提供的新用法
在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即
v-slot
指令)。它取代了slot
和slot-scope
这两个目前已被废弃但未被移除且仍在文档中的 attribute。新语法的由来可查阅这份 RFC。
processSlotContent(element);
processSlotOutlet(element);
这里有两个方法,一个是处理调用方传递的插槽内容的信息的,一个是定义插槽处的信息处理
下面<slot name="header" v-bind:user="user"></slot>
逻辑走进入processSlotContent
被处理
Vue.component('slot-test', {
template: `
<div style="border: 1px solid red">
<slot name="header" v-bind:user="user"></slot>
</div>
`,
data() {
return {
user: {
name: 'songyu',
sex: "boy"
}
}
}
})
下面<template v-slot:header="slotProps">
逻辑走进入processSlotContent
被处理
<slot-test>
<template v-slot:header="slotProps">
<div>name: {{ slotProps.user.name }}</div>
<div>sex: {{ slotProps.user.sex }}</div>
</template>
</slot-test>
processSlotContent: 如<template v-slot:header="slotProps">
解析
// handle content being passed to a component as slot,
function processSlotContent (el) {
let slotScope
//... 老语法 忽略
// 2.6 v-slot syntax
if (process.env.NEW_SLOT_SYNTAX) {
if (el.tag === 'template') {
// v-slot on <template>
const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
if (slotBinding) {
const { name, dynamic } = getSlotName(slotBinding)
el.slotTarget = name
el.slotTargetDynamic = dynamic
// 所以这里的el.slotScope一定有值
// force it into a scoped slot for perf
el.slotScope = slotBinding.value || emptySlotScopeToken
}
} else {
// v-slot on component, denotes default slot
//... 独占插槽用法,暂忽略 ❎
}
}
}
- 忽略部分特性,如
- 以我们上面demo中的
<template v-slot:header="slotProps">
被解析时为例,从属性中解析出如下信息,并添加到AST节点上{ slotScope: 'slotProps', // 作用域插槽的信息,接受来自内部的数据 slotTargetDynamic: false, // 是否是动态插槽 slotTarget: 'header' // 应用到哪个插槽的名称 }
processSlotOutlet: 如<slot name="header" v-bind:user="user">
解析
// handle <slot/> outlets
function processSlotOutlet (el) {
if (el.tag === 'slot') {
el.slotName = getBindingAttr(el, 'name'); // 保存插槽名称
}
}
processAttrs
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, syncGen, isDynamic
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
//...
if (bindRE.test(name)) { // v-bind
//...
} else if (onRE.test(name)) { // v-on
//...
} else { // normal directives
//...
}
} else {
addAttr(el, name, JSON.stringify(value), list[i])
}
}
}
根据dirRE: /^v-|^@|^:|^\.|^#/
直接将attrList
中的属性划分为两类:动态或者静态属性),并将这些信息保存到el.attrs
或者el.dynamicAttrs
中
- 动态属性:v-xxx、@xxx、:xxx、#xxx,主要分为三个场景进行信息的解析和存储( 涉及修饰符、动态参数 等特性),暂不深入分析 ❎
- v-bind:xxx or :xxx
- v-on:xxx or @xxx
- normal directives
- 静态属性
总结
主要流程是在simple-html-parse提供的几个钩子上来创建AST节点,并建立父子关系构造AST。另外更重要的是从simple-html-parse解析的属性中收集和信息的再次解析,并将信息保存到AST节点上(在运行时显然是需要这些元数据来帮忙的)。
- start中处理了 v-for、v-if、v-once
- end中处理了 :ref、:key、:is、slot,attrs
另外web平台下提供的几个模块(src/platforms/web/compiler/modules/index.js)中通过preTransforms、transforms、postTransforms参与到AST节点的构造过程,并收集自己关心的一些特性的信息(:class
、:style
、v-model
),暂不深入 ❎
转载自:https://juejin.cn/post/7202935318458253367