vue编译过程分析

3,666 阅读9分钟

前言

先正式讲解之前先看一张来自Vue官网的实例生命周期图。 vue实例生命周期图示

由图中可知,当我们实例化一个Vue对象并完成初始化后,Vue会检查el和template属性,以获取模板字符串。然后将得到的模板编译成render函数。

只有当template未指定时,vue才会以所制定的el元素的outerHTML作为模板。

但如果这时我们还指定了自定义的render函数,vue不会再通过前两者去获取模板了。

本文重点聊聊vue是如何将通过template或者el获得的模板最终编译成render函数的。

将HTML模板解析为AST节点树

如上所述,vue在获取到模板字符串后,通过正则表达式对HTML模板逐字符解析,分别解析出元素节点以及每个元素节点上所设置的指令、attribute、事件绑定等,最终构建成一个完整描述HTML节点信息的AST节点树。

先认识一下几个核心的正则表达式和AST对象。

html字符匹配核心正则表达式

// 1. 匹配开始标签(不包括结尾的>),如匹配<div
const startTagOpen = /^<((?:[a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*\:)?[a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*)/

// 2. 匹配普通html属性,如id="app"
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/

// 3. 匹配动态属性 如v-for="(item, index) in roles",v-bind:src="imageSrc" :[key]="value"
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/

4. 匹配开始标签的尾部,如>或者/>
const startTagClose = /^\s*(\/?)>/

// 5. 匹配闭合标签,如</div>
const endTag = /^<\\/((?:[a-zA-Z_][\\-\\.0-9_a-zA-Z((?:[a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*\:)?[a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*)]*\\:)?[a-zA-Z_][\\-\\.0-9_a-zA-Z((?:[a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*\:)?[a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*)]*)[^>]*>/

// 6. 匹配DOCTYPE
const doctype = /^<!DOCTYPE [^>]+>/i

// 7. 匹配HTML注释
const comment = /^<!\--/

// 8. 匹配HTML条件注释
const conditionalComment = /^<!\[/

AST节点对象

下面一段代码是AST节点的创建函数。通过代码,我们可以对AST节点有个基本了解。简单来说,

AST节点是对HTML节点信息的描述。例如AST将HTML标签属性解析出来的内容保存到attrsList、attrsMap和rawAttrsMap中,在parent中保持对父节点的引用,通过children来指向自己的子节点。

{
    // 节点类型
    type: 1,
    // 标签名,如div
    tag: "div",
    // 节点所包含的属性
    attrsList: [],
    attrsMap: {},
    rawAttrsMap: {},
    // 父节点指针
    parent: undefined,
    // 子节点指针
    children: []
}

简单标签解析

假定有如下模板:

<div id="app">{{msg}}</div>

具体的解析过程是通过几个核心正则表达式分别捕获到标签名以及属性的名称和值,然后使用这些信息创建AST节点对象,主要代码如下:

// 1. 解析标签和属性
function parseStartTag () {
  // 匹配开始标签
  var start = html.match(startTagOpen);
  if (start) {
    var match = {
      tagName: start[1],
      attrs: [],
      start: index
    };
    // 将指针向前移动
    advance(start[0].length);
    var end, attr;
    // 匹配标签中的属性
    while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
      attr.start = index;
      advance(attr[0].length);
      attr.end = index;
      match.attrs.push(attr);
    }
    if (end) {
      match.unarySlash = end[1];
      advance(end[0].length);
      match.end = index;
      return match
    }
  }
}

...省略其他代码

// 2. 创建AST节点对象
var element = createASTElement(tag, attrs, currentParent);

...省略其他代码

经过以上解析,得到如下AST节点对象:

{
    attrsList: [{name: "id", value: "app", start: 5, end: 13}],
    attrsMap: {id: "app"},
    children: [],
    end: 14,
    parent: undefined,
    rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}},
    start: 0,
    tag: "div",
    type: 1
}

接下来需要处理的是div的子节点{{msg}}。由于其子节点是文本节点,这里使用parseText来处理文本节点,然后使用这些信息创建AST节点对象,主要代码如下:

// 1. 解析文本节点中的字符
function parseText (
  text: string,
  delimiters?: [string, string]
): TextParseResult | void {
  // ...省略其他代码
  const tokens = []
  const rawTokens = []
  // tagRE默认值为/\{\{((?:.|\r?\n)+?)\}\}/g,识别出文本中通过{{value}}插入的值
  let lastIndex = tagRE.lastIndex = 0
  let match, index, tokenValue
  while ((match = tagRE.exec(text))) {
    index = match.index
    if (index > lastIndex) {
      rawTokens.push(tokenValue = text.slice(lastIndex, index))
      tokens.push(JSON.stringify(tokenValue))
    }
    
    // 如果模板插值中使用了过滤器,需要先解析过滤器
    const exp = parseFilters(match[1].trim())
    tokens.push(`_s(${exp})`)
    rawTokens.push({ '@binding': exp })
    lastIndex = index + match[0].length
  }
  if (lastIndex < text.length) {
    rawTokens.push(tokenValue = text.slice(lastIndex))
    tokens.push(JSON.stringify(tokenValue))
  }
  // 解析结果为expression和tokens组成的对象
  return {
    expression: tokens.join('+'),
    tokens: rawTokens
  }
}

//...省略其他代码

// 2. 创建文本AST对象

child = {
    type: 2,
    expression: res.expression,
    tokens: res.tokens,
    text: text
}
child.start = start;
child.end = end;

//...省略其他代码

// 添加到div的子节点数组里
children.push(child);

文本节点解析后的结果如下:

{
    type: 2, 
    expression: "_s(message)", 
    tokens: [{@binding: "message"}],
    text: "{{message}}"
}

解析完子节点后的结果根节点div的AST更新为:

{
    attrsList: [{name: "id", value: "app", start: 5, end: 13}]
    attrsMap: {id: "app"}
    children: [{type: 2, expression: "_s(msg)", tokens: [{@binding: "msg"}], text: "{{msg}}"}]
    end: 14
    parent: undefined
    rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}}
    start: 0
    tag: "div"
    type: 1
}

最后是解析闭合标签</div>。 当解析器匹配到闭合标签后,意味着一个标签的匹配结束了。

因为在标签中除了会使用id="app",placeholder="edit me"等HTML attribute外,还有很多是使用了vue的指令属性,如v-for,v-on,v-model。解析器会对前面已经生成的AST节点对象,进一步处理。最终这部分信息会以directives、on、domProps等属性的形式添加到AST对象上。

// 节点的收尾处理
function closeElement (element) {
    // 去除元素的空子节点
    trimEndingWhitespace(element)
    // 处理AST,添加额外属性
    if (!inVPre && !element.processed) {
      element = processElement(element, options)
    }
    // ... 省略其他代码
}

// 给AST添加额外属性
function processElement (
  element: ASTElement,
  options: CompilerOptions
) {
  // 给AST添加key属性
  processKey(element)

  // 给AST添加plain属性
  element.plain = (
    !element.key &&
    !element.scopedSlots &&
    !element.attrsList.length
  )

  // 处理v-ref,给AST对象添加ref属性
  processRef(element)
  // 处理传递给组件的slot,给AST添加slotScope属性
  processSlotContent(element)
  // 处理slot标签,给AST添加slotName属性
  processSlotOutlet(element)

  // 给AST添加component或inlineTemplate属性
  processComponent(element)
  // 处理
  for (let i = 0; i < transforms.length; i++) {
    element = transforms[i](element, options) || element
  }
  // 根据属性的不同,给AST对象添加directives、events、props等属性
  processAttrs(element)
  return element
}

了解了简单标签解析过程,我们再来看下对于v-for、v-if、v-model、v-on几个常见指令的解析。

v-for指令的处理

我们还是通过一个简单的HTML模板来看。给定如下HTML模板:

<div id="app"><p v-for="(item, index) in items">{{item}}</p></div>

在解析</p>标签之前,可以得到大致如下的初始AST对象:

{
    // 重点看这里,初步解析后,属性先暂存在attrsList里
    attrsList: [
        {name: "v-for", value: "(item, index) in items", start: 17, end: 47}, 
        {name: ":key", value: "index", start: 48, end: 60}
    ],
    attrsMap: {v-for: "(item, index) in items", :key: "index"},
    children: [],
    parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
    rawAttrsMap: {},
    tag: "p",
    type: 1
}

接着调用processFor,解析v-for指令,将解析结果添加到AST对象上中:

function processFor (el: ASTElement) {
  // 解析节点上的v-for指令
  let exp
  // 从attrsList获取v-for的表达式,并从attrsList中移除v-for
  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
    // 解析v-for表达式,得到一个对象{alias: "item",for:"items",iterator1: "index"}
    const res = parseFor(exp)
    // 合并到AST对象中
    if (res) {
      extend(el, res)
    } else if (process.env.NODE_ENV !== 'production') {
      warn(
        `Invalid v-for expression: ${exp}`,
        el.rawAttrsMap['v-for']
      )
    }
  }
}

AST对象变成如下形式:

{
    // v-for以属性的方式混入了AST对象
    alias: "item",
    for: "items",
    iterator1: "index",
    // 此时,v-for已经从attrsList中移除
    attrsList: [
        {name: ":key", value: "index", start: 48, end: 60}
    ],
    attrsMap: {v-for: "(item, index) in items", :key: "index"},
    children: [],
    parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
    rawAttrsMap: {},
    tag: "p",
    type: 1
}

为简单起见,我们在例子中所展示的AST节点对象包含的信息并不完整,只列出对讲解有帮助的重要信息。

v-if指令的处理

我们将上例中的HTML模板,调整如下:

<div id="app"><p v-if="seen">you can see me</p></div>

经过解析,p标签的AST对象大致如下:

{
    // 重点看attrsList
    attrsList: [{name: "v-if", value: "seen", start: 17, end: 28}],
    attrsMap: {v-if: "seen"},
    children: [],
    parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
    rawAttrsMap: {},
    tag: "p",
    type: 1
}

紧接着,vue对调用processIf 对 v-if进一步处理:

function processIf (el) {
  // 获取v-if的表达式,并从attrsList中删除v-if
  const exp = getAndRemoveAttr(el, 'v-if')
  if (exp) {
    // 为AST添加if和ifConditions属性
    el.if = exp
    addIfCondition(el, {
      exp: exp,
      block: el
    })
  } else {
    // 为AST添加else或elseif 
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}

AST对象变成如下形式:

{
    if: "seen",
    // ifConditions数组,数组中的每一项代表一个条件语句,其中exp表示条件语句的表达式,block是该条件语句所应用的标签的AST对象。
    ifConditions: [{exp: "seen", block: {…}}],
    // 重点看attrsList
    attrsList: [{name: "v-if", value: "seen", start: 17, end: 28}],
    attrsMap: {v-if: "seen"},
    children: [],
    parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
    rawAttrsMap: {},
    tag: "p",
    type: 1
}

如果在v-if之后还有一个p标签使用了v-else指令,还需要执行processIfConditions将其解析结果push到前面这个有if属性的节点的ifConditions数组中:

function processIfConditions (el, parent) {
  // 获取v-else的上一个节点
  const prev = findPrevElement(parent.children)
  // 如果上一个节点存在且有if属性,则将v-else的解析结果,push到该节点的ifConditions属性数组
  if (prev && prev.if) {
    addIfCondition(prev, {
      exp: el.elseif,
      block: el
    })
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `v-${el.elseif ? ('else-if="' + el.elseif + '"') : 'else'} ` +
      `used on element <${el.tag}> without corresponding v-if.`,
      el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else']
    )
  }
}

v-if所在的AST节点变成了以下的形式:

{
    if: "seen",
    // v-if后面的v-else节点会添加到ifConditions数组。
    ifConditions: [
        {exp: "seen", block: {…}},
        {exp: undefined, block: {…}}
    ],
    // 重点看attrsList
    attrsList: [{name: "v-if", value: "seen", start: 17, end: 28}],
    attrsMap: {v-if: "seen"},
    children: [],
    parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
    rawAttrsMap: {},
    tag: "p",
    type: 1
}

v-on指令的处理

将上例中的HTML模板调整如下:

<div id="app"><p @click="show">click me</p></div>

经过解析,p标签的AST对象大致如下:

{
    attrsList: [ {name: "@click", value: "show", start: 17, end: 30],
    attrsMap: {@click: "show"},
    children: [],
    parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
    rawAttrsMap: {},
    tag: "p",
    type: 1
}

vue在在下一步的处理中,会将通过v-on所绑定的事件及时间处理函数,添加到AST对象的events属性中:

    // ...省略其他代码
    if (onRE.test(name)) {
        // 如果是v-on指令,给一条事件处理器
        name = name.replace(onRE, '')
        isDynamic = dynamicArgRE.test(name)
        if (isDynamic) {
          name = name.slice(1, -1)
        }
        // 为AST对象添加on属性事件处理器
        addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
    }
    // 添加事件处理器
    function addHandler (
      el: ASTElement,
      name: string,
      value: string,
      modifiers: ?ASTModifiers,
      important?: boolean,
      warn?: ?Function,
      range?: Range,
      dynamic?: boolean
    ) {
      //...省略其他代码
      
      let events = el.events || (el.events = {})
    
      const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range)
    
      const handlers = events[name]
      // 将事件处理器添加到events中
      /* istanbul ignore if */
      if (Array.isArray(handlers)) {
        important ? handlers.unshift(newHandler) : handlers.push(newHandler)
      } else if (handlers) {
        events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
      } else {
        events[name] = newHandler
      }
      el.plain = false
    }

AST对象变成如下形式:

{
    attrsList: [ {name: "@click", value: "show", start: 17, end: 30}],
    attrsMap: {@click: "show"},
    children: [],
    parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
    rawAttrsMap: {},
    tag: "p",
    type: 1,
    // v-on绑定的事件,会在events中添加一条记录
    events: {click: {value: "show", dynamic: false, start: 17, end: 30}},
    hasBindings: true
}

v-model指令的处理

将上例中的HTML模板调整如下:

<div id="app"><input v-model="message" placeholder="edit me"></div>

经过解析,input标签的AST对象大致如下:

{
    attrsList: [ 
        {name: "v-model", value: "message", start: 21, end: 38},
        {name: "placeholder", value: "edit me", start: 39, end: 60}
    ],
    attrsMap: {v-model: "message",placeholder: "edit me"},
    children: [],
    parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
    rawAttrsMap: {},
    tag: "input",
    type: 1
}

在下一步的处理中,编译器调用addDirective给AST节点添加directives属性,并将v-model的解析结果添加到directives中。

// 将一条指令对象添加到AST节点的directives属性数组中,el.directives = [{name,rawName,...}]
function addDirective (
  el: ASTElement,
  name: string,
  rawName: string,
  value: string,
  arg: ?string,
  isDynamicArg: boolean,
  modifiers: ?ASTModifiers,
  range?: Range
) {
  (el.directives || (el.directives = [])).push(rangeSetItem({
    name,
    rawName,
    value,
    arg,
    isDynamicArg,
    modifiers
  }, range))
  el.plain = false
}

AST节点变成如下内容:

{
    attrsList: [ {name: "placeholder", value: "edit me", start: 39, end: 60}],
    attrsMap: {v-model: "message",placeholder: "edit me"},
    children: [],
    // 添加directives属性,并在此数组中添加一条记录
    directives:[{name: "model", rawName: "v-model", value: "message", arg: null, isDynamicArg: false,modifiers: undefined,start:21,end:38}],
    parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
    rawAttrsMap: {},
    tag: "input",
    type: 1
}

为AST生成render函数

以上我们了解到HTML模板经过解析处理,最终会变成一个AST节点树。随后编译器执行generate函数,为AST生成render函数的代码体。

function generate (
    ast,
    options
  ) {
    var state = new CodegenState(options);
    // 根据AST节点生成代码
    var code = ast ? genElement(ast, state) : '_c("div")';
    return {
      render: ("with(this){return " + code + "}"),
      staticRenderFns: state.staticRenderFns
    }
  }

render函数的主要作用是创建虚拟节点vnode。而创建一个vnode,需要用到三个参数:元素标签,数据对象和子元素列表。genElement作为核心的代码生成方法,会按照顺序去生成这三部分的代码。

/**
 * AST元素的代码生成函数
 * @param {ASTElement} el AST对象
 * @param {CodegenState} state 代码生成状态
 */
function genElement (el: ASTElement, state: CodegenState): string {
  // ... 省略其他代码
  let data
      // 1. 首先生成节点本身的data代码,例如<div ref="myref" id="app"></div> 生成的data数据代码为"{ref:"myref",attrs:{"id":"app"}}"
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData(el, state)
      }

      // 2. 其次生成子元素创建代码
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      
      // 3. 拼装成一个元素节点创建方法的字符串 形式如下:_c(tag,data,children)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
  // ... 省略其他代码

data数据对象代码

仍以第一个HTML模板进行举例:

<div id="app">{{msg}}</div>

经解析后的AST对象如下:

{
    attrsList: [{name: "id", value: "app", start: 5, end: 13}],
    attrsMap: {id: "app"},
    children: [{type: 2, expression: "_s(msg)", tokens: [{@binding: "msg"}], text: "{{msg}}"}],
    end: 14,
    parent: undefined,
    plain: false,
    rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}},
    start: 0,
    tag: "div",
    type: 1,
    static: false,
    staticRoot: false
}

下面一段代码是data数据对象代码的生成逻辑:

function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'
  // 先为指令生成代码,因为指令可能会修改元素的其他属性
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ','

  // key
  if (el.key) {
    data += `key:${el.key},`
  }
  // ref 给定ref='myref',则生成 'ref: "myref"'
  if (el.ref) {
    data += `ref:${el.ref},`
  }
  // refInFor
  if (el.refInFor) {
    data += `refInFor:true,`
  }
  // pre
  if (el.pre) {
    data += `pre:true,`
  }
  // record original tag name for components using "is" attribute
  if (el.component) {
    data += `tag:"${el.tag}",`
  }
  // 主要是class和style代码的生成 staticClass,staticStyle,classBinding,styleBinding
  for (let i = 0; i < state.dataGenFns.length; i++) {
    data += state.dataGenFns[i](el)
  }
  // attributes元素attribute属性的生成,如给定el.attrs=[{name: 'id', value:'app', dynamic: undefined, start:0,end:5}],则返回'attrs:{"id":"app"}'
  if (el.attrs) {
    data += `attrs:${genProps(el.attrs)},`
  }
  // DOM props
  if (el.props) {
    data += `domProps:${genProps(el.props)},`
  }
  // event handlers
  if (el.events) {
    data += `${genHandlers(el.events, false)},`
  }
  if (el.nativeEvents) {
    data += `${genHandlers(el.nativeEvents, true)},`
  }
  // slot target
  // only for non-scoped slots
  if (el.slotTarget && !el.slotScope) {
    data += `slot:${el.slotTarget},`
  }
  // scoped slots
  if (el.scopedSlots) {
    data += `${genScopedSlots(el, el.scopedSlots, state)},`
  }
  // component v-model
  if (el.model) {
    data += `model:{value:${
      el.model.value
    },callback:${
      el.model.callback
    },expression:${
      el.model.expression
    }},`
  }
  // inline-template
  if (el.inlineTemplate) {
    const inlineTemplate = genInlineTemplate(el, state)
    if (inlineTemplate) {
      data += `${inlineTemplate},`
    }
  }

  // 删除尾部的逗号,并添加花括号
  data = data.replace(/,$/, '') + '}'
 
  if (el.dynamicAttrs) {
    data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
  }
  // v-bind data wrap
  if (el.wrapData) {
    data = el.wrapData(data)
  }
  // v-on data wrap
  if (el.wrapListeners) {
    data = el.wrapListeners(data)
  }
  return data
}

上述代码中可以看到,vue针对不同的props、attrs、events、directives等分别生成各自的代码,在本例中的AST节点对象只存在attrs属性,因此其处理过程只会执行下面这一个语句:

if (el.attrs) {
    data += `attrs:${genProps(el.attrs)},`
  }

genProps函数如下:

function genProps (props: Array<ASTAttr>): string {
  let staticProps = ``
  let dynamicProps = ``
  for (let i = 0; i < props.length; i++) {
    const prop = props[i]
    const value = __WEEX__
      ? generateValue(prop.value)
      : transformSpecialNewlines(prop.value)
    if (prop.dynamic) {
      dynamicProps += `${prop.name},${value},`
    } else {
      staticProps += `"${prop.name}":${value},`
    }
  }
  staticProps = `{${staticProps.slice(0, -1)}}`
  if (dynamicProps) {
    return `_d(${staticProps},[${dynamicProps.slice(0, -1)}])`
  } else {
    return staticProps
  }
}

可以看到genProps 会将 el.attrs,也就是[{name: "id", value: "app", start: 5, end: 13}] 处理成如下字符串:

"{attrs:{"id":"app"}}"

生成children代码

子元素的创建代码生成函数如下:

function genChildren (
  el: ASTElement,
  state: CodegenState,
  checkSkip?: boolean,
  altGenElement?: Function,
  altGenNode?: Function
): string | void {
  const children = el.children
  // 无子元素不处理
  if (children.length) {
    const el: any = children[0]
    // 如果子元素使用了v-for指令
    if (children.length === 1 &&
      el.for &&
      el.tag !== 'template' &&
      el.tag !== 'slot'
    ) {
      const normalizationType = checkSkip
        ? state.maybeComponent(el) ? `,1` : `,0`
        : ``
      return `${(altGenElement || genElement)(el, state)}${normalizationType}`
    }
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
      
    // 遍历children数组,为每个子元素生成代码
    const gen = altGenNode || genNode
    return `[${children.map(c => gen(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}

/**
 * 生成创建node节点的代码,对各种node类型进行了封装
 * @param {*} node 
 * @param {*} state 
 */
function genNode (node: ASTNode, state: CodegenState): string {
  // 元素节点
  if (node.type === 1) {
    return genElement(node, state)
    // 注释
  } else if (node.type === 3 && node.isComment) {
    return genComment(node)
    // 文本节点
  } else {
    return genText(node)
  }
}

/**
 * 生成创建文本节点字符串,返回内容形如:"_v("see me")"
 * @param {*} text 文本类型的AST节点
 */
function genText (text: ASTText | ASTExpression): string {
  return `_v(${text.type === 2
    ? text.expression // no need for () because already wrapped in _s()
    : transformSpecialNewlines(JSON.stringify(text.text))
  })`
}

对于本例而言,div的子元素是一个文本节点,执行genText会生成如下代码:

"[_v(_s(message))]"

render函数的完整代码

最后在genElement函数中,将各部分代码拼接到一起,组成一段完整的代码:

'with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(message))])}'

以上是render函数基本的创建步骤,接下来我们再看下针对于v-if、v-model和v-for(v-on在v-model的处理中也有涉及,不单独举例了)几个常见的指令,生成的代码有什么不同。

v-if的处理

再来看一下genElement函数中关于v-if的处理逻辑:

function genElement (el: ASTElement, state: CodegenState): string {
  //... 省略其他代码
  // 节点存在v-if指令执行genIf
  if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } 
}

可以发现如果模板中有HTML标签使用了v-if,编译器会调用genIf,其主要代码如下:

function genIf (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  el.ifProcessed = true 
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}

function genIfConditions (
  conditions: ASTIfConditions,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  if (!conditions.length) {
    return altEmpty || '_e()'
  }
  
  // 将ifConditions数组中的条件语句转成三元运算表达式
  const condition = conditions.shift()
  // 生成三元表达式形如a?1:2
  if (condition.exp) {
    return `(${condition.exp})?${
      genTernaryExp(condition.block)
    }:${
      genIfConditions(conditions, state, altGen, altEmpty)
    }`
  } else {
    return `${genTernaryExp(condition.block)}`
  }

  // v-if 使用了 v-once  (a)?_m(0):_m(1)
  function genTernaryExp (el) {
    return altGen
      ? altGen(el, state)
      : el.once
        ? genOnce(el, state)
        : genElement(el, state)
  }
}

执行完genIf,会创建一个三元运算表达式。如果以如下模板为例:

div id="app"><p v-if="seen">you can see me</p><p v-else>you can not see me</p></div>

那么使用v-if和v-else的p标签所生成的对应代码如下:

"(seen)?_c('p',[_v("you can see me")]):_c('p',[_v("you can not see me")])"

从生成的代码中,我们就能理解v-show和v-if的区别了。当使用v-if来控制dom元素的隐藏和显示的时候,每次都需要移除和重新创建的。

v-model的处理

如果我们在如下模板使用了v-model:

<div id="app"><input v-model="message" placeholder="edit me"></div>

input标签所对应的AST节点核心信息如下:

{
    attrs: [{name: "placeholder", value: ""edit me"", dynamic: undefined, start: 39, end: 60}]
    attrsList: [{name: "v-model", value: "message", start: 21, end: 38}, {name: "placeholder", value: "edit me", start: 39, end: 60}],
    attrsMap: {v-model: "message",placeholder: "edit me"},
    children: [],
    directives:[{name: "model", rawName: "v-model", value: "message", arg: null, isDynamicArg: false,modifiers: undefined,start:21,end:38}],
    events:{input: {value: "if($event.target.composing)return;message=$event.target.value", dynamic: undefined}},
    props:[{name: "value", value: "(message)", dynamic: undefined}],
    parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
    rawAttrsMap: {placeholder: {name: "placeholder", value: "edit me", start: 39, end: 60},v-model: {name: "v-model", value: "message", start: 21, end: 38}},
    hasBindings: true,
    tag: "input",
    type: 1
}

input没有子元素,因此只需要生成data数据对象。生成的代码如下:

"{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"placeholder":"edit me"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return;message=$event.target.value}}}"

从代码中可以看到,data数据对象中,新增了在domProps和on部分代码,其中指定了value和input事件处理函数。因此也就理解了v-model在本质上是把v-bind与v-on:input封装之后的语法糖。

经拼接后,完整的input生成的代码如下:

"_c('input',{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"placeholder":"edit me"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return;message=$event.target.value}}})"

v-for的处理

如果如下模板中使用了v-for:

<div id="app"><p v-for="(item, index) in items" :key="index">{{item}}</p></div>

p标签对应的AST核心信息如下:

{
    alias: "item",
    for: "items",
    iterator1: "index",
    forProcessed: true,
    key: "index",
    attrsList: [],
    attrsMap: {v-for: "(item, index) in items", :key: "index"},
    children: [{type: 2, expression: "_s(item)", tokens: Array(1), text: "{{item}}", start: 61, …}],
    parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
    rawAttrsMap: {:key: {name: ":key", value: "index", start: 48, end: 60},v-for: {name: "v-for", value: "(item, index) in items", start: 17, end: 47}},
    tag: "p",
    type: 1
}

调用genFor函数:

function genFor (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altHelper?: string
): string {
  const exp = el.for
  const alias = el.alias
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
  // 非生产环境下,如果未指定:key,控制台会提示用户
  if (process.env.NODE_ENV !== 'production' &&
    state.maybeComponent(el) &&
    el.tag !== 'slot' &&
    el.tag !== 'template' &&
    !el.key
  ) {
    state.warn(
      `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
      `v-for should have explicit keys. ` +
      `See https://vuejs.org/guide/list.html#key for more info.`,
      el.rawAttrsMap['v-for'],
      true /* tip */
    )
  }

  el.forProcessed = true 
  // 拼接完整代码
  return `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
      `return ${(altGen || genElement)(el, state)}` +
    '})'
}

可以生成如下代码:

"_l((items),function(item,index){return _c('p',{key:index},[_v(_s(item))])}),0"

下面的是render函数中所用到的帮助方法的说明:

  • Vue.prototype._s 转换为字符类型
  • Vue.prototype._l 渲染列表
  • Vue.prototype._v 创建文本类型的vnode
  • Vue.prototype._c 创建vnode

总结

以上就是vue模板编译的总体过程:通过核心正则表达式,逐个将HTML标签解析成AST节点,最后根据AST节点,生成render函数的函数体。render函数负责生成vnode。

文章力求能够清楚地讲解vue的编译过程,但写的过程中,心中仍觉得讲述的有点别扭,如果大家有不理解的地方,可以留言讨论。

讲完了编译过程,下一次再聊聊vue的渲染过程,如vnode的创建、diff,敬请期待。

关注我们

公众号@前端论道