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的渲染过程。

相关文章
相关标签/搜索