Vue 源碼解析 - 模板編譯

[TOC]

模板編譯

前文在對(duì) Vue 源碼解析 - 主線流程 進(jìn)行分析時(shí),我們已經(jīng)知道對(duì)于 Runtime + Compiler 的編譯版本來(lái)說(shuō)日矫,Vue 在實(shí)例化前總共會(huì)經(jīng)歷兩輪mount過(guò)程梧躺,分別為:

  • 定義于src\platforms\web\runtime\index.js$mount函數(shù),主要負(fù)責(zé)組件掛載功能。

  • 定義于src\platforms\web\entry-runtime-with-compiler.js$mount函數(shù),主要負(fù)責(zé)模板編譯 + 組件掛載(其會(huì)緩存src\platforms\web\runtime\index.js中定義的$mount函數(shù),最后的組件掛載轉(zhuǎn)交給該函數(shù)進(jìn)行處理)功能盐捷。

以下我們對(duì)src\platforms\web\entry-runtime-with-compiler.js$mount函數(shù)進(jìn)行解析,主要分析 模板編譯 部分內(nèi)容:

// src/platforms/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (
    el?: string | Element,
    hydrating?: boolean,
): Component {
    // 獲取 el 元素對(duì)象默勾,找不到則返回一個(gè) div
    el = el && query(el);
    ...
    const options = this.$options;
    // resolve template/el and convert to render function
    if (!options.render) {
        let template = options.template;
        if (template) {
            // Vue.$options.template 為字符串
            if (typeof template === 'string') {
                if (template.charAt(0) === '#') {
                    // 由 id 取得對(duì)應(yīng)的 DOM 元素的 innerHTML
                    template = idToTemplate(template);
                    ...
                }
            } 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) { // 沒(méi)有 template
            template = getOuterHTML(el);
        }
        if (template) {
            ...
            // 對(duì)模板進(jìn)行編譯
            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;
            ...
        }
    }
    return mount.call(this, el, hydrating);
};

function getOuterHTML(el: Element): string {
    if (el.outerHTML) {
        return el.outerHTML
    } else {
        const container = document.createElement('div')
        container.appendChild(el.cloneNode(true))
        return container.innerHTML
    }
}

const idToTemplate = cached(id => {
    const el = query(id)
    return el && el.innerHTML
})

// src/shared/util.js
export function cached<F: Function>(fn: F): F {
    const cache = Object.create(null)
    return (function cachedFn(str: string) {
        const hit = cache[str]
        return hit || (cache[str] = fn(str))
    }: any)
}

從源碼中可以看到碉渡,只有在Options沒(méi)有定義render函數(shù)時(shí),才會(huì)進(jìn)行模板編譯母剥。

模板編譯步驟共分兩步:

  1. 獲取模板字符串:模板字符串的獲取包含以下幾種情況:

    • 如果沒(méi)有定義template滞诺,則直接獲取el元素的outerHTML,即把el元素作為template环疼。

    • 如果template為字符串习霹,并且以#開(kāi)頭,則表明template是以id進(jìn)行指定炫隶,則通過(guò)該id獲取對(duì)應(yīng)元素的innerHTML淋叶。

      cached函數(shù)參數(shù)為一個(gè)函數(shù),返回為一個(gè)參數(shù)為string的函數(shù)伪阶,在該返回函數(shù)內(nèi)部會(huì)調(diào)用cached函數(shù)的函數(shù)參數(shù)煞檩,并做一個(gè)緩存處理。
      對(duì)應(yīng)于我們編譯這部分栅贴,即會(huì)緩存以id進(jìn)行聲明的templateinnerHTML斟湃。

    • 如果template為字符串,并且不以#開(kāi)頭檐薯,則表明template是一個(gè)完整的模板字符串凝赛,直接返回本身即可。

    • 如果templatenodeType類型坛缕,直接返回其innerHTML墓猎。

    • 如果定義了template,但格式無(wú)法識(shí)別(即不是字符串祷膳,也不是nodeType類型)陶衅,則給出警告,并退出編譯流程直晨。

  2. 將模板字符串編譯為render函數(shù):該功能主要由函數(shù)compileToFunctions進(jìn)行實(shí)現(xiàn)搀军,其源碼如下所示:

// src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (...): CompiledResult {...})

// src/platforms/web/compiler/index.js
const { compile, compileToFunctions } = createCompiler(baseOptions)

compileToFunctions是由createCompiler(baseOptions)返回的,而createCompilercreateCompilerCreator(function baseCompile (...){...})勇皇,這里其實(shí)使用了 函數(shù)柯里化 的思想罩句,將接收多個(gè)參數(shù)的函數(shù)轉(zhuǎn)化為接收單一參數(shù)的函數(shù),這樣做的原因是 編譯 這個(gè)流程和平臺(tái)或構(gòu)建方式相關(guān)敛摘,采用 函數(shù)柯里化门烂,將與平臺(tái)無(wú)關(guān)的東西固定化,只留出平臺(tái)相關(guān)的內(nèi)容作為參數(shù),簡(jiǎn)化調(diào)用屯远。比如蔓姚,這里固定化參數(shù)為baseCompile,其主要負(fù)責(zé)模板的解析慨丐,優(yōu)化并最終生成模板代碼的字符串(具體詳情見(jiàn)后文)坡脐,該操作是平臺(tái)無(wú)關(guān)操作,而與平臺(tái)相關(guān)的參數(shù)為baseOptions房揭,不同的平臺(tái)該參數(shù)不同备闲。

簡(jiǎn)而言之,compileToFunctions會(huì)經(jīng)由createCompilerCreator(function baseCompile (...){...}) --> createCompiler(baseOptions)而得到捅暴。

因此恬砂,我們先來(lái)看下createCompilerCreator(function baseCompile (...){...})的源碼實(shí)現(xiàn):

// src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (...){...})

// src\compiler\create-compiler.js
export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {

    function compile (...): CompiledResult {
       ...
      const compiled = baseCompile(template.trim(), finalOptions)
      ...
    }

    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

所以createCompilerCreator就是固定了參數(shù)baseCompile,并返回一個(gè)函數(shù)createCompiler蓬痒,該函數(shù)內(nèi)部又會(huì)返回一個(gè)包含兩個(gè)函數(shù)的實(shí)例泻骤,這其中就有一個(gè)我們需要分析的函數(shù)compileToFunctions(這個(gè)就是$mount函數(shù)內(nèi)部使用的createCompileToFunctionFn),其指向?yàn)楹瘮?shù)createCompileToFunctionFn(compile)的執(zhí)行結(jié)果乳幸,我們先對(duì)函數(shù)createCompileToFunctionFn源碼進(jìn)行查看:

// src/compiler/to-function.js
export function createCompileToFunctionFn(compile: Function): Function {
    ...
    return function compileToFunctions(...): CompiledFunctionResult {...}
}

可以看到又是一個(gè) 函數(shù)柯里化 的操作瞪讼,固定了平臺(tái)無(wú)關(guān)參數(shù)compile,并返回了我們最終需要的compileToFunctions函數(shù)粹断。

compileToFunctions函數(shù)獲取這部分的代碼由于采用了多個(gè) 函數(shù)柯里化 操作符欠,導(dǎo)致代碼邏輯比較混亂,下面是該部分代碼的整個(gè)調(diào)用鏈:

// src/platforms/web/entry-runtime-with-compiler.js
const {render, staticRenderFns} = compileToFunctions(template, {...}, this)

// src/platforms/web/compiler/index.js
const {compile, compileToFunctions} = createCompiler(baseOptions)

// src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile(...) {...})

// src/compiler/create-compiler.js
export function createCompilerCreator(baseCompile: Function): Function {
    return function createCompiler(baseOptions: CompilerOptions) {

        function compile(...): CompiledResult {...}

        return {
            compile,
            compileToFunctions: createCompileToFunctionFn(compile)
        }
    }
}

// src/compiler/to-function.js
export function createCompileToFunctionFn(compile: Function): Function {
    return function compileToFunctions(...): CompiledFunctionResult {
          ...
        const compiled = compile(template, options)
        ...
    }
}

可以看到瓶埋,compileToFunctions的獲取調(diào)用鏈為:createCompilerCreator --> createCompiler --> createCompileToFunctionFn --> compileToFunctions希柿。

到這里我們才理清了compileToFunctions函數(shù)的定義出處,現(xiàn)在回到主線流程养筒,看下compileToFunctions是怎樣具體編譯出render函數(shù):

// src/compiler/to-function.js
export function createCompileToFunctionFn(compile: Function): Function {
    const cache = Object.create(null)

    return function compileToFunctions(
        template: string,
        options?: CompilerOptions,
        vm?: Component
    ): CompiledFunctionResult {
        ...
        // check cache
        const key = options.delimiters
            ? String(options.delimiters) + template
            : template
        if (cache[key]) {
            return cache[key]
        }

        // compile
        const compiled = compile(template, options)
        ...
        // turn code into functions
        const res = {}
        const fnGenErrors = []
        res.render = createFunction(compiled.render, fnGenErrors) // 生成渲染函數(shù)
        res.staticRenderFns = compiled.staticRenderFns.map(code => {
            return createFunction(code, fnGenErrors)
        })
        ...
        return (cache[key] = res)
    }
}

function createFunction(code, errors) {
    try {
        return new Function(code) // 將字符串 code 渲染成函數(shù)
    } catch (err) {
        errors.push({err, code})
        return noop
    }
}

所以當(dāng)我們調(diào)用compileToFunctions時(shí)曾撤,其會(huì)做如下三件事:

  • 模板編譯:通過(guò)函數(shù)compile進(jìn)行編譯。

  • 生成渲染函數(shù):通過(guò)函數(shù)createFunction將編譯完成的模板生成相應(yīng)的渲染函數(shù)(其實(shí)就是使用Function構(gòu)造函數(shù)將編譯完成的模板代碼字符串轉(zhuǎn)換成函數(shù))晕粪。

  • 緩存渲染函數(shù):依據(jù)模板字符串內(nèi)容作為鍵值挤悉,緩存其編譯結(jié)果。

這里面最核心的就是 模板編譯 步驟巫湘,目的就是編譯出模板對(duì)應(yīng)的渲染函數(shù)字符串装悲。
我們著重對(duì)這步進(jìn)行分析,對(duì)compile函數(shù)進(jìn)行源碼查看:

// src/compiler/create-compiler.js
export function createCompilerCreator(baseCompile: Function): Function {
    return function createCompiler(baseOptions: CompilerOptions) {
        function compile(
            template: string,
            options?: CompilerOptions
        ): CompiledResult {
            const finalOptions = Object.create(baseOptions)
            ...
            const compiled = baseCompile(template.trim(), finalOptions)
            ...
            return compiled
        }
        ...
    }
}

compile內(nèi)部會(huì)將編譯過(guò)程交由參數(shù)baseCompile進(jìn)行實(shí)際處理尚氛,而根據(jù)我們前面的分析诀诊,baseCompile就是函數(shù)createCompilerCreator采用 函數(shù)柯里化 固定的平臺(tái)無(wú)關(guān)的參數(shù),其源碼如下所示:

// src/compiler/index.js
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
    }
}

因此阅嘶,$mount函數(shù)內(nèi)部的compileToFunctions函數(shù)最終調(diào)用的就是baseCompile函數(shù)進(jìn)行模板編譯流程属瓣。

從源碼中可以看到载迄,baseCompile函數(shù)內(nèi)部主要做了三件事:

  • 模板編譯:由函數(shù)parse進(jìn)行模板編譯,并生成抽象語(yǔ)法樹(shù) AST抡蛙。

  • 優(yōu)化 AST:由函數(shù)optimize負(fù)責(zé)护昧。

  • 生成代碼:由函數(shù)generate負(fù)責(zé)根據(jù) AST 和編譯選項(xiàng)生成相應(yīng)代碼(字符串形式)。

我們下面針對(duì)這三個(gè)過(guò)程繼續(xù)進(jìn)行分析:

  • 模板編譯:編譯過(guò)程的第一步就是解析模板字符串溜畅,生成抽象語(yǔ)法樹(shù) AST捏卓。
    我們進(jìn)入parse函數(shù)极祸,查看其源碼:
// src/compiler/parser/index.js
/**
 * Convert HTML string to AST.
 */
export function parse(
    template: string,
    options: CompilerOptions
): ASTElement | void {
    ...
    let root
    ...
    parseHTML(template, {
        ...
        start(tag, attrs, unary, start, end) {
            ...
            let element: ASTElement = createASTElement(tag, attrs, currentParent)
            ...
            if (!root) {
                root = element
                ...
            }
            ...
        },

        end(tag, start, end) {
            const element = stack[stack.length - 1]
            // pop stack
            stack.length -= 1
            currentParent = stack[stack.length - 1]
            ...
            closeElement(element)
        },

        chars(text: string, start: number, end: number) {
            ...
            parseText(text, delimiters)
            ...
            children.push(child)
            ...
        },
        comment(text: string, start, end) {
            if (currentParent) {
                ...
                currentParent.children.push(child)
            }
        }
    })
    return root
}

parse函數(shù)最終通過(guò)調(diào)用函數(shù)parseHTML對(duì)模板進(jìn)行解析慈格,查看parseHTML源碼:

// src/compiler/parser/html-parser.js
// Regular Expressions for parsing tags and attributes
...
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/
...
export function parseHTML(html, options) {
    ...
    let index = 0
    let last, lastTag
    while (html) {
        last = html
        // Make sure we're not in a plaintext content element like script/style
        if (!lastTag || !isPlainTextElement(lastTag)) {
            let textEnd = html.indexOf('<')
            if (textEnd === 0) {
                // Comment:
                if (comment.test(html)) {
                    ...
                    advance(commentEnd + 3)
                    continue
                    }
                }

                // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
                if (conditionalComment.test(html)) {
                    ...
                    advance(conditionalEnd + 2)
                    continue
                    }
                }

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

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

                // Start tag:
                const startTagMatch = parseStartTag()
                if (startTagMatch) {
                        ...
                        advance(1)
                    }
                    continue
                }
            }
            ...
            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
                    ...
                text = html.substring(0, textEnd)
            }
            ...
            advance(text.length)
            ...
        } else {
            ...
            parseEndTag(stackedTag, index - endTagLength, index)
        }
        ...
    }

    // Clean up any remaining tags
    parseEndTag()

    function advance(n) {
        index += n
        html = html.substring(n)
    }
    ...
}

簡(jiǎn)單來(lái)說(shuō),parseHTML函數(shù)采用正則表達(dá)式來(lái)解析模板template遥金,其解析步驟大概如下所示:

  • 首先獲取模板字符<的索引位置浴捆,如果索引為0,則表明當(dāng)前模板以<開(kāi)頭稿械,則使用正則依次判斷template是否匹配Comment选泻,conditionalCommentDoctype美莫,End Tag還是Start Tag页眯,匹配完成后,依據(jù)不同的匹配標(biāo)簽進(jìn)行各自的解析厢呵,比如窝撵,對(duì)于Comment標(biāo)簽,則會(huì)進(jìn)行如下解析:

    // Comment:
    if (comment.test(html)) {
        const commentEnd = html.indexOf('-->')
    
        if (commentEnd >= 0) {
            ...
            options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
            ...
            advance(commentEnd + 3)
            continue
        }
    }
    

    即如果template匹配Comment標(biāo)簽襟铭,則找到-->的索引位置碌奉,即找到注釋標(biāo)簽的末尾位置,然后取出注釋內(nèi)容html.substring(4,commentEnd)赐劣,交由options.comment函數(shù)進(jìn)行處理(options.comment其實(shí)是parse函數(shù)內(nèi)調(diào)用parseHTML傳遞進(jìn)行的options.comment,因此魁兼,這里其實(shí)起一個(gè)回調(diào)作用漠嵌,parse函數(shù)內(nèi)就可以通過(guò)回調(diào)獲取當(dāng)前模板解析得到的注釋節(jié)點(diǎn)的內(nèi)容,從而可以進(jìn)行處理或保存)碉考,解析完成后會(huì)通過(guò)advance函數(shù)將當(dāng)前template的字符串進(jìn)行截取挺身,只保留還未進(jìn)行解析的內(nèi)容。

    其他節(jié)點(diǎn)解析處理與上述操作邏輯類似,均是根據(jù)節(jié)點(diǎn)特點(diǎn)進(jìn)行解析墙贱,然后通過(guò)advance函數(shù)去除已解析的內(nèi)容热芹,只保留未解析的模板字符串,繼續(xù)新一輪的解析惨撇。

    舉個(gè)栗子:比如對(duì)于如下模板:

    template: `<h2 style="color:red">Hi, {{message}}</h2>`
    

    其解析流程如下圖所示:

parseHTML

parseHTML每次解析完成一個(gè)節(jié)點(diǎn)時(shí)伊脓,就會(huì)將結(jié)果回調(diào)給parse函數(shù),parse函數(shù)就可以根據(jù)這些結(jié)果進(jìn)行抽象語(yǔ)法樹(shù)(AST)的構(gòu)建魁衙,其實(shí)質(zhì)就是構(gòu)建一個(gè)javascript對(duì)象报腔,比如,上述模板構(gòu)建得到的 AST 如下所示:

{
"type": 1,
"tag": "h2",
"attrsList": [],
"attrsMap": {
  "style": "color:red"
},
"rawAttrsMap": {
  "style": {
    "name": "style",
    "value": "color:red",
    "start": 4,
    "end": 21
  }
},
"children": [
  {
    "type": 2,
    "expression": "\"Hi, \"+_s(message)",
    "tokens": [
      "Hi, ",
      {
        "@binding": "message"
      }
    ],
    "text": "Hi, {{message}}",
    "start": 22,
    "end": 37
  }
],
"start": 0,
"end": 42,
"plain": false,
"staticStyle": "{\"color\":\"red\"}"
}

到這里剖淀,我們就大概了解了模板字符串template解析成抽象語(yǔ)法樹(shù)(AST)的整個(gè)過(guò)程纯蛾。

  • 優(yōu)化 AST:當(dāng)完成 AST 的構(gòu)建后,就可以對(duì) AST 進(jìn)行一些優(yōu)化纵隔。

    Vue 之所以有 優(yōu)化 AST 這個(gè)過(guò)程翻诉,主要是因?yàn)?Vue 的特性之一是 數(shù)據(jù)驅(qū)動(dòng),并且數(shù)據(jù)具備響應(yīng)式功能捌刮,因此碰煌,當(dāng)更改數(shù)據(jù)的時(shí)候,模板會(huì)重新進(jìn)行渲染绅作,顯示最新的數(shù)據(jù)芦圾。但是,模板中并不是所有的節(jié)點(diǎn)都需要進(jìn)行重新渲染棚蓄,對(duì)于不包含響應(yīng)式數(shù)據(jù)的節(jié)點(diǎn)堕扶,從始至終只需一次渲染即可。

    我們進(jìn)入optimize函數(shù)梭依,查看其源碼:

    // src/compiler/optimizer.js
    /**
    * Goal of the optimizer: walk the generated template AST tree
    * and detect sub-trees that are purely static, i.e. parts of
    * the DOM that never needs to change.
    *
    * Once we detect these sub-trees, we can:
    *
    * 1. Hoist them into constants, so that we no longer need to
    *    create fresh nodes for them on each re-render;
    * 2. Completely skip them in the patching process.
    */
    export function optimize(root: ?ASTElement, options: CompilerOptions) {
        ...
        // first pass: mark all non-static nodes.
        markStatic(root)
        // second pass: mark static roots.
        markStaticRoots(root, false)
    }
    

    optimize主要就是做了兩件事:

    • markStatic:標(biāo)記靜態(tài)節(jié)點(diǎn)稍算。其源碼如下:
    // src/compiler/optimizer.js
    function markStatic(node: ASTNode) {
        node.static = isStatic(node)
        if (node.type === 1) {
            ...
            for (let i = 0, l = node.children.length; i < l; i++) {
                const child = node.children[i]
                markStatic(child)
                if (!child.static) {
                    node.static = false
                }
            }
            if (node.ifConditions) {
                for (let i = 1, l = node.ifConditions.length; i < l; i++) {
                    const block = node.ifConditions[i].block
                    markStatic(block)
                    if (!block.static) {
                        node.static = false
                    }
                }
            }
        }
    }
    

    parse函數(shù)解析生成的抽象語(yǔ)法樹(shù) AST 中,其元素節(jié)點(diǎn)總有三種類型役拴,如下所示:

    type 類型
    1 普通元素
    2 表達(dá)式(expression)
    3 文本(text)

    從源碼中可以看到糊探,markStatic函數(shù)會(huì)對(duì)當(dāng)前節(jié)點(diǎn)科平,以及當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)和v-if的子節(jié)點(diǎn)進(jìn)行靜態(tài)節(jié)點(diǎn)標(biāo)記瞪慧,靜態(tài)節(jié)點(diǎn)的判定標(biāo)準(zhǔn)由函數(shù)isStatic判定:

    // src/compiler/optimizer.js
    function isStatic(node: ASTNode): boolean {
        if (node.type === 2) { // expression
            return false
        }
        if (node.type === 3) { // text
            return true
        }
        return !!(node.pre || (
            !node.hasBindings && // no dynamic bindings
            !node.if && !node.for && // not v-if or v-for or v-else
            !isBuiltInTag(node.tag) && // not a built-in
            isPlatformReservedTag(node.tag) && // not a component
            !isDirectChildOfTemplateFor(node) &&
            Object.keys(node).every(isStaticKey)
        ))
    }
    

    可以看到氨菇,靜態(tài)節(jié)點(diǎn)的判定標(biāo)準(zhǔn)為:純文本類型 或者 沒(méi)有動(dòng)態(tài)綁定且沒(méi)有v-if且沒(méi)有v-for指令且不是內(nèi)置slot/component且是平臺(tái)保留標(biāo)簽且不是帶有v-for指令的template標(biāo)簽的直接子節(jié)點(diǎn)且節(jié)點(diǎn)的所有屬性都是靜態(tài)key
    當(dāng)滿足靜態(tài)節(jié)點(diǎn)的判定時(shí)豌研,就會(huì)為該節(jié)點(diǎn)打上static=true屬性鹃共,作為標(biāo)記及汉。

    • markStaticRoots:標(biāo)記靜態(tài)根節(jié)點(diǎn)。其源碼如下:
    // src/compiler/optimizer.js
    function markStaticRoots(node: ASTNode, isInFor: boolean) {
        if (node.type === 1) {
            ...
            // For a node to qualify as a static root, it should have children that
            // are not just static text. Otherwise the cost of hoisting out will
            // outweigh the benefits and it's better off to just always render it fresh.
            if (node.static && node.children.length && !(
                node.children.length === 1 &&
                node.children[0].type === 3
            )) {
                node.staticRoot = true
                return
            } else {
                node.staticRoot = false
            }
            if (node.children) {
                for (let i = 0, l = node.children.length; i < l; i++) {
                    markStaticRoots(node.children[i], isInFor || !!node.for)
                }
            }
            if (node.ifConditions) {
                for (let i = 1, l = node.ifConditions.length; i < l; i++) {
                    markStaticRoots(node.ifConditions[i].block, isInFor)
                }
            }
        }
    }
    

    根節(jié)點(diǎn)即為普通元素節(jié)點(diǎn),從源碼中可以看到翁狐,markStaticRoots函數(shù)會(huì)對(duì)當(dāng)前節(jié)點(diǎn)露懒,以及當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)和帶有v-if的子節(jié)點(diǎn)進(jìn)行靜態(tài)根節(jié)點(diǎn)標(biāo)記懈词。
    靜態(tài)根節(jié)點(diǎn)的判定標(biāo)準(zhǔn)為:節(jié)點(diǎn)為靜態(tài)節(jié)點(diǎn)坎弯,且其有子節(jié)點(diǎn)抠忘,并且子節(jié)點(diǎn)不能只是一個(gè)文本節(jié)點(diǎn)。
    當(dāng)滿足靜態(tài)根節(jié)點(diǎn)的判定時(shí)囚灼,就會(huì)為該節(jié)點(diǎn)打上staticRoot=true屬性,作為標(biāo)記谭网。

    因此,Vue 對(duì)模板解析生成的 AST 的優(yōu)化就是對(duì) AST 元素節(jié)點(diǎn)進(jìn)行靜態(tài)節(jié)點(diǎn)和靜態(tài)根節(jié)點(diǎn)的標(biāo)記锥涕,以避免重新渲染靜態(tài)節(jié)點(diǎn)元素层坠。

  • 生成代碼:當(dāng)對(duì) AST 進(jìn)行優(yōu)化后破花,編譯的最后一步就是將優(yōu)化過(guò)后的 AST 樹(shù)轉(zhuǎn)換成可執(zhí)行代碼座每。

    我們進(jìn)入generate函數(shù),查看其源碼:

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

    generate函數(shù)內(nèi)部主要就是調(diào)用了函數(shù)genElement對(duì) AST 樹(shù)進(jìn)行解析并生成對(duì)應(yīng)可執(zhí)行代碼葱椭,最后返回一個(gè)對(duì)象,該對(duì)象含有兩個(gè)函數(shù)renderstate.staticRenderFns窃祝,其中大磺,render函數(shù)會(huì)封裝一下genElement函數(shù)生成的代碼杠愧。

    我們下面對(duì)genElement函數(shù)進(jìn)行分析流济,看下其代碼生成邏輯:

    // src/compiler/codegen/index.js
    export function genElement(el: ASTElement, state: CodegenState): string {
        ...
        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 {
            // component or element
            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)
                ...
            }
            ...
            return code
        }
    }
    

    可以看到雕憔,genElement函數(shù)內(nèi)部會(huì)依據(jù) AST 樹(shù)的節(jié)點(diǎn)類別分別調(diào)用不同的函數(shù)生成對(duì)應(yīng)的代碼斤彼,比如:

    • 對(duì)于靜態(tài)根節(jié)點(diǎn)琉苇,使用genStatic函數(shù)進(jìn)行代碼生成。
    • 對(duì)于帶有v-once指令的節(jié)點(diǎn)穷蛹,使用genOnce函數(shù)進(jìn)行代碼生成俩莽。
    • 對(duì)于帶有v-for指令的節(jié)點(diǎn),使用genFor函數(shù)進(jìn)行代碼生成。
    • 對(duì)于帶有v-if指令的節(jié)點(diǎn)坯辩,使用genIf函數(shù)進(jìn)行代碼生成漆魔。
    • 對(duì)于不帶slot指令的template標(biāo)簽,會(huì)使用genChildren函數(shù)遍歷其子節(jié)點(diǎn)并進(jìn)行代碼生成阿纤。
    • 對(duì)于slot標(biāo)簽胰锌,使用genSlot函數(shù)進(jìn)行代碼生成资昧。
    • 對(duì)于組件或元素格带,使用genComponent等函數(shù)進(jìn)行代碼生成。
      ...

    我們這里就隨便找一個(gè)函數(shù)簡(jiǎn)單分析下代碼生成的具體邏輯嘶卧,比如:genStatic侦铜,其源碼如下所示:

    // src/compiler/codegen/index.js
    function genStatic(el: ASTElement, state: CodegenState): string {
        ...
        state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
        ...
        return `_m(${
            state.staticRenderFns.length - 1
            }${
            el.staticInFor ? ',true' : ''
            })`
    }
    

    其實(shí)最終就是使用字符串拼接將對(duì)應(yīng)元素的內(nèi)容轉(zhuǎn)換為字符串代碼。

    舉個(gè)栗子:比如我們構(gòu)造一個(gè)靜態(tài)根節(jié)點(diǎn)的模板贡未,如下所示:

    template: `
        <div>
            <h2 style="color:red">Hi</h2>
        </div>
    `
    

    最后俊卤,經(jīng)過(guò)genStatic后,最終生成的代碼為:_m(0)狠怨。
    所以上述模板最終經(jīng)過(guò)generate函數(shù)后,生成的代碼如下:

    {
        "render": "with(this){return _m(0)}",
        "staticRenderFns": [
                "with(this){return _c('div',[_c('h2',{staticStyle:{\"color\":\"red\"}},[_v(\"Hi\")])])}"
            ]
    }
    

    _m函數(shù)其實(shí)是renderStatic函數(shù)茵汰,Vue 中還設(shè)置了_o蹂午,_l栏豺,_v等函數(shù),如下所示:

    // src/core/instance/render-helpers/index.js
    export function installRenderHelpers (target: any) {
      target._o = markOnce
      target._n = toNumber
      target._s = toString
      target._l = renderList
      target._t = renderSlot
      target._q = looseEqual
      target._i = looseIndexOf
      target._m = renderStatic
      target._f = resolveFilter
      target._k = checkKeyCodes
      target._b = bindObjectProps
      target._v = createTextVNode
      target._e = createEmptyVNode
      target._u = resolveScopedSlots
      target._g = bindObjectListeners
      target._d = bindDynamicKeys
      target._p = prependModifier
    }
    

    其余的代碼生成函數(shù)就不進(jìn)行分析了豆胸,大概的原理就是根據(jù)不同節(jié)點(diǎn)特征奥洼,在 AST 樹(shù)中獲取需要的數(shù)據(jù),拼接成可執(zhí)行代碼的字符串代碼晚胡。

到這里灵奖,模板編譯的一個(gè)大概完整過(guò)程便完成了。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末估盘,一起剝皮案震驚了整個(gè)濱河市擅编,隨后出現(xiàn)的幾起案子锦担,更是在濱河造成了極大的恐慌,老刑警劉巖宙橱,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件菊卷,死亡現(xiàn)場(chǎng)離奇詭異扑眉,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門理肺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事转晰∫铣茫” “怎么了淆党?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)谨读。 經(jīng)常有香客問(wèn)我,道長(zhǎng)劳景,這世上最難降的妖魔是什么烹吵? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮先较,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己时鸵,他們只是感情好锁保,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著篇梭,像睡著了一般氢橙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上恬偷,一...
    開(kāi)封第一講書(shū)人閱讀 52,441評(píng)論 1 310
  • 那天悍手,我揣著相機(jī)與錄音,去河邊找鬼袍患。 笑死坦康,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的诡延。 我是一名探鬼主播滞欠,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼肆良!你這毒婦竟也來(lái)了仑撞?” 一聲冷哼從身側(cè)響起赤兴,我...
    開(kāi)封第一講書(shū)人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎隧哮,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體座舍,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡沮翔,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了曲秉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片采蚀。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖承二,靈堂內(nèi)的尸體忽然破棺而出榆鼠,到底是詐尸還是另有隱情,我是刑警寧澤亥鸠,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布妆够,位于F島的核電站,受9級(jí)特大地震影響负蚊,放射性物質(zhì)發(fā)生泄漏神妹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一家妆、第九天 我趴在偏房一處隱蔽的房頂上張望鸵荠。 院中可真熱鬧,春花似錦伤极、人聲如沸蛹找。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)庸疾。三九已至,卻和暖如春齿税,著一層夾襖步出監(jiān)牢的瞬間彼硫,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工凌箕, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拧篮,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓牵舱,卻偏偏與公主長(zhǎng)得像串绩,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子芜壁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容