「.vue文件的编译」3. 模板编译之AST生成

2023-02-24 10:30:51 浏览数 (1)

demo

下面parseHTML方法是用来遍历html字符串的并解析出标签(当然包含标签中的属性)、文本等信息,详细分析参考这里。

下面看vue是如何基于parseHTML暴露的几个钩子来定制化自己的能力(主要是指令v-forv-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

为了保证整体逻辑的清晰性,删掉了以下部分特性

  1. <pre>标签以及v-pre中的相关逻辑
    • v-pre :Skip compilation for this element and all its children.
    • <pre> 元素可定义预格式化的文本。被包围在 pre 元素中的文本通常会保留空格和换行符。而文本也会呈现为等宽字体。<pre> 标签的一个常见应用就是用来表示计算机的源代码。
  2. 忽略forbiddenTag(style、script#type=text/javascript)处理的逻辑
代码语言: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节点上

代码语言:javascript复制
{
alias: "item"
for: "items"
iterator1: "index"
}

尝试获取v-ifv-elsev-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 &amp;&amp; (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 &amp;&amp;
代码语言:txt复制
!element.scopedSlots &amp;&amp;
代码语言: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 &amp;&amp; !isDynamic) {
代码语言:txt复制
        name = camelize(name)
代码语言:txt复制
        if (name === 'innerHtml') name = 'innerHTML'
代码语言:txt复制
      }
代码语言:txt复制
      if (modifiers.camel &amp;&amp; !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 &amp;&amp; modifiers.prop) || (!el.component &amp;&amp; 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 &amp;&amp; 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

  1. 动态属性:v-xxx、@xxx、:xxx、#xxx
代码语言:txt复制
- [修饰符](https://v2.cn.vuejs.org/v2/guide/syntax.html#修饰符)处理,[动态参数](https://v2.cn.vuejs.org/v2/guide/syntax.html#动态参数)等信息的收集,暂不深入❎  ```  

<a :key="url"> ... `

  1. 静态属性

总结

主要流程是在simple-html-parse提供的几个钩子上来创建AST节点,并建立父子关系构造AST。另外更重要的是从simple-html-parse解析的属性中收集和信息的再次解析,并将信息保存到AST节点上(在运行时显然是需要这些元数据来帮忙的)。

另外web平台下提供的几个模块(src/platforms/web/compiler/modules/index.js)中通过preTransforms、transforms、postTransforms参与到AST节点的构造过程,并收集自己关心的一些特性的信息(:class:stylev-model),暂不深入 ❎

0 人点赞