5. Vue的模板編譯器原理

Vue的渲染機制指的是Vue怎麼將單文件組件中的template轉換爲AST(語法樹),再將AST轉換成render函數,最後生成虛擬dom節點(包含建立元素節點的一切信息的JavaScript對象),並建立元素節點掛載到頁面上,基本過程以下圖: 本節先介紹模板編譯生成render函數的過程。html

模板編譯過程

模板編譯成渲染函數經歷了三個階段: 將模板解析成AST、遍歷AST標記靜態節點以及靜態根節點和使用AST生成render函數。 如下面模板爲例:node

<div id="app">{{ message }}</div>
複製代碼

首先獲取組件的模板內容express

var template = options.template;
    if (template) {
        // 針對字符串模板和選擇符匹配模板
    if (typeof template === 'string') {
        // 選擇符匹配模板,以'#'爲前綴的選擇符
        if (template.charAt(0) === '#') {
            // 獲取匹配元素的innerHTML
            template = idToTemplate(template);
        }
    } else if (template.nodeType) {
        // 針對DOM元素匹配,獲取匹配元素的innerHTML
        template = template.innerHTML;
    } else {
        {
        warn('invalid template option:' + template, this);
        }
        return this
    }
    } else if (el) {
        // 若是沒有傳入template模板,則默認以el元素所屬的根節點做爲基礎模板
    template = getOuterHTML(el);
}
複製代碼

獲取模板後處理的核心過程以下:數組

compileToFunctions(template, {
    outputSourceRange: "development" !== 'production',
    shouldDecodeNewlines: shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
    delimiters: options.delimiters,
    comments: options.comments
}, this)
...
var compiled = compile(template, options);
...
var compiled = baseCompile(template.trim(), finalOptions);
複製代碼

上面的代碼是在建立編譯器,真正的編譯過程: 解析、優化以及生成render函數,代碼以下:bash

var ast = parse(template.trim(), options);
if (options.optimize !== false) {
    optimize(ast, options);
}
var code = generate(ast, options);
複製代碼

template解析

真正的解析函數是parseHTML,它的參數是template,和一個options對象,這個對象包含了start、end、chars以及comment對標籤處理的函數:app

parseHTML(template, {
      warn: warn$2,
      expectHTML: options.expectHTML,
      isUnaryTag: options.isUnaryTag,
      canBeLeftOpenTag: options.canBeLeftOpenTag,
      shouldDecodeNewlines: options.shouldDecodeNewlines,
      shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
      shouldKeepComment: options.comments,
      outputSourceRange: options.outputSourceRange,
      // 處理起始標籤
      start: function start (tag, attrs, unary, start$1, end) {
        ...
        var element = createASTElement(tag, attrs, currentParent);
        ...
      },
      // 用來處理結束標籤
      end: function end (tag, start, end$1) {
        var element = stack[stack.length - 1];
        // pop stack
        stack.length -= 1;
        currentParent = stack[stack.length - 1];
        if (options.outputSourceRange) {
          element.end = end$1;
        }
        closeElement(element);
      },
      // 用來處理文本
      chars: function chars (text, start, end) {
        ...
      },
      // 處理評論內容
      comment: function comment (text, start, end) {
        // adding anyting as a sibling to the root node is forbidden
        // comments should still be allowed, but ignored
        if (currentParent) {
          var child = {
            type: 3,
            text: text,
            isComment: true
          };
          if (options.outputSourceRange) {
            child.start = start;
            child.end = end;
          }
          currentParent.children.push(child);
        }
      }
    });
    return root
}
複製代碼

parseHTML函數核心內容爲:dom

while (html) {
      last = html;
      // Make sure we're not in a plaintext content element like script/style // 父元素爲正常元素 if (!lastTag || !isPlainTextElement(lastTag)) { var textEnd = html.indexOf('<'); // html以標籤開頭 if (textEnd === 0) { // Comment: if (comment.test(html)) { var commentEnd = html.indexOf('-->'); if (commentEnd >= 0) { if (options.shouldKeepComment) { options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3); } advance(commentEnd + 3); continue } } // Doctype: var doctypeMatch = html.match(doctype); if (doctypeMatch) { advance(doctypeMatch[0].length); continue } // End tag: 處理結束標籤 ... // Start tag: // 解析起始標籤 ... } ... } else { // 父元素爲script、style、textarea的處理邏輯 ... } if (html === last) { options.chars && options.chars(html); if (!stack.length && options.warn) { options.warn(("Mal-formatted tag at end of template: \"" + html + "\""), { start: index + html.length }); } break } } 複製代碼

基本過程以下:async

  1. html=<div id="app">{{ message }}</div>首先獲取textEnd === 0,接下來可判斷html是以div標籤起始的,進行parseStartTag處理
var startTagMatch = parseStartTag();
if (startTagMatch) {
    // 對獲取起始標籤的屬性,生成鍵值對
    handleStartTag(startTagMatch);
    if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
        advance(1);
    }
    continue
}
複製代碼

返回對象爲函數

{
    attrs: [" id="app"", "id", "=", "app", undefined, undefined, index: 0, input: " id="app">{{ message }}<button @click="update">更新</button></div>", groups: undefined, start: 4, end: 13],
    end: 14,
    start: 0,
    tagName: "div",
    unarySlash: ""
}
複製代碼

其中unarySlash表示是不是閉合標籤。在通過handleStartTag函數處理後調用start函數優化

if (!unary) {
    stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs,start: match.start, end: match.end });
    lastTag = tagName;
}
// 上面根據parseStartTag返回的對象生成attrs
if (options.start) {
    options.start(tagName, attrs, unary, match.start, match.end);
}
複製代碼

options.start函數處理後生成

{attrsList: [{
    end: 13
    name: "id"
    start: 5
    value: "app"
}],
attrsMap: {id: "app"},
children: [],
end: 14,
parent: undefined,
rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}},
start: 0,
tag: "div",
type: 1}
複製代碼

先判斷標籤是不是閉合標籤,若是是的話直接closeElement,不是的話,更新currentParent而且將當前元素推入stack棧。

if (!unary) {
    currentParent = element;
    stack.push(element);
} else {
    closeElement(element);
}
複製代碼

須要注意的是: stack棧的做用是維護DOM的層級,防止HTML標籤的不匹配。 2. 這個while的第一次循環結束,html被截取爲{{ message }}</div>,此時計算textEnd爲13,此時處理的是文本元素

// 爲文本節點
var text = (void 0), rest = (void 0), next = (void 0);
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);
}
複製代碼

此時rest = </div>, text = {{ message }},由 options.chars函數處理文本,具體代碼爲

...
var children = currentParent.children;
...
if (text) {
    if (!inPre && whitespaceOption === 'condense') {
        // condense consecutive whitespaces into single space
        text = text.replace(whitespaceRE$1, ' ');
    }
    var res;
    var child;
    // 帶變量的文本節點, type = 2
    if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
        child = {
            type: 2,
            expression: res.expression,
            tokens: res.tokens,
            text: text
        };
    } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
        // 不帶變量的文本節點,type = 3
        child = {
            type: 3,
            text: text
        };
    }
    if (child) {
        if (options.outputSourceRange) {
            child.start = start;
            child.end = end;
        }
        children.push(child);
    }
}
複製代碼

經由parseText函數

function parseText (
    text,
    delimiters
  ) {
      // 匹配 {{ message }}
    var tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;
    if (!tagRE.test(text)) {
      return
    }
    var tokens = [];
    var rawTokens = [];
    var lastIndex = tagRE.lastIndex = 0;
    var match, index, tokenValue;
    while ((match = tagRE.exec(text))) {
      index = match.index;
      // push text token
      // 把{{左邊的文本內容添加到tokens中
      if (index > lastIndex) {
        rawTokens.push(tokenValue = text.slice(lastIndex, index));
        tokens.push(JSON.stringify(tokenValue));
      }
      // tag token
      // 將 {{ message }} 轉成 _s(message) 添加到數組
      var exp = parseFilters(match[1].trim());
      tokens.push(("_s(" + exp + ")"));
      rawTokens.push({ '@binding': exp });
      lastIndex = index + match[0].length;
    }
    // 把{{右邊的文本內容添加到tokens中
    if (lastIndex < text.length) {
      rawTokens.push(tokenValue = text.slice(lastIndex));
      tokens.push(JSON.stringify(tokenValue));
    }
    return {
      expression: tokens.join('+'),
      tokens: rawTokens
    }
  }
複製代碼

處理後返回

{
    expression: "_s(message)",
    tokens: [{@binding: "message"}]
}
複製代碼

將該節點push進children,生成

[
    {
        end: 27,
        expression: "_s(message)",
        start: 14,
        text: "{{ message }}",
        tokens: [{@binding: "message"}],
        type: 2
    }
]
複製代碼
  1. 第三輪循環時html爲</div>,計算的textEnd = 0,匹配到結束標籤
var endTagMatch = html.match(endTag);
if (endTagMatch) {
    var curIndex = index;
    advance(endTagMatch[0].length);
    parseEndTag(endTagMatch[1], curIndex, index);
    continue
}
複製代碼

endTagMatch爲

[
    "</div>", "div", groups: undefined, index: 0, input: "</div>"
]
複製代碼

棧進行遍歷,尋找與當前結束標籤匹配的起始標籤,options.end函數處理

var element = stack[stack.length - 1];
// pop stack
stack.length -= 1;
currentParent = stack[stack.length - 1];
if (options.outputSourceRange) {
    element.end = end$1;
}
closeElement(element);
複製代碼

將棧中的元素pop出,currentParent爲棧的頂層第一個元素element, 更新元素element的end屬性,在進行closeElement主要代碼爲:

// 對元素的屬性進行處理,如ref,slot,is,attrs
if (!inVPre && !element.processed) {
    element = processElement(element, options);
}
...
// 將父子關係確認好
if (currentParent && !element.forbidden) {
    if (element.elseif || element.else) {
        processIfConditions(element, currentParent);
    } else {
    if (element.slotScope) {
        // scoped slot
        // keep it in the children list so that v-else(-if) conditions can
        // find it as the prev node.
        var name = element.slotTarget || '"default"'
        ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
        }
        // 將當前元素推入到當前父節點的children數組中,更新當前元素的父元素
        currentParent.children.push(element);
        element.parent = currentParent;
    }
}
複製代碼

生成的AST爲

{
    attrs: [{
        dynamic: undefined
        end: 13
        name: "id"
        start: 5
        value: ""app""
    }]
    attrsList: [{name: "id", value: "app", start: 5, end: 13}]
    attrsMap: {id: "app"}
    children: [{
        end: 27,
        expression: "_s(message)",
        start: 14,
        text: "{{ message }}",
        tokens: [{@binding: "message"}],
        type: 2
    }]
    end: 33
    parent: undefined
    plain: false
    rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}}
    start: 0
    tag: "div"
    type: 1
}
複製代碼

最後再更新stack和lastTag,stack=[],lastTag='div',循環結束,返回root這個生成的ast。

優化器

接下來進入優化階段,

optimize(ast, options);
// 生成靜態節點以及靜態根節點
function optimize (root, options) {
    if (!root) { return }
    isStaticKey = genStaticKeysCached(options.staticKeys || '');
    isPlatformReservedTag = options.isReservedTag || no;
    // first pass: mark all non-static nodes.
    markStatic$1(root);
    // second pass: mark static roots.
    markStaticRoots(root, false);
  }
複製代碼

首先給ast標記非靜態節點

function markStatic$1 (node) {
    // 先判斷該節點是不是靜態節點
    node.static = isStatic(node);
    if (node.type === 1) {
      // do not make component slot content static. this avoids
      // 1. components not able to mutate slot nodes
      // 2. static slot content fails for hot-reloading
      if (
        !isPlatformReservedTag(node.tag) &&
        node.tag !== 'slot' &&
        node.attrsMap['inline-template'] == null
      ) {
        return
      }
      for (var i = 0, l = node.children.length; i < l; i++) {
        var child = node.children[i];
        // 遞歸子節點標記靜態節點
        markStatic$1(child);
        // 若是子節點打完標記後,判斷子節點是不是靜態節點,若是不是,則父節點node不多是靜態節點,此時需將父節點設置static = false
        if (!child.static) {
          node.static = false;
        }
      }
      ...
    }
}
複製代碼

判斷是不是靜態節點的函數

function isStatic (node) {
    if (node.type === 2) { // expression,表達式
      return false
    }
    if (node.type === 3) { // text 文本節點
      return true
    }
    // 若是元素節點沒有v-pre,必須同時知足
    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 內置標籤,如slot、component
      isPlatformReservedTag(node.tag) && // not a component,必須是保留標籤,<list></list>就不是保留標籤
      !isDirectChildOfTemplateFor(node) && // 當前節點的父節點不能是帶v-for指令的template標籤
      Object.keys(node).every(isStaticKey) // 
    ))
  }
複製代碼

接着找出全部的靜態根節點並標記

// second pass: mark static roots.
markStaticRoots(root, false);
複製代碼

具體爲

function markStaticRoots (node, isInFor) {
    if (node.type === 1) {
      if (node.static || node.once) {
        node.staticInFor = isInFor;
      }
      // 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 (var i = 0, l = node.children.length; i < l; i++) { markStaticRoots(node.children[i], isInFor || !!node.for); } } ... } } 複製代碼

返回的AST爲:

{
    attrs: [{
        dynamic: undefined
        end: 13
        name: "id"
        start: 5
        value: ""app""
    }]
    attrsList: [{name: "id", value: "app", start: 5, end: 13}]
    attrsMap: {id: "app"}
    children: [{
        end: 27,
        expression: "_s(message)",
        start: 14,
        static: false,
        text: "{{ message }}",
        tokens: [{@binding: "message"}],
        type: 2
    }]
    end: 33
    parent: undefined
    plain: false,
    rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}}
    start: 0,
    static: false,
    staticRoot: false,
    tag: "div",
    type: 1
}
複製代碼

代碼生成器

做用將ast生成render渲染函數,不一樣的節點生成的方式不同,具體代碼以下:

function generate (
    ast,
    options
  ) {
    var state = new CodegenState(options);
    var code = ast ? genElement(ast, state) : '_c("div")';
    return {
      render: ("with(this){return " + code + "}"),
      staticRenderFns: state.staticRenderFns
    }
  }
複製代碼

先判斷ast是否存在,不存在默認是_c('div'),_c表示

vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
複製代碼

存在則對ast具體處理

function genElement (el, state) {
    if (el.parent) {
      el.pre = el.pre || el.parent.pre;
    }
    // 處理靜態節點
    if (el.staticRoot && !el.staticProcessed) {
      return genStatic(el, state)
    } else if (el.once && !el.onceProcessed) {
        // 處理v-once指令
      return genOnce(el, state)
    } else if (el.for && !el.forProcessed) {
        // 處理v-for指令
      return genFor(el, state)
    } else if (el.if && !el.ifProcessed) {
        // 處理v-if指令
      return genIf(el, state)
    } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
        // 處理template標籤
      return genChildren(el, state) || 'void 0'
    } else if (el.tag === 'slot') {
        // 處理slot內置組件
      return genSlot(el, state)
    } else {
      // component or element
      var code;
      if (el.component) {
          // 處理組件
        code = genComponent(el.component, el, state);
      } else {
        var data;
        // 處理元素
        if (!el.plain || (el.pre && state.maybeComponent(el))) {
          data = genData$2(el, state);
        }
        // 生成子節點的render函數部分
        var children = el.inlineTemplate ? null : genChildren(el, state, true);
        code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
      }
      // module transforms
      for (var i = 0; i < state.transforms.length; i++) {
        code = state.transforms[i](el, code);
      }
      return code
    }
  }
複製代碼

進入genData$2函數爲

function genData$2 (el, state) {
    var data = '{';

    // directives first.
    // directives may mutate the el's other properties before they are generated. var dirs = genDirectives(el, state); if (dirs) { data += dirs + ','; } // key if (el.key) { data += "key:" + (el.key) + ","; } // ref if (el.ref) { data += "ref:" + (el.ref) + ","; } if (el.refInFor) { data += "refInFor:true,"; } // pre if (el.pre) { data += "pre:true,"; } // record original tag name for components using "is" attribute if (el.component) { data += "tag:\"" + (el.tag) + "\","; } // module data generation functions for (var i = 0; i < state.dataGenFns.length; i++) { data += state.dataGenFns[i](el); } // attributes,更新attrs的屬性形式 if (el.attrs) { data += "attrs:" + (genProps(el.attrs)) + ","; } // DOM props if (el.props) { data += "domProps:" + (genProps(el.props)) + ","; } // event handlers if (el.events) { data += (genHandlers(el.events, false)) + ","; } if (el.nativeEvents) { data += (genHandlers(el.nativeEvents, true)) + ","; } // slot target // only for non-scoped slots if (el.slotTarget && !el.slotScope) { data += "slot:" + (el.slotTarget) + ","; } // scoped slots if (el.scopedSlots) { data += (genScopedSlots(el, el.scopedSlots, state)) + ","; } // component v-model if (el.model) { data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},"; } // inline-template if (el.inlineTemplate) { var inlineTemplate = genInlineTemplate(el, state); if (inlineTemplate) { data += inlineTemplate + ","; } } data = data.replace(/,$/, '') + '}'; // v-bind dynamic argument wrap // v-bind with dynamic arguments must be applied using the same v-bind object // merge helper so that class/style/mustUseProp attrs are handled correctly. if (el.dynamicAttrs) { data = "_b(" + data + ",\"" + (el.tag) + "\"," + (genProps(el.dynamicAttrs)) + ")"; } // v-bind data wrap if (el.wrapData) { data = el.wrapData(data); } // v-on data wrap if (el.wrapListeners) { data = el.wrapListeners(data); } return data } 複製代碼

其功能就是拼接字符串,先給data賦值一個'{', 而後發現節點有哪些屬性就將其拼接到data,最後加上一個'}',最後返回一個完整得data:

"{attrs:{"id":"app"}}"
複製代碼

接着對元素節點的子節點進行處理

genChildren(el, state, true);
複製代碼

具體函數爲:

function genChildren (
    el,
    state,
    checkSkip,
    altGenElement,
    altGenNode
  ) {
    var children = el.children;
    if (children.length) {
        ...
      var gen = altGenNode || genNode;
      return ("[" + (children.map(function (c) { return gen(c, state); }).join(',')) + "]" + (normalizationType$1 ? ("," + normalizationType$1) : ''))
    }
  }

複製代碼

根據不一樣子節點類型生成不一樣的節點字符串將其拼接在一塊兒,genNode函數爲:

function genNode (node, state) {
    if (node.type === 1) {
      return genElement(node, state)
    } else if (node.type === 3 && node.isComment) {
      return genComment(node)
    } else {
      return genText(node)
    }
  }
複製代碼

遞歸子節點來生成子節點的子節點,最後拼接到一塊兒返回。文本節點的處理

function genText (text) {
    return ("_v(" + (text.type === 2
      ? text.expression // no need for () because already wrapped in _s()
      : transformSpecialNewlines(JSON.stringify(text.text))) + ")")
  }
複製代碼

動態文本使用express表達式,靜態文本用text,把文本放在_v中做爲參數,生成的code即render函數爲:

"_c('div',{attrs:{"id":"app"}},[_v(_s(message))])"
複製代碼

最後由

updateComponent = function () {
    vm._update(vm._render(), hydrating);
};
複製代碼

中的vm._render()函數調用生成VNode,代碼爲

vnode = render.call(vm._renderProxy, vm.$createElement);
複製代碼

調用生成的render函數,指向vm._renderProxy,with語句的做用是將代碼的做用域設置到一個特定的做用域this中,調用後進入以下:

with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(message))])}
複製代碼

其中_s表示toString(),生成的VNode爲

{
    asyncFactory: undefined
    asyncMeta: undefined
    children: [
    {
        asyncFactory: undefined,
        asyncMeta: undefined,
        children: undefined,
        componentInstance: undefined,
        componentOptions: undefined,
        context: undefined,
        data: undefined,
        elm: undefined,
        fnContext: undefined,
        fnOptions: undefined,
        fnScopeId: undefined,
        isAsyncPlaceholder: false,
        isCloned: false,
        isComment: false,
        isOnce: false,
        isRootInsert: true,
        isStatic: false,
        key: undefined
        ns: undefined,
        parent: undefined,
        raw: false,
        tag: undefined,
        text: "Hello Wolrd",
        child: undefined
    }],
    componentInstance: undefined
    componentOptions: undefined
    context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
    data: {attrs: {id: 'app'}}
    elm: undefined,
    fnContext: undefined,
    fnOptions: undefined,
    fnScopeId: undefined,
    isAsyncPlaceholder: false,
    isCloned: false,
    isComment: false,
    isOnce: false,
    isRootInsert: true,
    isStatic: false,
    key: undefined,
    ns: undefined,
    parent: undefined,
    raw: false,
    tag: "div",
    text: undefined,
    child: undefined
}
複製代碼

此時編譯過程結束,下篇介紹VNode的渲染過程。

相關文章
相關標籤/搜索