Recommended PC viewing, mobile code highlighting disorder

The compilation process begins by parsing the template to generate an AST, which is an abstract syntax tree, a tree-like representation of the abstract syntax structure of the source code. In many compilation techniques, ES6 code compiled by Babel is encoded as an AST.

First take a look at the definition of the parse in SRC/compiler/parser/index in js:

export function parse (

  template: string.

  options: CompilerOptions

) :ASTElement | void 
{

  // Get methods and configurations from options...



  parseHTML(template, {

    // some options...

    

    start (tag, attrs, unary, start, end) {

      // ...

    },

    end (tag, start, end) {

      // ...

    },

    chars (text: string, start: number, end: number) {

      // ...

    },

    comment (text: string, start, end) {

      // ...

    }

  })

  return root

}

Copy the code

The parse function is long in code and performs the parseHTML function to parseHTML templates.

1. Parse the HTML template

parseHTML(template, options)
Copy the code

Template template parsing is mainly through the parseHTML function, here to show the pseudo-code for easy understanding, the following step in the analysis of the source code.

// src/compiler/parser/html-parser

export function parseHTML (html, options{

  const stack = [] // stack, used to hold the start label

  const isUnaryTag = options.isUnaryTag || no // a function to determine if it is a unary tag, such as

  let index = 0

  let last // Used to save the last HTML string

  let lastTag // Represents the last label, the current element at the top of the stack

  

  while (html) {

    last = html

    // Make sure we're not in a plaintext content element like script/style

    // The previous tag does not exist or the previous tag is not one of script/style/textarea

    // Because things like the style tag are not compiled

    if(! lastTag || ! isPlainTextElement(lastTag)) {

      let textEnd = html.indexOf('<')

      if (textEnd === 0) {

        // Process comment tags

        

        // Process the condition tag

        

        // Process the docType tag

        

        // Process the closing tag

        

        // Process the start tag        

      }

      

      // Process text

      

    } else {

      // when the last tag is one of script/style/textarea

      

    }



  }

}

Copy the code

The logic of parseHTML is mainly to loop through templates:

  • When the previous label does not exist, or the previous label is notscript/style/textareaOne of them:
    • The first character is<, handle various tag cases.
    • Otherwise process text, for examplexxxx</div>.
  • Otherwise the last label to deal with isscript/style/textareaOne of them

A few concepts to know in advance in parseHTML:

  1. It’s going to happen during the matchstackThe concept of the stack is used to store the parsed start label. The stack is used to maintain the one-to-one correspondence between the start label and the end label.

  1. In the process of matchingadvanceThe function continues through the template string until the end of the string.
function advance (n{

  index += n

  html = html.substring(n)

}

Copy the code

To illustrate the role of Advance in a more intuitive way, it can be shown in the following figure:

Call advance:

advance(4)
Copy the code

  1. The following regular expressions are used in the matching process:
const attribute = /^\s*([^\s"'<>\/=]+)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /

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}) ` // Tags may contain namespaces

const startTagOpen = new RegExp(` ^ <${qnameCapture}`)

const startTagClose = /^\s*(\/?) >/

const endTag = new RegExp(` ^ < \ \ /${qnameCapture}[^ >] * > `)

const doctype = / ^ ]+>/i

// #7298: escape - to avoid being passed as HTML comment when inlined in page

const comment = / ^

const conditionalComment = / ^

Copy the code

If you need to read regular expressions, you can use this website to generate graphs for easy comprehension.

With these three concepts in mind, let’s move on to the parseHTML function:

1.1 Handling comments, conditions, document type nodes

The source code is as follows:

// comment = /^
      

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

  }

}



// conditionalComment = /^

if (conditionalComment.test(html)) {

  const conditionalEnd = html.indexOf('] > ')



  if (conditionalEnd >= 0) {

    advance(conditionalEnd + 2)

    continue

  }

}



// doctype = /^ ]+>/i

const doctypeMatch = html.match(doctype)

if (doctypeMatch) {

  advance(doctypeMatch[0].length)

  continue

}

Copy the code

In all three cases we just need to call advance to get to the right place.

1.2 Processing start Labels

The source code is as follows:

// Process the start tag

const startTagMatch = parseStartTag()

if (startTagMatch) {

  handleStartTag(startTagMatch)

  if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {

    advance(1)

  }

  continue

}

Copy the code

1.2.1 parseStartTag

First parse the start tag with parseStartTag:

function parseStartTag ({

  // Matches the beginning of the start tag

  const start = html.match(startTagOpen)

  if (start) {

    const match = {

      tagName: start[1].

      attrs: [],

      start: index

    }

    advance(start[0].length)

    let end, attr

    // Matches the attributes of the start tag and the end tag

    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// Whether there is a unary slash

      advance(end[0].length)

      match.end = index

      return match

    }

  }

}

Copy the code

Use the following example to analyze the parseTag function:

<img class="test"/>
Copy the code

The function matches the start tag with the regular expression startTagOpen, where start: [“

The loop then matches the attributes in the start tag and adds them to mate.attrs until the closing of the matched start tag ends. The re of the attribute is dynamicArgAttribute and attribute. With the help of this website, we can look at the re diagram of the attribute:

After executing the while loop, match. Attrs is shown as follows:

Finally, if a closure is matched, the unary slash is obtained, the end of the closure is advanced, and the current index is assigned to mate.end. Match after function execution is shown as follows:

1.2.2 handleStartTag

Return to parseHTML function, when parseStartTag parse the start tag to get the match, then execute handleStartTag to handle the match.

function handleStartTag (match{

  const tagName = match.tagName

  const unarySlash = match.unarySlash



  // ...



  constunary = isUnaryTag(tagName) || !! unarySlash// Whether to close the label



  const l = match.attrs.length

  const attrs = new Array(l)

  for (let i = 0; i < l; i++) {

    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) // decode value

    }

    if(process.env.NODE_ENV ! = ='production' && options.outputSourceRange) {

      attrs[i].start = args.start + args[0].match(/^\s*/).length // Skip the space

      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)

  }

}

Copy the code

The core logic of handleStartTag is simple:

  • Check whether the start tag is unary, similarThe < img >, < br / >Like this.
  • According to thematch.attrsBuilding a newattrsThe array.

  • If it’s a non-unary tag, push an object into the stack and assign the tagName to lastTag.

  • Finally, we call the options.start callback, which I’ll describe in more detail later, and pass in some parameters.

1.3 Handling Closed Labels

The source code is as follows:

// Process the closing tag

const endTagMatch = html.match(endTag)

if (endTagMatch) {

  const curIndex = index

  advance(endTagMatch[0].length)

  parseEndTag(endTagMatch[1], curIndex, index)

  continue

}

Copy the code

The closed tag is first matched by the regular endTag, and then advanced to the end of the closed tag. Finally, the parseEndTag method is executed to parse the closed tag.

1.3.1 parseEndTag

The source code for parseEndTag is as follows

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)

    }

  }

}

Copy the code

The core logic of parseEndTag is as follows:

  • Find the start tag that matches the current closing tag and record the subscriptpos. If it is a normal tag match, thenstackShould match the closing tag.
  • To deal withpos >= 0When:
    • Handle exceptions such as<div><span></div>, and then calledoptions.endCallback function, which is described in more detail later.
    • To the stackposPosition both pop-ups and fromstackThe tail to getlastTag.
  • Otherwise it ispos < 0I won’t do the analysis here.

Consider the following error cases:

<div><span></div>
Copy the code

This time when closing tag is < / div >, from the rear of the stack is the tag found < span >, cannot match.

1.4 Processing Text

Look at the code for parseHTML

let textEnd = html.indexOf('<')

if (textEnd === 0) {

  // Process comment tags



  // Process the condition tag



  // Process the docType tag



  // Process the closing tag



  // Process the start tag

}



// Process text

Copy the code

We have examined several cases where textEnd === 0. If the textEnd is not 0, there is text to process, such as “XXX “.

Here is the source code for this part:

let text, rest, next

if (textEnd >= 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 < 0break

    textEnd += next

    rest = html.slice(textEnd)

  }

  text = html.substring(0, textEnd)

}



if (textEnd < 0) {

  text = html

}



if (text) {

  advance(text.length)

}



if (options.chars && text) {

  options.chars(text, index - text.length, index)

}

Copy the code
  • If < is a character in plain text, we continue to find the actual end of the text, and then proceed to the end of the text.

  • If textEnd is less than 0, the entire template is parsed and the rest of the HTML is assigned to the text.

  • Finally, we call the options.chars callback, which I’ll describe in more detail later, and pass in the text argument.

1.4 When the previous tag is one of script/style/textarea

Look at the code for parseHTML

while (html) {

  last = html

  // Make sure we're not in a plaintext content element like script/style

  // The previous tag does not exist or the previous tag is not one of script/style/textarea

  // Because things like the style tag are not compiled

  if(! lastTag || ! isPlainTextElement(lastTag)) {

    let textEnd = html.indexOf('<')

    if (textEnd === 0) {

      // Process comment tags



      // Process the condition tag



      // Process the docType tag



      // Process the closing tag



      // Process the start tag

    }



    // Process text



  } else {

    // when the last tag is one of script/style/textarea



  }

}

Copy the code

if (! lastTag || ! We have finished analyzing isPlainTextElement(lastTag).

Let’s look at the else logic:

else {

  let endTagLength = 0

  const stackedTag = lastTag.toLowerCase()

  const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?) (< / ' + stackedTag + '[^ >] * >)'.'i'))

  const rest = html.replace(reStackedTag, function (all, text, endTag{

    endTagLength = endTag.length

    // ...

    if (options.chars) {

      options.chars(text)

    }

    return ' '

  })

  index += html.length - rest.length

  html = rest

  parseEndTag(stackedTag, index - endTagLength, index)

}

Copy the code

This part of logic is also very simple, let’s analyze with examples:

<div><style>xxx</style></div>
Copy the code

  1. LastTag and stackedTag are both styles

  2. Construct the regular expression reStackedTag as /([\s\ s]*?). [^ (< / style >] * >)/I.

  3. Then execute the following logic:

const rest = html.replace(reStackedTag, function (all, text, endTag) {
  // all: "xxx</style>"
  // text: "xxx"
  // endTag: "</style>"
  
  endTagLength = endTag.length / / 8
  // ...
  if (options.chars) {
    options.chars(text)
  }
  return ' '
})
Copy the code

The main function is to replace the “XXX ” of” XXX ” with an empty string to get “”, which is rest.

  1. Continue with the following logic:
index += html.length - rest.length // index += "xxx</style></div>".length - "</div>".length
html = rest // html = "</div>"
parseEndTag(stackedTag, index - endTagLength, index)
Copy the code

ParseEndTag (stackedTag, index-endtagLength, index)

Now that we’ve covered the main logic of the while loop, let’s take a look at some of the callbacks mentioned above

  • options.start
  • options.end
  • options.chars

These callbacks are passed in as properties of the configuration object when parseHTML is called.

2. options.start

When the start tag is parsed, the start callback is finally executed.

start (tag, attrs, unary, start, end) {

  / / create the AST

  let element: ASTElement = createASTElement(tag, attrs, currentParent)



  // Handle style/script elements

  if(isForbiddenTag(element) && ! isServerRendering()) {

    element.forbidden = true

process.env.NODE_ENV ! = ='production' && warn(

      'Templates should only be responsible for mapping the state to the ' +

      'UI. Avoid placing tags with side-effects in your templates, such as ' +

      ` <${tag}> ` + ', as they will not be parsed.'.

      { start: element.start }

    )

  }



  if(! inVPre) {

    processPre(element) // v-pre

    if (element.pre) {

      inVPre = true

    }

  }

  

  if (inVPre) {

    // Skip compilation if in V-pre

    processRawAttrs(element)

  } else if(! element.processed) {

    // Structure instruction

    processFor(element) // v-for

    processIf(element) // v-if

    processOnce(element) // v-once

  }



  if(! root) {

    root = element

    if(process.env.NODE_ENV ! = ='production') {

      checkRootConstraints(root)

    }

  }



  if(! unary) {

    currentParent = element

    stack.push(element)

  } else {

    closeElement(element)

  }

}

Copy the code

The following steps introduce this function:

  • createASTThe element
  • To deal withASTThe element
  • Pressure stack processing

2.1 Creating AST Elements

let element: ASTElement = createASTElement(tag, attrs, currentParent)

Copy the code

Create an AST with the createASTElement function:

export function createASTElement (

  tag: string.

  attrs: Array<ASTAttr>,

  parent: ASTElement | void

) :ASTElement 
{

  return {

    type1.

    tag,

    attrsList: attrs,

    attrsMap: makeAttrsMap(attrs),

    rawAttrsMap: {},

    parent,

    children: []

  }

}



function makeAttrsMap (attrs: Array<Object>) :Object {

  const map = {}

  for (let i = 0, l = attrs.length; i < l; i++) {

    if (

process.env.NODE_ENV ! = ='production' &&

map[attrs[i].name] && ! isIE && ! isEdge

    ) {

      warn('duplicate attribute: ' + attrs[i].name, attrs[i])

    }

    map[attrs[i].name] = attrs[i].value

  }

  return map

}

Copy the code

Each AST element is a normal JavaScript object:

  • typesaidASTThe element type
  • tagLabel name
  • attrsListRepresent property list
  • attrsMapRepresents the attribute mapping table,keyattrs[i].name.valueattrs[i].value
  • parentSaid the fatherASTThe element
  • childrenSaid the childASTElements in the collection

2.2 Processing AST Elements

// Check if the element is style/script

if(isForbiddenTag(element) && ! isServerRendering()) {

  element.forbidden = true

process.env.NODE_ENV ! = ='production' && warn(

    'Templates should only be responsible for mapping the state to the ' +

    'UI. Avoid placing tags with side-effects in your templates, such as ' +

    ` <${tag}> ` + ', as they will not be parsed.'.

    { start: element.start }

  )

}



// Call the module's preTransforms function

for (let i = 0; i < preTransforms.length; i++) {

  element = preTransforms[i](element, options) || element

}



if(! inVPre) {

  processPre(element) // v-pre

  if (element.pre) {

    inVPre = true

  }

}

if (platformIsPreTag(element.tag)) {

  inPre = true

}

if (inVPre) {

  // Skip compilation if it is V-pre

  processRawAttrs(element)

else if(! element.processed) {

  // Structure instruction

  processFor(element) // v-for

  processIf(element) // v-if

  processOnce(element) // v-once

}

Copy the code

The first is to determine if the element is script/style and throw a warning if it is.

This is followed by a call to the module preTransforms, In fact all modules preTransforms, transforms and postTransforms defined in SRC/platforms/web/compiler/modules directory, this part we temporarily not introduced.

It then determines whether the Element contains instructions to be processed by processXXX, which extends the attributes of the AST element.

Here’s a quick look at processFor and processIf:

2.2.1 introduces processFor

The code is as follows:

export function processFor (el: ASTElement{

  let exp

  if ((exp = getAndRemoveAttr(el, 'v-for'))) {

    const res = parseFor(exp)

    if (res) {

      extend(el, res)

    } else if(process.env.NODE_ENV ! = ='production') {

      warn(

        `Invalid v-for expression: ${exp}`.

        el.rawAttrsMap['v-for']

      )

    }

  }

}

Copy the code

GetAndRemoveAttr getAndRemoveAttr getAndRemoveAttr

// Only remove the attr from the array attrsList, so it is not handled by processAttrs

// It is not removed from attrsMap by default, as this map is used during codeGen

export function getAndRemoveAttr (

  el: ASTElement,

  name: string.

removeFromMap? :boolean

): ?string 
{

  let val

  if((val = el.attrsMap[name]) ! =null) {

    const list = el.attrsList

    for (let i = 0, l = list.length; i < l; i++) {

      if (list[i].name === name) {

        list.splice(i, 1)

        break

      }

    }

  }

  if (removeFromMap) {

    delete el.attrsMap[name]

  }

  return val

}

Copy the code

The logic of this function is very simple: get the value and remove the attr from el.attrslist. This function will be used later.

Return to processFor, get the value exp for v-for, pass it to parseFor as an argument, and execute it in order to parse exp and get the RES object. ParseFor = parseFor

export const forAliasRE = / (. *?) \s+(? :in|of)\s+(.*)/

export const forIteratorRE = / ([^, \} \]] *) (? : ([^, \} \]] *))? $/

const stripParensRE = /^\(|\)$/g



export function parseFor (exp: string): ?ForParseResult {

  const inMatch = exp.match(forAliasRE)

  if(! inMatch)return

  const res = {}

  res.for = inMatch[2].trim()

  const alias = inMatch[1].trim().replace(stripParensRE, ' ')

  const iteratorMatch = alias.match(forIteratorRE)

  if (iteratorMatch) {

    res.alias = alias.replace(forIteratorRE, ' ').trim()

    res.iterator1 = iteratorMatch[1].trim()

    if (iteratorMatch[2]) {

      res.iterator2 = iteratorMatch[2].trim()

    }

  } else {

    res.alias = alias

  }

  return res

}

Copy the code

For, Alias, iterator1, iterator2, etc., are parsed and added to the res element. V -for=”(item,index) in data” v-for=”(item,index) in data”

  • fordata
  • aliasitem
  • iterator1index
  • There is noiterator2.

Back to processFor, parseFor gets the RES object and extends (el, res) to extend the RES properties to the AST element.

That concludes the processFor function.

2.2.2 introduce processIf

The code is as follows:

function processIf (el{

  const exp = getAndRemoveAttr(el, 'v-if')

  if (exp) {

    el.if = exp

    addIfCondition(el, {

      exp: exp,

      block: el

    })

  } else {

    if (getAndRemoveAttr(el, 'v-else') != null) {

      el.else = true

    }

    const elseif = getAndRemoveAttr(el, 'v-else-if')

    if (elseif) {

      el.elseif = elseif

    }

  }

}



export function addIfCondition (el: ASTElement, condition: ASTIfCondition{

  if(! el.ifConditions) {

    el.ifConditions = []

  }

  el.ifConditions.push(condition)

}

Copy the code

ProcessIf takes the contents of the V-if directive from the element. If so, it adds the if attribute and ifConditions attribute to the AST element. Otherwise try to take the contents of the V-else and V-else -if directives, and if so add else and elseIf attributes to the AST element, respectively.

2.3 Handling stack pressing

Finally, look at the remaining logic of options.start:

if(! root) {

  root = element

  if(process.env.NODE_ENV ! = ='production') {

    checkRootConstraints(root)

  }

}



if(! unary) {

  currentParent = element

  stack.push(element)

else {

  closeElement(element)

}

Copy the code

If there is no root root element, use the current element as root and call checkRootConstraints:

function checkRootConstraints (el{

  if (el.tag === 'slot' || el.tag === 'template') {

    warnOnce(

      `Cannot use <${el.tag}> as component root element because it may ` +

      'contain multiple nodes.'.

      { start: el.start }

    )

  }

  if (el.attrsMap.hasOwnProperty('v-for')) {

    warnOnce(

      'Cannot use v-for on stateful component root element because ' +

      'it renders multiple elements.'.

      el.rawAttrsMap['v-for']

    )

  }

}

Copy the code

This function checks whether root is slot/template/ V-for.

Then, if the current element is a non-unary tag, save element as currentParent and push it.

Otherwise, if it is a unary tag, closeElement is executed, which is described later in options.end.

Note that the stack here is not the same stack as the previous stack, which holds the AST elements.

3. options.end

When the closed tag is parsed, the end callback is finally executed:

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)

},

Copy the code

The end callback mainly executes two pieces of logic:

  • The top element goes off the stack, updatescurrentParent
  • performcloseElementMethods.

3.1 closeElement

function closeElement (element{

  

  // 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, {

        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) {

      // ...

    } else {

      // ...

      currentParent.children.push(element)

      element.parent = currentParent

    }

  }



  // ...

}

Copy the code

When meet! stack.length && element ! If == root, the template has more than one root element

  • Multiple root elements are allowed, but must bev-if, v-else-if and v-elseIn the case
  • Otherwise an error

When currentParent exists, build parent-child relationships between AST elements.

4. options.chars

In addition to dealing with opening and closing tags, we also deal with some text content during template parsing:

chars (text: string, start: number, end: number) {

  if(! currentParent) {

    if(process.env.NODE_ENV ! = ='production') {

      if (text === template) {

        warnOnce(

          'Component template requires a root element, rather than just text.'.

          { start }

        )

      } else if ((text = text.trim())) {

        warnOnce(

          `text "${text}" outside root element will be ignored.`.

          { start }

        )

      }

    }

    return

  }

  

  const children = currentParent.children

  

  if (text) {

    let res

    letchild: ? ASTNode

    if(! inVPre && text ! = =' ' && (res = parseText(text, delimiters))) {

      child = {

        type2.

        expression: res.expression,

        tokens: res.tokens,

        text

      }

    } else if(text ! = =' '| |! children.length || children[children.length -1].text ! = =' ') {

      child = {

        type3.

        text

      }

    }

    if (child) {

      if(process.env.NODE_ENV ! = ='production' && options.outputSourceRange) {

        child.start = start

        child.end = end

      }

      children.push(child)

    }

  }

},

Copy the code

There are two types of AST elements constructed by text:

  • I have an expression,type2
  • Plain text,type3

Take this example:

<ul :class="bindCls" class="list" v-if="isShow">
    <li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>
Copy the code

Parses the text by executing parseText(text, delimiters), which is defined in SRC/Compiler /parser/text-parsre.js:

const defaultTagRE = / \ {\ {((? :.|\n)+?) \}\}/g

const regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g



const buildRegex = cached(delimiters= > {

  const open = delimiters[0].replace(regexEscapeRE, '\ \ $&')

  const close = delimiters[1].replace(regexEscapeRE, '\ \ $&')

  return new RegExp(open + '((? :.|\\n)+?) ' + close, 'g')

})



export function parseText (

  text: string.

delimiters? : [string.string]

) :TextParseResult | void 
{

  const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE

  if(! tagRE.test(text)) {

    return

  }

  const tokens = []

  const rawTokens = []

  let lastIndex = tagRE.lastIndex = 0

  let match, index, tokenValue

  while ((match = tagRE.exec(text))) {

    index = match.index

    // push text token

    if (index > lastIndex) {

      rawTokens.push(tokenValue = text.slice(lastIndex, index))

      tokens.push(JSON.stringify(tokenValue))

    }

    // tag token

    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))

  }

  return {

    expression: tokens.join('+'),

    tokens: rawTokens

  }

}

Copy the code

ParseText first constructs regular expressions for text matching according to delimiters (default: {{}}), then loops through the matching text, and pushes rawTokens and tokens into regular text. _s(${exp}) push tokens and {@binding:exp} push rawTokens.

For our example tokens are [_s(item),'”:”‘,_s(index)]; RawTokens is [{‘ @ binding ‘:’ item ‘}, ‘:’ {‘ @ binding ‘:’ index ‘}]. The returned object is as follows:

return {
 expression: '_s(item)+":"+_s(index)',
 tokens: [{'@binding':'item'},':', {'@binding':'index'}}]Copy the code

5. Two special cases

In the handleStartTag function there is this logic:

function handleStartTag (match{

  // ...

  

  if (expectHTML) {

    if (lastTag === 'p' && isNonPhrasingTag(tagName)) {

      parseEndTag(lastTag)

    }

    if (canBeLeftOpenTag(tagName) && lastTag === tagName) {

      parseEndTag(tagName)

    }

  }

  // ...

}

Copy the code

ExpectHTML is true in the Web environment, and there are two special cases where the test is expectHTML.

5.1 is a

First look at this condition lastTag = = = ‘p’ && isNonPhrasingTag (tagName), isNonPhrasingTag functions defined in SRC/platforms/web/compiler/util. Js:

// HTML5 tags https://html.spec.whatwg.org/multipage/indices.html#elements-3

// Phrasing Content https://html.spec.whatwg.org/multipage/dom.html#phrasing-content

export const isNonPhrasingTag = makeMap(

  'address,article,aside,base,blockquote,body,caption,col,colgroup,dd,' +

  'details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,' +

  'h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,' +

  'optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,' +

  'title,tr,track'

)

Copy the code

It can be seen that the above elements are not Phrasing elements and you can refer to this article for knowledge on this aspect.

To put it simply: all elements that can be put into the P tag and constitute the content of paragraph are Phrasing elements. What if the p tag had a non-phrasing element in it? Here are some examples:

<p><div>111</div></p>
Copy the code

In the browser it is processed like this:

So during compilation of Vue, parseEndTag is manually called to close the tag in compliance with the standard implementation, so

111

is equivalent to

111

.

So how did the last single

become

?

In fact, when parsed to

and parseEndTag is called, there is this logic:

else if (lowerCasedTagName === 'p') {

  if (options.start) {

    options.start(tagName, [], false, start, end)

  }

  if (options.end) {

    options.end(tagName, start, end)

  }

}

Copy the code

The else if logic that goes here when pos is less than 0 manually calls options.start and options.end to generate AST for start and end tags, which in our case is

.

5.2 case 2

Then look at the this condition canBeLeftOpenTag (tagName) && lastTag = = = tagName, canBeLeftOpenTag function is defined in SRC/platforms/web/compiler/util. Js

// Elements that you can, intentionally, leave open

// (and which close themselves)

export const canBeLeftOpenTag = makeMap(

  'colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source'

)

Copy the code

Take this example of a nested P tag:

<p>111<p>222</p></p>
Copy the code

In the browser, it will parse like this:

So during Vue compilation, parseEndTag is manually called to close the tag in order to comply with the standard implementation, so

111

222

is the same as

111

222

.

In the same way, the final single

becomes

as in case one.

conclusion