demo
下面parseHTML
方法是用来遍历html
字符串的并解析出标签(当然包含标签中的属性)、文本等信息,详细分析参考这里。
下面看vue
是如何基于parseHTML
暴露的几个钩子来定制化自己的能力(主要是指令v-for
,v-if
等)的
整体的结构如下
代码语言:javascript复制// 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、父子关系
代码语言:javascript复制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
的值 `js // 有 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的值
代码语言:javascript复制3. `v-once`,
```js
el.once = true
代码语言:txt复制将第一个元素设置AST根节点
是否是一元标签
- 如果不是(如`<div></div>`),则设置为父元素,显然目的是为了建立父子关系啊;并push到stack中
- 如果是(如`<img />`),则调用`closeElement`,稍后单独说一下这个方法(同样是涉及一些指令的处理、`postTransforms`的执行)
# end
```javascript
const element = stackstack.length - 1
// pop stack
stack.length -= 1
currentParent = stackstack.length - 1
closeElement(element)
代码语言:txt复制当前元素可以正确关闭了,然后将栈中的上一个元素设置为`currentParent`,比如此时要关闭的元素是id='2'(此时这个元素当然是栈顶元素),然后将上一个元素id='1'设置为`currentParent`,显然是合理的。注意,在start中的一元标签和这里的情况有些区别,一元标签压根不会入栈,因此直接`closeElement`,没有这里重新设置`currentParent`的过程。
```javascript
<div id='1'>
代码语言:txt复制<span id='2'>second</span>
代码语言:txt复制<span id='3'>second</span>
</div>
代码语言:txt复制下面重点看看`closeElement`方法的逻辑,当一个元素关闭时需要做哪些事情。
## closeElement
```javascript
function closeElement(element) {
element = processElement(element, options)
// tree management
if (!stack.length && element !== root) {
代码语言:txt复制// allow root elements with v-if, v-else-if and v-else
代码语言:txt复制if (root.if && (element.elseif || element.else)) {
代码语言:txt复制 addIfCondition(root, {
代码语言:txt复制 exp: element.elseif,
代码语言:txt复制 block: element
代码语言:txt复制 })
代码语言:txt复制}
}
if (currentParent) {
代码语言:txt复制if (element.elseif || element.else) {
代码语言:txt复制 processIfConditions(element, currentParent)
代码语言:txt复制} else {
代码语言:txt复制 if (element.slotScope) {
代码语言:txt复制 //... 特殊场景,暂忽略 ❎
代码语言:txt复制 }
代码语言:txt复制 // 建立父子关系,一对多啊
代码语言:txt复制 currentParent.children.push(element)
代码语言:txt复制 element.parent = currentParent
代码语言:txt复制}
}
// 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 ) {
代码语言:txt复制postTransforms[i](element, options)
}
}
代码语言:txt复制processElement:处理部分指令如`:key`、`:ref`、`:is`、`<template slot="xxx">, <div slot-scope="xxx">`、`<slot></slot>`等场景,详见`processElement`方法的分析
处理下面场景,允许根节点使用`v-if/else/else-if`来变更,此时`rootElement.ifConditions`就会有多个可能得根节点
```javascript
<div v-if='flag_1'>1</div>
<div v-else-if='flag_2'>2</div>
<div v-else>3</div>
代码语言:txt复制如有此时有父亲则
当前元素有`else`,`else-if`:则找到上一个标签节点(非文本,非注释),如果有这样的节点(即pre.if存在),在`preElement.ifConditions`添加当前el的信息。(因为if-else-else-if是一组信息,将这些信息全部保存到第一个节点上,当解析到第一个节点的时候去除所有的条件信息进行判断决定渲染哪一个。看起来是这样)
```javascript
function processIfConditions (el, parent) {
const prev = findPrevElement(parent.children) // 找到上一个标签节点(非文本,非注释)
if (prev && prev.if) { // 如果有if,在preElement.ifConditions添加这个信息
代码语言:txt复制addIfCondition(prev, {
代码语言:txt复制 exp: el.elseif,
代码语言:txt复制 block: el
代码语言:txt复制})
}
}
function findPrevElement (children: Array<any>): ASTElement | void {
let i = children.length
while (i--) {
代码语言:txt复制if (children[i].type === 1) { // 非文本,非注释,即常规DOM标签
代码语言:txt复制 return children[i]
代码语言:txt复制} else {
代码语言:txt复制 children.pop()
代码语言:txt复制}
}
}
代码语言:txt复制否则:**建立父子关系**
过滤掉scoped slot,触发postTransforms执行。
## processElement:指令等相关信息的收集
```javascript
export function processElement (element: ASTElement, options: CompilerOptions) {
processKey(element)
// determine whether this is a plain element after
// removing structural attributes
element.plain = (
代码语言:txt复制!element.key &&
代码语言:txt复制!element.scopedSlots &&
代码语言:txt复制// attrsList 在处理v-for/v-if/v-once等时会从attrsList将相应属性删除。
代码语言:txt复制!element.attrsList.length
)
processRef(element)
processSlotContent(element)
processSlotOutlet(element)
processComponent(element)
for (let i = 0; i < transforms.length; i ) {
代码语言:txt复制element = transforms[i](element, options) || element
}
processAttrs(element)
return element
}
代码语言:txt复制transforms 的触发
### 动态绑定之 :key
```javascript
function processKey (el) {
// 获取:key的值,你看哈,下面的变量是exp,是expressin的缩写,
// 也就说这里会返回一个表达式(什么是表达式呢,读者)。
const exp = getBindingAttr(el, 'key')
if (exp) {
el.key = exp // 保存到节点上
}
}
代码语言:txt复制----
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
```javascript
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)
}
}
}
代码语言:txt复制### 动态绑定之 :ref
```javascript
function processRef (el) {
const ref = getBindingAttr(el, 'ref')
if (ref) {
el.ref = ref
el.refInFor = checkInFor(el)
}
}}
代码语言:txt复制 还记得`parseFor`方法吗,如果该元素设置了`v-for`则会添加`for`属性。注意 refInFor,看起来是针对父元素有`v-for`的场景。
checkInFor:判断父元素是否有`v-for`
```javascript
function checkInFor (el: ASTElement): boolean {
let parent = el
while (parent) {
if (parent.for !== undefined) {
代码语言:txt复制return true
}
parent = parent.parent
}
return false
}
代码语言:txt复制### 动态组件 :is
```javascript
function processComponent (el) {
let binding
if ((binding = getBindingAttr(el, 'is'))) {
代码语言:txt复制el.component = binding
}
if (getAndRemoveAttr(el, 'inline-template') != null) {
代码语言:txt复制el.inlineTemplate = true
}
}
代码语言:txt复制[:is](https://v2.cn.vuejs.org/v2/api/#is)、[动态组件](https://v2.cn.vuejs.org/v2/guide/components-dynamic-async.html)
[内联模板](https://v2.cn.vuejs.org/v2/guide/components-edge-cases.html#内联模板) 当 `inline-template` 这个特殊的 attribute 出现在一个子组件上时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容。这使得模板的撰写工作更加灵活。
```javascript
<my-component inline-template>
<div>
代码语言:txt复制<p>These are compiled as the component's own template.</p>
代码语言:txt复制<p>Not parent's transclusion content.</p>
</div>
</my-component>
代码语言:txt复制 内联模板需要定义在 Vue 所属的 DOM 元素内。
不过,`inline-template` 会让模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择 `template` 选项或 `.vue` 文件里的一个 `<template>` 元素来定义模板。
### 插槽相关
下面只关注2.6之后提供的[新用法](https://v2.cn.vuejs.org/v2/guide/components-slots.html)
> 在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 `v-slot` 指令)。它取代了 `slot` 和 `slot-scope` 这两个目前已被废弃但未被移除且仍在[文档中](https://v2.cn.vuejs.org/v2/guide/components-slots.html#废弃了的语法)的 attribute。新语法的由来可查阅这份 [RFC](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0001-new-slot-syntax.md)。
>
这里有两个方法,一个是处理调用方传递的插槽内容的信息的,一个是定义插槽处的信息处理
```javascript
processSlotContent(element);
processSlotOutlet(element);
代码语言:txt复制以[demo](https://github.com/yusongjohn/vue-relevant-tech/tree/main/analyze-vue-2.6.11/slot-test)为例,
```javascript
/ global Vue /
Vue.component('slot-test', {
template: '<div id="a"><div style="background:red">header:</div><slot name="header" v-bind:user="user"></slot><div style="background:red">default:</div><slot></slot><div style="background:red">footer:</div><slot name="footer"></slot></div>',
data() {
代码语言:txt复制return {
代码语言:txt复制 user: {
代码语言:txt复制 name: 'songyu',
代码语言:txt复制 sex: "box"
代码语言:txt复制 }
代码语言:txt复制}
}
})
new Vue({
el: '#app'
})
代码语言:txt复制```javascript
<!DOCTYPE html>
<html>
<head>
<script src="/node_modules/vue/dist/vue.js"></script>
</head>
<body>
<div id="app" class="container">
代码语言:txt复制<div>-------------------------slot begin--------------</div>
代码语言:txt复制<slot-test>
代码语言:txt复制 <template v-slot:header="slotProps">
代码语言:txt复制 <div>name: {{ slotProps.user.name }}</div>
代码语言:txt复制 <div>sex: {{ slotProps.user.sex }}</div>
代码语言:txt复制 <h1>Here might be a page title</h1>
代码语言:txt复制 </template>
代码语言:txt复制 <p>A paragraph for the main content.</p>
代码语言:txt复制 <p>And another one.</p>
代码语言:txt复制 <template v-slot:footer>
代码语言:txt复制 <p>Here's some contact info</p>
代码语言:txt复制 </template>
代码语言:txt复制</slot-test>
代码语言:txt复制<div>-------------------------slot end--------------</div>
</div>
<script src="app.js"></script>
</body>
</html>
代码语言:txt复制----
#### processSlotContent: 如`<template v-slot:header="slotProps">` 解析
```javascript
// 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) {
代码语言:txt复制if (el.tag === 'template') {
代码语言:txt复制 // v-slot on <template>
代码语言:txt复制 const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
代码语言:txt复制 if (slotBinding) {
代码语言:txt复制 const { name, dynamic } = getSlotName(slotBinding)
代码语言:txt复制 el.slotTarget = name
代码语言:txt复制 el.slotTargetDynamic = dynamic
代码语言:txt复制 el.slotScope = slotBinding.value || emptySlotScopeToken // force it into a scoped slot for perf
代码语言:txt复制 }
代码语言:txt复制} else {
代码语言:txt复制 // v-slot on component, denotes default slot
代码语言:txt复制 //... 独占插槽用法,暂忽略 ❎
代码语言:txt复制}
}
}
代码语言:txt复制独占插槽用法,暂忽略,[独占插槽](https://v2.cn.vuejs.org/v2/guide/components-slots.html#独占默认插槽的缩写语法)
以我们上面demo中的`<template v-slot:header="slotProps">`被解析时为例,从属性中解析出如下信息,并添加到AST节点上
```javascript
{
代码语言:txt复制slotScope: 'slotProps', // 作用域插槽的信息,接受来自内部的数据
代码语言:txt复制slotTargetDynamic: false, // 是否是动态插槽
代码语言:txt复制slotTarget: 'header' // 应用到哪个插槽的名称
}
代码语言:txt复制- [动态插槽参考](https://v2.cn.vuejs.org/v2/guide/components-slots.html#动态插槽名)
#### processSlotOutlet: 如`<slot name="header" v-bind:user="user">`解析
```javascript
// handle <slot/> outlets
function processSlotOutlet (el) {
代码语言:txt复制if (el.tag === 'slot') {
代码语言:txt复制 el.slotName = getBindingAttr(el, 'name');
代码语言:txt复制}
}
代码语言:txt复制保存插槽名称
后面如果时间允许的话,看下运行时是怎么处理这部分的。
### processAttrs
```javascript
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 ) {
代码语言:txt复制name = rawName = list[i].name
代码语言:txt复制value = list[i].value
代码语言:txt复制if (dirRE.test(name)) {
代码语言:txt复制 // mark element as dynamic
代码语言:txt复制 el.hasBindings = true
代码语言:txt复制 // modifiers
代码语言:txt复制 modifiers = parseModifiers(name.replace(dirRE, ''))
代码语言:txt复制 // support .foo shorthand syntax for the .prop modifier
代码语言:txt复制 if (modifiers) {
代码语言:txt复制 name = name.replace(modifierRE, '')
代码语言:txt复制 }
代码语言:txt复制 if (bindRE.test(name)) { // v-bind
代码语言:txt复制 name = name.replace(bindRE, '')
代码语言:txt复制 value = parseFilters(value)
代码语言:txt复制 isDynamic = dynamicArgRE.test(name)
代码语言:txt复制 if (isDynamic) {
代码语言:txt复制 name = name.slice(1, -1)
代码语言:txt复制 }
代码语言:txt复制 if (modifiers) {
代码语言:txt复制 if (modifiers.prop && !isDynamic) {
代码语言:txt复制 name = camelize(name)
代码语言:txt复制 if (name === 'innerHtml') name = 'innerHTML'
代码语言:txt复制 }
代码语言:txt复制 if (modifiers.camel && !isDynamic) {
代码语言:txt复制 name = camelize(name)
代码语言:txt复制 }
代码语言:txt复制 if (modifiers.sync) {
代码语言:txt复制 syncGen = genAssignmentCode(value, `$event`)
代码语言:txt复制 if (!isDynamic) {
代码语言:txt复制 addHandler(el, `update:${camelize(name)}`, syncGen, null, false, warn, list[i])
代码语言:txt复制 if (hyphenate(name) !== camelize(name)) {
代码语言:txt复制 addHandler(el, `update:${hyphenate(name)}`, syncGen, null, false, warn, list[i])
代码语言:txt复制 }
代码语言:txt复制 } else {
代码语言:txt复制 // handler w/ dynamic event name
代码语言:txt复制 addHandler(el, `"update:" (${name})`, syncGen, null, false, warn, list[i], true // dynamic )
代码语言:txt复制 }
代码语言:txt复制 }
代码语言:txt复制 }
代码语言:txt复制 if ((modifiers && modifiers.prop) || (!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name) )) {
代码语言:txt复制 addProp(el, name, value, list[i], isDynamic)
代码语言:txt复制 } else {
代码语言:txt复制 addAttr(el, name, value, list[i], isDynamic)
代码语言:txt复制 }
代码语言:txt复制 } else if (onRE.test(name)) { // v-on
代码语言:txt复制 name = name.replace(onRE, '')
代码语言:txt复制 isDynamic = dynamicArgRE.test(name)
代码语言:txt复制 if (isDynamic) {
代码语言:txt复制 name = name.slice(1, -1)
代码语言:txt复制 }
代码语言:txt复制 addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
代码语言:txt复制 } else { // normal directives
代码语言:txt复制 name = name.replace(dirRE, '')
代码语言:txt复制 // parse arg
代码语言:txt复制 const argMatch = name.match(argRE)
代码语言:txt复制 let arg = argMatch && argMatch[1]
代码语言:txt复制 isDynamic = false
代码语言:txt复制 if (arg) {
代码语言:txt复制 name = name.slice(0, -(arg.length 1))
代码语言:txt复制 if (dynamicArgRE.test(arg)) {
代码语言:txt复制 arg = arg.slice(1, -1)
代码语言:txt复制 isDynamic = true
代码语言:txt复制 }
代码语言:txt复制 }
代码语言:txt复制 addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
代码语言:txt复制 }
代码语言:txt复制} else {
代码语言:txt复制 addAttr(el, name, JSON.stringify(value), list[i])
代码语言:txt复制}
}
}
代码语言:txt复制
根据dirRE: /^v-|^@|^:|^.|^#/
直接将attrList
中的属性划分为两类:动态或者静态属性),并将这些信息保存到el.attrs
或者el.dynamicAttrs
中
- 动态属性:v-xxx、@xxx、:xxx、#xxx
- [修饰符](https://v2.cn.vuejs.org/v2/guide/syntax.html#修饰符)处理,[动态参数](https://v2.cn.vuejs.org/v2/guide/syntax.html#动态参数)等信息的收集,暂不深入❎ ```
<a :key="url"> ... `
- 静态属性
总结
主要流程是在simple-html-parse提供的几个钩子上来创建AST节点,并建立父子关系构造AST。另外更重要的是从simple-html-parse解析的属性中收集和信息的再次解析,并将信息保存到AST节点上(在运行时显然是需要这些元数据来帮忙的)。
另外web平台下提供的几个模块(src/platforms/web/compiler/modules/index.js)中通过preTransforms、transforms、postTransforms参与到AST节点的构造过程,并收集自己关心的一些特性的信息(:class
、:style
、v-model
),暂不深入 ❎