Vue 源码学习之模板编译

编译入口

// src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
    
    
    Vue.prototype._init = function (options?: Object) {
    
    
        const vm: Component = this
        // ... 其他代码
        if (vm.$options.el) {
    
    
            vm.$mount(vm.$options.el)
        }
    }
}

$mount的定义:定义变量 mount为原型上的 $mount方法,然后对 $mount进行重新定义;

// src/platforms/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
// 重新定义 $mount
Vue.prototype.$mount = function (
    el?: string | Element,
    hydrating?: boolean
): Component {
    
    
    el = el && query(el)
    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
    // 解析 template 或 el 并转换为 render
    if (!options.render) {
    
    
        let template = options.template
        // 对模板进行处理
        if (template) {
    
    
            // 如果是字符串,且首字符是 #,表示根据 id 来获取 template 模板
            if (typeof template === 'string') {
    
    
                if (template.charAt(0) === '#') {
    
    
                    template = idToTemplate(template)
                    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) {
    
    
            // 如果不存在 render 和 template,但存在 el,则直接将 el 对应的外层 html 结构赋值给 template
            template = getOuterHTML(el)
        }
        // 处理完 template 之后,将其转换为 render
        if (template) {
    
    
            if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    
    
                mark('compile')
            }
            // 根据 template 获取 render 和静态节点
            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
            if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    
    
                mark('compile end')
                measure(`vue ${
      
      this._name} compile`, 'compile', 'compile end')
            }
        }
    }
    // 调用原先 Vue 原型上 $mount 方法
    return mount.call(this, el, hydrating)
}

从上面的代码可以看出,$mount主要做了以下几件事:

  • 判断 el是否是挂载到正常节点,而不是 bodyhtml
  • 如果存在 template属性,则对其进行处理:
    • 如果传入的是以 #开头的字符串,则认为是根据 id来获取 template模板;
    • 如果传入的是一个节点,直接获取节点内容;
    • 以上两种情况都没有,则抛出警告;
  • 如果不存在 rendertemplate属性,则直接将 el对应的外层 html 结构赋值给 template
  • 处理完 template之后,调用 compileToFunctions将其转为 render函数并赋值给 options
  • 返回原先方法 mount.call(this, el, hydrating)的结果,后续再进行补充说明;

compileToFunctions是模板编译的核心方法,我们可以看看是怎么实现的:

// src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
	template: string,
    options: CompilerOptions
): CompiledResult {
    
    
    // 将传进来的 template 转化为 ast 树,描述自身形成的结构
   	const ast = parse(template.trim(), options)
	// 静态节点的优化
    if (options.optimize !== false) {
    
    
        optimize(ast, options)
    }
	// 根据生成的 ast 树和配置 options 生成代码,也就是 render 函数
    const code = generate(ast, options)
    return {
    
    
        ast,
        render: code.render,
        staticRenderFns: code.staticRenderFns
    }
})

分析上面的代码,我们可以将它分为几个步骤:

  • 将传进来的 template模板转化为 ast 抽象语法树;
  • 对静态节点进行优化;
  • 根据转化的 ast树生成 code,也就是 render函数;

我们先来看看 parse方法的具体实现:

// src/compiler/parser/index.js
// 创建 ast 
export function createASTElement (
    tag: string,
    attrs: Array<ASTAttr>,
    parent: ASTElement | void
): ASTElement {
    
    
    return {
    
    
        type: 1,
        tag,
        attrsList: attrs,
        attrsMap: makeAttrsMap(attrs),
        rawAttrsMap: {
    
    },
        parent,
        children: []
    }
}

export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
    
    
    // ...
    // 只关注重点代码,其他代码省略
    function closeElement (element) {
    
    
        trimEndingWhitespace(element)
        if (!inVPre && !element.processed) {
    
    
            element = processElement(element, options)
        }
        if (!stack.length && element !== root) {
    
    
            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) {
    
    
                processIfConditions(element, currentParent)
            } else {
    
    
                if (element.slotScope) {
    
    
                    const name = element.slotTarget || '"default"'
                    ;(currentParent.scopedSlots || (currentParent.scopedSlots = {
    
    }))[name] = element
                }
                // 建立父子关系
                currentParent.children.push(element)
                element.parent = currentParent
            }
        }
    }
    parseHTML(template, {
    
    
        warn,
        expectHTML: options.expectHTML,
        isUnaryTag: options.isUnaryTag,
        canBeLeftOpenTag: options.canBeLeftOpenTag,
        shouldDecodeNewlines: options.shouldDecodeNewlines,
        shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
        shouldKeepComment: options.comments,
        outputSourceRange: options.outputSourceRange,
        // 匹配到开始标签的回调
        start (tag, attrs, unary, start, end) {
    
    
            const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)

            if (isIE && ns === 'svg') {
    
    
                attrs = guardIESVGBug(attrs)
            }
        	// 创建节点
            let element: ASTElement = createASTElement(tag, attrs, currentParent)
            if (ns) {
    
    
                element.ns = ns
            }

            if (process.env.NODE_ENV !== 'production') {
    
    
                if (options.outputSourceRange) {
    
    
                    element.start = start
                    element.end = end
                    element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
    
    
                        cumulated[attr.name] = attr
                        return cumulated
                    }, {
    
    })
                }
                attrs.forEach(attr => {
    
    
                    if (invalidAttributeRE.test(attr.name)) {
    
    
                        warn(
                            `Invalid dynamic argument expression: attribute names cannot contain ` +
                            `spaces, quotes, <, >, / or =.`,
                            {
    
    
                                start: attr.start + attr.name.indexOf(`[`),
                                end: attr.start + attr.name.length
                            }
                        )
                    }
                })
            }

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

            for (let i = 0; i < preTransforms.length; i++) {
    
    
                element = preTransforms[i](element, options) || element
            }

            if (!inVPre) {
    
    
                processPre(element)
                if (element.pre) {
    
    
                    inVPre = true
                }
            }
            if (platformIsPreTag(element.tag)) {
    
    
                inPre = true
            }
        	// for、if、once 等指令的处理
            if (inVPre) {
    
    
                processRawAttrs(element)
            } else if (!element.processed) {
    
    
                processFor(element)
                processIf(element)
                processOnce(element)
            }

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

            if (!unary) {
    
    
                currentParent = element
                stack.push(element)
            } else {
    
    
                closeElement(element)
            }
        },
        // 匹配到结束标签的回调
        end (tag, start, end) {
    
    
            // 取栈中最后一个开始标签,即与结束标签匹配的开始标签
            const element = stack[stack.length - 1]
            stack.length -= 1
            // 将 currentParent 置为当前匹配到的完整标签内容的父节点
            currentParent = stack[stack.length - 1]
            if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
    
    
                element.end = end
            }
            closeElement(element)
        },
        // 匹配到文本时的回调
        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
            }
            if (isIE &&
                currentParent.tag === 'textarea' &&
                currentParent.attrsMap.placeholder === text
               ) {
    
    
                return
            }
            const children = currentParent.children
            // 对文本进行处理
            if (inPre || text.trim()) {
    
    
                text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
            } else if (!children.length) {
    
    
                text = ''
            } else if (whitespaceOption) {
    
    
                if (whitespaceOption === 'condense') {
    
    
                    text = lineBreakRE.test(text) ? '' : ' '
                } else {
    
    
                    text = ' '
                }
            } else {
    
    
                text = preserveWhitespace ? ' ' : ''
            }
            // 创建 ast 文本节点
            if (text) {
    
    
                if (!inPre && whitespaceOption === 'condense') {
    
    
                    text = text.replace(whitespaceRE, ' ')
                }
                let res
                let child: ?ASTNode
                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)
                }
            }
        },
        // 匹配到注释时的回调
        comment (text: string, start, end) {
    
    
            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)
            }
        }
    })
    return root
}

parse方法中可以看出,其主要是执行 parseHTML来对模板进行解析,解析过程通过回调来生成对应的 ast并建立父子关联,执行回调的时机包括匹配到开始标签、结束标签、文本、注释parseHTML具体怎么做我们可以继续看:

// src/compiler/parser/html-parser.js
// 解析 HTML 相关的正则表达式
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})`
const startTagOpen = new RegExp(`^<${
      
      qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${
      
      qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/

export function parseHTML (html, options) {
    
    
    const stack = []
    const expectHTML = options.expectHTML
    const isUnaryTag = options.isUnaryTag || no
    const canBeLeftOpenTag = options.canBeLeftOpenTag || no
    let index = 0 // 当前字符串的索引
    let last, lastTag
    while (html) {
    
    
        last = html
        if (!lastTag || !isPlainTextElement(lastTag)) {
    
    
            // 查找第一个 "<" 的位置
            let textEnd = html.indexOf('<')
            if (textEnd === 0) {
    
    
                // 匹配到注释
                if (comment.test(html)) {
    
    
                    // 找到第一个 "-->" 的位置
                    const commentEnd = html.indexOf('-->')
                    if (commentEnd >= 0) {
    
    
                        if (options.shouldKeepComment) {
    
    
                            // 截取注释内容,执行回调
                            options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
                        }
                        // index 往后走,并截取字符串
                        advance(commentEnd + 3)
                        continue
                    }
                }
                // 匹配条件注释
                if (conditionalComment.test(html)) {
    
    
                    const conditionalEnd = html.indexOf(']>')

                    if (conditionalEnd >= 0) {
    
    
                        advance(conditionalEnd + 2)
                        continue
                    }
                }
                // doctype 匹配
                const doctypeMatch = html.match(doctype)
                if (doctypeMatch) {
    
    
                    advance(doctypeMatch[0].length)
                    continue
                }
                // 匹配结束标签
                const endTagMatch = html.match(endTag)
                if (endTagMatch) {
    
    
                    const curIndex = index
                    advance(endTagMatch[0].length)
                    parseEndTag(endTagMatch[1], curIndex, index)
                    continue
                }
                // 匹配开始标签
                const startTagMatch = parseStartTag()
                if (startTagMatch) {
    
    
                    handleStartTag(startTagMatch)
                    if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
    
    
                        advance(1)
                    }
                    continue
                }
            }

            let text, rest, next
            if (textEnd >= 0) {
    
    
                rest = html.slice(textEnd)
                // 循环往后走,直到找到开始标签 | 结束标签 | 注释 | 条件注释
                while (
                    !endTag.test(rest) &&
                    !startTagOpen.test(rest) &&
                    !comment.test(rest) &&
                    !conditionalComment.test(rest)
                ) {
    
    
                    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)
            }
        } 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 (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
    
    
                    text = text
                        .replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
                        .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
                }
                if (shouldIgnoreFirstNewline(stackedTag, text)) {
    
    
                    text = text.slice(1)
                }
                if (options.chars) {
    
    
                    options.chars(text)
                }
                return ''
            })
            index += html.length - rest.length
            html = rest
            parseEndTag(stackedTag, index - endTagLength, index)
        }

        if (html === last) {
    
    
            options.chars && options.chars(html)
            if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {
    
    
                options.warn(`Mal-formatted tag at end of template: "${
      
      html}"`, {
    
     start: index + html.length })
            }
            break
        }
    }

    parseEndTag()
    // 截取 html, 记录当前字符串的位置
    function advance (n) {
    
    
        index += n
        html = html.substring(n)
    }
    // 解析开始标签,返回一个对象,包括该标签的属性和位置
    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
            }
        }
    }

    function handleStartTag (match) {
    
    
        const tagName = match.tagName
        const unarySlash = match.unarySlash

        if (expectHTML) {
    
    
            if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
    
    
                parseEndTag(lastTag)
            }
            if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
    
    
                parseEndTag(tagName)
            }
        }

        const unary = isUnaryTag(tagName) || !!unarySlash

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

        if (!unary) {
    
    
            // 入栈,后续可用来匹配结束标签
            stack.push({
    
     tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
            lastTag = tagName
        }

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

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

        if (tagName) {
    
    
            lowerCasedTagName = tagName.toLowerCase()
            // 找到匹配标签在栈中的位置
            for (pos = stack.length - 1; pos >= 0; pos--) {
    
    
                if (stack[pos].lowerCasedTag === lowerCasedTagName) {
    
    
                    break
                }
            }
        } else {
    
    
            pos = 0
        }

        if (pos >= 0) {
    
    
            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)
                }
            }
            // 从栈中移除开始标签
            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)
            }
        }
    }
}

分析上面的关键代码,我们可以得知 parseHTML是利用正则表达式匹配模板字符串,遇到开始标签、结束标签、文本、注释则进行解析,解析完毕之后就能生成对应的 ast树,并建立相应的父子关系,然后不断截取字符串,直到字符串解析完毕;

解析完模板字符串生成 ast树,接下来就是根据 ast树生成相应的 code

// src/compiler/codegen/index.js
export function generate (
    ast: ASTElement | void,
    options: CompilerOptions
): CodegenResult {
    
    
    const state = new CodegenState(options)
    const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
    return {
    
    
        render: `with(this){return ${
      
      code}}`,
        staticRenderFns: state.staticRenderFns
    }
}

export function genElement (el: ASTElement, state: CodegenState): string {
    
    
    if (el.parent) {
    
    
        el.pre = el.pre || el.parent.pre
    }
    // 静态节点、指令节点、插槽和 template 标签的处理
    if (el.staticRoot && !el.staticProcessed) {
    
    
        return genStatic(el, state)
    } else if (el.once && !el.onceProcessed) {
    
    
        return genOnce(el, state)
    } else if (el.for && !el.forProcessed) {
    
    
        return genFor(el, state)
    } else if (el.if && !el.ifProcessed) {
    
    
        return genIf(el, state)
    } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    
    
        return genChildren(el, state) || 'void 0'
    } else if (el.tag === 'slot') {
    
    
        return genSlot(el, state)
    } else {
    
    
        let code
        // 组件
        if (el.component) {
    
    
            code = genComponent(el.component, el, state)
        } else {
    
    
            let data
            if (!el.plain || (el.pre && state.maybeComponent(el))) {
    
    
                data = genData(el, state)
            }

            const children = el.inlineTemplate ? null : genChildren(el, state, true)
            code = `_c('${
      
      el.tag}'${
      
      
            data ? `,${ 
        data}` : '' // data
            }${
      
      
                children ? `,${ 
        children}` : '' // children
            })`
        }
        for (let i = 0; i < state.transforms.length; i++) {
    
    
            code = state.transforms[i](el, code)
        }
        return code
    }
}

// 获取子节点的 code
export function genChildren (
    el: ASTElement,
    state: CodegenState,
    checkSkip?: boolean,
    altGenElement?: Function,
    altGenNode?: Function
): string | void {
    
    
    const children = el.children
    if (children.length) {
    
    
        const el: any = children[0]
        if (children.length === 1 &&
            el.for &&
            el.tag !== 'template' &&
            el.tag !== 'slot'
           ) {
    
    
            const normalizationType = checkSkip
            ? state.maybeComponent(el) ? `,1` : `,0`
            : ``
            return `${
      
      (altGenElement || genElement)(el, state)}${
      
      normalizationType}`
        }
        const normalizationType = checkSkip
        ? getNormalizationType(children, state.maybeComponent)
        : 0
        const gen = altGenNode || genNode
        return `[${
      
      children.map(c => gen(c, state)).join(',')}]${
      
      
        	normalizationType ? `,${ 
        normalizationType}` : ''
    	}`
    }
}

function genNode (node: ASTNode, state: CodegenState): string {
    
    
    // 判断节点类型,根据节点类型返回相应的 code
    if (node.type === 1) {
    
    
        return genElement(node, state)
    } else if (node.type === 3 && node.isComment) {
    
    
        return genComment(node)
    } else {
    
    
        return genText(node)
    }
}

上面的代码是 generate方法对应文件中的部分代码,像 genCommentgenTextgenProps等方法有兴趣的可以自行看源码,这里主要讲生成 code的主要思路;

generate方法主要通过拿到 ast树进行解析,执行 getElement来获取 codegetElement会对 el进行判断

  • 如果是静态节点、包含指令 v-oncev-forv-if的节点(这里也可以看出 v-for的指令比v-if指令的优先级高)、template、插槽等,则进行相应的处理;
  • 如果是组件,则通过 genComponent生成 code
  • 否则执行 genChildren递归创建;

最后执行完毕后会生成类似的结构:

// _c 代表创建节点,_v 代表创建文本,_s 代表变量,下面只是个例子,还有其他的函数
_c('div',{
    
    id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))

总结:

模板编译的入口是vm.$mount(vm.$options.el)$mount会解析 templateel并转换为 render,然后执行原先的 $mount进行渲染,转换 render的核心方法是 compileToFunctions,该方法的核心步骤主要分三步:

  • 将传进去的 template转换为 ast树;
  • 对静态节点进行优化;
  • 根据生成的 ast树生成 code

猜你喜欢

转载自blog.csdn.net/Ljwen_/article/details/124787912