Vue.js 源碼剖析-模板編譯

Study Notes

本博主會持續(xù)更新各種前端的技術(shù)劫樟,如果各位道友喜歡唱逢,可以關(guān)注螟凭、收藏蔓姚、點贊下本博主的文章酌儒。

Vue.js 源碼剖析-模板編譯

Vue 模板編譯

  • 為什么需要模板編譯

    Vue 2.x 使用 VNode 描述視圖以及各種交互,用戶自己編寫 VNode 比較復雜

  • 模板編譯的目的

    • 將模板(template)字符串轉(zhuǎn)換為渲染函數(shù)(render)
    • 用戶只需要編寫類似 HTML 的代碼 - Vue 模板叨襟,通過編譯器將模板轉(zhuǎn)換為返回 VNode 的 render 函數(shù)
    • .vue 文件在 webpack 構(gòu)建的過程中會被轉(zhuǎn)換成 render 函數(shù)

沙盒工具

官方提供 Vue 2.x 模板編譯沙盒

<iframe src="https://icqo5.csb.app/" style="width: 100%; height: 500px; border: 0px; border-radius: 4px; overflow: hidden; --darkreader-inline-border-top: initial; --darkreader-inline-border-right: initial; --darkreader-inline-border-bottom: initial; --darkreader-inline-border-left: initial;" title="vue-20-template-compilation" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin" data-darkreader-inline-border-top="" data-darkreader-inline-border-right="" data-darkreader-inline-border-bottom="" data-darkreader-inline-border-left=""></iframe>

Vue 2.6 Template Explorer 模板編譯沙盒

<iframe src="https://vue-template-explorer.netlify.app/#%3Cdiv%3E%0A%20%20%20%20%20%20%20%20%3Cheader%3E%0A%20%20%20%20%20%20%20%20%20%20%3Ch1%3EI'm%20a%20template!%3C%2Fh1%3E%0A%20%20%20%20%20%20%20%20%3C%2Fheader%3E%0A%20%20%20%20%20%20%20%20%3Cp%20v-if%3D%22message%22%3E%7B%7B%20message%20%7D%7D%3C%2Fp%3E%0A%20%20%20%20%20%20%20%20%3Cp%20v-else%3ENo%20message.%3C%2Fp%3E%0A%20%20%20%20%20%20%3C%2Fdiv%3E" style="width: 100%; height: 500px; border: 0px; border-radius: 4px; overflow: hidden; --darkreader-inline-border-top: initial; --darkreader-inline-border-right: initial; --darkreader-inline-border-bottom: initial; --darkreader-inline-border-left: initial;" title="vue-20-template-compilation" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin" data-darkreader-inline-border-top="" data-darkreader-inline-border-right="" data-darkreader-inline-border-bottom="" data-darkreader-inline-border-left=""></iframe>

Vue 3 Template Explorer 模板編譯沙盒

<iframe src="https://vue-next-template-explorer.netlify.app/#%7B%22src%22%3A%22%3Cdiv%3E%5Cr%5Cn%20%20%3Cheader%3E%5Cr%5Cn%20%20%20%20%3Ch1%3EI'm%20a%20template!%3C%2Fh1%3E%5Cr%5Cn%20%20%3C%2Fheader%3E%5Cr%5Cn%20%20%3Cp%20v-if%3D%5C%22message%5C%22%3E%7B%7B%20message%20%7D%7D%3C%2Fp%3E%5Cr%5Cn%20%20%3Cp%20v-else%3ENo%20message.%3C%2Fp%3E%5Cr%5Cn%3C%2Fdiv%3E%22%2C%22ssr%22%3Afalse%2C%22options%22%3A%7B%22mode%22%3A%22function%22%2C%22prefixIdentifiers%22%3Afalse%2C%22optimizeImports%22%3Afalse%2C%22hoistStatic%22%3Afalse%2C%22cacheHandlers%22%3Afalse%2C%22scopeId%22%3Anull%2C%22ssrCssVars%22%3A%22%7B%20color%20%7D%22%2C%22bindingMetadata%22%3A%7B%22TestComponent%22%3A%22setup%22%2C%22foo%22%3A%22setup%22%2C%22bar%22%3A%22props%22%7D%7D%7D" style="width: 100%; height: 500px; border: 0px; border-radius: 4px; overflow: hidden; --darkreader-inline-border-top: initial; --darkreader-inline-border-right: initial; --darkreader-inline-border-bottom: initial; --darkreader-inline-border-left: initial;" title="vue-20-template-compilation" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin" data-darkreader-inline-border-top="" data-darkreader-inline-border-right="" data-darkreader-inline-border-bottom="" data-darkreader-inline-border-left=""></iframe>

分析 render 函數(shù)

function anonymous() {
  with (this) {
    return _c('div', [
      _m(0),
      message ? _c('p', [_v(_s(message))]) : _c('p', [_v('No message.')]),
    ]);
  }
}
  • _c 是 createElement() 方法床蜘,定義的位置 src/core/instance/render.js 中
  • 相關(guān)的渲染函數(shù)(_開頭的方法定義),在 src/core/instance/render-helps/index.js 中
// src/core/instance/render.js
// 對編譯生成的render進行渲染的方法
// 具體可前往上一章虛擬Dom源碼剖析
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);

// src/core/instance/render-helps/index.js
// 生成文本虛擬節(jié)點
target._v = createTextVNode;
// 用于渲染靜態(tài)資源的方法
target._m = renderStatic;
// 將數(shù)據(jù)轉(zhuǎn)換為字符串格式
target._s = toString;

// src/core/vdom/vnode.js
// 生成文本虛擬節(jié)點
export function createTextVNode(val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val));
}

// src/core/instance/render-helpers/render-static.js
/**
 * Runtime helper for rendering static trees.
 * 運行時油航,用于渲染靜態(tài)抽象語法樹
 */
export function renderStatic(
  index: number,
  isInFor?: boolean,
): VNode | Array<VNode> {
  // static trees can be rendered once and cached on the contructor options
  // so every instance shares the same cached trees
  // 靜態(tài)樹可以渲染一次并緩存在構(gòu)造器選項上,每個實例共享相同的緩存中的靜態(tài)樹
  const renderFns = this.$options.staticRenderFns;
  const cached = renderFns.cached || (renderFns.cached = []);
  let tree = cached[index];
  // if has already-rendered static tree and not inside v-for,
  // we can reuse the same tree by doing a shallow clone.
  // 如果已經(jīng)渲染了靜態(tài)樹而不是在v-for內(nèi)部卢鹦,
  // 我們可以通過執(zhí)行淺復制來重用同一棵樹。
  if (tree && !isInFor) {
    return Array.isArray(tree) ? cloneVNodes(tree) : cloneVNode(tree);
  }
  // otherwise, render a fresh tree.
  // 否則劝堪,渲染一棵新的樹
  tree = cached[index] = renderFns[index].call(this._renderProxy, null, this);
  markStatic(tree, `__static__${index}`, false);
  return tree;
}

// src/shared/util.js
/**
 * Convert a value to a string that is actually rendered.
 * 將值轉(zhuǎn)換為實字符串冀自。
 */
export function toString(val: any): string {
  return val == null
    ? ''
    : typeof val === 'object'
    ? JSON.stringify(val, null, 2)
    : String(val);
}

編譯過程

編譯入口分析

// 把 template 轉(zhuǎn)換成 render 函數(shù)
const { render, staticRenderFns } = compileToFunctions(
  template,
  {
    shouldDecodeNewlines,
    delimiters: options.delimiters,
    comments: options.comments,
  },
  this,
);
/* @flow */

import { baseOptions } from './options';
import { createCompiler } from 'compiler/index';

// baseOptions 平臺相關(guān)的options
const { compile, compileToFunctions } = createCompiler(baseOptions);

export { compile, compileToFunctions };
export const createCompiler = createCompilerCreator(function baseCompile(
  template: string,
  options: CompilerOptions,
): CompiledResult {
  // 把模板轉(zhuǎn)換成 ast 抽象語法樹
  // 抽象語法樹,用來以樹形的方式描述代碼結(jié)構(gòu)
  const ast = parse(template.trim(), options);
  // 優(yōu)化抽象語法樹
  optimize(ast, options);
  // 把抽象語法樹生成字符串形式的 js 代碼
  const code = generate(ast, options);
  return {
    // 抽象語法樹
    ast,
    // 渲染函數(shù)
    render: code.render,
    // 靜態(tài)渲染函數(shù)秒啦,生成靜態(tài) VNode 樹
    staticRenderFns: code.staticRenderFns,
  };
});
// baseOptions 平臺相關(guān)的options
// src/platforms/web/compiler/options.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 errors = [];
      const tips = [];
      finalOptions.warn = (msg, tip) => {
        (tip ? tips : errors).push(msg);
      };

      if (options) {
        // merge custom modules
        // 合并自定義模塊
        if (options.modules) {
          finalOptions.modules = (baseOptions.modules || []).concat(
            options.modules,
          );
        }
        // merge custom directives
        // 合并自定義指令
        if (options.directives) {
          finalOptions.directives = extend(
            Object.create(baseOptions.directives),
            options.directives,
          );
        }
        // copy other options
        // 克隆其他配置
        for (const key in options) {
          if (key !== 'modules' && key !== 'directives') {
            finalOptions[key] = options[key];
          }
        }
      }

      const compiled = baseCompile(template, finalOptions);
      if (process.env.NODE_ENV !== 'production') {
        errors.push.apply(errors, detectErrors(compiled.ast));
      }
      compiled.errors = errors;
      compiled.tips = tips;
      return compiled;
    }

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

解析 - parse

解析器將模板解析為抽象語樹 AST熬粗,只有將模板解析成 AST 后,才能基于它做優(yōu)化或者生成代碼字符串余境。

/**
 * Convert HTML string to AST.
 * 將HTML字符串轉(zhuǎn)換為AST
 */
export function parse(
  template: string,
  options: CompilerOptions,
): ASTElement | void {
  warn = options.warn || baseWarn;

  // 解析 options
  // no: false
  platformIsPreTag = options.isPreTag || no;
  platformMustUseProp = options.mustUseProp || no;
  platformGetTagNamespace = options.getTagNamespace || no;

  // 根據(jù)key獲取options.modules對應的數(shù)據(jù)
  transforms = pluckModuleFunction(options.modules, 'transformNode');
  preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');
  postTransforms = pluckModuleFunction(options.modules, 'postTransformNode');

  // delimiters完整版的vue才有驻呐,只有在編譯的時候才會使用到灌诅,
  // 作用是改變差值表達式使用的符號
  // https://cn.vuejs.org/v2/api/#delimiters
  delimiters = options.delimiters;

  const stack = [];
  const preserveWhitespace = options.preserveWhitespace !== false;
  let root;
  let currentParent;
  let inVPre = false;
  let inPre = false;
  let warned = false;

  function warnOnce(msg) {
    if (!warned) {
      warned = true;
      warn(msg);
    }
  }

  function endPre(element) {
    // check pre state
    if (element.pre) {
      inVPre = false;
    }
    if (platformIsPreTag(element.tag)) {
      inPre = false;
    }
  }

  // 對模板解析
  parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldKeepComment: options.comments,
    // 解析過程中的回調(diào)函數(shù),生成 AST
    start(tag, attrs, unary) {
      // check namespace.
      // inherit parent ns if there is one
      const ns =
        (currentParent && currentParent.ns) || platformGetTagNamespace(tag);

      // handle IE svg bug
      /* istanbul ignore if */
      if (isIE && ns === 'svg') {
        attrs = guardIESVGBug(attrs);
      }

      // 生成AST(抽象語法樹)
      let element: ASTElement = createASTElement(tag, attrs, currentParent);
      if (ns) {
        element.ns = ns;
      }

      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.',
          );
      }

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

      // 跳過這個元素和它的子元素的編譯過程含末。
      // 可以用來顯示原始 Mustache 標簽猜拾。
      // 跳過大量沒有指令的節(jié)點會加快編譯。
      // https://cn.vuejs.org/v2/api/#v-pre
      if (!inVPre) {
        processPre(element);
        if (element.pre) {
          inVPre = true;
        }
      }
      if (platformIsPreTag(element.tag)) {
        inPre = true;
      }
      if (inVPre) {
        // 處理el屬性
        processRawAttrs(element);
      } else if (!element.processed) {
        // structural directives
        // 結(jié)構(gòu)化指令的處理
        // v-for
        processFor(element);
        // v-if
        processIf(element);
        // v-once
        processOnce(element);
        // element-scope stuff
        // 處理el
        processElement(element, options);
      }

      // 檢查根元素約束
      function checkRootConstraints(el) {
        if (process.env.NODE_ENV !== 'production') {
          if (el.tag === 'slot' || el.tag === 'template') {
            warnOnce(
              `Cannot use <${el.tag}> as component root element because it may ` +
                'contain multiple nodes.',
            );
          }
          if (el.attrsMap.hasOwnProperty('v-for')) {
            warnOnce(
              'Cannot use v-for on stateful component root element because ' +
                'it renders multiple elements.',
            );
          }
        }
      }

      // tree management
      // 抽象語法樹管理
      if (!root) {
        root = element;
        checkRootConstraints(root);
      } else if (!stack.length) {
        // allow root elements with v-if, v-else-if and v-else
        // 允許根元素帶有v-if佣盒,v-else-if和v-else
        if (root.if && (element.elseif || element.else)) {
          checkRootConstraints(element);
          // 將if語法樹添加到根語法樹里
          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.`,
          );
        }
      }
      if (currentParent && !element.forbidden) {
        if (element.elseif || element.else) {
          // 處理if語法樹
          processIfConditions(element, currentParent);
        } else if (element.slotScope) {
          // scoped slot
          currentParent.plain = false;
          const name = element.slotTarget || '"default"';
          (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[
            name
          ] = element;
        } else {
          currentParent.children.push(element);
          element.parent = currentParent;
        }
      }
      if (!unary) {
        currentParent = element;
        stack.push(element);
      } else {
        endPre(element);
      }
      // apply post-transforms
      for (let i = 0; i < postTransforms.length; i++) {
        postTransforms[i](element, options);
      }
    },

    end() {
      // remove trailing whitespace
      // 刪除尾部空格
      const element = stack[stack.length - 1];
      const lastNode = element.children[element.children.length - 1];
      if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
        element.children.pop();
      }
      // pop stack
      stack.length -= 1;
      currentParent = stack[stack.length - 1];
      endPre(element);
    },

    // 處理文本元素
    chars(text: string) {
      if (!currentParent) {
        if (process.env.NODE_ENV !== 'production') {
          if (text === template) {
            warnOnce(
              'Component template requires a root element, rather than just text.',
            );
          } else if ((text = text.trim())) {
            warnOnce(`text "${text}" outside root element will be ignored.`);
          }
        }
        return;
      }
      // IE textarea placeholder bug
      /* istanbul ignore if */
      if (
        isIE &&
        currentParent.tag === 'textarea' &&
        currentParent.attrsMap.placeholder === text
      ) {
        return;
      }
      const children = currentParent.children;
      text =
        inPre || text.trim()
          ? isTextTag(currentParent)
            ? text
            : decodeHTMLCached(text)
          : // only preserve whitespace if its not right after a starting tag
          preserveWhitespace && children.length
          ? ' '
          : '';
      if (text) {
        let expression;
        if (
          !inVPre &&
          text !== ' ' &&
          (expression = parseText(text, delimiters))
        ) {
          children.push({
            type: 2,
            expression,
            text,
          });
        } else if (
          text !== ' ' ||
          !children.length ||
          children[children.length - 1].text !== ' '
        ) {
          children.push({
            type: 3,
            text,
          });
        }
      }
    },
    // 處理注釋元素
    comment(text: string) {
      currentParent.children.push({
        type: 3,
        text,
        isComment: true,
      });
    },
  });
  return root;
}

Vue 官網(wǎng)查看

v-if/v-for 結(jié)構(gòu)化指令只能在編譯階段處理挎袜,如果我們要在 render 函數(shù)處理條件或循環(huán)只能使用
js 中的 if 和 for

在模板中使用的 v-if 和 v-for:

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>

這些都可以在渲染函數(shù)中用 JavaScript 的 if/else 和 map 來重寫:

props: ['items'],
render: function (createElement) {
  if (this.items.length) {
    return createElement('ul', this.items.map(function (item) {
      return createElement('li', item.name)
    }))
  } else {
    return createElement('p', 'No items found.')
  }
}

<iframe src="https://astexplorer.net/#/gist/f409cd9d81c248be28486f20e174784c/a4ed34b2e2532619ca2a2b41317b45790026224b" style="width: 100%; height: 500px; border: 0px; border-radius: 4px; overflow: hidden; --darkreader-inline-border-top: initial; --darkreader-inline-border-right: initial; --darkreader-inline-border-bottom: initial; --darkreader-inline-border-left: initial;" title="vue-20-template-compilation" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin" data-darkreader-inline-border-top="" data-darkreader-inline-border-right="" data-darkreader-inline-border-bottom="" data-darkreader-inline-border-left=""></iframe>

優(yōu)化 - optimize

  • 優(yōu)化抽象語法樹
  • 靜態(tài)節(jié)點 --> 永遠不會更改的節(jié)點
  • 標記非靜態(tài)節(jié)點和標記靜態(tài)根節(jié)點
  • 檢測靜態(tài)節(jié)點,設(shè)置為靜態(tài)肥惭,不需要在每次重新渲染的時候重新生成節(jié)點
  • patch 階段跳過靜態(tài)節(jié)點

src/compiler/index.js

// 優(yōu)化抽象語法樹
optimize(ast, options);

src/compiler/optimizer.js

export function optimize(root: ?ASTElement, options: CompilerOptions) {
  if (!root) return;
  isStaticKey = genStaticKeysCached(options.staticKeys || '');
  isPlatformReservedTag = options.isReservedTag || no;
  // first pass: mark all non-static nodes.
  // 第一次通過:標記非靜態(tài)節(jié)點盯仪。
  markStatic(root);
  // second pass: mark static roots.
  // 第二遍:標記靜態(tài)根節(jié)點。
  markStaticRoots(root, false);
}

function markStatic(node: ASTNode) {
  // 判斷當前的astNode是否是靜態(tài)的
  node.static = isStatic(node);
  // 元素節(jié)點
  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
    // 不要將組件插槽的內(nèi)容設(shè)為靜態(tài)
    // 1.組件無法更改插槽節(jié)點
    // 2.靜態(tài)插槽內(nèi)容無法進行熱重裝

    // 是組件蜜葱,不是slot全景,沒有inline-template,直接返回牵囤,阻止向下執(zhí)行
    if (
      !isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {
      return;
    }
    // 遍歷children
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i];
      // 標記非靜態(tài)
      markStatic(child);
      if (!child.static) {
        // 如果有一個child不是靜態(tài)爸黄,設(shè)置當前節(jié)點不是靜態(tài)
        node.static = false;
      }
    }
    // 處理條件渲染
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        const block = node.ifConditions[i].block;
        // 標記非靜態(tài)
        markStatic(block);
        if (!block.static) {
          // 如果有一個block不是靜態(tài)帕胆,設(shè)置當前節(jié)點不是靜態(tài)
          node.static = false;
        }
      }
    }
  }
}

function markStaticRoots(node: ASTNode, isInFor: boolean) {
  // 判斷是否為元素節(jié)點
  if (node.type === 1) {
    // 判斷當前節(jié)點是否是靜態(tài)節(jié)點或者是否只渲染一次
    if (node.static || node.once) {
      // 標記其在for循環(huán)中是否是靜態(tài)的
      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.
    // 如果一個元素內(nèi)只有文本節(jié)點屠阻,此時這個元素不是靜態(tài)的Root
    // vue 認為這種優(yōu)化會帶來負面的影響

    // 判斷當前節(jié)點是靜態(tài)節(jié)點,并且存在子節(jié)點糠亩,并且當前節(jié)點的子節(jié)點不能只存在一個文本節(jié)點
    // 如果不滿足汹桦,優(yōu)化成本將大于收益
    if (
      node.static &&
      node.children.length &&
      !(node.children.length === 1 && node.children[0].type === 3)
    ) {
      // 標記根節(jié)點為靜態(tài)
      node.staticRoot = true;
      return;
    } else {
      // 標記根節(jié)點為非靜態(tài)
      node.staticRoot = false;
    }
    // 檢測當前節(jié)點的子節(jié)點中是否有靜態(tài)的Root
    if (node.children) {
      for (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for);
      }
    }
    // 檢測當前節(jié)點的條件渲染中是否有靜態(tài)的Root
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor);
      }
    }
  }
}

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 不能是內(nèi)置組件
    isPlatformReservedTag(node.tag) && // not a component 不能是組件
    !isDirectChildOfTemplateFor(node) && // 不能是v-for下的直接節(jié)點
      Object.keys(node).every(isStaticKey))
  );
}

生成 - generate

  • 將抽象語法樹轉(zhuǎn)換為字符串形式的 js 代碼

src/compiler/index.js

// 將抽象語法樹轉(zhuǎn)換為字符串形式的 js 代碼
const code = generate(ast, options);

src/compiler/codegen/index.js

export function generate(
  ast: ASTElement | void,
  options: CompilerOptions,
): CodegenResult {
  // CodegenState 代碼生成過程中使用的狀態(tài)對象
  const state = new CodegenState(options);
  // 如果存在ast鲁驶,生成代碼,否則返回_c("div")
  const code = ast ? genElement(ast, state) : '_c("div")';
  return {
    render: `with(this){return ${code}}`,
    // 靜態(tài)渲染函數(shù)舞骆,即靜態(tài)根節(jié)點的渲染函數(shù)
    staticRenderFns: state.staticRenderFns,
  };
}

export function genElement(el: ASTElement, state: CodegenState): string {
  // el.staticProcessed用于判斷當前靜態(tài)根節(jié)點是否已處理钥弯,防止重復處理
  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);
    // 以下處理非靜態(tài)節(jié)點
  } else if (el.tag === 'template' && !el.slotTarget) {
    return genChildren(el, state) || 'void 0';
  } else if (el.tag === 'slot') {
    return genSlot(el, state);
  } else {
    // component or element
    // 處理組件和內(nèi)置標簽
    let code;
    if (el.component) {
      code = genComponent(el.component, el, state);
    } else {
      // 生成元素的屬性/指令/事件等
      // 處理各種指令,包括genDirective(model/text/html)
      const data = el.plain ? undefined : genData(el, state);

      // 將節(jié)點轉(zhuǎn)換為相應的代碼
      const children = el.inlineTemplate ? null : genChildren(el, state, true);
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`;
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code);
    }
    return code;
  }
}

// hoist static sub-trees out
function genStatic(el: ASTElement, state: CodegenState): string {
  // 將狀態(tài)置為已處理
  el.staticProcessed = true;
  // 將靜態(tài)根節(jié)點轉(zhuǎn)換為生成vnode的js代碼
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`);
  return `_m(${state.staticRenderFns.length - 1}${
    el.staticInFor ? ',true' : ''
  })`;
}

總結(jié)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末督禽,一起剝皮案震驚了整個濱河市脆霎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌狈惫,老刑警劉巖睛蛛,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異胧谈,居然都是意外死亡忆肾,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進店門菱肖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來客冈,“玉大人,你說我怎么就攤上這事稳强〕≈伲” “怎么了和悦?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長渠缕。 經(jīng)常有香客問我鸽素,道長,這世上最難降的妖魔是什么亦鳞? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任馍忽,我火速辦了婚禮,結(jié)果婚禮上蚜迅,老公的妹妹穿的比我還像新娘舵匾。我一直安慰自己俊抵,他們只是感情好谁不,可當我...
    茶點故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著徽诲,像睡著了一般刹帕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上谎替,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天偷溺,我揣著相機與錄音,去河邊找鬼钱贯。 笑死挫掏,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的秩命。 我是一名探鬼主播尉共,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼弃锐!你這毒婦竟也來了袄友?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤霹菊,失蹤者是張志新(化名)和其女友劉穎剧蚣,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體旋廷,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡鸠按,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了饶碘。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片待诅。...
    茶點故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖熊镣,靈堂內(nèi)的尸體忽然破棺而出卑雁,到底是詐尸還是另有隱情募书,我是刑警寧澤,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布测蹲,位于F島的核電站莹捡,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏扣甲。R本人自食惡果不足惜篮赢,卻給世界環(huán)境...
    茶點故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望琉挖。 院中可真熱鬧启泣,春花似錦、人聲如沸示辈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽矾麻。三九已至纱耻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間险耀,已是汗流浹背弄喘。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留甩牺,地道東北人蘑志。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像贬派,于是被迫代替她去往敵國和親急但。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,689評論 2 354

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