Vue3 source code internal reference <3> Compiler-core core

Vue3 source code internal reference <3>Compiler-core core

Previous articles (all have corresponding source code)

  • Vue3 source code internal reference <2> Reactivity core
  • Vue3 source code internal reference <1> Handwritten mini-vue3 pre-preparation

The address of the code demonstration in this section is more appetizing to eat with the source code!

  • The code is implemented with reference to mini-vue, if you are interested, you can also see the original author’s implementation

Prerequisite knowledge

Both compiler-core and compiler-dom are compiler modules in Vue3, but their functions and application scenarios are slightly different.

  • The compiler-core module is the core implementation of the Vue3 compiler, which is responsible for compiling templates into rendering functions. It includes some basic compiler functions, such as AST generation, instruction and expression processing, and optimization and code generation etc.

  • The compiler-core module can run in various JavaScript environments, not limited to the browser environment, so it can be used to develop cross-platform applications based on Vue3, such as desktop applications, mobile applications wait.

  • The compiler-dom module is the implementation of the Vue3 compiler in the browser environment. It extends the functions of the compiler-core module to meet the special needs of the browser environment. For example, compiling properties and events of DOM elements.

  • The code generated by the compiler-dom module is executed directly in the browser, so it generates code specific to the browser environment.

In short, the difference between the compiler-core module and the compiler-dom module is that the former is the core implementation of the Vue3 compiler, and the latter is the extension implementation in the browser environment. Mainly used to compile templates into code that can be executed directly in the browser.

Specific operation: the compiler-core module generates ast from template through parse, and then processes instruction and expression processing, optimization and code generation in ast through compile. After generating render, it processes vdom through patch and diff to generate Interface UI.

mini-vue minimal implementation of parse module in compiler-core

  • Parse {{}} interpolation expressions
  • Parse text text
  • Parsing tags

Reminder Download the source code of this section, and debug it step by step through jest debug, which makes it easier to understand the function points

Encapsulate public functions

// template is a string, after the parser parses and generates ast, it needs to consume the parsed characters
function advanceBy(context, length) {<!-- -->
    context.source = context.source.slice(length);
}

// Builder
export function baseParse(content) {<!-- -->
    const context = createParserContent(content)
    return createRoot(parseChildren(context, []))
}

function createRoot(children) {<!-- -->
    return {<!-- --> children, type: NodeTypes.ROOT }
}

function createParserContent(content) {<!-- -->
    return {<!-- -->
        source: content
    }
}

// Analyze the judgment conditions of the template loop
function isEnd(context, ancestors) {<!-- -->
    // 1. When the source has a value
    // 2. When encountering the end tag
    const s = context.source;
    if (s. startsWith('</')) {<!-- -->
        for (let i = ancestors. length - 1; i >= 0; i--) {<!-- -->
            const tag = ancestors[i].tag;
            if (startsWithEndTagOpen(s, tag)) {<!-- -->
                return true;
            }
        }
    }
    return !s;
}

Parse {{}}

function parseChildren(context, ancestors) {<!-- -->
    const nodes: any = []
    while (!isEnd(context, ancestors)) {<!-- -->
        let node
        const s = context.source;
        if (s.startsWith('{<!-- -->{')) {<!-- -->
            node = parseInterpolation(context)
        }
        nodes. push(node)
    }
    return nodes
}

function parseInterpolation(context) {<!-- -->
    // {<!-- -->{message}}
    // The advantage of taking out the definition is that if it needs to be changed, the change will be very small
    const openDelimiter = '{<!-- -->{'
    const closeDelimiter = '}}'

    // We need to know where to close
    // indexOf means search }} starting from 2
    const closeIndex = context.source.indexOf(
        closeDelimiter,
        openDelimiter. length
    )

    // delete the first two strings
    // context.source = context.source.slice(openDelimiter.length)
    advanceBy(context, openDelimiter. length)

    // The length of the content is equal to the length of closeIndex - openDelimiter
    const rawContentLength = closeIndex - openDelimiter.length
    const rawContent = parseTextData(context, rawContentLength)
    const content = rawContent. trim()

    // Then you need to delete this string. The template is a string, and you need to traverse the following content
    // context.source = context.source.slice(rawContentLength + closeDelimiter.length);
    advanceBy(context, closeDelimiter. length)

    return {<!-- -->
        type: NodeTypes. INTERPOLATION,
        content: {<!-- -->
            type: NodeTypes. SIMPLE_EXPRESSION,
            content
        }
    }
}

Parse text

function parseChildren(context, ancestors) {<!-- -->
    const nodes: any = []
    while (!isEnd(context, ancestors)) {<!-- -->
        let node
        const s = context.source;
        // parse interpolation
        if (s.startsWith('{<!-- -->{')) {<!-- -->
            node = parseInterpolation(context)
        }
        // parse text
        if (!node) {<!-- -->
            node = parseText(context);
        }

        nodes. push(node)
    }
    return nodes
}
function parseText(context) {<!-- -->
    const endToken = ['{<!-- -->{', '</'] // If the stop conditions exist at the same time, then the index should be left as far as possible to the smallest
    let endIndex = context.source.length // stop index
    for (let i = 0; i < endToken. length; i ++ ) {<!-- -->
        const index = context.source.indexOf(endToken[i])
        if (index !== -1 & amp; & amp; endIndex > index) {<!-- -->
            endIndex = index
        }
    }

    // Before parsing the text, it was intercepted from the beginning to the end, but the real environment is that there will be other types of elements after the text, so it is necessary to specify the stop position
    const content = parseTextData(context, endIndex)

    return {<!-- -->
        type: NodeTypes. TEXT,
        content
    }
}

Parse tags

import {<!-- --> NodeTypes } from "./ast";

const enum TagType {<!-- -->
    Start,
    end
}

function parseChildren(context, ancestors) {<!-- -->
    const nodes: any = []
    while (!isEnd(context, ancestors)) {<!-- -->
        let node
        const s = context.source;
        if (s.startsWith('{<!-- -->{')) {<!-- -->...
        } else if (s[0] === "<") {<!-- -->
            // Need to use regular expression to judge
            // <div></div>
            // /^<[a-z]/i/
            if (/[a-z]/i.test(s[1])) {<!-- -->
                node = parseElement(context, ancestors);
            }
        }
        nodes. push(node)
    }
    return nodes
}

function parseElement(context, ancestors) {<!-- -->
    // parse tags
    const element: any = parseTag(context, TagType. Start)
    ancestors. push(element)
    // After getting the tags, you need to save the internal elements, and you need to traverse the internal elements recursively
    element.children = parseChildren(context, ancestors)
    ancestors. pop()

    // Here you need to judge whether the start tag and the end tag are consistent, and return if you can't directly consume
    if (startsWithEndTagOpen(context.source, element.tag)) {<!-- -->
        parseTag(context, TagType. End)
    } else {<!-- -->
        throw new Error(`Missing end tag: ${<!-- -->element.tag}`)
    }

    return element
}

function parseTag(context, type) {<!-- -->
    // <div></div>
    // match parsing
    // advance
    const match: any = /^<\/?([a-z]*)/i.exec(context.source)
    const tag = match[1]
    // Advance after acquisition
    advanceBy(context, match[0].length)
    advanceBy(context, 1);

    if (type === TagType. End) return
    return {<!-- -->
        type: NodeTypes. ELEMENT,
        tag
    }
}

function startsWithEndTagOpen(source, tag) {<!-- -->
    // It only makes sense to start with a left parenthesis and also needs to be converted to lowercase for comparison
    return (
        source.startsWith("</") & amp; & amp;
        source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase()
    );
}

A brief description of the implementation steps

  1. source =

    , and maintain a label into and out of the stack ancestors

  2. Strings are processed recursively. The isEnd function returns a boolean to indicate whether an end tag has been encountered. If a stage is encountered, there is no need to recursively process the string.
  3. Call the parseElement function and parseTag function to take out the tag name
  4. tag = div, then the string advances, source =

, ancestors = [{tag:'div'}]

  • Then after the first processing, it returns to 2 to process the next character, and isEnd finds that it is the end tag
  • It will traverse the ancestors array and traverse in reverse order
  • Take out each comparison, it will return true when compared, and there will be no recursion, and take out the last element of ancestors
  • Determine whether the start tag and end tag are consistent (compare the popped tag with the processed source name). If they are the same, the string will be consumed, and if they are different, an error will be reported.
  • END

    In this article, the function of parsing interpolation, text and labels in the parse module in the compiler is simply realized. Detailed content Portal, if you find it useful, please give a star. In addition, the author believes that before learning the source code, you can simply implement some basic functions first, and then look at the source code. There are many boundary conditions in the source code, which is not conducive to reading.