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

我心飞翔 分类:vue

demo

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

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

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

  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)处理的逻辑
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-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的值

3. `v-once`,
```js
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 &amp;&amp; element !== root) {
    // allow root elements with v-if, v-else-if and v-else
    if (root.if &amp;&amp; (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>

如有此时有父亲则

当前元素有elseelse-if:则找到上一个标签节点(非文本,非注释),如果有这样的节点(即pre.if存在),在preElement.ifConditions添加当前el的信息。(因为if-else-else-if是一组信息,将这些信息全部保存到第一个节点上,当解析到第一个节点的时候去除所有的条件信息进行判断决定渲染哪一个。看起来是这样)

function processIfConditions (el, parent) {
  const prev = findPrevElement(parent.children) // 找到上一个标签节点(非文本,非注释)
  if (prev &amp;&amp; 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:指令等相关信息的收集

export function processElement (element: ASTElement, options: CompilerOptions) {
  processKey(element)

  // determine whether this is a plain element after
  // removing structural attributes
  element.plain = (
    !element.key &amp;&amp;
    !element.scopedSlots &amp;&amp;

    // 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 出现在一个子组件上时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容。这使得模板的撰写工作更加灵活。

<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之后提供的新用法

在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slot 和 slot-scope 这两个目前已被废弃但未被移除且仍在文档中的 attribute。新语法的由来可查阅这份 RFC

这里有两个方法,一个是处理调用方传递的插槽内容的信息的,一个是定义插槽处的信息处理

processSlotContent(element);
processSlotOutlet(element);

demo为例,

/* 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'
})
<!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"> 解析

// 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
      //... 独占插槽用法,暂忽略 ❎      
    }
  }
}

独占插槽用法,暂忽略,独占插槽

以我们上面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)) {
      // 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 &amp;&amp; !isDynamic) {
            name = camelize(name)
            if (name === 'innerHtml') name = 'innerHTML'
          }
          if (modifiers.camel &amp;&amp; !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 &amp;&amp; modifiers.prop) || (!el.component &amp;&amp; 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 &amp;&amp; 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

  1. 动态属性:v-xxx、@xxx、:xxx、#xxx

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

  2. 静态属性

总结

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

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

回复

我来回复
  • 暂无回复内容