Vue parse之 从template到astElement 源码详解

前奏

在紧张的一个星期的整理,笔者的前端小组每一个人都整理了一篇文章,笔者整理了Vue编译模版到虚拟树的思想这一篇幅。建议读者看到这篇以前,先点击这里预习一下整个流程的思想和思路。html

本文介绍的是Vue编译中的parse部分的源码分析,也就是从template 到 astElemnt的解析到程。前端

正文

从笔者的 Vue编译思想详解一文中,咱们已经知道编译个四个流程分别为parse、optimize、code generate、render。具体细节这里不作赘述,附上以前的一张图。vue

编译流程图

本文则旨在从思想落实到源代码分析,固然只是针对parse这一部分的。node

1、 源码结构。

笔者先列出咱们在看源码以前,须要先预习的一些概念和准备。ios

准备

1.正则

parse的最终目标是生成具备众多属性的astElement树,而这些属性有不少则摘自标签的一些属性。 如 div上的v-for、v-if、v-bind等等,最终都会变成astElement的节点属性。 这里先给个例子:web

<div v-for="(item,index) in options" :key="item.id"></div>正则表达式

{
    alias: "item"
    attrsList: [],
    attrsMap: {"v-for": "(item,index) in options", :key: "item.id"},
    children: (2) [{…}, {…}],
    end: 139,
    for: "options",
    iterator1: "index",
    key: "item.id",
    parent: {type: 1, tag: "div", attrsList: Array(0), attrsMap: {…}, rawAttrsMap: {…}, …},
    plain: false,
    rawAttrsMap: {v-for: {…}, :key: {…}},
    start: 15,
    tag: "div",
    type: 1,
}
复制代码

能够看到v-for的属性已经被解析和从摘除出来,存在于astElement的多个属性上面了。而摘除的这个功能就是出自于正则强大的力量。下面先列出一些重要的正则预热。express

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/  // 重要1
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ // 重要二
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
// #7298: escape - to avoid being pased as HTML comment when inlined in page
const comment = /^<!\--/
const conditionalComment = /^<!\[/

export const onRE = /^@|^v-on:/ 
export const dirRE = process.env.VBIND_PROP_SHORTHAND
  ? /^v-|^@|^:|^\./
  : /^v-|^@|^:/
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g  // 在v-for中去除 括号用的。
const dynamicArgRE = /^\[.*\]$/  // 判断是否为动态属性

const argRE = /:(.*)$/ // 配置 :xxx
export const bindRE = /^:|^\.|^v-bind:/  // 匹配bind的数据,若是在组件上会放入prop里面  不然放在attr里面。
const propBindRE = /^\./
const modifierRE = /\.[^.\]]+(?=[^\]]*$)/g

const slotRE = /^v-slot(:|$)|^#/

const lineBreakRE = /[\r\n]/
const whitespaceRE = /\s+/g

const invalidAttributeRE = /[\s"'<>\/=]/ 复制代码

正则基础不太好的同窗能够先学两篇正则基础文章,特别详细:数组

而且附带上两个网站,供你们学习正则。浏览器

一次性看到这么多正则是否是有点头晕目眩。不要慌,这里给你们详细讲解下比较复杂的几条正则。

1)获取属性的正则

attribute 和 dynamicArgAttribute 分别获取普通属性和动态属性的正则表达式。 普通属性你们必定十分熟悉了,这里对动态属性作下解释。

动态属性,就是key值可能会发生变更的属性,vue的写法如 v-bind:[attrName]="attrVal",经过改变attrName来改变传递的属性的key值。(非动态属性只能修改val值)。

咱们先对attribute这个通用正则作一个详细的讲解:

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/

很长对不对??

可是细细的拆分的化,一共五个分组。

  • 1.([^\s"'<>/=]+)

这个分组是匹配 非空格、"、'、<、>、/、= 等符号的字符串。 主要会匹配到属性的key值部分。以下面的属性:

id="container"
复制代码

([^\s"'<>/=]+)会匹配到id。

  • 2.\s*(=)\s* 这个是 匹配 = 号,固然了空格页一并匹配了。好比下面的属性:
id="container"
id = "container"
复制代码

都会匹配到 = 号,第二个会把空格一块儿匹配了。

  • 3."([^"])"正则一 、'([^'])'正则二 、([^\s"'=<>`]+)正则三 . 这三个正则分别匹配三种状况 "val" 、'val' 、val。仍是继续拿例子来说。
id="container" // exp1
id='container' // exp2
id=container // exp3
复制代码

对于exp1正则一会匹配到"container", exp2正则2匹配到'container',exp3的话正则三会匹配到container。

Vue源码的正则基本将大多数状况都考虑在内了。

这样的话应该比较清晰了,咱们来归纳下:

attribute匹配的一共是三种状况, name="xxx" name='xxx' name=xxx。可以保证属性的全部状况都能包含进来。 须要注意的是正则处理后的数组的格式是:

['name','=','val','',''] 
或者
['name','=','','val',''] 
或者
['name','=','','','val'] 
复制代码

下面讲源码的时候,会知道这种数组格式是attr属性的原始状态,parse后期会将这种属性处理成attrMap的形式,大体以下:

{
    name:'xxx',
    id:'container'
}
复制代码

关于这个正则,咱们附上一个讲解图:

而关于dynamicArgAttribute, 则是大同小异:

主要是多了\[[^=]+\][^\s"'<>\/=]* 也就是 [name] 或者 [name]key 这类状况,附上正则详解图:

2)标签处理正则

标签主要包含开始标签 (如<div>)和结束标签(如</div>),正则分别为如下两个:

const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
复制代码

可以看到标签的匹配是以qnameCapture为基础的,那么这玩意又是啥呢? 其实qname就是相似于xml:xxx的这类带冒号的标签,因此startTagOpen是匹配<div<xml:xxx的标签。 endTag匹配的是如</div>或</xml:xxx>的标签

3)处理vue的标签
export const onRE = /^@|^v-on:/ 处理绑定事件的正则
export const dirRE = process.env.VBIND_PROP_SHORTHAND
  ? /^v-|^@|^:|^\./  // v-   | @click | :name | .stop  指令匹配
  : /^v-|^@|^:/
复制代码

一眼就能看出来,对不对?直接进入复杂的for标签。

for 标签比较重要,匹配也稍微复杂点,这里作个详解:

export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
复制代码

首先申明这里的正则是依赖于attribute正则的,咱们会拿到v-for里面的内容,举个例子v-for="item in options",咱们最终会处理成一个map的形式,大体以下:

const element = {
    attrMap: {
        'v-for':'item in options',
        ...
    }
}
复制代码

也就是说咱们会在item in options的基础上进行正则匹配。 先看forAliasRE的分组,一共两个分组分别是([\s\S]*?)([\s\S]*) 会分别匹配 itemoptions。这里举的例子比较简单。 实际上 inof以前的内容可能会比较复杂的,如(value,key) 或者(item,index)等,甚至可能(value,key,index),这个时候就是forIteratorRE开始起做用了。 它一共两个分组都是([^,\}\]]*),其实就是拿到alias的最后两个参数,你们都知道Vue对于Object的循环,是能够这么作的,例子以下:

<div v-for="(value,key,index)">
复制代码

forIteratorRE则是为了获取keyindex的。最终会放在astElement的iterator1iterator2

{
    iterator1:',key',
    iterator2:',index'
}
复制代码

好了关于正则就说这么多了,具体的状况仍是得本身去看看源码的。

2.源码结构

依然是在开始讲源码前,先大体介绍下源码的结构。先贴个代码出来

function parse() {
    模块一:初始化须要的方法
    模块二: 初始化全部标记
    模块三: 开始识别并建立 astElement 树。
}
复制代码

模块一大体是一些功能函数,给出代码:

platformIsPreTag = options.isPreTag || no  //判断是否为 pre 标签
  platformMustUseProp = options.mustUseProp || no // 判断某个属性是不是某个标签的必要属性,如selected 对于option
  platformGetTagNamespace = options.getTagNamespace || no  // 判断是否为 svg or math标签 对函数
  const isReservedTag = options.isReservedTag || no // 判断是否为该平台对标签,目前vue源码只有 web 和weex两个平台。
  maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag) //是否可能为组件

  transforms = pluckModuleFunction(options.modules, 'transformNode')  // 数组,成员是方法, 用途是摘取 staticStyle styleBinding staticClass classBinding
  preTransforms = pluckModuleFunction(options.modules, 'preTransformNode') // ??
  postTransforms = pluckModuleFunction(options.modules, 'postTransformNode') // ??

  delimiters = options.delimiters // express标志
  
  function closeElement() {...} // 处理astElement对结尾函数
  function trimEndingWhitespace() {...} // 处理尾部空格
  function checkRootConstraints() {...} // 检查root标签对合格性
复制代码

模块二则是一些parse函数做用域内的全局标志和存储容器,代码以下:

const stack = [] // 配合使用的栈 主要目的是为了完成树状结构。
  
  let root // 根节点记录,树顶
  let currentParent // 当前父节点
  let inVPre = false // 标记是否在v-pre节点 当中
  let inPre = false // 是否在pre标签当中
  let warned = false
复制代码

模块三是核心部分,也就是解析template的部分,这个函数一旦执行完, 模块2的root会变成一颗以astElement为节点的dom树。

,其代码大体为:

parseHTML(template,options)
复制代码

parseHTML函数和 options 是解析的关键,options包括不少平台配置和 传入的四个处理方法。大体以下:

options = {
    warn,
    expectHTML: options.expectHTML, // 是否指望和浏览器器保证一致。
    isUnaryTag: options.isUnaryTag, // 是否为一元标签的判断函数
    canBeLeftOpenTag: options.canBeLeftOpenTag, // 能够直接进行闭合的标签
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments, // 是否保留注释
    outputSourceRange: options.outputSourceRange,
    // 这里分开,上面是平台配置、下面是处理函数。
    start, // 解析处理函数(1)
    end, //解析处理函数(2)
    chars, //解析处理函数(3)
    commend //解析处理函数(4)
}
复制代码

笔者以前的parse思想的文章,已经介绍过两个处理函数start和end了,一个是建立astElement另外一个是创建父子关系,其中细节会在下文中,详细介绍,这也是本文的重点。

chars函数处理的是文本节点,commend处理的则是注释节点。 切记这四个函数相当重要,下面会用代号讲解。

2、各模块重点功能。

Vue的html解析并不是一步到位,先来介绍一些重点的函数功能

1.parseHTML函数内部功能函数详细讲解。

(1)解析开始标签和处理属性,生成初始化match。

前面咱们说到了startTagOpen是用来匹配开始标签的。而parseHTML里面的parseStartTag函数则是利用该正则,匹配开始标签,创立一种初始的数据结构match,保存相应的属性,对于开始标签里的全部属性,如id、class、v-bind,都会保存到match.attr中。

代码以下:

/**
   * 建立match数据结构
   * 初始化的状态
   * 只有
   * tagName
   * attrs
   *    attrs本身是个数组 也就是 正则达到的效果。。
   * start
   * end
   */
  function parseStartTag () {
    const start = html.match(startTagOpen) // 匹配开始标签。c
    if (start) {
      const match = { // 建立相应的数据结构
        tagName: start[1],
        attrs: [],
        start: index
      }
      advance(start[0].length)
      let end, attr
      //遍历的摘取取属性值,并保存到attrs
      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
      }
    }
  }
复制代码

上面的while中,咱们是用开始标签的结束符做为结束条件的。 startTagClose的正则是

const startTagClose = /^\s*(\/?)>/
复制代码

它自己除了判断是否已经结束,还有一个\/?是用来判断是否为一元标签的。 一元标签就是如<img/>能够只写一个标签的元素。这个标记后面会用到。

parseStartTag的目标是比较原始的,得到相似于

const match = { // 匹配startTag的数据结构
        tagName: 'div',
        attrs: [
            { 'id="xxx"','id','=','xxx' },
            ...
        ],
        start: index,
        end: xxx
      }
复制代码

match大体能够归纳为获取标签、属性和位置信息。并将此传递给下个函数。

(2)handleStartTag处理parseStartTag传递过来的match。

// parseStartTag 拿到的是 match
  function handleStartTag (match) {
    const tagName = match.tagName
    const unarySlash = match.unarySlash

    if (expectHTML) { // 是否指望和浏览器的解析保持一致。
      if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
        parseEndTag(lastTag)
      }
      if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
        parseEndTag(tagName)
      }
    }

    const unary = isUnaryTag(tagName) || !!unarySlash // 一元判断

    const l = match.attrs.length
    const attrs = new Array(l)
    for (let i = 0; i < l; i++) { // 将attrs的 数组模式变成  { name:'xx',value:'xxx' }
      const args = match.attrs[i]
      const value = args[3] || args[4] || args[5] || ''
      const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
        ? options.shouldDecodeNewlinesForHref
        : options.shouldDecodeNewlines
      attrs[i] = {
        name: args[1],
        value: decodeAttr(value, shouldDecodeNewlines)
      }
      if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
        attrs[i].start = args.start + args[0].match(/^\s*/).length
        attrs[i].end = args.end
      }
    }

    if (!unary) { // 非一元标签处理方式
      stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
      lastTag = tagName
    }

    if (options.start) {
      options.start(tagName, attrs, unary, match.start, match.end)
    }
  }

复制代码

handleStartTag的自己效果其实很是简单直接,就是吧match的attrs从新处理,由于以前是数组结构,在这里他们将全部的数组式attr变成一个对象,流程大体以下:

从这样:

attrs: [
        { 'id="xxx"','id','=','xxx' },
        ...
],
复制代码

变成这样:

attrs: [
        {name='id',value='xxx' },
        ...
],
复制代码

那么其实还有些特殊处理expectHTML一元标签

expectHTML 是为了处理一些异常状况。如 p标签的内部出现div等等、浏览器会特殊处理的状况,而Vue会尽可能和浏览器保持一致。具体参考 p标签标准

最后handleStartTag会调用 从parse传递的start(1)函数来作处理,start函数会在下文中有详细的讲解。

(3) parseEndTag

parseEndTag自己的功能特别简单就是直接调用options传递进来的end函数,可是咱们观看源码的时候会发现源码还蛮长的。

function parseEndTag (tagName, start, end) {
    let pos, lowerCasedTagName
    if (start == null) start = index
    if (end == null) end = index

    // Find the closest opened tag of the same type
    if (tagName) {
      lowerCasedTagName = tagName.toLowerCase()
      for (pos = stack.length - 1; pos >= 0; pos--) {
        if (stack[pos].lowerCasedTag === lowerCasedTagName) {
          break
        }
      }
    } else {
      // If no tag name is provided, clean shop
      pos = 0
    }

    if (pos >= 0) {
      // Close all the open elements, up the stack
      for (let i = stack.length - 1; i >= pos; i--) {
        if (process.env.NODE_ENV !== 'production' &&
          (i > pos || !tagName) &&
          options.warn
        ) {
          options.warn(
            `tag <${stack[i].tag}> has no matching end tag.`,
            { start: stack[i].start, end: stack[i].end }
          )
        }
        if (options.end) {
          options.end(stack[i].tag, start, end)
        }
      }

      // Remove the open elements from the stack
      stack.length = pos
      lastTag = pos && stack[pos - 1].tag
    } else if (lowerCasedTagName === 'br') {
      if (options.start) {
        options.start(tagName, [], true, start, end)
      }
    } else if (lowerCasedTagName === 'p') {
      if (options.start) {
        options.start(tagName, [], false, start, end)
      }
      if (options.end) {
        options.end(tagName, start, end)
      }
    }
  }
}

复制代码

看起来还蛮长的,其实主要都是去执行options.end, Vue的源码有不少的代码量都是在处理特殊状况,因此看起来很臃肿。这个函数的特殊状况主要有两种:

  • 1.编写者失误,有标签没有闭合。会直接一次性和检测的闭合标签一块儿进入options.end。 如:
<div>
        <span>
        <p>
    </div>
复制代码

在处理div的标签时,根据pos的位置,将pos以前的全部标签和匹配到的标签都会一块儿遍历的去执行end函数。

    1. p标签和br标签

可能会遇到</p></br>标签 这个时候 p标签会走跟浏览器自动补全效果,先start再end。 而br则是一元标签,直接进入end效果。

2.start、end、comment、chars四大函数。

1)start函数

start函数很是长。这里截取重点部分

start() {
    ...
    let element: ASTElement = createASTElement(tag, attrs, currentParent) // 1.建立astElement
    ...
    
      if (!inVPre) {
        processPre(element)
        if (element.pre) {
          inVPre = true
        }
      }
      if (platformIsPreTag(element.tag)) {
        inPre = true
      }
      if (inVPre) { // 处理属性两种状况
        processRawAttrs(element)
      } else if (!element.processed) {
        // structural directives
        processFor(element)
        processIf(element)
        processOnce(element)
      }

      if (!root) {
        root = element
        if (process.env.NODE_ENV !== 'production') {
          checkRootConstraints(root)
        }
      }

      if (!unary) {
        currentParent = element
        stack.push(element)
      } else {
        closeElement(element)
      }
}
复制代码
  • 1).建立astElement节点。

结构以下:

{
    type: 1,
    tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    rawAttrsMap: {},
    parent,
    children: []
  }
复制代码
  • 2)处理属性 固然在这里只是处理部分属性,且分为两种状况:

    (1)pre模式 直接摘取全部属性

    (2)普通模式 分别处理processFor(element) 、processIf(element) 、 processOnce(element)。

    这些函数的详细细节,后文会有讲解,这里只是让你们有个印象。

2)end函数

end函数很是短

end (tag, start, end) {
      const element = stack[stack.length - 1]
      // pop stack
      stack.length -= 1
      currentParent = stack[stack.length - 1]
      if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
        element.end = end
      }
      closeElement(element)
    },
复制代码

end函数第一件事就是取出当前栈的父元素赋值给currentParent,而后执行closeElement,为的就是可以建立完整的树节点关系。 因此closeElement才是end函数的重点。

下面详细解释下closeElement

function closeElement (element) {
    trimEndingWhitespace(element) // 去除 未部对空格元素
    if (!inVPre && !element.processed) {
      element = processElement(element, options) // 处理Vue相关的一些属性关系
    }
    // 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)) {
        if (process.env.NODE_ENV !== 'production') {
          checkRootConstraints(element)
        }
        addIfCondition(root, { // 处理root的条件展现
          exp: element.elseif,
          block: element
        })
      } else if (process.env.NODE_ENV !== 'production') {
        warnOnce(
          `Component template should contain exactly one root element. ` +
          `If you are using v-if on multiple elements, ` +
          `use v-else-if to chain them instead.`,
          { start: element.start }
        )
      }
    }
    if (currentParent && !element.forbidden) {
      if (element.elseif || element.else) { // 处理 elseif else 块级
        processIfConditions(element, currentParent)
      } else {
        if (element.slotScope) { // 处理slot, 将生成的各个slot的astElement 用对象展现出来。
          // scoped slot
          // keep it in the children list so that v-else(-if) conditions can
          // find it as the prev node.
          const name = element.slotTarget || '"default"'
          ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
        }
        currentParent.children.push(element)
        element.parent = currentParent
      }
    }

    // final children cleanup
    // filter out scoped slots
    element.children = element.children.filter(c => !(c: any).slotScope)
    // remove trailing whitespace node again
    trimEndingWhitespace(element)

    // check pre state
    if (element.pre) {
      inVPre = false
    }
    if (platformIsPreTag(element.tag)) {
      inPre = false
    }
    // apply post-transforms
    for (let i = 0; i < postTransforms.length; i++) {
      postTransforms[i](element, options)
    }
  }
复制代码

主要是作了五个操做:

  • 1.processElement。

processElement是closeElement很是重要的一个处理函数。先把代码贴出来。

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 &&
    !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
}
复制代码

能够看到主要是processKey、processRef、processSlotContent、processSlotOutlet、processComponent、processAttrs和最后一个遍历的执行的transforms。

咱们一个个来探讨一下,给你们留个印象,实际上,后面会有案例详细讲解函数们的做用。

  • 1.首先最为简单的是processKey和processRef,在这两个函数处理以前,咱们的key属性和ref属性都是保存在astElement上面的attrs和attrsMap,通过这两个函数以后,attrs里面的key和ref会被干掉,变成astElement的直属属性。

  • 2.探讨一下slot的处理方式,咱们知道的是,slot的具体位置是在组件中定义的,而须要替换的内容又是组件外面嵌套的代码,Vue对这两块的处理是分开的。

先说组件内的属性摘取,主要是slot标签的name属性,这是processSlotOutLet完成的。

// handle <slot/> outlets
function processSlotOutlet (el) {
  if (el.tag === 'slot') {
    el.slotName = getBindingAttr(el, 'name') // 就是这一句了。
    if (process.env.NODE_ENV !== 'production' && el.key) {
      warn(
        `\`key\` does not work on <slot> because slots are abstract outlets ` +
        `and can possibly expand into multiple elements. ` +
        `Use the key on a wrapping element instead.`,
        getRawBindingAttr(el, 'key')
      )
    }
  }
}
复制代码

其次是摘取须要替换的内容,也就是 processSlotContent,这是是处理展现在组件内部的slot,可是在这个地方只是简单的将给el添加两个属性做用域插槽的slotScope和 slotTarget,也就是目标slot。

processComponent 并非处理component,而是摘取动态组件的is属性。 processAttrs是获取全部的属性和动态属性。

transforms是处理class和style的函数数组。这里不作赘述了。

  • 2.添加elseif 或else的block。

最终生成的的ifConditions块级的格式大体为:

[
    {
        exp:'showToast',
        block: castElement1
    },
    {
        exp:'showOther',
        block: castElement2
    },
    {
        exp: undefined,
        block: castElement3
    }
]
复制代码

这里会将条件展现处理成一个数组,exp存放全部的展现条件,若是是else 则为undefined。

  • 3.处理slot,将各个slot对号入座到一个对象scopedSlots。

processElement完成的slotTarget的赋值,这里则是将全部的slot建立的astElement以对象的形式赋值给currentParent的scopedSlots。以便后期组件内部实例话的时候能够方便去使用vm.?slot。有兴趣的童鞋能够去看看vm.$slot的初始化。

  • 4.处理树到父子关系,element.parent = currentParent。

  • 5.postTransforms。

不作具体介绍了,感兴趣的同窗本身去研究下吧。

3)chars函数

chars(){
    ...
    const children = currentParent.children
    ...
     if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
          child = {
            type: 2,
            expression: res.expression,
            tokens: res.tokens,
            text
          }
        } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
          child = {
            type: 3,
            text
          }
        }
}
复制代码

chars主要处理两中文本状况,静态文本和表达式,举个例子:

<div>name</div>
复制代码

name就是静态文本,建立的type为3.

<div>{{name}}</div>
复制代码

而在这个里面name则是表达式,建立的节点type为2。

作个总结就是:普通tag的type为1,纯文本type为2,表达式type为3。

4)comment函数比较简单

comment (text: string, start, end) {
      // adding anyting as a sibling to the root node is forbidden
      // comments should still be allowed, but ignored
      if (currentParent) {
        const child: ASTText = {
          type: 3,
          text,
          isComment: true
        }
        if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
          child.start = start
          child.end = end
        }
        currentParent.children.push(child)
      }
    }
复制代码

也是纯文本,只是节点加上了一个isComment:true的标志。

3.核心代码parseHTML内部探索

上面完成了一些重要函数的讲解,下面开始识别器的探索。

咱们的主要目的是了解parse的主要目的和过程。不会在一些细枝末节做太多赘述。

1)概览

parseHTML函数的结构以下:

function parseHTML (html, options) {
    const stack = []
    const expectHTML = options.expectHTML
    const isUnaryTag = options.isUnaryTag || no
    const canBeLeftOpenTag = options.canBeLeftOpenTag || no
    let index = 0 // 坐标标志
    let last, lastTag // 上一个标签
    
    while(html) {
        last = html;
        ...
    }
    
    function advance (n) {
        index += n
        html = html.substring(n)
    }

}
复制代码

parseHTML原理是用各个正则,不断的识别并前进的的过程。举个列子:

<div id="xxx">text<div>
复制代码

startTagOpen会先匹配到<div,而后index会前进四个位置到4,并将html去掉前面到部分,而后匹配id="xxx",index前进了8个位置到了13,空格也会算一个位置,html去掉这一部分。而后匹配text,最后经过endTag正则匹配<div>。这样就结束了。

固然了,匹配到到结果都是经过各个功能函数去处理。

2)标记

先介绍下各个参数的做用,在详细了解while里面的逻辑。

这里的核心参数一共有stack、index、last、lastTag。

他们贯穿了整个匹配线路,index相信你们已经明白是起什么做用的了。咱们这里分析下其余属性的做用域。

  • 1)现看下stack的功能吧:

先看一个示例

<div>
    <span>
</div>
复制代码

这种误写的状况,若是按顺序识别的话,那么span标签永远不会获得end函数的处理,由于没有识别到闭合标签。因此stack有着检查错误的功能。

stack的处理方式是,识别到开始标签就会推入stack。识别到闭合标签就会把对应的闭合标签推出来。

像上面那种状况,当识别到到时候,咱们会发现,stack里面上面到span,下面才是div,咱们会把这两个一块儿处理掉。这样能保证生成的astElement树的结构包括span。

  • 2)last的做用

请你们思考一个问题,何时咱们才会结束?

其实就是parseHTML函数不起做用了,换句话说就是while绕了一圈发现,index没有变,html也没有变。 剩下的部分,咱们会看成文本处理掉。

而这块的逻辑就是:

while(html){
    last = html;
    ....
    ....
    if(last===html){
        optios.chars(html);
    }
}
复制代码

有没有恍然大悟的感受? 原来最后一步都是判断中间的处理部分有没有动html。last就是记录处理前的样式,而后在后面对比。没有变更了就只剩下文本了。咱们直接当文本处理了。

    1. lastTag。

这个标记使用的地方特别多,记录的是上个标签。由于有些特殊的状况,须要判断上个标签。 如p标签,记录了上个标签是lastTag,若是里面出现了div等标签,咱们会从:

<p>
    <div></div>
</p>
复制代码

变成:

<p></p>
<div></div>
<p></p>
复制代码

缘由请参考这里

3)while循环解析器之剖析

while的轮廓:

while(html) {
    last = html;
    if (!lastTag || !isPlainTextElement(lastTag)) {
        let textEnd = html.indexOf('<');
        
        if (textEnd === 0) {
            ... 模块一
        }
        let text, rest, next
        if (textEnd >= 0) { 
            ... 模块二
        }
        
        if (textEnd < 0) {
            text = html 
        }
        
        if (text) {
            advance(text.length)
        }

       if (options.chars && text) { // 空格等 通过这个函数处理为文本节点
            options.chars(text, index - text.length, index) // 模块三
       }
    } else {
        // 模块四
    }
}
复制代码

笔者将上面的代码大体分为四个模块,咱们逐一来分析讲解。

模块一的代码:
if (comment.test(html)) {
          const commentEnd = html.indexOf('-->')

          if (commentEnd >= 0) {
            if (options.shouldKeepComment) {
              options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
            }
            advance(commentEnd + 3)
            continue
          }
        }

        // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
        if (conditionalComment.test(html)) {
          const conditionalEnd = html.indexOf(']>')

          if (conditionalEnd >= 0) {
            advance(conditionalEnd + 2)
            continue
          }
        }

        // Doctype:
        const doctypeMatch = html.match(doctype)
        if (doctypeMatch) {
          advance(doctypeMatch[0].length)
          continue
        }

        // End tag:
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          const curIndex = index
          advance(endTagMatch[0].length)
          parseEndTag(endTagMatch[1], curIndex, index)
          continue
        }

        // Start tag:
        const startTagMatch = parseStartTag()
        if (startTagMatch) {
          handleStartTag(startTagMatch)
          if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
            advance(1)
          }
          continue
        }
复制代码

模块一是在let textEnd = html.indexOf('<');的textEnd为0的时候,才进入的。

模块一的主要功能是匹配comment、conditionalComment、doctypeMatch、endTagMatch、startTagMatch五种状况。他们的共同特性是匹配而且处理完后,会调用advance函数进行前进。

不一样的是comment、endTagMatch、startTagMatch会分别进入options.comment、options.end和options.start函数。 comment函数比较简单,这里不作赘述来,让咱们具体看endTagMatch和startTagMatch。

  • 1) 先看startTag:
const startTagMatch = parseStartTag()
if (startTagMatch) {
  handleStartTag(startTagMatch)
  if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
    advance(1)
  }
  continue
}
复制代码

parseStartTag函数以前咱们有说过,除了匹配还会经过attribute正则摘取全部的属性,并生成一个match对象。 格式以下:

match = {
    tagName:'xxx',
    attrs:[
        ['id','=','container','',''],
        ['v-if','=','show','','']
    ],
    start:xx,
    end: xx
}
复制代码

而后把结果交给handleStartTag进行处理。 handleStartTag的功能前面也有说明,主要是将原始的正则匹配到到内容,格式一下:

attrs:[
        ['id','=','container','',''],
        ['v-if','=','show','','']
],
复制代码

会变成:

attrs:[
    {name:'id',value:'container'},
    {name:'v-if',value:'show'}
]
复制代码

并把类match结构推入到stack当中,最后执行了options.start函数。

  • 2)再看endTag
const endTagMatch = html.match(endTag)
if (endTagMatch) {
  const curIndex = index
  advance(endTagMatch[0].length) // 前进
  parseEndTag(endTagMatch[1], curIndex, index) // 进入
  continue
}
复制代码

能够看到匹配到endTag,主要是进入了parseEndTag函数。 前面已经说过,parseEndTag函数主要是判断结束标签,再stack到位置,并把stack尾部到这个位置之间到全部到标签都经过options.end函数处理掉。options.end则使用closeElement去处理各个astElement到父子关系。

模块二
let text, rest, next
  if (textEnd >= 0) { // 有0的状况,是由于模块一都没有匹配上。
    rest = html.slice(textEnd)
    while (
      !endTag.test(rest) &&
      !startTagOpen.test(rest) &&
      !comment.test(rest) &&
      !conditionalComment.test(rest)
    ) {
      // < in plain text, be forgiving and treat it as text
      next = rest.indexOf('<', 1) // 后面是否还有 <
      if (next < 0) break // 没有就不玩了
      textEnd += next
      rest = html.slice(textEnd)
    }
    text = html.substring(0, textEnd)
  }
复制代码

模块二主要是检查下<符号以后的代码,其中的全部非特殊代码都赋值到text上,换言之,就是不断检查有咩有endTag、startTagOpen、comment等特殊状况,一旦检测到就中止,将前面到多是文本到部分赋值给text。而text会看成文本信息让模块三去处理。

模块三

if (options.chars && text) { // 空格等 通过这个函数处理为文本节点
    options.chars(text, index - text.length, index) // 模块三
}
复制代码

模块三为类文本信息,咱们会经过options.chars函数去处理,这个函数则会进一步,判断是否存在表达式文本,就是咱们常常绑定到值如:

{{name}}
复制代码

模块四

这个模块处理到是script或style标签,这里暂且不作赘述了,请你们自行去研究。

3、具体示例探索。

说了太多概念,难免会有些抽象,那么直接给出一个具体的示例吧。

<div class="container" id="root">

    <div v-if="show">
          show attr bind
    </div>

    <div v-for="(item,index) in options" :key="item.id">
      <span>{{item.id}}</span> 
      <div>{{item.text}}</div>
    </div>
        
 </div>
复制代码

刚进来到达while流程的是html就是完整的代码:

html = "<div class="container" id="root"> <div v-if="show"> show attr bind </div> <div v-for="(item,index) in options" :key="item.id"> <span>{{item.id}}</span> <div>{{item.text}}</div> </div> </div>"
复制代码

先经过parseStartTag解析<div class="container" id="root">,获得的结果为:

match = {

    attrs:[
         {
            0:class="container",
            1: "class",
            2: "=",
            3: "container",
            4: undefined,
            5: undefined,
            end: 22,
            groups: undefined,
            index: 0,
            input: " class="container" id="root">↵↵ <div v-if="show">↵ show attr bind↵ </div>↵↵ <div v-for="(item,index) in options" :key="item.id">↵ <span>{{item.id}}</span> ↵ <div>{{item.text}}</div>↵ </div>↵ ↵ </div>",
            start: 4
        },
        {
            0: " id="root"",
            1: "id"
            2: "="
            3: "root"
            4: undefined
            5: undefined
            end: 32
            groups: undefined
            index: 0
            input: " id="root">↵↵ <div v-if="show">↵ show attr bind↵ </div>↵↵ <div v-for="(item,index) in options" :key="item.id">↵ <span>{{item.id}}</span> ↵ <div>{{item.text}}</div>↵ </div>↵ ↵ </div>"
            start: 22
        },
    ],
    end: 33
    start: 0
    tagName: "div"
    unarySlash: ""
}
复制代码

咱们能看到解析到每一个属性,也就是attrs的对象的时候,都会用input去记录还剩下的html。 而后将这个结果交给handleStartTag,去处理。

handleStartTag会将上面的attrs从新加工下,从数组变成:

[
    { //以前是数组的形式
        "name":"class",
        "value":"container",
        "start":5,
        "end":22
    },
    {"name":"id","value":"root","start":23,"end":32}
]
复制代码

将相应的参数传递给options.start去处理。这个函数的入参大体以下:

options.start(
    tagName, // div
    attrs, // 上面处理过的attrs
    unary,  // 一元标签
    match.start, // 开始
    match.end // 结束
)
复制代码

那么start函数自己呢,就去建立astElement,并处理掉v-for、v-if、v-once几种标签,这几种标签的处理方式,大体相同,从attrs去掉对应的属性,而后直接给astElement自己建立新的属性,下面给出处理后的格式以下:

  • 1.v-if
{   
    if:'show',
    ifConditions:[
        {
            exp:'show',
            block: astElement
        },
        {
            exp: 'show2',
            block: astElement
        },
        {
            exp: undefined,
            block: astElement
        }
    ],
}
复制代码

猜猜上述对ifConditions的第三个exp的undefined会是什么状况?

其实就是v-else的处理方式。

神秘面纱能够揭开了,关于v-if 、v-else-if 不会同时做为父节点的chidren而存在,而是只有一个children,那就是v-if,而后其余的会存放在ifConditions里面。

那么它们在源码的具体流程是怎么样子的?

// 1.遇到v-if节点,则在start函数中,使用processIf函数,添加ifConditions.
function processIf (el) {
  const exp = getAndRemoveAttr(el, 'v-if') // 添加v-if属性
  if (exp) { // 是if ,
    el.if = exp
    addIfCondition(el, { // 让咱们直接为astElement添加一个ifConditions属性
      exp: exp,
      block: el
    })
  } else { // 不是v-if 只是 给节点加上  el.else 或  el.elseif
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}
// 2. 那么何时加上其余条件的节点,别急别急,还记得前面的流程吗,end函数里面咱们会执行closeElement。
// 而这个函数有一个processIfConditions,若是不记得了,请翻上去看一看。
function processIfConditions (el, parent) {
  const prev = findPrevElement(parent.children) // 找到上一个节点,其实就是 倒数最后一个
  if (prev && prev.if) { // 若是上一个节点是if 那么ok,咱们就是要把当前节点推到这个节点里面。
    addIfCondition(prev, {
      exp: el.elseif,
      block: el
    })
  } else if (process.env.NODE_ENV !== 'production') { // 天呐,你写错了,v-else或v-else-if以前没有v-if,直接给错误。
    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']
    )
  }
}
复制代码
  • 2.v-for

那么v-for咱们最终会处理成什么样子呢?以及又是这么处理成这种样子的。

若是咱们的案例是这样的:

v-for="(item,index) in list"
复制代码

咱们获得的结果会是:

{
    for:'list',
    alias:'item',
    iterator1:'index'
}
复制代码

这里没有牵扯到closeElement了,直接在processFor一步到味,咱们详细的看看吧。

// 1.processFor函数,主要是经过parseFor摘取属性,而后经过extend拷贝给el。因此重点仍是parseFor函数。
export function processFor (el: ASTElement) {
  let exp
  if ((exp = getAndRemoveAttr(el, 'v-for'))) { // exp摘取的是v-for里面的内容,这里是(item,index) in list
    const res = parseFor(exp) // 摘取属性
    if (res) {
      extend(el, res) // 拷贝给el
    } else if (process.env.NODE_ENV !== 'production') {
      warn(
        `Invalid v-for expression: ${exp}`,
        el.rawAttrsMap['v-for']
      )
    }
  }
}
2.详细结果在注释里面了。
export function parseFor (exp: string): ?ForParseResult {
  // 传入了 exp =  (item,index) in list
  const inMatch = exp.match(forAliasRE) // 获取了一个数组,这个正则咱们前面说了,这里是
  // ['(item,index) in list','(item,index)','list']
  if (!inMatch) return
  const res = {}
  res.for = inMatch[2].trim() // list 不是吗?
  const alias = inMatch[1].trim().replace(stripParensRE, '') // item,index 对吗
  const iteratorMatch = alias.match(forIteratorRE) // 这个正则咱们也说过了,也是数组
  // [',index','index']
  if (iteratorMatch) {
    res.alias = alias.replace(forIteratorRE, '').trim()
    res.iterator1 = iteratorMatch[1].trim() // index对吗
    if (iteratorMatch[2]) {
      res.iterator2 = iteratorMatch[2].trim()
    }
  } else {
    res.alias = alias
  }
  return res
}
复制代码

好的,结果出来了。

接着咱们对解析案例,咱们已经处理了开始标签<div class="container" id="root">,那么剩下对还有

<div v-if="show">
          show attr bind
    </div>

    <div v-for="(item,index) in options" :key="item.id">
      <span>{{item.id}}</span> 
      <div>{{item.text}}</div>
    </div>
        
 </div>
复制代码

那么接下来呢? parseStartTag会匹配到什么呢?

<div v-if="show">吗?

很差意思,并非。现实的template各个标签之间都有空格,因此在while循环中,对于<符号的匹配根本不会为0,因此进不了前面所说到模块一,而是经过模块二匹配到下一个<符号,并判断是否为注释开始标签结束标签的一种。 若是是,那么从位置0 到 下一个<符号之间的字符串,咱们有理由相信这是一个文本节点,交给模块三到options.chars去处理。

很显然,从位置0到,下一个开始标签<div v-if="show">之间是有不少空格的,咱们会生成一个文本空节点。

而后中间的过程咱们省略的说吧。

  • 处理<div v-if="show">

  • 处理文本节点show attr bind

  • 处理结束标签

好了,这是咱们处理的第一个结束标签 ,咱们详细的看看吧。

// 咱们知道对于结束标签咱们匹配到后,是直接交给parseEndTag函数处理的。这个函数容错能力咱们不说了,前面已经
// 有了详细的讲解,咱们须要明白它会调用options.end函数。end会交给closeElement。
// closeElement会创建父子关系并处理好多好多属性
1.processKey
2.processRef
3.processSlotContent
4.processSlotContent
5.processComponent
6.processIfConditions
....
复制代码

到了这里咱们还剩下:

<div v-for="(item,index) in options" :key="item.id">
      <span>{{item.id}}</span> 
      <div>{{item.text}}</div>
    </div>
 </div>
复制代码

而后继续省略的讲解:

  • 处理<div v-for="(item,index) in options" :key="item.id">,能够参照上面笔者描述的v-for处理方式看。
  • 处理空节点
  • 处理<span>开始标签
  • 处理文本标签{{item.id}},须要注意的是,expression创建的astElement的type为2。
  • 处理</span>结束标签
  • 处理空节点
  • 处理<div>开始标签
  • 处理文本标签{{item.text}},type也是2
  • 处理</div>结束标签,结束处理方式相同。
  • 处理空节点
  • 处理</div>结束标签,结束处理方式相同。
  • 处理空节点
  • 处理</div>结束标签,结束处理方式相同。
  • 处理空节点

4、总体流程总结。

普通标签处理流程描述

  • 1.识别开始标签,生成匹配结构match。
const match = { // 匹配startTag的数据结构
    tagName: 'div',
    attrs: [
        { 'id="xxx"','id','=','xxx' },
        ...
    ],
    start: index,
    end: xxx
}
复制代码
  • 2.处理attrs,将数组处理成 {name:'xxx',value:'xxx'}
  • 3.生成astElement,处理for,if和once的标签。
  • 4.识别结束标签,将没有闭合标签的元素一块儿处理。
  • 5.创建父子关系,最后再对astElement作全部跟Vue 属性相关对处理。slot、component等等。

文本或表达式的处理流程描述。

  • 一、截取符号<以前的字符串,这里必定是全部的匹配规则都没有匹配上,只多是文本了。
  • 二、使用chars函数处理该字符串。
  • 三、判断字符串是否含有delimiters,默认也就是${},有的话建立type为2的节点,不然type为3.

注释流程描述

  • 一、匹配注释符号。
  • 二、 使用comment函数处理。
  • 三、直接建立type为3的节点。

完结感言

时间仓促,但愿多多支持。

相关文章
相关标签/搜索