下面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) {
// 注释相关,暂忽略
}
})
}
<div id='app' v-if='showFlag' >
</div>
为了保证整体逻辑的清晰性,删掉了以下部分特性
<pre>
标签以及v-pre
中的相关逻辑<pre>
元素可定义预格式化的文本。被包围在 pre 元素中的文本通常会保留空格和换行符。而文本也会呈现为等宽字体。<pre>
标签的一个常见应用就是用来表示计算机的源代码。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
的值 `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的值
3. `v-once`,
```js
el.once = true
将第一个元素设置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)
当前元素可以正确关闭了,然后将栈中的上一个元素设置为`currentParent`,比如此时要关闭的元素是id='2'(此时这个元素当然是栈顶元素),然后将上一个元素id='1'设置为`currentParent`,显然是合理的。注意,在start中的一元标签和这里的情况有些区别,一元标签压根不会入栈,因此直接`closeElement`,没有这里重新设置`currentParent`的过程。
```javascript
<div id='1'>
<span id='2'>second</span>
<span id='3'>second</span>
</div>
下面重点看看`closeElement`方法的逻辑,当一个元素关闭时需要做哪些事情。
## closeElement
```javascript
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`就会有多个可能得根节点
```javascript
<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是一组信息,将这些信息全部保存到第一个节点上,当解析到第一个节点的时候去除所有的条件信息进行判断决定渲染哪一个。看起来是这样)
```javascript
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,触发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 = (
!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
```javascript
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
```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)
}
}
}
### 动态绑定之 :ref
```javascript
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`
```javascript
function checkInFor (el: ASTElement): boolean {
let parent = el
while (parent) {
if (parent.for !== undefined) {
return true
}
parent = parent.parent
}
return false
}
### 动态组件 :is
```javascript
function processComponent (el) {
let binding
if ((binding = getBindingAttr(el, 'is'))) {
el.component = binding
}
if (getAndRemoveAttr(el, 'inline-template') != null) {
el.inlineTemplate = true
}
}
[: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#%E5%86%85%E8%81%94%E6%A8%A1%E6%9D%BF) 当 `inline-template` 这个特殊的 attribute 出现在一个子组件上时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容。这使得模板的撰写工作更加灵活。
```javascript
<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>
内联模板需要定义在 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#%E5%BA%9F%E5%BC%83%E4%BA%86%E7%9A%84%E8%AF%AD%E6%B3%95)的 attribute。新语法的由来可查阅这份 [RFC](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0001-new-slot-syntax.md)。
>
这里有两个方法,一个是处理调用方传递的插槽内容的信息的,一个是定义插槽处的信息处理
```javascript
processSlotContent(element);
processSlotOutlet(element);
以[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() {
return {
user: {
name: 'songyu',
sex: "box"
}
}
}
})
new Vue({
el: '#app'
})
```javascript
<!DOCTYPE html>
<html>
<head>
<script src="/node_modules/vue/dist/vue.js"></script>
</head>
<body>
<div id="app" class="container">
<div>-------------------------slot begin--------------</div>
<slot-test>
<template v-slot:header="slotProps">
<div>name: {{ slotProps.user.name }}</div>
<div>sex: {{ slotProps.user.sex }}</div>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</slot-test>
<div>-------------------------slot end--------------</div>
</div>
<script src="app.js"></script>
</body>
</html>
----
#### 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) {
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 = slotBinding.value || emptySlotScopeToken // force it into a scoped slot for perf
}
} else {
// v-slot on component, denotes default slot
//... 独占插槽用法,暂忽略 ❎
}
}
}
独占插槽用法,暂忽略,[独占插槽](https://v2.cn.vuejs.org/v2/guide/components-slots.html#%E7%8B%AC%E5%8D%A0%E9%BB%98%E8%AE%A4%E6%8F%92%E6%A7%BD%E7%9A%84%E7%BC%A9%E5%86%99%E8%AF%AD%E6%B3%95)
以我们上面demo中的`<template v-slot:header="slotProps">`被解析时为例,从属性中解析出如下信息,并添加到AST节点上
```javascript
{
slotScope: 'slotProps', // 作用域插槽的信息,接受来自内部的数据
slotTargetDynamic: false, // 是否是动态插槽
slotTarget: 'header' // 应用到哪个插槽的名称
}
- [动态插槽参考](https://v2.cn.vuejs.org/v2/guide/components-slots.html#%E5%8A%A8%E6%80%81%E6%8F%92%E6%A7%BD%E5%90%8D)
#### processSlotOutlet: 如`<slot name="header" v-bind:user="user">`解析
```javascript
// handle <slot/> outlets
function processSlotOutlet (el) {
if (el.tag === 'slot') {
el.slotName = getBindingAttr(el, 'name');
}
}
保存插槽名称
后面如果时间允许的话,看下运行时是怎么处理这部分的。
### 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++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
// mark element as dynamic
el.hasBindings = true
// modifiers
modifiers = parseModifiers(name.replace(dirRE, ''))
// support .foo shorthand syntax for the .prop modifier
if (modifiers) {
name = name.replace(modifierRE, '')
}
if (bindRE.test(name)) { // v-bind
name = name.replace(bindRE, '')
value = parseFilters(value)
isDynamic = dynamicArgRE.test(name)
if (isDynamic) {
name = name.slice(1, -1)
}
if (modifiers) {
if (modifiers.prop && !isDynamic) {
name = camelize(name)
if (name === 'innerHtml') name = 'innerHTML'
}
if (modifiers.camel && !isDynamic) {
name = camelize(name)
}
if (modifiers.sync) {
syncGen = genAssignmentCode(value, `$event`)
if (!isDynamic) {
addHandler(el, `update:${camelize(name)}`, syncGen, null, false, warn, list[i])
if (hyphenate(name) !== camelize(name)) {
addHandler(el, `update:${hyphenate(name)}`, syncGen, null, false, warn, list[i])
}
} else {
// handler w/ dynamic event name
addHandler(el, `"update:"+(${name})`, syncGen, null, false, warn, list[i], true // dynamic )
}
}
}
if ((modifiers && modifiers.prop) || (!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name) )) {
addProp(el, name, value, list[i], isDynamic)
} else {
addAttr(el, name, value, list[i], isDynamic)
}
} else if (onRE.test(name)) { // v-on
name = name.replace(onRE, '')
isDynamic = dynamicArgRE.test(name)
if (isDynamic) {
name = name.slice(1, -1)
}
addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
} else { // normal directives
name = name.replace(dirRE, '')
// parse arg
const argMatch = name.match(argRE)
let arg = argMatch && argMatch[1]
isDynamic = false
if (arg) {
name = name.slice(0, -(arg.length + 1))
if (dynamicArgRE.test(arg)) {
arg = arg.slice(1, -1)
isDynamic = true
}
}
addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
}
} else {
addAttr(el, name, JSON.stringify(value), list[i])
}
}
}
根据dirRE: /^v-|^@|^:|^\.|^#/
直接将attrList
中的属性划分为两类:动态或者静态属性),并将这些信息保存到el.attrs
或者el.dynamicAttrs
中
- [修饰符](https://v2.cn.vuejs.org/v2/guide/syntax.html#%E4%BF%AE%E9%A5%B0%E7%AC%A6)处理,[动态参数](https://v2.cn.vuejs.org/v2/guide/syntax.html#%E5%8A%A8%E6%80%81%E5%8F%82%E6%95%B0)等信息的收集,暂不深入❎ ```
<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
),暂不深入 ❎