學(xué)習(xí)vue2.5源碼之第五篇——compiler簡(jiǎn)述

template 與 render

在使用vue的時(shí)候,我們有兩種方式來(lái)創(chuàng)建我們的HTML頁(yè)面,第一種情況,也是大多情況下,我們會(huì)使用模板template的方式,因?yàn)檫@更易讀易懂也是官方推薦的方法;第二種情況是使用render函數(shù)來(lái)生成HTML,它比template更接近編譯器,這也對(duì)我們的JavaScript的編程能力要求更高。

實(shí)際上使用render函數(shù)的方式會(huì)比使用template的效率更高,因?yàn)関ue會(huì)先將template編譯成render函數(shù),然后再走之后的流程,也就是說(shuō)使用template會(huì)比render多走編譯成render這一步,而這一步就是由我們的compiler來(lái)實(shí)現(xiàn)的啦,本節(jié)講述的就是vue源碼中的編譯器compiler,它是如何一步步將template最后轉(zhuǎn)換為render。

$mount小插曲

由于在vue實(shí)例的那一篇我漏掉了$mount的具體實(shí)現(xiàn),而理解$mount的流程也會(huì)幫助我們更好地引入compiler,那我們就快速地看看$mount(假如覺(jué)得只想看編譯部分的同學(xué)可以跳過(guò)這一段哦

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) {
    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
  // 沒(méi)有render時(shí)將template轉(zhuǎn)化為render
  if (!options.render) {
    let template = options.template

    // 有template
    if (template) {
      // 判斷template類型(#id、模板字符串、dom元素)
      // template是字符串
      if (typeof template === 'string') {
        // template是#id
        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
            )
          }
        }
      // template是dom元素
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        // 無(wú)效template
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    // 無(wú)template
    } else if (el) {
      // 如果 render 函數(shù)和 template 屬性都不存在,掛載 DOM 元素的 HTML 會(huì)被提取出來(lái)用作模板
      template = getOuterHTML(el)
    }

    // 執(zhí)行template => compileToFunctions()
    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')
      }
    }
  }
  // 有render
  return mount.call(this, el, hydrating)
}

整體流程:

  • 用一個(gè)變量mount把原來(lái)的$mount方法存起來(lái),再重寫$mount方法
  • 然后對(duì)el進(jìn)行處理,el可以是dom節(jié)點(diǎn)或者是節(jié)點(diǎn)的選擇器字符串,若是后者的話在通過(guò)query(el)進(jìn)行轉(zhuǎn)換
  • el不能是html或者body元素(也就是說(shuō)不能直接將vue綁定在html或者body標(biāo)簽上)
  • 若沒(méi)有render函數(shù)
    • 若有template,判斷template類型(#id、模板字符串、dom元素)
    • render函數(shù)和template都不存在,掛載DOM元素的HTML會(huì)被提取出來(lái)用作template
    • 執(zhí)行template => compileToFunctions(),將template轉(zhuǎn)換為render
  • 若有render函數(shù)
    • 走原來(lái)的$mount方法

這里就證明了使用template的話還是會(huì)先轉(zhuǎn)換為render再進(jìn)行下一步的操作,我們接著看下一步發(fā)生了什么吧~

runtime/index.js

上一個(gè)文件中的vue是來(lái)自這里的,我們?cè)谶@里可以看到

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

可以看出這個(gè)$mount方法返回的是mountComponent這個(gè)方法,我們又繼續(xù)找找

instance/lifecycle.js

原來(lái)mountComponent是在lifecycle.js中,兜兜轉(zhuǎn)轉(zhuǎn)我們又回到了實(shí)例的這一塊來(lái)~

export function mountComponent () {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
  }
  callHook(vm, 'beforeMount')

  let updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  vm._watcher = new Watcher(vm, updateComponent, noop)
  hydrating = false
  
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

這里的操作就是在調(diào)用beforeMount鉤子前檢查選項(xiàng)里有沒(méi)有render函數(shù),沒(méi)有的話我們就給它建個(gè)空的,然后我們執(zhí)行vm._update(vm._render(), hydrating),再用watcher進(jìn)行數(shù)據(jù)綁定,然后調(diào)用mounted鉤子。關(guān)于_update_render的實(shí)現(xiàn)我們先賣個(gè)關(guān)子~ 等我們學(xué)到虛擬dom實(shí)現(xiàn)的時(shí)候再看。

compiler 整體流程

前面搞了這么多前戲,終于開始講compiler了~ 還記得剛提到重寫的$mount方法嗎,里面將template轉(zhuǎn)換為render是通過(guò)compileToFunctions方法實(shí)現(xiàn)的,我們看看他的來(lái)頭,之后的邏輯會(huì)有點(diǎn)繞但是不難理解,提醒~~~~ 對(duì)于繞來(lái)繞去的源碼有一個(gè)好的方法就是寫demo + 打斷點(diǎn)!根據(jù)你的需求去打斷點(diǎn)看一下輸出的內(nèi)容是否符合你的預(yù)期,這會(huì)對(duì)你理解源碼很有幫助哦,在后面的學(xué)習(xí)中我們也會(huì)用例子去分析~~~ 跟隨著compileToFunctions的源頭,我們走起!~

platforms/web/compiler/index.js

const { compile, compileToFunctions } = createCompiler(baseOptions)

src/compiler/index.js

export const createCompiler = createCompilerCreator(function baseCompile () {
  ...
})

src/compiler/create-compiler.js

export function createCompilerCreator (baseCompile){
  return function createCompiler (baseOptions) {
    function compile (template, options) {
      const finalOptions = Object.create(baseOptions)

      // merge
      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]
          }
        }
      }

      // finalOptions 合并 baseOptions 和 options
      const compiled = baseCompile(template, finalOptions)
      return compiled
    }

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

呼~一下子貼這么多代碼,好怕被打,我們可以先把路找著,具體代碼再慢慢看。
createCompilerCreator是個(gè)高階函數(shù),接受一個(gè)函數(shù)baseCompile,返回了一個(gè)函數(shù)createCompilercreateCompiler函數(shù)里又有一個(gè)compile函數(shù),里面調(diào)用了baseCompile和最初傳入的baseOptions,最后返回compile函數(shù)和compileToFunctions函數(shù)。emmm...有點(diǎn)亂呵,我畫個(gè)圖給你們將就著看吧。。。

我們先看create-compiler.js中的createCompilerCreator函數(shù)中的createCompiler函數(shù)中的compile函數(shù)中(好累。。。):

先是將參數(shù)baseOptions和傳入的options進(jìn)行合并得到finalOptions,再進(jìn)行最關(guān)鍵一步(終于!):const compiled = baseCompile(template, finalOptions)

baseCompile函數(shù)就是最外層createCompilerCreator函數(shù)的一個(gè)參數(shù),這個(gè)關(guān)鍵的流程我們等下就看,我們先繼續(xù),由baseCompile得到了我們想要的結(jié)果compiled,再返回給上一個(gè)函數(shù)createCompiler,在return中有我們要的一個(gè)函數(shù),就是我們最開始調(diào)用的compileToFunctions,原來(lái)他就是通過(guò)一個(gè)函數(shù)將我們的compile結(jié)果轉(zhuǎn)換為compileToFunctions

我們?nèi)タ纯催@個(gè)轉(zhuǎn)換函數(shù)createCompileToFunctionFn,然后對(duì)比一下轉(zhuǎn)換前后兩者的差別。在src/compiler/to-function.js
文件中,我就不貼代碼了,你們自己對(duì)著源碼看吧,我說(shuō)一下里面主要完成的操作就是執(zhí)行了compile函數(shù)得到原來(lái)的值再進(jìn)行轉(zhuǎn)化,再將其存進(jìn)緩存中。

而原compile返回的結(jié)構(gòu)是:

{
    ast,
    render,
    staticRenderFns
}

經(jīng)過(guò)轉(zhuǎn)化后沒(méi)有了ast,而且將renderstaticRenderFns轉(zhuǎn)換為函數(shù)的形式:

{
    render,
    staticRenderFns
}

看完了整體流程,我們看回很關(guān)鍵的函數(shù)baseCompile

baseCompile

function baseCompile (template, options) {
  const ast = parse(template.trim(), options)
  optimize(ast, options)
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
}

其實(shí)這個(gè)函數(shù)很短但是闡述了我們編譯的全過(guò)程

parse -> optimize -> generate

step 1 :先對(duì)template進(jìn)行parse得到抽象語(yǔ)法樹AST

step 2 :將AST進(jìn)行靜態(tài)優(yōu)化

step 3 :由AST生成render

返回的格式就是

{
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
}

最后我放上來(lái)自web平臺(tái)中的baseOptions的配置含義,方便你們以后看源碼可以查詢

{
  expectHTML: true, // 是否期望HTML,不知道是啥反正web中的是true
  modules, // klass和style,對(duì)模板中類和樣式的解析
  directives, // v-model、v-html、v-text
  isPreTag, // v-pre標(biāo)簽
  isUnaryTag, // 單標(biāo)簽,比如img、input、iframe
  mustUseProp, // 需要使用props綁定的屬性,比如value、selected等
  canBeLeftOpenTag, // 可以不閉合的標(biāo)簽,比如tr、td等
  isReservedTag, // 是否是保留標(biāo)簽,html標(biāo)簽和SVG標(biāo)簽
  getTagNamespace, // 命名空間,svg和math
  staticKeys: genStaticKeys(modules) // staticClass,staticStyle。
}

這三個(gè)步驟在接下來(lái)的文章里我們會(huì)進(jìn)行更詳細(xì)的分析~ 對(duì)于compiler的概念和整體的流程都基本講完啦,謝謝你們的支持,如有分析錯(cuò)誤之處可以隨意提出來(lái),我們一起探討探討~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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