Compilation Principle (PART 1)

If you think it is good, please send me a Star at GitHub

Vue2.0 source code analysis: componentized (under) Next: Vue2.0 source code analysis: compilation principle (under)

Due to the word limit of digging gold article, we had to split the top and the next two articles.

introduce

As mentioned earlier, Vue provides different versions of vue.js package files for different use scenarios. The Runtime + Compiler version allows us to write components with the Template option, which can compile template. The Runtime + only version does not allow us to do this. The scaffolders we use vue-CLI3.0 and above create projects in the Runtime + only version by default. It relies on vue-loader to compile into the render function, not vue.js.

In this chapter of compilation principle, we mainly analyze vue.js of Runtime + Compiler version in order to deeply understand its internal implementation principle. This version has an entry file at SRC /platforms/web/entry-runtime-with-compiler.js. In this entry file, we can find that it not only redefines the $mount method, but also mounts a compile global API.

import { compileToFunctions } from './compiler/index'
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
) :Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) { process.env.NODE_ENV ! = ='production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if(! options.render) {let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) = = =The '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if(process.env.NODE_ENV ! = ='production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`.this)}}}else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if(process.env.NODE_ENV ! = ='production') {
          warn('invalid template option:' + template, this)}return this}}else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
        mark('compile')}const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV ! = ='production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
        mark('compile end')
        measure(`vue The ${this._name} compile`.'compile'.'compile end')}}}return mount.call(this, el, hydrating)
}

Vue.compile = compileToFunctions
Copy the code

The $mount method has been introduced separately in the componentalization chapter. In the compilation principle chapter, we divided it into three steps: Parse template parsing, optimize optimization and CodeGen code generation to learn the implementation principle in depth. That’s the implementation logic to compileToFunctions.

compileToFunctions

Compile core method

We know that the $mount method and the Vue.com compile global API both use compileToFunctions in the Runtime + compiler versions. In the Web platform, it is from the SRC/platforms/Web/compiler/index, introduced in js files, the code is as follows:

import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
Copy the code

In the above code, you can see that compileToFunctions are deconstructed from the call to the createCompiler method, which in turn is imported from the Compiler /index.js file. Compiler: SRC /compiler/index.js SRC /compiler/index.js

import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'

export const createCompiler = createCompilerCreator(function baseCompile (template: string, options: CompilerOptions) :CompiledResult {
  const ast = parse(template.trim(), options)
  if(options.optimize ! = =false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
Copy the code

We find the createCompiler, which in turn is the result of a call to the createCompilerCreator method, where we can see the definition of the baseCompile function parameters passed to it. In the baseCompile method, it’s not a lot of code, but it covers the three main steps of compilation: parse, optimize, and generate. This shows that baseCompile is our most core and basic compilation method.

So what is creator creator? How does it return a function? Let’s look at its implementation code:

export function createCompilerCreator (baseCompile: Function) :Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (template: string, options? : CompilerOptions) :CompiledResult {
      const finalOptions = Object.create(baseOptions)
      const errors = []
      const tips = []

      let warn = (msg, range, tip) = > {
        (tip ? tips : errors).push(msg)
      }

      if (options) {
        if(process.env.NODE_ENV ! = ='production' && options.outputSourceRange) {
          // $flow-disable-line
          const leadingSpaceLength = template.match(/^\s*/) [0].length

          warn = (msg, range, tip) = > {
            const data: WarningMessage = { msg }
            if (range) {
              if(range.start ! =null) {
                data.start = range.start + leadingSpaceLength
              }
              if(range.end ! =null) {
                data.end = range.end + leadingSpaceLength
              }
            }
            (tip ? tips : errors).push(data)
          }
        }
        // merge custom modules
        if (options.modules) {
          finalOptions.modules =
            (baseOptions.modules || []).concat(options.modules)
        }
        // merge custom directives
        if (options.directives) {
          finalOptions.directives = extend(
            Object.create(baseOptions.directives || null),
            options.directives
          )
        }
        // copy other options
        for (const key in options) {
          if(key ! = ='modules'&& key ! = ='directives') {
            finalOptions[key] = options[key]
          }
        }
      }

      finalOptions.warn = warn

      const compiled = baseCompile(template.trim(), finalOptions)
      if(process.env.NODE_ENV ! = ='production') {
        detectErrors(compiled.ast, warn)
      }
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }

    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}
Copy the code

Although the createCompilerCreator method is quite long, it should be very clear if we simplify it properly:

// Simplify the code
export function createCompilerCreator (baseCompile: Function) :Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile () {
      const compiled = baseCompile(template.trim(), finalOptions)
      return compiled
    }
    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}
Copy the code

See here we can string together the compileToFunctions of the $mount method and the compileToFunctions of the Vue.compile result object returned by the createCompiler call. The attribute value is createCompileToFunctionFn method calls it as a result, the parameter is defined in createCompiler a compile method, we further to see createCompileToFunctionFn code:

export function createCompileToFunctionFn (compile: Function) :Function {
  const cache = Object.create(null)

  return function compileToFunctions (template: string, options? : CompilerOptions, vm? : Component) :CompiledFunctionResult {
    options = extend({}, options)
    // ...
    // check cache
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }
    // ...
    // compile
    const compiled = compile(template, options)
    const res = {}
    const fnGenErrors = []
    res.render = createFunction(compiled.render, fnGenErrors)
    res.staticRenderFns = compiled.staticRenderFns.map(code= > {
      return createFunction(code, fnGenErrors)
    })
    return (cache[key] = res)
  }
}
Copy the code

In this approach, let’s simplify and focus on just a few pieces of code. We can find that in createCompileToFunctionFn approach we finally found the final definition of compileToFunctions method, its only a core code:

const compiled = compile(template, options)
Copy the code

Compile: baseCompile (); compile: baseCompile ();

function compile () {
  const compiled = baseCompile(template.trim(), finalOptions)
  return compiled
}
function baseCompile (template: string, options: CompilerOptions) :CompiledResult {
  const ast = parse(template.trim(), options)
  if(options.optimize ! = =false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
}
Copy the code

The code analysis

After introducing the core compile method, let’s examine the implementation logic for compileToFunctions:

  • CSP limit:CSPContent security policy, which we can use inMDNSee it on rightCSPThe definition, description, and some examples of “, “and we can see that it has the following description:

A policy consists of a series of policy directives, each of which describes a policy for a particular type of resource and its scope of effect. Your policy should include a default-src policy directive that applies when no other resource type matches its policy (see default-src for a complete list). A policy can include the default-src or script-src directives to prevent inline scripts from running and to eliminate the use of eval(). A policy can also contain a default-src or style-src directive to restrict inline styles from a style element or style attribute.

To learn more about CSPS, you can click on Content Security Policy to learn more about them.

Note: Vue only provides a specific CSP-compatible version in 1.0+. You can check out the source code for this version in the Vue Github branch repository.

As described above, we may not be able to use the text-to-javascript mechanism if there are some CSP restrictions, which means that the following code may not work:

const func = new Function('return 1')
evel('alert(1)')
Copy the code

In compileToFunctions we use try/catch to try to detect CSP restrictions on new Function(‘return 1’) and give an error message if they exist.

'It seems you are using the standalone build of Vue.js in an ' +
'environment with Content Security Policy that prohibits unsafe-eval. ' +
'The template compiler cannot work in this environment. Consider ' +
'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
'templates into render functions.'
Copy the code

If not, then it is safe to use text-to-javascript, which in compileToFunctions has the following core code:

const compiled = compile(template, options)
Copy the code

Compiled. Render is a string after the above code is executed, for example:

const compiled = {
  render: 'with(this){return 1}'
}
Copy the code

In the RES return object, compileToFunctions are assigned using this code:

const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code= > {
  return createFunction(code, fnGenErrors)
})
Copy the code

As you can see, both Render and staticRenderFns use createFunction. The main function of this method is to wrap a string of code into a function and return it:

function createFunction (code, errors) {
  try {
    return new Function(code)
  } catch (err) {
    errors.push({ err, code })
    return noop
  }
}
Copy the code

If the new Function doesn’t go wrong, then we return the anonymous Function, and if it does, we push the error message into the Errors array. Using the above example, it wraps like this:

/ / before packaging
const compiled = {
  render: 'with(this){return 1}'
}

/ / after encapsulation
const res = {
  render: function () { with(this){return 1}}}Copy the code
  • Core compilation: We introduced it earliercompileToFunctionsMethod, which contains only one core piece of code:
// Core code
const compiled = compile(template, options)

function compile () {
  const compiled = baseCompile(template.trim(), finalOptions)
  return compiled
}
function baseCompile (template: string, options: CompilerOptions) :CompiledResult {
  const ast = parse(template.trim(), options)
  if(options.optimize ! = =false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
}
Copy the code

When the core compilation method compile is executed, baseCompile is automatically executed together. When baseCompile is executed, it means that the three major steps of compilation begin. Because these three steps are complicated, we will cover them separately in the following chapters.

  • Compile cache: We should compile the same component only once when it is compiled. When the compilation is completed for the first time, we should cache the compilation result. The next time we encounter the same component and compile it again, we should get it from the cache first. If there is one in the cache, it will return directly, if there is not, we will go through the compilation process. The compile cache is implemented with the following code:
const cache = Object.create(null)
return function compileToFunctions (template: string, options? : CompilerOptions, vm? : Component) :CompiledFunctionResult {
  // ...
  const key = options.delimiters
    ? String(options.delimiters) + template
    : template
  if (cache[key]) {
    return cache[key]
  }
  // ...
  return (cache[key] = res)
}
Copy the code

Let’s take roots as an example:

import Vue from 'vue'
import App from './App'
new Vue({
  el: '#app'.components: { App },
  template: '<App/>'
})
Copy the code

After compiling the cache, the cache object is as follows:

const cache = {
  '<App/>': 'with(this) { xxxx }'
}
Copy the code

When the App component is recompiled, the key is already in the cache object, so it is returned.

Parse template parsing

In the baseCompile basic compilation method mentioned earlier, there is this code:

import { parse } from './parser/index'
function baseCompile (template: string, options: CompilerOptions) :CompiledResult {
  const ast = parse(template.trim(), options)
  // ...
}
Copy the code

As you can see, it calls the Parse method to compile the Template template. The result is an AST abstract syntax tree that will be used later. Our goal in this section is to understand how the Parse template is compiled.

AST Abstract syntax tree

In JavaScript AST

AST is the abbreviation of Abstract Syntax Tree, which is the tree-like representation of Abstract Syntax structure of source code. AST is found in many excellent open source libraries, such as Babel, Webpack, TypeScript, JSX and ESlint.

We won’t cover the AST too much here, but we’ll give examples of how to use it in JavaScript. Assuming we have the following method definition, we need to parse this code:

function add (a, b) {
  return a + b
}
Copy the code
  • For the whole code, it belongs to oneFunctionDeclarationFunction definition, so we can use an object to represent:
const FunctionDeclaration = {
  type: 'FunctionDeclaration'
}
Copy the code
  • We can layer the above function definition into three main parts:The function name,Function parametersAs well asThe body of the functionWhere they are used separatelyid,paramsAs well asbodyAdd these attributes to the function object as follows:
const FunctionDeclaration = {
  type: 'FunctionDeclaration'.id: {},
  params: [].body: {}}Copy the code
  • For the function nameidWe can’t split it anymore, because it’s already the smallest unit we can useIdentifierTo represent:
const FunctionDeclaration = {
  type: 'FunctionDeclaration'.id: {
    type: 'Identifier'.name: 'add'},... }Copy the code
  • For function parametersparamsIn terms of, we can view it as aIdentifierAn array of:
const FunctionDeclaration = {
  type: 'FunctionDeclaration'.params: [{type: 'Identifier'.name: 'a' },
    { type: 'Identifier'.name: 'b'}],... }Copy the code
  • For the body of the function, which is curly braces and what’s inside curly braces, we use firstBlockStatementTo represent curly braces, and then usebodyTo indicate the contents of the curly braces:
const FunctionDeclaration = {
  type: 'FunctionDeclaration'.body: {
    type: 'BlockStatement'.body: []}}Copy the code

In curly braces, we can have multiple pieces of code, so it’s an array. In our case, it returns the value of an expression using a return, which we can use as a return Statement, or a + b expression. For BinaryExpression, there are three attributes left(a), operator(+), and right(b). After introducing the above concepts, its full parsed object for the above example can be represented by the following object:

const FunctionDeclaration = {
  type: 'FunctionDeclaration'.id: { type: 'Identifier'.name: 'add' },
  params: [{type: 'Identifier'.name: 'a' },
    { type: 'Identifier'.name: 'b'}].body: {
    type: 'BlockStatement'.body: [{type: 'ReturnStatement'.argument: {
          type: 'BinaryExpression'.left: { type: 'Identifier'.name: 'a' },
          operator: '+'.right: { type: 'Identifier'.name: 'a'}}}]}}Copy the code

If you’re interested in parsing JavaScript into an AST, you can see an AST generated in real time from JavaScript code on the AST Explorer website.

The Vue AST

During the template compilation phase of Vue, it uses the createASTElement method to create the AST, which looks like this:

export function createASTElement (
  tag: string,
  attrs: Array<ASTAttr>,
  parent: ASTElement | void
) :ASTElement {
  return {
    type: 1,
    tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    rawAttrsMap: {},
    parent,
    children: []}}Copy the code

As you can see, the createASTElement method is simple and simply returns an object of type ASTElement, whose type definition is found in flow/ Compiler.js. There are many attributes, but we will only introduce a few:

declare type ASTElement = { type: 1; // Element type tag: string; // The element tag attrsList: Array<ASTAttr>; AttrsMap: {[key: string]: any}; / / element attribute key - value parent: ASTElement | void; // children: Array<ASTNode>; // Set of child elements}Copy the code

To better understand the AST generated by template compilation, let’s take the following template as an example:

Note: If you want to debug and view the compiled AST, you should use the Runtime + Compiler version. If the runtime + only version is used, the components will be processed by vue-Loader and will not be parsed.

new Vue({
  el: '#app',
  data () {
    return {
      list: ['AAA'.'BBB'.'CCC']}},template: ` 
      
  • {{item}}
`
}) Copy the code

In the baseCompile method, if we break the parse line, we can see the generated AST as follows:

// UL label AST Thin object
const ulAST = {
  type: 1.tag: 'ul'.attrsList: [{name: 'v-show'.value: 'list.length'}].attrsMap: {
    'v-show': "list.length"
  },
  parent: undefined.directives: [{name: 'show'.rawName: 'v-show'.value: 'list.length'}].children: [].// The AST object of li
}
// AST compact object of the li label
const liAST = {
  type: 1.tag: 'li'.alias: 'item'.attrsList: [].attrsMap: {
    'v-for': 'item in list'.'class': 'list-item'.':key': 'item'
  },
  for: 'list'.forProcessed: true.key: 'item'.staticClass: '"list-item"'.parent: {}, // Ul AST object
  children: [].// AST object of the text node
}
// AST compact object for text nodes
const textAST = {
  type: 2.expression: "_s(item)".text: "{{item}}".tokens: [{'@binding': 'item'}}]Copy the code

Based on the above AST objects of UL, LI and text nodes, a simple AST tree structure can be constructed by linking parent and children together.

HTML parser

When parsing a parse template, there are three different parsers depending on the situation: AN HTML parser, a text parser, and a filter parser. Among them, the HTML parser is the most important and core parser.

The whole idea

In the parse method, we can see that it calls the parseHTML method to compile the template, which is defined in the html-parser.js file:

export function parseHTML (html, options) {
  let index = 0
  let last, lastTag
  while (html) {
    // ...}}Copy the code

Because the parseHTML code is extremely complex, we don’t have to understand the meaning of each line of code. The whole idea is to intercept the HTML string through the string substring method until the whole HTML is parsed, that is, the while loop ends when the HTML is empty.

To better understand this while loop, let’s give an example:

// Variable and method definitions
let html = `<div class="list-box">{{msg}}</div>`
let index = 0
function advance (n) {
  index += n
  html = html.substring(n)
}

// The first interception
advance(4)
let html = ` class="list-box">{{msg}}</div>`

// The second interception
advance(17)
let html = `>{{msg}}</div>`

// ...

// The last interception
let html = `</div>`
advance(6)
let html = ` `
Copy the code

At the end of the last interception, the HTML becomes an empty string, and the while loop ends, which means that parsing the parse template is complete. In a while loop, there is a particular rule about where to cut a string. It essentially uses a regular expression to match a string. When certain conditions are met, the corresponding hook function is fired.

Hook function

We found that when the parseHTML method is called, it passes an object, Options, which contains hook functions that are automatically triggered when the HTML is parsed. These hook functions include:

parseHTML(template, {
  start () {
    // Start tag hook function
  },
  end () {
    // End the tag hook function
  },
  char () {
    // The text hook function
  },
  comment () {
    // Comment the hook function}})Copy the code

To better understand hook functions, let’s say we have the following template template:

<div>The text</div>
Copy the code

Analysis:

  • The start tag hook function: when the template starts parsing, it follows this code:
// Start tag:
const startTagMatch = parseStartTag()
if (startTagMatch) {
  handleStartTag(startTagMatch)
  if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
    advance(1)}continue
}
Copy the code

As far as the whole code logic is concerned, it looks at the result of the call to the parseStartTag method. If the condition is true, it calls the handleStartTag method, where it calls the options.start hook function.

function handleStartTag () {
  // ...
  if (options.start) {
    options.start(tagName, attrs, unary, match.start, match.end)
  }
}
Copy the code

Let’s go back to the parseStartTag method, which looks like this:

import { unicodeRegExp } from 'core/util/lang'
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}] * `
const qnameCapture = ` ((? :${ncname}\ \ :)?${ncname}) `
const startTagOpen = new RegExp(` ^ <${qnameCapture}`)

function parseStartTag () {
  const start = html.match(startTagOpen)
  if (start) {
    const match = {
      tagName: start[1].attrs: [].start: index
    }
    advance(start[0].length)
    let 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
    }
  }
}
Copy the code

At the very beginning of the parseStartTag method, it uses the match method and passes a regular expression that matches the start tag, returning an object if the match is successful. At this stage, we don’t need to focus too much on the details of the parseStartTag method, just two things to know:

  1. When the start tag is matched successfully, an object is returned.
  2. During the match, is calledadvanceMethod to intercept the start tag.
/ / before invoking it
let html = Text: '< div > < / div >'

/ / call
parseStartTag()

/ / after the call
let html = 'text < / div >'
Copy the code
  • Text hook function: passes after intercepting the start tagcontinueGo for the second timewhileCycle, at this pointtextEndWill be reevaluated:
let html = 'text < / div >'
let textEnd = html.indexOf('<') / / 2
Copy the code

Since the second while loop has a textEnd value of 2, the following logic is used:

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

When the above code completes the while loop, the text value is text, and then advence is called and the chars hook function is triggered.

/ / before the interception
let html = 'text < div >'

/ / interception
advence(2)

/ / after the interception
let html = '<div>'
Copy the code
  • The closing tag hook function: after the text is truncated, it begins the next loop, re-alignmenttextEndEvaluate:
let html = '</div>'
let textEnd = html.indexOf('<') / / 0
Copy the code

When textEnd is 0, the following logic is used:

import { unicodeRegExp } from 'core/util/lang'
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}] * `
const qnameCapture = ` ((? :${ncname}\ \ :)?${ncname}) `
const endTag = new RegExp(` ^ < \ \ /${qnameCapture}[^ >] * > `)

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

When a regular expression that matches an end tag is passed with match, an object is returned if the match is successful and the advance and parseEndTag methods are called. When advence is called, the closing tag is truncated and the HTML is an empty string. In the parseEndTag method, it calls the options.end hook function.

function parseEndTag () {
  // ...
  if (options.end) {
    options.end(tagName, start, end)
  }
}
Copy the code
  • Comment hook functions: ForHTMLFor the comment node, it follows the logic of this code:
// Comment an example of a node
let html = '<! -- Comment node -->'
// 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}}Copy the code

When a comment node is matched, the Options.com ment hook function is triggered and advence is called to intercept the comment node. For what the COMMENT hook function does, it’s pretty simple:

comment (text: string, start, end) {
  // adding anything 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)
  }
}
Copy the code

As you can see from the code above, when this hook function is triggered, it simply generates an AST object for the annotation node and then pushes it into its parent’s Children array.

Different parsing types

In this section on hook functions, we have already encountered several different parsing types: start tags, end tags, comment tags, and text tags. The first few are the most common when parsing HTML templates, but there are several other parsing types that we need to understand as well.

  1. The start tag
  2. End tag
  3. Text labels
  4. Comment tags
  5. DOCTYPE
  6. Conditional comment tag

Since we’ve already looked at the first four parsing types, let’s look at the last two.

DOCTYPE

When parsing a DOCTYPE, the parser needs to do nothing more complicated than intercept it. It doesn’t need to trigger the corresponding hook function or do anything else. Suppose we have an HTML template like this:

let html = ` 
           `
Copy the code

The HTML template parser uses regular expressions to match the DOCTYPE when parsing, which follows the logic of this code.

const doctype = / ^ 
      ]+>/i
// Doctype:
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
  advance(doctypeMatch[0].length)
  continue
}
Copy the code

If the re match is successful, the call to Advence intercepts it, and continue continues the while loop. In the above example, the truncated DOCTYPE values are as follows:

let html = `     `
Copy the code
Conditional comment tag

Conditional comment tags are the same as docTypes. We don’t need to do anything extra, just intercept them. Suppose we have a template for comment tags like this:

let html = ` 
       
       
       `
Copy the code

When the HTML parser parses it, it follows the logic of this code.

const conditionalComment = / ^ 
      
if (conditionalComment.test(html)) {
  const conditionalEnd = html.indexOf('] > ')

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

When the HTML parser first executes, the conditional comment is matched by its regular expression and then intercepted by a call to Advence. As for the link in the middle, it goes through the normal tag parsing process. During the last parsing, it encounters the closed tag of the conditional comment tag, which also meets its regular expression. Then, it is intercepted by Advence.

// After the first parsing
let html = ` 
       
       `
// ...

// Last parse
let html = ` 
      `
advence(n)
let html = ` `
Copy the code

Note: From the above analysis of the conditional comment parsing process, we can conclude that it is useless to write a conditional comment statement in the Vue template, because it will be truncated.

<! <template> <div> <! [if !IE]> <link href="xxx.css" rel="stylesheet"> <! [endif]> <p>{{msg}}</p> </div> </template> <! - the parsed HTML -- > < div > < link href = "XXX. CSS" rel = "stylesheet" > < / p > < p > XXX < / div >Copy the code

DOM hierarchy maintenance

We all know that HTML is a DOM tree structure, and we need to maintain this DOM hierarchy properly when parsing templates. In Vue, it defines an array of stacks to implement. This stack array not only helps us maintain the DOM hierarchy, but also helps us do other things.

So how does Vue maintain this relationship with stack arrays? To maintain this DOM hierarchy, you need to work with the two hook functions mentioned earlier: the start tag hook function and the end tag hook function. The implementation idea is: when the start tag hook function is triggered, the current node is pushed into the stack array; When the closing tag hook function is triggered, the top element of the stack array is pushed out.

To better understand this, let’s say we have the following template template and stack array:

const stack = []
let html = ` 
      

`
Copy the code

Analytical process analysis:

  • When the start tag hook function is triggered for the first time, i.edivThe node’s start tag hook function, which needs to push the current node instackStack array:
// For example, it is an AST object
const stack = ['div']
Copy the code
  • When the start tag hook function is triggered the second time, i.epThe node’s start tag hook function, which needs to push the current node instackStack array:
// For example, it is an AST object
const stack = ['div'.'p']
Copy the code
  • When the closing tag hook function is triggered for the first time, i.epThe end tag of the node is the hook function, which needs to push the top element out of the stackstackStack array:
const stack = ['div']
Copy the code
  • When the start tag hook function is triggered for the third time, i.espanThe node’s start tag hook function, which needs to push the current node instackStack array:
// For example, it is an AST object
const stack = ['div'.'span']
Copy the code
  • When the closing tag hook function is triggered the second time, i.espanThe end tag of the node is the hook function, which needs to push the top element out of the stackstackStack array:
const stack = ['div']
Copy the code
  • When the closing tag hook function is triggered for the third time, i.edivThe end tag of the node is the hook function, which needs to push the top element out of the stackstackStack array:
const stack = []
Copy the code

After analyzing the above parsing process, let’s take a look at the source code in the hook function, how to handle:

parseHTML(template, {
  start (tag, attrs, unary, start, end) {
    // ...
    let element: ASTElement = createASTElement(tag, attrs, currentParent)
    // ...
    if(! unary) { currentParent = element stack.push(element) }else {
      closeElement(element)
    }
  },
  end (tag, start, end) {
    const element = stack[stack.length - 1]
    // pop stack
    stack.length -= 1
    // ...
    closeElement(element)
  }
})
Copy the code

Code analysis:

  • start: first of all instartAt the end of the hook function, it has a sectionif/elseBranch logic, inifIn the branch it goes straight toelementPush thestackStack array, while inelseCalled in branch logiccloseElementMethods. The key to the existence of this logical branch isunaryParameter, thenunaryWhat is it? Since it isstartThe argument to the hook function is passed in the same place that the hook function is calledhandleStartTagA constant defined in the method:
export const isUnaryTag = makeMap(
  'area,base,br,col,embed,frame,hr,img,input,isindex,keygen,' +
  'link,meta,param,source,track,wbr'
)
const options.isUnaryTag = isUnaryTag
const isUnaryTag = options.isUnaryTag || no
constunary = isUnaryTag(tagName) || !! unarySlashCopy the code

Unary stands for unary. It can be found that during the assignment of isUnaryTag constant, all parameter labels passed to makeMap are self-closing labels. For these self-closing tags, we can fire the opening tag hook function, but not the closing tag hook function, so if the current tag is a self-closing tag, we need to manually handle what the closing tag hook function does by calling closeElement in the else branch logic. I don’t have to push it into the stack array.

  • endWhen the end tag hook function is triggered, it does nothing complicated: first fetch the top element of the stack, then add the stack arraylengthThe length of the minus1To push out the top element of the stack, and finally calledcloseElementMethods to deal with the follow-up. Due to thecloseElementMethod is a lot of code, and we don’t need to understand it all. instackStack array maintenanceDOMAll we need to know in the hierarchy section is thatcloseElementMethod, it will do the right thingASTThe object’sparentandchildrenProperties.

As mentioned earlier, the stack array not only helps us maintain the DOM hierarchy, it also helps us to check that the element tag is closed properly, and if it is not closed properly, an error message will be displayed. Suppose we have the following template template:

// The p label is not closed correctly
let html = `<div><p></div>`
Copy the code

When we provide the above error HTML template, Vue will not only prompt us with the following error message, but also automatically help us to close the P tag:

tag <p> has no matching end tag.
Copy the code

So how does Vue find this error? How do you close it? When the p node’s start tag hook function is triggered, the stack array looks like this:

// For example, it is an AST object
const stack = ['div'.'p']
Copy the code

Since the p tag is not closed, the following code logic is executed when the div node’s closing tag hook function is triggered:

function parseEndTag (tagName, start, end) {
  // ...
  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
  }
  // ...
}
Copy the code

In the first for loop, it finds the positional index of the div node in the stack array, which in the case of the previous example has a pos value of 0. And then in the second for loop, we find that there are other elements from the top of the stack to index 0. This means that there must be an element tag that is not closed properly, so an error message is raised and the options.end hook function is triggered. In the end hook function, close the P tag manually with closeElement.

Attribute resolution

In all of the previous sections, we didn’t mention how the Parse template parses properties. In this section, we’ll take a closer look at how properties are resolved.

To better understand the principle of attribute resolution, let’s give an example. Suppose we have the following template template:

const boxClass = 'box-red'
let html = '
      
Copy the code

When the

// Start tag:
const startTagMatch = parseStartTag()
if (startTagMatch) {
  handleStartTag(startTagMatch)
  if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
    advance(1)}continue
}
Copy the code

In the above code, we need to pay attention to two methods, one is parseStartTag and the other is handleStartTag. Let’s start with the parseStartTag method, which has a while loop in which attrs is matched and processed as follows:

function parseStartTag () {
  const match = {
    tagName: start[1].attrs: [].start: index
  }
  advance(start[0].length)
  let 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)
  }
}
Copy the code

Code analysis:

  • On the first calladvanceWhen, will put<divI’m going to cut it off. I’m going to cut it offhtmlValues are as follows:
let html = 'id="box" class="box-class" :class="boxClass"> 
Copy the code

Then, two regular expressions are matched in the condition of the while loop:

const attribute = /^\s*([^\s"'<>\/=]+)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /
const dynamicArgAttribute = /^\s*((? :v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /
Copy the code

As you can see from the naming, one is for matching dynamic properties, and one is for matching properties.

  • inwhileIn the judgment condition, it will first matchidProperty, condition true, first executionwhileCycle. inwhileIn the loop, it doesn’t just calladvanceMethods the id="box"The string is truncated and the result of the match is added tomatch.attrsArray, first timewhileAfter the loop completes, the result is as follows:
let html = 'class="box-class" :class="boxClass"> 
const match = {
  tagName: 'div'.attrs: [[' id="box"'.'id'.'='.'box']]}Copy the code
  • In the second judgmentwhileWhen conditions are metidSame match toclassProperty, second timewhileAfter the loop completes, the result is as follows:
let html = ':class="boxClass"> '
const match = {
  tagName: 'div'.attrs: [[' id="box"'.'id'.'='.'box'],
    [' class="box-class"'.'class'.'='.'box-class']]}Copy the code
  • In the third judgmentwhileConditional, it matches the dynamic property, this time roundwhileAfter executing the loop, the result is as follows:
let html = '> attribute resolution '
const match = {
  tagName: 'div'.attrs: [[' id="box"'.'id'.'='.'box'],
    [' class="box-class"'.'class'.'='.'box-class'],
    [' :class="boxClass"'.':class'.'='.'boxClass']]}Copy the code
  • whileAfter the loop completes, it decidesend, includingendWas the last timewhileIt is used when judging cyclic conditionsstartTagCloseThe result of a regular expression match. In the example above, it matched successfully>, so goifBranching logic.
const startTagClose = /^\s*(\/?) >/
let html = 'Attribute resolution '
Copy the code

Having analyzed parseStartTag, let’s go back to the handleStartTag method, which uses a for loop to iterate over mate. attrs and then format attrs as follows:

function handleStartTag (match) {
  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)
    }
    if(process.env.NODE_ENV ! = ='production' && options.outputSourceRange) {
      attrs[i].start = args.start + args[0].match(/^\s*/).length
      attrs[i].end = args.end
    }
  }

  // ...
  if (options.start) {
    options.start(tagName, attrs, unary, match.start, match.end)
  }
}
Copy the code

In the handleStartTag method, attrs is mainly normalized to a two-dimensional array of objects in the form of name/value. After the for loop is completed, attrs array results are as follows:

const attrs = [
  { name: 'id'.value: 'box' },
  { name: 'class'.value: 'box-class' },
  { name: ':class'.value: 'boxClass'}]Copy the code

Once attrs is normalized, we need to create an AST object in the start hook function. Let’s review the createASTElement method:

export function createASTElement (
  tag: string,
  attrs: Array<ASTAttr>,
  parent: ASTElement | void
) :ASTElement {
  return {
    type: 1,
    tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    rawAttrsMap: {},
    parent,
    children: []}}Copy the code

The code above is fairly simple, but the only thing worth paying attention to is the makeAttrsMap method, which is implemented as follows:

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

The makeAttrsMap method converts an array of name/value objects into a key/value object, for example:

const arr = [
  { name: 'id'.value: 'box' },
  { name: 'class'.value: 'box-class'}]const obj = makeAttrsMap(arr) // { id: 'box', class: 'box-class' }
Copy the code

After the makeAttrsMap method is introduced, the AST objects generated are as follows:

const ast = {
  type: 1.tag: 'div'.attrsList: [{name: 'id'.value: 'box' },
    { name: 'class'.value: 'box-class' },
    { name: ':class'.value: 'boxClass'}].attrsMap: {
    id: 'box'.class: 'box-class'To:class: 'boxClass'},rawAttrsMap: {},
  parent: undefined.children: []}Copy the code

Command parsing

After analyzing the principle of attribute resolution, let’s look at the instruction resolution process which is very similar to it. In this section, we look at two very representative instructions: V-if and V-for.

Suppose we have the following template template:

const list = ['AAA'.'BBB'.'CCC']
let html = ` 
      
  • {{item}}
`
Copy the code

For instruction parsing, they are very similar to the ATTS parsing process, because in the dynamicArgAttribute regular expression, it supports matching instructions:

const dynamicArgAttribute = /^\s*((? :v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /
Copy the code

After the parseStartTag method is executed, the attrs value of the UL tag is as follows:

const match = {
  attrs: [' v-if="list.length"'.'v-if'.'='.'list.length']}Copy the code

After the handleStartTag method is executed, the normalized value of attrs for the UL tag is as follows:

const attrs = [
  { name: 'v-if'.value: 'list.length'}]Copy the code

After the createASTElement method is called, the AST object for the UL tag is:

const ast = {
  type: 1.tag: 'ul'.attrsList: [{name: 'v-if'.value: 'list.length'}].attrsMap: {
    v-if: 'list.length'},... }Copy the code

One more step than attribute resolution, for the V-if directive, it calls the processIf method to handle the V-IF directive after the AST object is created, which looks like this:

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

As you can see, the processIf method handles not only v-if instructions, but also V-else/V-else -if instructions. For v-IF, it adds ifConditions to the AST object by calling addIfCondition. When processIf completes, the AST object’s latest value is:

const ast = {
  type: 1.tag: 'ul'.attrsList: [{name: 'v-if'.value: 'list.length'}].attrsMap: {
    v-if: 'list.length'
  },
  if: 'list.length'.ifConditions: [{exp: 'list.length'.block: 'AST object itself',}],... }Copy the code

The parsing process of the V-for instruction is basically the same as that of v-IF, except that v-IF uses processIf and V-for uses processFor.

For parsing li tags, before the processFor method is called, the AST object is:

const ast = {
  type: 1.tag: 'li'.attrsList: [{name: 'v-for'.value: '(item, index) in list' },
    { name: ':key'.value: 'index'}].attrsMap: {
    v-for: '(item, index) in list',
    :key: 'index'},... }Copy the code

Next, let’s look at the processFor method, which looks like this:

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 is called to remove v-for from the AST object’s attrsList property array and return its value, that is, (item, index) in list. Then use the parseFor method to parse the string as follows:

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

In the example above, after parseFor is used to parse the value, the res object has the following values:

const res = {
  alias: 'item'.iterator1: 'index'.for: 'list'
}
Copy the code

And then we extend this object to the AST object using the extend method, which we’ve talked about before and we won’t go over here. After calling the processFor method, the latest value of the latest AST object looks like this:

const ast = {
  type: 1.tag: 'li'.attrsList: [{name: ':key'.value: 'index'}].attrsMap: {
    v-for: '(item, index) in list',
    :key: 'index'
  },
  alias: 'item'.iterator1: 'index'.for: 'list'. }Copy the code

Text parser

For text, there are usually two ways to write text when developing Vue applications:

/ / plain text
let html = '
      
Plain text
'
// Text with variables const msg = 'Hello, Vue.js' let html = '<div>{{msg}}</div>' Copy the code

Next, we will introduce them separately in these two ways.

Plain text

As we explained earlier, after the first while loop completes, the HTML value is as follows:

let html = 'Plain text '
Copy the code

On the second execution of the while loop, the text regex will be matched, triggering the options.chars hook function, where we only need to focus on the following code:

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
  }
}
if (child) {
  if(process.env.NODE_ENV ! = ='production' && options.outputSourceRange) {
    child.start = start
    child.end = end
  }
  children.push(child)
}
Copy the code

As you can see, in the if/else branch logic, it creates different types of children based on the value judged by the condition. Type =2 indicates the AST with variable text, and type=3 indicates the AST with plain text. The key logic to distinguish which type to create is in the parseText method, which looks like this:

const defaultTagRE = / \ {\ {((? :.|\r? \n)+?) \}\}/g
export function parseText (text: string, delimiters? : [string, string]) :TextParseResult | void {
  const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
  if(! tagRE.test(text)) {return
  }
  // omit handles text logic with variables
}
Copy the code

Delimiters, {{}} double curly braces by default if we do not pass it in, can be specified using vue.config. delimiters. Obviously, it doesn’t match for plain text, so just return to terminate the parseText method. That is, it does the else if branch to create a plain text AST object of Type =3, and finally pushes this object into the children array of the parent AST.

Text with variables

ParseText: parseText: parseText: parseText: parseText: parseText

export function parseText (text: string, delimiters? : [string, string]) :TextParseResult | void {
  const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE

  // omit plain text logic
  
  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

As you can see in the parseText method, it returns an object at the end of the method with two attributes: expression and tokens. The code before the return is mainly to parse interpolated text.

At the beginning of the while loop, we first define two key arrays: tokens and rawTokens. We then execute the while loop, and the condition of the while loop determines whether the {{}} double curly braces or our custom delimiter can still be matched. We do this because we can write multiple interpolated text, such as:

let html = '<div>{{msg}}{{msg1}}{{msg2}}</div>'
Copy the code

In the while loop, the element we push into the tokens array has one feature: _s(exp), where _S () is short for toString(), and exp is the parsed variable name. The element pushed into the rawTokens array is even simpler. It is an object with a fixed @binding and the value is exp that we parse out.

For the example we wrote, after the while loop completes, the objects returned together with the parseText method are as follows:

// After the while loop completes
const tokens = ['_s(msg)']
const rawTokens = [{ '@binding': 'msg' }]

// parseText returns an object
const returnObj = {
  expression: '_s(msg)'.tokens: [{ '@binding': 'msg'}}]Copy the code

Since parseText returns an object, follow the if branch and create an AST object of type=2:

const ast = {
  type: 2.expression: '_s(msg)'.tokens: [{ '@binding': 'msg'}].text: '{{msg}}'
}

// Add to the children array of the parent
parent.children.push(ast)
Copy the code

Abnormal situation

The logic for parsing text is simple enough, but sometimes the parse template fails when the text is written in the wrong place. For example, we have the following template template:

// Template is plain text
let html1 = ` Hello, Vue.js `

// The text is written outside the root node
let html2 = Text 1 
      
Text 2
'
Copy the code

In both cases, they throw the following error message on the console:

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

'text "xxx" outside root element will be ignored.'
Copy the code

For the second error, the text we write outside the root node is ignored. For both types of error handling, in the options.chars hook function, the code looks like this:

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
Copy the code

Filter parser

When writing interpolated text, Vue allows us to use filters, such as:

const reverse = (text) = > {
  return text.split(' ').reverse.join(' ')}const toUpperCase = (text) = > {
  return text.toLocaleUpperCase()
}
let html = '<div>{{ msg | reverse | toUpperCase }}</div>'
Copy the code

You may be wondering how parse handles filters when it compiles. In fact, the filter parsing is done in the parseText method, which we deliberately neglected to cover in the previous section. In this section, we will look in detail at how filters are parsed.

For text parsers, the parseText method is defined in the text-parser.js file. For filter parsers, the parseFilters method is defined in the filter-parser.js file that is the same as the parseFilters method.

export function parseFilters (exp: string) :string {
  let inSingle = false
  let inDouble = false
  let inTemplateString = false
  let inRegex = false
  let curly = 0
  let square = 0
  let paren = 0
  let lastFilterIndex = 0
  let c, prev, i, expression, filters

  for (i = 0; i < exp.length; i++) {
    prev = c
    c = exp.charCodeAt(i)
    if (inSingle) {
      if (c === 0x27&& prev ! = =0x5C) inSingle = false
    } else if (inDouble) {
      if (c === 0x22&& prev ! = =0x5C) inDouble = false
    } else if (inTemplateString) {
      if (c === 0x60&& prev ! = =0x5C) inTemplateString = false
    } else if (inRegex) {
      if (c === 0x2f&& prev ! = =0x5C) inRegex = false
    } else if (
      c === 0x7C && // pipe
      exp.charCodeAt(i + 1)! = =0x7C &&
      exp.charCodeAt(i - 1)! = =0x7C&&! curly && ! square && ! paren ) {if (expression === undefined) {
        // first filter, end of expression
        lastFilterIndex = i + 1
        expression = exp.slice(0, i).trim()
      } else {
        pushFilter()
      }
    } else {
      switch (c) {
        case 0x22: inDouble = true; break         // "
        case 0x27: inSingle = true; break         / / '
        case 0x60: inTemplateString = true; break / / `
        case 0x28: paren++; break                 / / (
        case 0x29: paren--; break                 // )
        case 0x5B: square++; break                / / /
        case 0x5D: square--; break                // ]
        case 0x7B: curly++; break                 / / {
        case 0x7D: curly--; break                 // }
      }
      if (c === 0x2f) { // /
        let j = i - 1
        let p
        // find first non-whitespace prev char
        for (; j >= 0; j--) {
          p = exp.charAt(j)
          if(p ! = =' ') break
        }
        if(! p || ! validDivisionCharRE.test(p)) { inRegex =true}}}}if (expression === undefined) {
    expression = exp.slice(0, i).trim()
  } else if(lastFilterIndex ! = =0) {
    pushFilter()
  }

  function pushFilter () {
    (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim())
    lastFilterIndex = i + 1
  }

  if (filters) {
    for (i = 0; i < filters.length; i++) {
      expression = wrapFilter(expression, filters[i])
    }
  }

  return expression
}
Copy the code

Code analysis:

  • whenparseFiltersWhen it’s called,expThe argument passes the entire text content, which in our case has the value:
const exp = '{{ msg | reverse | toUpperCase }}'
Copy the code

The main purpose of the for loop is to process exp and assign it to the expression variable, which in our case has the value:

const expression = 'msg | reverse | toUpperCase'
Copy the code
  • whenforWhen the loop completes, at this pointexpressionValue judged to be true, callpushFilterMethods. When performing thepushFilterMethods after,filtersThe values of the array are as follows:
const filters = ['reverse'.'toUpperCase']
Copy the code
  • Finally judgedfiltersIf true, traversal if truefiltersArray, called during each iterationwrapFilterAgain processingexpression.wrapFilterThe method code is as follows:
function wrapFilter (exp: string, filter: string) :string {
  const i = filter.indexOf('(')
  if (i < 0) {
    // _f: resolveFilter
    return `_f("${filter}"),${exp}) `
  } else {
    const name = filter.slice(0, i)
    const args = filter.slice(i + 1)
    return `_f("${name}"),${exp}${args ! = =') ' ? ', ' + args : args}`}}Copy the code

For our example, the wrapFilter method will follow the if branch, and the else branch will follow when we write the filter as follows.

let html = '<div>{{ msg | reverse() | toUpperCase() }}</div>'
Copy the code

After the wrapFilter method is executed, the expression variable has the following values:

const expression = '_f("toUpperCase")(_f("reverse")(msg))'
Copy the code

Since the content of the filter is also text, the final difference text is wrapped in _s.

const tokens = ['_s(_f("toUpperCase")(_f("reverse")(msg)))']
Copy the code

Note: we will explain what the _f function is in a later section.

If you think it is good, please send me a Star at GitHub

Vue2.0 source code analysis: componentized (under) Next: Vue2.0 source code analysis: compilation principle (under)

Due to the word limit of digging gold article, we had to split the top and the next two articles.