Vue 源碼深入解析之 編譯、編譯入口、parse、optimize 和 codegen

一、編譯

  1. 模板到真實 DOM 渲染的過程,中間有一個環節是把模板編譯成 render 函數,這個過程我們把它稱作編譯。雖然我們可以直接爲組件編寫 render 函數,但是編寫 template 模板更加直觀,也更符合我們的開發習慣。

  2. Vue.js 提供了兩個版本,一個是 Runtime + Compiler 的,一個是 Runtime only 的,前者是包含編譯代碼的,可以把編譯過程放在運行時做,後者是不包含編譯代碼的,需要藉助 webpackvue-loader 事先把模板編譯成 render函數。

  3. 這裏我們就來分析編譯的過程,對編譯過程的瞭解會讓我們對 Vue 的指令、內置組件等有更好的理解。不過由於編譯的過程是一個相對複雜的過程,我們只要求理解整體的流程、輸入和輸出即可。

二、編譯入口

  1. 當我們使用 Runtime + CompilerVue.js,它的入口是 src/platforms/web/entry-runtime-with-compiler.js,看一下它對 $mount 函數的定義:
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}
  1. 這段函數邏輯之前分析過,關於編譯的入口就是在這裏:
const { render, staticRenderFns } =  compileToFunctions(template, {
    shouldDecodeNewlines,
    shouldDecodeNewlinesForHref,
    delimiters: options.delimiters,
    comments: options.comments
  }, this)
options.render = render
options.staticRenderFns = staticRenderFns
  1. compileToFunctions 方法就是把模板 template 編譯生成 render 以及 staticRenderFns,它的定義在 src/platforms/web/compiler/index.js 中:
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'

const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }
  1. 可以看到 compileToFunctions 方法實際上是 createCompiler 方法的返回值,該方法接收一個編譯配置參數,接下來我們來看一下 createCompiler 方法的定義,在 src/compiler/index.js 中:
// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(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
  }
})
  1. createCompiler 方法實際上是通過調用 createCompilerCreator 方法返回的,該方法傳入的參數是一個函數,真正的編譯過程都在這個 baseCompile 函數裏執行,那麼 createCompilerCreator 又是什麼呢,它的定義在 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 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 || null),
            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)
    }
  }
}
  1. 可以看到該方法返回了一個 createCompiler 的函數,它接收一個 baseOptions 的參數,返回的是一個對象,包括 compile 方法屬性和 compileToFunctions 屬性,這個 compileToFunctions 對應的就是 $mount 函數調用的 compileToFunctions 方法,它是調用 createCompileToFunctionFn 方法的返回值,我們接下來看一下 createCompileToFunctionFn 方法,它的定義在 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 {
    options = extend({}, options)
    const warn = options.warn || baseWarn
    delete options.warn

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production') {
      // detect possible CSP restriction
      try {
        new Function('return 1')
      } catch (e) {
        if (e.toString().match(/unsafe-eval|CSP/)) {
          warn(
            'It seems you are using the standalone build of Vue.js in an ' +
            'environment with Content Security Policy that prohibits unsafe-eval. ' +
            'The template compiler cannot work in this environment. Consider ' +
            'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
            'templates into render functions.'
          )
        }
      }
    }

    // check cache
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }

    // compile
    const compiled = compile(template, options)

    // check compilation errors/tips
    if (process.env.NODE_ENV !== 'production') {
      if (compiled.errors && compiled.errors.length) {
        warn(
          `Error compiling template:\n\n${template}\n\n` +
          compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
          vm
        )
      }
      if (compiled.tips && compiled.tips.length) {
        compiled.tips.forEach(msg => tip(msg, vm))
      }
    }

    // turn code into functions
    const res = {}
    const fnGenErrors = []
    res.render = createFunction(compiled.render, fnGenErrors)
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })

    // check function generation errors.
    // this should only happen if there is a bug in the compiler itself.
    // mostly for codegen development use
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production') {
      if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
        warn(
          `Failed to generate render function:\n\n` +
          fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),
          vm
        )
      }
    }

    return (cache[key] = res)
  }
}
  1. 至此我們總算找到了 compileToFunctions 的最終定義,它接收三個參數、編譯模板 template,編譯配置 optionsVue 實例 vm。核心的編譯過程就一行代碼:
const compiled = compile(template, options)
  1. compile 函數在執行 createCompileToFunctionFn 的時候作爲參數傳入,它是 createCompiler 函數中定義的 compile 函數,如下所示:
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 || null),
        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
}
  1. compile 函數執行的邏輯是先處理配置參數,真正執行編譯過程就一行代碼:
const compiled = baseCompile(template, finalOptions)
  1. baseCompile 在執行 createCompilerCreator 方法時作爲參數傳入,如下所示:
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  optimize(ast, options)
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
  1. 編譯的入口我們終於找到了,它主要就是執行了如下幾個邏輯:
  • 解析模板字符串生成 AST
const ast = parse(template.trim(), options)
  • 優化語法樹
optimize(ast, options)
  • 生成代碼
const code = generate(ast, options)
  1. 總結:編譯入口邏輯之所以這麼繞,是因爲 Vue.js 在不同的平臺下都會有編譯的過程,因此編譯過程中的依賴的配置 baseOptions 會有所不同。而編譯過程會多次執行,但這同一個平臺下每一次的編譯過程配置又是相同的,爲了不讓這些配置在每次編譯過程都通過參數傳入,Vue.js 利用了函數柯里化的技巧很好的實現了 baseOptions 的參數保留。同樣,Vue.js 也是利用函數柯里化技巧把基礎的編譯過程函數抽出來,通過 createCompilerCreator(baseCompile) 的方式把真正編譯的過程和其它邏輯如對編譯配置處理、緩存處理等剝離開。

三、parse 的理解

  1. 編譯過程首先就是對模板做解析,生成 AST,它是一種抽象語法樹,是對源代碼的抽象語法結構的樹狀表現形式。在很多編譯技術中,如 babel 編譯 ES6 的代碼都會先生成 AST

  2. 這個過程是比較複雜的,它會用到大量正則表達式對字符串解析,如果對正則不是很瞭解,建議先去補習正則表達式的知識。爲了演示 parse 的過程,我們先來看一個例子:

<ul :class="bindCls" class="list" v-if="isShow">
    <li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>

經過 parse 過程後,生成的 AST 如下:

ast = {
  'type': 1,
  'tag': 'ul',
  'attrsList': [],
  'attrsMap': {
    ':class': 'bindCls',
    'class': 'list',
    'v-if': 'isShow'
  },
  'if': 'isShow',
  'ifConditions': [{
    'exp': 'isShow',
    'block': // ul ast element
  }],
  'parent': undefined,
  'plain': false,
  'staticClass': 'list',
  'classBinding': 'bindCls',
  'children': [{
    'type': 1,
    'tag': 'li',
    'attrsList': [{
      'name': '@click',
      'value': 'clickItem(index)'
    }],
    'attrsMap': {
      '@click': 'clickItem(index)',
      'v-for': '(item,index) in data'
     },
    'parent': // ul ast element
    'plain': false,
    'events': {
      'click': {
        'value': 'clickItem(index)'
      }
    },
    'hasBindings': true,
    'for': 'data',
    'alias': 'item',
    'iterator1': 'index',
    'children': [
      'type': 2,
      'expression': '_s(item)+":"+_s(index)'
      'text': '{{item}}:{{index}}',
      'tokens': [
        {'@binding':'item'},
        ':',
        {'@binding':'index'}
      ]
    ]
  }]
}

可以看到,生成的 AST 是一個樹狀結構,每一個節點都是一個 ast element,除了它自身的一些屬性,還維護了它的父子關係,如 parent 指向它的父節點,children 指向它的所有子節點。先對 AST 有一些直觀的印象,那麼接下來我們來分析一下這個 AST 是如何得到的。

  1. 整體流程,首先來看一下 parse 的定義,在 src/compiler/parser/index.js 中:
export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  getFnsAndConfigFromOptions(options)

  parseHTML(template, {
    // options ...
    start (tag, attrs, unary) {
      let element = createASTElement(tag, attrs)
      processElement(element)
      treeManagement()
    },

    end () {
      treeManagement()
      closeElement()
    },

    chars (text: string) {
      handleText()
      createChildrenASTOfText()
    },
    comment (text: string) {
      createChildrenASTOfComment()
    }
  })
  return astRootElement
}

parse 函數的代碼很長,先把它拆成僞代碼的形式,方便對於整體流程先有一個大致的瞭解。接下來我們就來分解分析每段僞代碼的作用。

  1. options 中獲取方法和配置,對應僞代碼:
getFnsAndConfigFromOptions(options)
  1. parse 函數的輸入是 templateoptions,輸出是 AST 的根節點。template 就是我們的模板字符串,而 options 實際上是和平臺相關的一些配置,它的定義在 src/platforms/web/compiler/options 中:
import {
  isPreTag,
  mustUseProp,
  isReservedTag,
  getTagNamespace
} from '../util/index'

import modules from './modules/index'
import directives from './directives/index'
import { genStaticKeys } from 'shared/util'
import { isUnaryTag, canBeLeftOpenTag } from './util'

export const baseOptions: CompilerOptions = {
  expectHTML: true,
  modules,
  directives,
  isPreTag,
  isUnaryTag,
  mustUseProp,
  canBeLeftOpenTag,
  isReservedTag,
  getTagNamespace,
  staticKeys: genStaticKeys(modules)
}
  1. 這些屬性和方法之所以放到 platforms 目錄下是因爲它們在不同的平臺(webweex)的實現是不同的。我們用僞代碼 getFnsAndConfigFromOptions 表示了這一過程,它的實際代碼如下:
warn = options.warn || baseWarn

platformIsPreTag = options.isPreTag || no
platformMustUseProp = options.mustUseProp || no
platformGetTagNamespace = options.getTagNamespace || no

transforms = pluckModuleFunction(options.modules, 'transformNode')
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')

delimiters = options.delimiters
  1. 解析 HTML 模板,對應僞代碼:
parseHTML(template, options)

對於 template 模板的解析主要是通過 parseHTML 函數,它的定義在 src/compiler/parser/html-parser 中:

export function parseHTML (html, options) {
  let lastTag
  while (html) {
    if (!lastTag || !isPlainTextElement(lastTag)){
      let textEnd = html.indexOf('<')
      if (textEnd === 0) {
         if(matchComment) {
           advance(commentLength)
           continue
         }
         if(matchDoctype) {
           advance(doctypeLength)
           continue
         }
         if(matchEndTag) {
           advance(endTagLength)
           parseEndTag()
           continue
         }
         if(matchStartTag) {
           parseStartTag()
           handleStartTag()
           continue
         }
      }
      handleText()
      advance(textLength)
    } else {
       handlePlainTextElement()
       parseEndTag()
    }
  }
}
  1. 由於 parseHTML 的邏輯也非常複雜,因此我也用了僞代碼的方式表達,整體來說它的邏輯就是循環解析 template ,用正則做各種匹配,對於不同情況分別進行不同的處理,直到整個 template 被解析完畢。
    在匹配的過程中會利用 advance 函數不斷前進整個模板字符串,直到字符串末尾,如下所示:
function advance (n) {
  index += n
  html = html.substring(n)
}
  1. 調用 advance 函數:
advance(4)

匹配的過程中主要利用了正則表達式,如下:

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/
  1. 通過這些正則表達式,我們可以匹配註釋節點、文檔類型節點、開始閉合標籤等,如下:
  • 註釋節點、文檔類型節點,對於註釋節點和文檔類型節點的匹配,如果匹配到我們僅僅做的是做前進即可,如下所示:
if (comment.test(html)) {
  const commentEnd = html.indexOf('-->')

  if (commentEnd >= 0) {
    if (options.shouldKeepComment) {
      options.comment(html.substring(4, commentEnd))
    }
    advance(commentEnd + 3)
    continue
  }
}

if (conditionalComment.test(html)) {
  const conditionalEnd = html.indexOf(']>')

  if (conditionalEnd >= 0) {
    advance(conditionalEnd + 2)
    continue
  }
}

const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
  advance(doctypeMatch[0].length)
  continue
}
  • 對於註釋和條件註釋節點,前進至它們的末尾位置;對於文檔類型節點,則前進它自身長度的距離。

  • 開始標籤,如下所示:

const startTagMatch = parseStartTag()
if (startTagMatch) {
  handleStartTag(startTagMatch)
  if (shouldIgnoreFirstNewline(lastTag, html)) {
    advance(1)
  }
  continue
}
  1. 首先通過 parseStartTag 解析開始標籤:
function parseStartTag () {
  const start = html.match(startTagOpen)
  if (start) {
    const match = {
      tagName: start[1],
      attrs: [],
      start: index
    }
    advance(start[0].length)
    let end, attr
    while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
      advance(attr[0].length)
      match.attrs.push(attr)
    }
    if (end) {
      match.unarySlash = end[1]
      advance(end[0].length)
      match.end = index
      return match
    }
  }
}
  1. 對於開始標籤,除了標籤名之外,還有一些標籤相關的屬性。函數先通過正則表達式 startTagOpen 匹配到開始標籤,然後定義了 match 對象,接着循環去匹配開始標籤中的屬性並添加到 match.attrs 中,直到匹配的開始標籤的閉合符結束。如果匹配到閉合符,則獲取一元斜線符,前進到閉合符尾,並把當前索引賦值給 match.endparseStartTag 對開始標籤解析拿到 match 後,緊接着會執行 handleStartTagmatch 做處理:
function handleStartTag (match) {
  const tagName = match.tagName
  const unarySlash = match.unarySlash
  
  if (expectHTML) {
    if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
      parseEndTag(lastTag)
    }
    if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
      parseEndTag(tagName)
    }
  }
  
  const unary = isUnaryTag(tagName) || !!unarySlash
  
  const l = match.attrs.length
  const attrs = new Array(l)
  for (let i = 0; i < l; i++) {
    const args = match.attrs[i]
    if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
      if (args[3] === '') { delete args[3] }
      if (args[4] === '') { delete args[4] }
      if (args[5] === '') { delete args[5] }
    }
    const value = args[3] || args[4] || args[5] || ''
    const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
      ? options.shouldDecodeNewlinesForHref
      : options.shouldDecodeNewlines
    attrs[i] = {
      name: args[1],
      value: decodeAttr(value, shouldDecodeNewlines)
    }
  }
  
  if (!unary) {
    stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
    lastTag = tagName
  }
  
  if (options.start) {
    options.start(tagName, attrs, unary, match.start, match.end)
  }
}
  1. handleStartTag 的核心邏輯很簡單,先判斷開始標籤是否是一元標籤,類似 <img>、<br/> 這樣,接着對 match.attrs 遍歷並做了一些處理,最後判斷如果非一元標籤,則往 stackpush 一個對象,並且把 tagName 賦值給 lastTag。最後調用了 options.start 回調函數,並傳入一些參數。

  2. 閉合標籤,如下所示:

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

先通過正則 endTag 匹配到閉合標籤,然後前進到閉合標籤末尾,然後執行 parseEndTag 方法對閉合標籤做解析。


function parseEndTag (tagName, start, end) {
  let pos, lowerCasedTagName
  if (start == null) start = index
  if (end == null) end = index
  
  if (tagName) {
    lowerCasedTagName = tagName.toLowerCase()
  }
  
  if (tagName) {
    for (pos = stack.length - 1; pos >= 0; pos--) {
      if (stack[pos].lowerCasedTag === lowerCasedTagName) {
        break
      }
    }
  } else {
    pos = 0
  }
  
  if (pos >= 0) {
    for (let i = stack.length - 1; i >= pos; i--) {
      if (process.env.NODE_ENV !== 'production' &&
        (i > pos || !tagName) &&
        options.warn
      ) {
        options.warn(
          `tag <${stack[i].tag}> has no matching end tag.`
        )
      }
      if (options.end) {
        options.end(stack[i].tag, start, end)
      }
    }
    stack.length = pos
    lastTag = pos && stack[pos - 1].tag
  } else if (lowerCasedTagName === 'br') {
    if (options.start) {
      options.start(tagName, [], true, start, end)
    }
  } else if (lowerCasedTagName === 'p') {
    if (options.start) {
      options.start(tagName, [], false, start, end)
    }
    if (options.end) {
      options.end(tagName, start, end)
    }
  }
}
  1. parseEndTag 的核心邏輯很簡單,在介紹之前我們回顧一下在執行 handleStartTag 的時候,對於非一元標籤(有 endTag)我們都把它構造成一個對象壓入到 stack 中。那麼對於閉合標籤的解析,就是倒序 stack,找到第一個和當前 endTag 匹配的元素。如果是正常的標籤匹配,那麼 stack 的最後一個元素應該和當前的 endTag 匹配,但是考慮到如下錯誤情況:
<div><span></div>
  1. 這個時候當 endTag</div> 的時候,從 stack 尾部找到的標籤是 <span>,就不能匹配,因此這種情況會報警告。匹配後把棧到 pos 位置的都彈出,並從 stack 尾部拿到 lastTag。最後調用了 options.end 回調函數,並傳入一些參數。

  2. 文本,如下所示:

let text, rest, next
if (textEnd >= 0) {
  rest = html.slice(textEnd)
  while (
    !endTag.test(rest) &&
    !startTagOpen.test(rest) &&
    !comment.test(rest) &&
    !conditionalComment.test(rest)
  ) {
    next = rest.indexOf('<', 1)
    if (next < 0) break
    textEnd += next
    rest = html.slice(textEnd)
  }
  text = html.substring(0, textEnd)
  advance(textEnd)
}

if (textEnd < 0) {
  text = html
  html = ''
}

if (options.chars && text) {
  options.chars(text)
}
  • 接下來判斷 textEnd 是否大於等於 0 的,滿足則說明到從當前位置到 textEnd 位置都是文本,並且如果 < 是純文本中的字符,就繼續找到真正的文本結束的位置,然後前進到結束的位置。

  • 再繼續判斷 textEnd 小於 0 的情況,則說明整個 template 解析完畢了,把剩餘的 html 都賦值給了 text

  • 最後調用了 options.chars 回調函數,並傳 text 參數,這個回調函數的作用稍後我會詳細介紹。

  • 因此,在循環解析整個 template 的過程中,會根據不同的情況,去執行不同的回調函數,下面我們來看看這些回調函數的作用。

  1. 處理開始標籤,對應僞代碼:
start (tag, attrs, unary) {
  let element = createASTElement(tag, attrs)
  processElement(element)
  treeManagement()
}
  1. 當解析到開始標籤的時候,最後會執行 start 回調函數,函數主要就做三件事情,創建 AST 元素,處理 AST 元素,AST 樹管理,下面我們來分別來看這幾個過程:
  • 創建 AST 元素
// 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)
}

let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
  element.ns = ns
}

export function createASTElement (
  tag: string,
  attrs: Array<Attr>,
  parent: ASTElement | void
): ASTElement {
  return {
    type: 1,
    tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    parent,
    children: []
  }
}

通過 createASTElement 方法去創建一個 AST 元素,並添加了 namespace。可以看到,每一個 AST 元素就是一個普通的 JavaScript 對象,其中,type 表示 AST 元素類型,tag 表示標籤名,attrsList 表示屬性列表,attrsMap 表示屬性映射表,parent 表示父的 AST 元素,children 表示子 AST 元素集合。

  • 處理 AST 元素
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
}

if (!inVPre) {
  processPre(element)
  if (element.pre) {
    inVPre = true
  }
}
if (platformIsPreTag(element.tag)) {
  inPre = true
}
if (inVPre) {
  processRawAttrs(element)
} else if (!element.processed) {
  // structural directives
  processFor(element)
  processIf(element)
  processOnce(element)
  // element-scope stuff
  processElement(element, options)
}

首先是對模塊 preTransforms 的調用,其實所有模塊的 preTransformstransformspostTransforms 的定義都在 src/platforms/web/compiler/modules 目錄中。接着判斷 element 是否包含各種指令通過 processXXX 做相應的處理,處理的結果就是擴展 AST 元素的屬性。這裏我並不會一一介紹所有的指令處理,而是結合我們當前的例子,我們來看一下 processForprocessIf

export function processFor (el: ASTElement) {
  let exp
  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
    const res = parseFor(exp)
    if (res) {
      extend(el, res)
    } else if (process.env.NODE_ENV !== 'production') {
      warn(
        `Invalid v-for expression: ${exp}`
      )
    }
  }
}

export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
export function parseFor (exp: string): ?ForParseResult {
  const inMatch = exp.match(forAliasRE)
  if (!inMatch) return
  const res = {}
  res.for = inMatch[2].trim()
  const alias = inMatch[1].trim().replace(stripParensRE, '')
  const iteratorMatch = alias.match(forIteratorRE)
  if (iteratorMatch) {
    res.alias = alias.replace(forIteratorRE, '')
    res.iterator1 = iteratorMatch[1].trim()
    if (iteratorMatch[2]) {
      res.iterator2 = iteratorMatch[2].trim()
    }
  } else {
    res.alias = alias
  }
  return res
}

processFor 就是從元素中拿到 v-for 指令的內容,然後分別解析出 foraliasiterator1iterator2 等屬性的值添加到 AST 的元素上。就我們的示例 v-for="(item,index) in data" 而言,解析出的的 fordataaliasitemiterator1index,沒有 iterator2,如下所示:

function processIf (el) {
  const exp = getAndRemoveAttr(el, 'v-if')
  if (exp) {
    el.if = exp
    addIfCondition(el, {
      exp: exp,
      block: el
    })
  } else {
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}
export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {
  if (!el.ifConditions) {
    el.ifConditions = []
  }
  el.ifConditions.push(condition)
}

processIf 就是從元素中拿 v-if 指令的內容,如果拿到則給 AST 元素添加 if 屬性和 ifConditions 屬性;否則嘗試拿 v-else 指令及 v-else-if 指令的內容,如果拿到則給 AST 元素分別添加 elseelseif 屬性。

  • AST 樹管理,我們在處理開始標籤的時候爲每一個標籤創建了一個 AST 元素,在不斷解析模板創建 AST 元素的時候,我們也要爲它們建立父子關係,就像 DOM 元素的父子關係那樣。AST 樹管理相關代碼如下:
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
  if (root.if && (element.elseif || element.else)) {
    checkRootConstraints(element)
    addIfCondition(root, {
      exp: element.elseif,
      block: element
    })
  } else if (process.env.NODE_ENV !== 'production') {
    warnOnce(
      `Component template should contain exactly one root element. ` +
      `If you are using v-if on multiple elements, ` +
      `use v-else-if to chain them instead.`
    )
  }
}
if (currentParent && !element.forbidden) {
  if (element.elseif || element.else) {
    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 {
  closeElement(element)
}
  1. AST 樹管理的目標是構建一顆 AST 樹,本質上它要維護 root 根節點和當前父節點 currentParent。爲了保證元素可以正確閉合,這裏也利用了 stack 棧的數據結構,和我們之前解析模板時用到的 stack 類似,如下所示:
  • 當我們在處理開始標籤的時候,判斷如果有 currentParent,會把當前 AST 元素 pushcurrentParent.chilldren 中,同時把 AST 元素的 parent 指向 currentParent

  • 接着就是更新 currentParentstack ,判斷當前如果不是一個一元標籤,我們要把它生成的 AST 元素 pushstack 中,並且把當前的 AST 元素賦值給 currentParent

  • stackcurrentParent 除了在處理開始標籤的時候會變化,在處理閉合標籤的時候也會變化,因此整個 AST 樹管理要結合閉合標籤的處理邏輯看。

  1. 處理閉合標籤,對應僞代碼:
end () {
  treeManagement()
  closeElement()
}

當解析到閉合標籤的時候,最後會執行 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]
closeElement(element)
  1. 首先處理了尾部空格的情況,然後把 stack 的元素彈一個出棧,並把 stack 最後一個元素賦值給 currentParent,這樣就保證了當遇到閉合標籤的時候,可以正確地更新 stack 的長度以及 currentParent 的值,這樣就維護了整個 AST 樹。最後執行了 closeElement(element)
function closeElement (element) {
  // check pre state
  if (element.pre) {
    inVPre = false
  }
  if (platformIsPreTag(element.tag)) {
    inPre = false
  }
  // apply post-transforms
  for (let i = 0; i < postTransforms.length; i++) {
    postTransforms[i](element, options)
  }
}

closeElement 邏輯很簡單,就是更新一下 inVPreinPre 的狀態,以及執行 postTransforms 函數。

  1. 處理文本內容,對應僞代碼:
chars (text: string) {
  handleText()
  createChildrenASTOfText()
}

除了處理開始標籤和閉合標籤,我們還會在解析模板的過程中去處理一些文本內容:

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 res
  if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
    children.push({
      type: 2,
      expression: res.expression,
      tokens: res.tokens,
      text
    })
  } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
    children.push({
      type: 3,
      text
    })
  }
}
  1. 文本構造的 AST 元素有兩種類型,一種是有表達式的,type2,一種是純文本,type3。在我們的例子中,文本就是 {{item}}:{{index}},是個表達式,通過執行 parseText(text, delimiters) 對文本解析,它的定義在 src/compiler/parser/text-parser.js 中:
const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g
const regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g

const buildRegex = cached(delimiters => {
  const open = delimiters[0].replace(regexEscapeRE, '\\$&')
  const close = delimiters[1].replace(regexEscapeRE, '\\$&')
  return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
})

export function parseText (
  text: string,
  delimiters?: [string, string]
): TextParseResult | void {
  const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
  if (!tagRE.test(text)) {
    return
  }
  const tokens = []
  const rawTokens = []
  let lastIndex = tagRE.lastIndex = 0
  let match, index, tokenValue
  while ((match = tagRE.exec(text))) {
    index = match.index
    // push text token
    if (index > lastIndex) {
      rawTokens.push(tokenValue = text.slice(lastIndex, index))
      tokens.push(JSON.stringify(tokenValue))
    }
    // tag token
    const exp = parseFilters(match[1].trim())
    tokens.push(`_s(${exp})`)
    rawTokens.push({ '@binding': exp })
    lastIndex = index + match[0].length
  }
  if (lastIndex < text.length) {
    rawTokens.push(tokenValue = text.slice(lastIndex))
    tokens.push(JSON.stringify(tokenValue))
  }
  return {
    expression: tokens.join('+'),
    tokens: rawTokens
  }
}
  1. parseText 首先根據分隔符(默認是 {{}})構造了文本匹配的正則表達式,然後再循環匹配文本,遇到普通文本就 pushrawTokenstokens 中,如果是表達式就轉換成 _s(${exp}) pushtokens 中,以及轉換成 {@binding:exp} pushrawTokens 中。對於我們的例子 {{item}}:{{index}}tokens 就是 [_s(item),'":"',_s(index)]rawTokens 就是 [{'@binding':'item'},':',{'@binding':'index'}]。那麼返回的對象如下:
return {
 expression: '_s(item)+":"+_s(index)',
 tokens: [{'@binding':'item'},':',{'@binding':'index'}]
}
  1. 總結:parse 的過程就分析完了,看似複雜,但我們可以拋開細節理清它的整體流程。parse 的目標是把 template 模板字符串轉換成 AST 樹,它是一種用 JavaScript 對象的形式來描述整個模板。那麼整個 parse 的過程是利用正則表達式順序解析模板,當解析到開始標籤、閉合標籤、文本的時候都會分別執行對應的回調函數,來達到構造 AST 樹的目的。AST 元素節點總共有三種類型,type1 表示是普通元素,爲 2 表示是表達式,爲 3 表示是純文本。當 AST 樹構造完畢,下一步就是 optimize 優化這顆樹。

三、optimize 的理解

  1. 當我們的模板 template 經過 parse 過程後,會輸出生成 AST 樹,那麼接下來我們需要對這顆樹做優化,optimize 的邏輯是遠簡單于 parse 的邏輯,所以理解起來會輕鬆很多。

  2. 我們知道 Vue 是數據驅動,是響應式的,但是我們的模板並不是所有數據都是響應式的,也有很多數據是首次渲染後就永遠不會變化的,那麼這部分數據生成的 DOM 也不會變化,我們可以在 patch 的過程跳過對他們的比對。來看一下 optimize 方法的定義,在 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) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // first pass: mark all non-static nodes.
  markStatic(root)
  // second pass: mark static roots.
  markStaticRoots(root, false)
}

function genStaticKeys (keys: string): Function {
  return makeMap(
    'type,tag,attrsList,attrsMap,plain,parent,children,attrs' +
    (keys ? ',' + keys : '')
  )
}

我們在編譯階段可以把一些 AST 節點優化成靜態節點,所以整個 optimize 的過程實際上就幹 2 件事情,markStatic(root) 標記靜態節點 ,markStaticRoots(root, false) 標記靜態根。

  1. 標記靜態節點,如下所示:
function markStatic (node: ASTNode) {
  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 (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
        }
      }
    }
  }
}

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)
  ))
}
  1. 首先執行 node.static = isStatic(node)isStatic 是對一個 AST 元素節點是否是靜態的判斷,如果是表達式,就是非靜態;如果是純文本,就是靜態;對於一個普通元素,如果有 pre 屬性,那麼它使用了 v-pre 指令,是靜態,否則要同時滿足以下條件:沒有使用 v-ifv-for,沒有使用其它指令(不包括 v-once),非內置組件,是平臺保留的標籤,非帶有 v-fortemplate 標籤的直接子節點,節點的所有屬性的 key 都滿足靜態 key;這些都滿足則這個 AST 節點是一個靜態節點。

  2. 如果這個節點是一個普通元素,則遍歷它的所有 children,遞歸執行 markStatic。因爲所有的 elseifelse 節點都不在 children 中, 如果節點的 ifConditions 不爲空,則遍歷 ifConditions 拿到所有條件中的 block,也就是它們對應的 AST 節點,遞歸執行 markStatic。在這些遞歸過程中,一旦子節點有不是 static 的情況,則它的父節點的 static 均變成 false

  3. 標記靜態根,如下所示:

function markStaticRoots (node: ASTNode, isInFor: boolean) {
  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 (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)
      }
    }
  }
}
  1. markStaticRoots 第二個參數是 isInFor,對於已經是 static 的節點或者是 v-once 指令的節點,node.staticInFor = isInFor。接着就是對於 staticRoot 的判斷邏輯,從註釋中我們可以看到,對於有資格成爲 staticRoot 的節點,除了本身是一個靜態節點外,必須滿足擁有 children,並且 children 不能只是一個文本節點,不然的話把它標記成靜態根節點的收益就很小了。接下來和標記靜態節點的邏輯一樣,遍歷 children 以及 ifConditions,遞歸執行 markStaticRoots

  2. 經過 optimize 後,AST 樹變成了如下:

ast = {
  'type': 1,
  'tag': 'ul',
  'attrsList': [],
  'attrsMap': {
    ':class': 'bindCls',
    'class': 'list',
    'v-if': 'isShow'
  },
  'if': 'isShow',
  'ifConditions': [{
    'exp': 'isShow',
    'block': // ul ast element
  }],
  'parent': undefined,
  'plain': false,
  'staticClass': 'list',
  'classBinding': 'bindCls',
  'static': false,
  'staticRoot': false,
  'children': [{
    'type': 1,
    'tag': 'li',
    'attrsList': [{
      'name': '@click',
      'value': 'clickItem(index)'
    }],
    'attrsMap': {
      '@click': 'clickItem(index)',
      'v-for': '(item,index) in data'
     },
    'parent': // ul ast element
    'plain': false,
    'events': {
      'click': {
        'value': 'clickItem(index)'
      }
    },
    'hasBindings': true,
    'for': 'data',
    'alias': 'item',
    'iterator1': 'index',
    'static': false,
    'staticRoot': false,
    'children': [
      'type': 2,
      'expression': '_s(item)+":"+_s(index)'
      'text': '{{item}}:{{index}}',
      'tokens': [
        {'@binding':'item'},
        ':',
        {'@binding':'index'}
      ],
      'static': false
    ]
  }]
}

我們發現每一個 AST 元素節點都多了 staic 屬性,並且 type 爲 1 的普通元素 AST 節點多了 staticRoot 屬性。

  1. 總結:分析完了 optimize 的過程,就是深度遍歷這個 AST 樹,去檢測它的每一顆子樹是不是靜態節點,如果是靜態節點則它們生成 DOM 永遠不需要改變,這對運行時對模板的更新起到極大的優化作用。我們通過 optimize 我們把整個 AST 樹中的每一個 AST 元素節點標記了 staticstaticRoot,它會影響我們接下來執行代碼生成的過程。

五、codegen 的理解

  1. 編譯的最後一步就是把優化後的 AST 樹轉換成可執行的代碼,瞭解整體流程即可。爲了方便理解,我們還是用之前的例子:
<ul :class="bindCls" class="list" v-if="isShow">
    <li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>

它經過編譯,執行 const code = generate(ast, options),生成的 render 代碼串如下:

with(this){
  return (isShow) ?
    _c('ul', {
        staticClass: "list",
        class: bindCls
      },
      _l((data), function(item, index) {
        return _c('li', {
          on: {
            "click": function($event) {
              clickItem(index)
            }
          }
        },
        [_v(_s(item) + ":" + _s(index))])
      })
    ) : _e()
}

這裏的 _c 函數定義在 src/core/instance/render.js 中。

vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

_l_v 定義在 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
}
  1. _c 就是執行 createElement 去創建 VNode,而 _l 對應 renderList 渲染列表;_v 對應 createTextVNode 創建文本 VNode_e 對於 createEmptyVNode創建空的 VNode。 在 compileToFunctions 中,會把這個 render 代碼串轉換成函數,它的定義在 src/compler/to-function.js 中:
const compiled = compile(template, options)
res.render = createFunction(compiled.render, fnGenErrors)

function createFunction (code, errors) {
  try {
    return new Function(code)
  } catch (err) {
    errors.push({ err, code })
    return noop
  }
}

實際上就是把 render 代碼串通過 new Function 的方式轉換成可執行的函數,賦值給 vm.options.render,這樣當組件通過 vm._render 的時候,就會執行這個 render 函數。那麼接下來我們就重點關注一下這個 render 代碼串的生成過程。

  1. generate,如下所示:
const code = generate(ast, options)

generate 函數的定義在 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
  }
}
  1. generate 函數首先通過 genElement(ast, state) 生成 code,再把 codewith(this){return ${code}}} 包裹起來。這裏的 stateCodegenState 的一個實例,先來看一下 genElement
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) {
    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 {
      const data = el.plain ? undefined : genData(el, state)

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

基本就是判斷當前 AST 元素節點的屬性執行不同的代碼生成函數,在我們的例子中,我們先了解一下 genForgenIf

  1. genIf,如下所示:
export function genIf (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  el.ifProcessed = true // avoid recursion
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}

function genIfConditions (
  conditions: ASTIfConditions,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  if (!conditions.length) {
    return altEmpty || '_e()'
  }

  const condition = conditions.shift()
  if (condition.exp) {
    return `(${condition.exp})?${
      genTernaryExp(condition.block)
    }:${
      genIfConditions(conditions, state, altGen, altEmpty)
    }`
  } else {
    return `${genTernaryExp(condition.block)}`
  }

  // v-if with v-once should generate code like (a)?_m(0):_m(1)
  function genTernaryExp (el) {
    return altGen
      ? altGen(el, state)
      : el.once
        ? genOnce(el, state)
        : genElement(el, state)
  }
}
  1. genIf 主要是通過執行 genIfConditions,它是依次從 conditions 獲取第一個 condition,然後通過對 condition.exp 去生成一段三元運算符的代碼,: 後是遞歸調用 genIfConditions,這樣如果有多個 conditions,就生成多層三元運算邏輯。這裏我們暫時不考慮 v-once 的情況,所以
    genTernaryExp 最終是調用了 genElement。在我們的例子中,只有一個 conditionexpisShow,因此生成如下僞代碼:
return (isShow) ? genElement(el, state) : _e()
  1. genFor,如下所示:
export function genFor (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altHelper?: string
): string {
  const exp = el.for
  const alias = el.alias
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''

  if (process.env.NODE_ENV !== 'production' &&
    state.maybeComponent(el) &&
    el.tag !== 'slot' &&
    el.tag !== 'template' &&
    !el.key
  ) {
    state.warn(
      `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
      `v-for should have explicit keys. ` +
      `See https://vuejs.org/guide/list.html#key for more info.`,
      true /* tip */
    )
  }

  el.forProcessed = true // avoid recursion
  return `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
      `return ${(altGen || genElement)(el, state)}` +
    '})'
}
  1. genFor 的邏輯很簡單,首先 AST 元素節點中獲取了和 for 相關的一些屬性,然後返回了一個代碼字符串。在我們的例子中,expdataaliasitemiterator1 ,因此生成如下僞代碼:
_l((data), function(item, index) {
  return genElememt(el, state)
})
  1. genData & genChildren,它的最外層是 ul,首先執行 genIf,它最終調用了 genElement(el, state) 去生成子節點,注意,這裏的 el 仍然指向的是 ul 對應的 AST 節點,但是此時的 el.ifProcessedtrue,所以命中最後一個 else 邏輯:
// component or element
let code
if (el.component) {
  code = genComponent(el.component, el, state)
} else {
  const data = el.plain ? undefined : genData(el, state)

  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

這裏我們只關注 2 個邏輯,genDatagenChildren

  1. genData,如下所示:
export function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'

  // directives first.
  // directives may mutate the el's other properties before they are generated.
  const 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 (let i = 0; i < state.dataGenFns.length; i++) {
    data += state.dataGenFns[i](el)
  }
  // attributes
  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, state.warn)},`
  }
  if (el.nativeEvents) {
    data += `${genHandlers(el.nativeEvents, true, state.warn)},`
  }
  // slot target
  // only for non-scoped slots
  if (el.slotTarget && !el.slotScope) {
    data += `slot:${el.slotTarget},`
  }
  // scoped slots
  if (el.scopedSlots) {
    data += `${genScopedSlots(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) {
    const inlineTemplate = genInlineTemplate(el, state)
    if (inlineTemplate) {
      data += `${inlineTemplate},`
    }
  }
  data = data.replace(/,$/, '') + '}'
  // v-bind data wrap
  if (el.wrapData) {
    data = el.wrapData(data)
  }
  // v-on data wrap
  if (el.wrapListeners) {
    data = el.wrapListeners(data)
  }
  return data
}
  1. genData 函數就是根據 AST 元素節點的屬性構造出一個 data 對象字符串,這個在後面創建 VNode 的時候的時候會作爲參數傳入。之前我們提到了 CodegenState 的實例 state,這裏有一段關於 state 的邏輯:
for (let i = 0; i < state.dataGenFns.length; i++) {
  data += state.dataGenFns[i](el)
}
  1. state.dataGenFns 的初始化在它的構造器中,如下所示:
export class CodegenState {
  constructor (options: CompilerOptions) {
    // ...
    this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
    // ...
  }
}
  1. 實際上就是獲取所有 modules 中的 genData 函數,其中,class modulestyle module 定義了 genData 函數。比如定義在 src/platforms/web/compiler/modules/class.js 中的 genData 方法:
function genData (el: ASTElement): string {
  let data = ''
  if (el.staticClass) {
    data += `staticClass:${el.staticClass},`
  }
  if (el.classBinding) {
    data += `class:${el.classBinding},`
  }
  return data
}
  1. 在我們的例子中,ul AST 元素節點定義了 el.staticClassel.classBinding,因此最終生成的 data 字符串如下:
{
  staticClass: "list",
  class: bindCls
}
  1. genChildren,接下來我們再來看一下 genChildren,它的定義在 src/compiler/codegen/index.js 中:
export function genChildren (
  el: ASTElement,
  state: CodegenState,
  checkSkip?: boolean,
  altGenElement?: Function,
  altGenNode?: Function
): string | void {
  const children = el.children
  if (children.length) {
    const el: any = children[0]
    if (children.length === 1 &&
      el.for &&
      el.tag !== 'template' &&
      el.tag !== 'slot'
    ) {
      return (altGenElement || genElement)(el, state)
    }
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
    const gen = altGenNode || genNode
    return `[${children.map(c => gen(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}
  1. 在我們的例子中,li AST 元素節點是 ul AST 元素節點的 children 之一,滿足 (children.length === 1 && el.for && el.tag !== 'template' && el.tag !== 'slot') 條件,因此通過 genElement(el, state) 生成 li AST元素節點的代碼,也就回到了我們之前調用 genFor 生成的代碼,把它們拼在一起生成的僞代碼如下:
return (isShow) ?
    _c('ul', {
        staticClass: "list",
        class: bindCls
      },
      _l((data), function(item, index) {
        return genElememt(el, state)
      })
    ) : _e()
  1. 在我們的例子中,在執行 genElememt(el, state) 的時候,el 還是 li AST 元素節點,el.forProcessed 已爲 true,所以會繼續執行 genDatagenChildren 的邏輯。由於 el.events 不爲空,在執行 genData 的時候,會執行 如下邏輯:
if (el.events) {
  data += `${genHandlers(el.events, false, state.warn)},`
}
  1. genHandlers 的定義在 src/compiler/codegen/events.js 中:
export function genHandlers (
  events: ASTElementHandlers,
  isNative: boolean,
  warn: Function
): string {
  let res = isNative ? 'nativeOn:{' : 'on:{'
  for (const name in events) {
    res += `"${name}":${genHandler(name, events[name])},`
  }
  return res.slice(0, -1) + '}'
}

genHandler 的邏輯就不介紹了,很大部分都是對修飾符 modifier 的處理,對於我們的例子,它最終 genData 生成的 data 字符串如下:

{
  on: {
    "click": function($event) {
      clickItem(index)
    }
  }
}

genChildren 的時候,會執行到如下邏輯:

export function genChildren (
  el: ASTElement,
  state: CodegenState,
  checkSkip?: boolean,
  altGenElement?: Function,
  altGenNode?: Function
): string | void {
  // ...
  const normalizationType = checkSkip
    ? getNormalizationType(children, state.maybeComponent)
    : 0
  const gen = altGenNode || genNode
  return `[${children.map(c => gen(c, state)).join(',')}]${
    normalizationType ? `,${normalizationType}` : ''
  }`
}

function genNode (node: ASTNode, state: CodegenState): string {
  if (node.type === 1) {
    return genElement(node, state)
  } if (node.type === 3 && node.isComment) {
    return genComment(node)
  } else {
    return genText(node)
  }
}
  1. genChildren 的就是遍歷 children,然後執行 genNode 方法,根據不同的 type 執行具體的方法。在我們的例子中,li AST 元素節點的 childrentype2 的表達式 AST 元素節點,那麼會執行到 genText(node) 邏輯,如下所示:
export function genText (text: ASTText | ASTExpression): string {
  return `_v(${text.type === 2
    ? text.expression
    : transformSpecialNewlines(JSON.stringify(text.text))
  })`
}

因此在我們的例子中,genChildren 生成的代碼串如下:

[_v(_s(item) + ":" + _s(index))]

和之前拼在一起,最終生成的 code 如下:

 return (isShow) ?
    _c('ul', {
        staticClass: "list",
        class: bindCls
      },
      _l((data), function(item, index) {
        return _c('li', {
          on: {
            "click": function($event) {
              clickItem(index)
            }
          }
        },
        [_v(_s(item) + ":" + _s(index))])
      })
    ) : _e()
  1. 總結:從 ast -> code 這一步有了一些瞭解,編譯後生成的代碼就是在運行時執行的代碼。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章