Vue.js 源碼學習七 —— template 解析過程學習

此次,來學習下Vue是如何解析HTML代碼的。

template 解析用在哪

從以前學習 Render 的過程當中咱們知道,template 的編譯在 $mount 方法中出現過。html

// src/platforms/web/entry-runtime-with-compiler.js
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) {
    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) === '#') {
          // 首字母爲#號,看做是ID。
          template = idToTemplate(template)
        }
      } else if (template.nodeType) {
        // 爲真實 DOM,直接獲取html
        template = template.innerHTML
      } else {
        return this
      }
    } else if (el) {
      // 獲取 HTML
      template = getOuterHTML(el)
    }
    if (template) {
      // 進行編譯並賦值給 vm.$options
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      // 渲染函數
      options.render = render
      // 靜態渲染方法
      options.staticRenderFns = staticRenderFns
    }
  }
  return mount.call(this, el, hydrating)
}

其實以上代碼總結起來就4步:前端

  1. 獲取el元素。
  2. 判斷el是否爲body或者html。
  3. 爲$options編譯render函數。
  4. 執行以前的mount函數。

關鍵在於第三步,編譯 render 函數上。先獲取 template,即獲取HTML內容,而後執行 compileToFunctions 來編譯,最後將 render 和 staticRenderFns 傳給 vm.$options 對象。
順便看看這兩個方法都用在哪裏?vue

// src/core/instance/render.js
  Vue.prototype._render = function (): VNode {
    try {
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
    }
    return vnode
  }
// src/core/instance/render-helpers/render-static.js
export function renderStatic (
  index: number,
  isInFor: boolean
): VNode | Array<VNode> {
  const cached = this._staticTrees || (this._staticTrees = [])
  let tree = cached[index]
  if (tree && !isInFor) {
    return tree
  }
  // otherwise, render a fresh tree.
  tree = cached[index] = this.$options.staticRenderFns[index].call(
    this._renderProxy,
    null,
    this 
  )
  markStatic(tree, `__static__${index}`, false)
  return tree
}

因而可知,template 編譯生成的方法都用在了渲染行爲中。node

編譯 template 的總體邏輯

下面咱們順着編譯代碼往下找。在 mount 方法中執行了 compileToFunctions 方法。git

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

找到方法的所在之處:github

// src/platforms/web/compiler/index.js
const { compile, compileToFunctions } = createCompiler(baseOptions)
// src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 將template轉爲AST語法樹對象
  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
  }
})

先看裏面的 baseCompile 方法,其做用爲將 HTML 字符串轉爲 AST 抽象語法樹對象,並進行優化,最後生成渲染代碼。返回值中 render 爲渲染字符串,staticRenderFns 爲渲染字符串數組。
以後再來看看 createCompilerCreator 方法:web

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

來看 compile 方法:合併 option 配置參數,而後執行外部傳入的 baseCompile 方法,返回方法執行的返回結果。最終返回 { compile, compileToFunctions }
createCompileToFunctionFn 代碼以下:正則表達式

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

    // 確認緩存,有緩存直接返回
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }

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

    // turn code into functions
    const res = {}
    const fnGenErrors = []
    // 生成 render 和 staticRenderFns 方法
    res.render = createFunction(compiled.render, fnGenErrors)
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })
    // 返回方法並緩存
    return (cache[key] = res)
  }
}

這裏就找到了咱們在 mount 方法中看到的 render 和 staticRenderFns 方法了。createCompileToFunctionFn 方法其實就是將傳入的 render 和 staticRenderFns 字符串轉爲真實方法。編程

至此,捋一下思路:
template的編譯用於render渲染行爲中,因此template最後生成渲染函數。
template 的解析過程當中數組

  • 經過 baseCompile 方法進行編譯;
  • 經過 createCompilerCreator 中的 compile 方法合併配置參數並返回 baseCompile 方法執行結果;
  • createCompilerCreator 返回 compile 方法和 compileToFunctions 方法;
  • compileToFunctions 方法用於將方法字符串生成真實方法。

其實 const { compile, compileToFunctions } = createCompiler(baseOptions) 就是 createCompilerCreator 的返回結果。因此,在 mount 中使用的 compileToFunctions 方法就是 createCompileToFunctionFn 方法生成的。

邏輯圖

baseCompile

總體思路濾清了,來看看關鍵的 baseCompile 方法。該方法進行了三步操做:

  • parse 將HTML解析爲 AST 元素。
  • optimize 渲染優化。
  • generate 解析成基本的 render 函數。

parse

先來說講AST抽象語法樹。維基百科的解釋是:

在計算機科學中,抽象語法樹(abstract syntax tree或者縮寫爲AST),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式,這裏特指編程語言的源代碼。

parse 方法的最終目的就是將 template 解析爲 AST 元素對象。在 parse 解析方法中,用到了大量的正則。正則的具體用法以前寫過一篇文章:一塊兒來理解正則表達式。代碼量不少,考慮了各類解析的狀況。這裏不贅述太多,找一條主線來學習,其餘內容我將在項目中註釋。

來看看 parse 方法。

export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  // 定義了各類參數和方法
  parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    start (tag, attrs, unary) {},
    end () {}
    chars (text: string) {},
    comment (text: string) {}
  )
  return root
}

實際上 parse 就是 parseHTML 的過程,最後返回AST元素對象。其中,傳入的 options 配置對象中,start、end、chars、comment方法都會在 parseHTML 方法中用到。其實相似於生命週期鉤子,在某個階段執行。
parseHTML 方法是正則解析HTML的過程,這部分我將在以後的博客中單獨說下,也能夠看項目的註釋,將不定時更新項目註釋。

optimize

該方法只是作了些標記靜態節點的行爲,目的是爲了在從新渲染時不重複渲染靜態節點,以達到性能優化的目的。

export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // 標記全部非靜態節點
  markStatic(root)
  // 標記靜態根節點
  markStaticRoots(root, false)
}

generate

generate 方法用於將 AST 元素生成 render 渲染字符串。

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

最後生成以下這樣的渲染字符串:

with(this){return _c('div',{attrs:{"id":"app"}},[_c('button',{on:{"click":hey}},[_v(_s(message))])])}

其中的 _c _v _s 等方法在哪裏呢~這個咱們以前提及過:

// src/core/instance/render.js
// 建立vnode元素
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// src/core/instance/render-helper/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
}

最後

其實template部分真的內容展開超級多,以後會展開細說。本來計劃大前天就把博客寫出來的,結果看代碼看着看着繞進去了。因此,仍是那句話,看代碼得抓住主線,帶着問題去看,不要在乎細枝末節。
這也算是個人經驗教訓了,之後每次看代碼,牢記待着明確的問題去看去解決。想一次看懂整個項目的代碼是不可行的。
下期預告,parseHTML 細節解析

Vue.js學習系列

鑑於前端知識碎片化嚴重,我但願可以系統化的整理出一套關於Vue的學習系列博客。

Vue.js學習系列項目地址

本文源碼已收入到GitHub中,以供參考,固然能留下一個star更好啦^-^。
https://github.com/violetjack/VueStudyDemos

關於做者

VioletJack,高效學習前端工程師,喜歡研究提升效率的方法,也專一於Vue前端相關知識的學習、整理。
歡迎關注、點贊、評論留言~我將持續產出Vue相關優質內容。

新浪微博: http://weibo.com/u/2640909603
掘金:https://gold.xitu.io/user/571...
CSDN: http://blog.csdn.net/violetja...
簡書: http://www.jianshu.com/users/...
Github: https://github.com/violetjack

相關文章
相關標籤/搜索