render 函數(shù)是怎么來(lái)的?深入淺出 Vue 中的模板編譯

new Vue({
  render: h => h(App)
})

這個(gè)大家都熟悉,調(diào)用 render 就會(huì)得到傳入的模板(.vue文件)對(duì)應(yīng)的虛擬 DOM,那么這個(gè) render 是哪來(lái)的呢?它是怎么把 .vue 文件轉(zhuǎn)成瀏覽器可識(shí)別的代碼的呢?

render 函數(shù)是怎么來(lái)的有兩種方式

  • 第一種就是經(jīng)過(guò)模板編譯生成 render 函數(shù)
  • 第二種是我們自己在組件里定義了 render 函數(shù),這種會(huì)跳過(guò)模板編譯的過(guò)程

本文將為大家分別介紹這兩種,以及詳細(xì)的編譯過(guò)程原理

認(rèn)識(shí)模板編譯

我們知道 <template></template> 這個(gè)是模板,不是真實(shí)的 HTML,瀏覽器是不認(rèn)識(shí)模板的,所以我們需要把它編譯成瀏覽器認(rèn)識(shí)的原生的 HTML

這一塊的主要流程就是

  1. 提取出模板中的原生 HTML 和非原生 HTML,比如綁定的屬性、事件、指令等等
  2. 經(jīng)過(guò)一些處理生成 render 函數(shù)
  3. render 函數(shù)再將模板內(nèi)容生成對(duì)應(yīng)的 vnode
  4. 再經(jīng)過(guò) patch 過(guò)程( Diff )得到要渲染到視圖中的 vnode
  5. 最后根據(jù) vnode 創(chuàng)建真實(shí)的 DOM 節(jié)點(diǎn),也就是原生 HTML 插入到視圖中,完成渲染

上面的 1、2、3 條就是模板編譯的過(guò)程了

那它是怎么編譯,最終生成 render 函數(shù)的呢?

模板編譯詳解——源碼

baseCompile()

這就是模板編譯的入口函數(shù),它接收兩個(gè)參數(shù)

  • template:就是要轉(zhuǎn)換的模板字符串
  • options:就是轉(zhuǎn)換時(shí)需要的參數(shù)

編譯的流程,主要有三步:

  1. 模板解析:通過(guò)正則等方式提取出 <template></template> 模板里的標(biāo)簽元素、屬性、變量等信息,并解析成抽象語(yǔ)法樹(shù) AST
  2. 優(yōu)化:遍歷 AST 找出其中的靜態(tài)節(jié)點(diǎn)和靜態(tài)根節(jié)點(diǎn),并添加標(biāo)記
  3. 代碼生成:根據(jù) AST 生成渲染函數(shù) render

這三步分別對(duì)應(yīng)三個(gè)函數(shù),后面會(huì)一一下介紹,先看一下 baseCompile 源碼中是在哪里調(diào)用的

源碼地址:src/complier/index.js - 11行

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string, // 就是要轉(zhuǎn)換的模板字符串
  options: CompilerOptions //就是轉(zhuǎn)換時(shí)需要的參數(shù)
): CompiledResult {
  // 1. 進(jìn)行模板解析,并將結(jié)果保存為 AST
  const ast = parse(template.trim(), options)
  
  // 沒(méi)有禁用靜態(tài)優(yōu)化的話
  if (options.optimize !== false) {
    // 2. 就遍歷 AST,并找出靜態(tài)節(jié)點(diǎn)并標(biāo)記
    optimize(ast, options)
  }
  // 3. 生成渲染函數(shù)
  const code = generate(ast, options)
  return {
    ast,
    render: code.render, // 返回渲染函數(shù) render
    staticRenderFns: code.staticRenderFns
  }
})

就這么幾行代碼,三步,調(diào)用了三個(gè)方法很清晰

我們先看一下最后 return 出去的是個(gè)啥,再來(lái)深入上面這三步分別調(diào)用的方法源碼,也好更清楚的知道這三步分別是要做哪些處理

編譯結(jié)果

比如有這樣的模板

<template>
    <div id="app">{{name}}</div>
</template>

打印一下編譯后的結(jié)果,也就是上面源碼 return 出去的結(jié)果,看看是啥

{
  ast: {
    type: 1,
    tag: 'div',
    attrsList: [ { name: 'id', value: 'app' } ],
    attrsMap: { id: 'app' },
    rawAttrsMap: {},
    parent: undefined,
    children: [
      {
        type: 2,
        expression: '_s(name)',
        tokens: [ { '@binding': 'name' } ],
        text: '{{name}}',
        static: false
      }
    ],
    plain: false,
    attrs: [ { name: 'id', value: '"app"', dynamic: undefined } ],
    static: false,
    staticRoot: false
  },
  render: `with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(name))])}`,
  staticRenderFns: [],
  errors: [],
  tips: []
}

看不明白也沒(méi)有關(guān)系,注意看上面提到的三步都干了啥

  • ast 字段,就是第一步生成的
  • static 字段,就是標(biāo)記,是在第二步中根據(jù) ast 里的 type 加上去的
  • render 字段,就是第三步生成的

有個(gè)大概的印象了,然后再來(lái)看源碼

1. parse()

源碼地址:src/complier/parser/index.js - 79行

就是這個(gè)方法就是解析器的主函數(shù),就是它通過(guò)正則等方法提取出 <template></template> 模板字符串里所有的 tag、props、children 信息,生成一個(gè)對(duì)應(yīng)結(jié)構(gòu)的 ast 對(duì)象

parse 接收兩個(gè)參數(shù)

  • template :就是要轉(zhuǎn)換的模板字符串
  • options:就是轉(zhuǎn)換時(shí)需要的參數(shù)。它包含有四個(gè)鉤子函數(shù),就是用來(lái)把 parseHTML 解析出來(lái)的字符串提取出來(lái),并生成對(duì)應(yīng)的 AST

核心步驟是這樣的:

調(diào)用 parseHTML 函數(shù)對(duì)模板字符串進(jìn)行解析

  • 解析到開(kāi)始標(biāo)簽、結(jié)束標(biāo)簽、文本、注釋分別進(jìn)行不同的處理
  • 解析過(guò)程中遇到文本信息就調(diào)用文本解析器 parseText 函數(shù)進(jìn)行文本解析
  • 解析過(guò)程中遇到包含過(guò)濾器,就調(diào)用過(guò)濾器解析器 parseFilters 函數(shù)進(jìn)行解析

每一步解析的結(jié)果都合并到一個(gè)對(duì)象上(就是最后的 AST)

這個(gè)地方的源碼實(shí)在是太長(zhǎng)了,有大幾百行代碼,我就只貼個(gè)大概吧,有興趣的自己去看一下

export function parse (
  template: string, // 要轉(zhuǎn)換的模板字符串
  options: CompilerOptions // 轉(zhuǎn)換時(shí)需要的參數(shù)
): ASTElement | void {
  parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    outputSourceRange: options.outputSourceRange,
    // 解析到開(kāi)始標(biāo)簽時(shí)調(diào)用,如 <div>
    start (tag, attrs, unary, start, end) {
        // unary 是否是自閉合標(biāo)簽,如 <img />
        ...
    },
    // 解析到結(jié)束標(biāo)簽時(shí)調(diào)用,如 </div>
    end (tag, start, end) {
        ...
    },
    // 解析到文本時(shí)調(diào)用
    chars (text: string, start: number, end: number) {
      // 這里會(huì)判斷判斷很多東西,來(lái)看它是不是帶變量的動(dòng)態(tài)文本
      // 然后創(chuàng)建動(dòng)態(tài)文本或靜態(tài)文本對(duì)應(yīng)的 AST 節(jié)點(diǎn)
      ...
    },
    // 解析到注釋時(shí)調(diào)用
    comment (text: string, start, end) {
      // 注釋是這么找的
      const comment = /^<!\--/
      if (comment.test(html)) {
      // 如果是注釋?zhuān)屠^續(xù)找 '-->'
      const commentEnd = html.indexOf('-->')
      ...
    }
  })
  // 返回的這個(gè)就是 AST
  return root
}

上面解析文本時(shí)調(diào)用的 chars() 會(huì)根據(jù)不同類(lèi)型節(jié)點(diǎn)加上不同 type,來(lái)標(biāo)記 AST 節(jié)點(diǎn)類(lèi)型,這個(gè)屬性在下一步標(biāo)記的時(shí)候會(huì)用到

type AST 節(jié)點(diǎn)類(lèi)型
1 元素節(jié)點(diǎn)
2 包含變量的動(dòng)態(tài)文本節(jié)點(diǎn)
3 沒(méi)有變量的純文本節(jié)點(diǎn)

2. optimize()

這個(gè)函數(shù)就是在 AST 里找出靜態(tài)節(jié)點(diǎn)和靜態(tài)根節(jié)點(diǎn),并添加標(biāo)記,為了后面 patch 過(guò)程中就會(huì)跳過(guò)靜態(tài)節(jié)點(diǎn)的對(duì)比,直接克隆一份過(guò)去,從而優(yōu)化了 patch 的性能

函數(shù)里面調(diào)用的外部函數(shù)就不貼代碼了,大致過(guò)程是這樣的

  • 標(biāo)記靜態(tài)節(jié)點(diǎn)(markStatic)。就是判斷 type,上面介紹了值為 1、2、3的三種類(lèi)型

    • type 值為1:就是包含子元素的節(jié)點(diǎn),設(shè)置 static 為 false 并遞歸標(biāo)記子節(jié)點(diǎn),直到標(biāo)記完所有子節(jié)點(diǎn)
    • type 值為 2:設(shè)置 static 為 false
    • type 值為 3:就是不包含子節(jié)點(diǎn)和動(dòng)態(tài)屬性的純文本節(jié)點(diǎn),把它的 static = true,patch 的時(shí)候就會(huì)跳過(guò)這個(gè),直接克隆一份去
  • 標(biāo)記靜態(tài)根節(jié)點(diǎn)(markStaticRoots),這里的原理和標(biāo)記靜態(tài)節(jié)點(diǎn)基本相同,只是需要滿足下面條件的節(jié)點(diǎn)才能算作是靜態(tài)根節(jié)點(diǎn)

    • 節(jié)點(diǎn)本身必須是靜態(tài)節(jié)點(diǎn)
    • 必須有子節(jié)點(diǎn)
    • 子節(jié)點(diǎn)不能只有一個(gè)文本節(jié)點(diǎn)

源碼地址:src/complier/optimizer.js - 21行

export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // 標(biāo)記靜態(tài)節(jié)點(diǎn)
  markStatic(root)
  // 標(biāo)記靜態(tài)根節(jié)點(diǎn)
  markStaticRoots(root, false)
}

3. generate()

這個(gè)就是生成 render 的函數(shù),就是說(shuō)最終會(huì)返回下面這樣的東西

// 比如有這么個(gè)模板
<template>
    <div id="app">{{name}}</div>
</template>

// 上面模板編譯后返回的 render 字段 就是這樣的
render: `with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(name))])}`

// 把內(nèi)容格式化一下,容易理解一點(diǎn)
with(this){
  return _c(
    'div',
    { attrs:{"id":"app"} },
    [  _v(_s(name))  ]
  )
}

這個(gè)結(jié)構(gòu)是不是有點(diǎn)熟悉?

了解虛擬 DOM 就可以看出來(lái),上面的 render 正是虛擬 DOM 的結(jié)構(gòu),就是把一個(gè)標(biāo)簽分為 tag、props、children,沒(méi)有錯(cuò)

在看 generate 源碼之前,我們要先了解一下上面這最后返回的 render 字段是什么意思,再來(lái)看 generate 源碼,就會(huì)輕松得多,不然連函數(shù)返回的東西是干嘛的都不知道怎么可能看得懂這個(gè)函數(shù)呢

render

我們來(lái)翻譯一下上面編譯出來(lái)的 render

這個(gè) with 在 《你不知道的JavaScript》上卷里介紹的是,用來(lái)欺騙詞法作用域的關(guān)鍵字,它可以讓我們更快的引用一個(gè)對(duì)象上的多個(gè)屬性

看個(gè)例子

const name = '掘金'
const obj = { name:'沐華', age: 18 }
with(obj){
    console.log(name) // 沐華  不需要寫(xiě) obj.name 了
    console.log(age) // 18   不需要寫(xiě) obj.age 了
}

上面的 with(this){} 里的 this 就是當(dāng)前組件實(shí)例。因?yàn)橥ㄟ^(guò) with 改變了詞法作用域中屬性的指向,所以標(biāo)簽里使用 name 直接用就是了,而不需要 this.name 這樣

_c_v_s 是什么呢?

在源碼里是這樣定義的,格式是:_c(縮寫(xiě)) = createElement(函數(shù)名)

源碼地址:src/core/instance/render-helpers/index.js - 15行

// 其實(shí)不止這幾個(gè),由于本文例子中沒(méi)有用到就沒(méi)都復(fù)制過(guò)來(lái)占位了
export function installRenderHelpers (target: any) {
  target._s = toString // 轉(zhuǎn)字符串函數(shù)
  target._l = renderList // 生成列表函數(shù)
  target._v = createTextVNode // 創(chuàng)建文本節(jié)點(diǎn)函數(shù)
  target._e = createEmptyVNode // 創(chuàng)建空節(jié)點(diǎn)函數(shù)
}
// 補(bǔ)充
_c = createElement // 創(chuàng)建虛擬節(jié)點(diǎn)函數(shù)

再來(lái)看是不是就清楚多了呢

with(this){ // 欺騙詞法作用域,將該作用域里所有屬姓和方法都指向當(dāng)前組件
  return _c( // 創(chuàng)建一個(gè)虛擬節(jié)點(diǎn)
    'div', // 標(biāo)簽為 div
    { attrs:{"id":"app"} }, // 有一個(gè)屬性 id 為 'app'
    [  _v(_s(name))  ] // 是一個(gè)文本節(jié)點(diǎn),所以把獲取到的動(dòng)態(tài)屬性 name 轉(zhuǎn)成字符串
  )
}

接下來(lái)我們?cè)賮?lái)看 generate() 源碼

generate

源碼地址:src/complier/codegen/index.js - 43行

這個(gè)流程很簡(jiǎn)單,只有幾行代碼,就是先判斷 AST 是不是為空,不為空就根據(jù) AST 創(chuàng)建 vnode,否則就創(chuàng)建一個(gè)空div 的 vnode

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  // 就是先判斷 AST 是不是為空,不為空就根據(jù) AST 創(chuàng)建 vnode,否則就創(chuàng)建一個(gè)空div的 vnode
  const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
  
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

可以看出這里面主要就是通過(guò) genElement() 方法來(lái)創(chuàng)建 vnode 的,所以我們來(lái)看一下它的源碼,看是怎么創(chuàng)建的

genElement()

源碼地址:src/complier/codegen/index.js - 56行

這里的邏輯還是很清晰的,就是一堆 if/else 判斷傳進(jìn)來(lái)的 AST 元素節(jié)點(diǎn)的屬性來(lái)執(zhí)行不同的生成函數(shù)

這里還可以發(fā)現(xiàn)另一個(gè)知識(shí)點(diǎn) v-for 的優(yōu)先級(jí)要高于 v-if,因?yàn)橄扰袛?for 的

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }

  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) { // v-once
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) { // v-for
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) { // v-if
    return genIf(el, state)
     
    // template 節(jié)點(diǎn) && 沒(méi)有插槽 && 沒(méi)有 pre 標(biāo)簽
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') { // v-slot
    return genSlot(el, state)
  } else {
    // component or element
    let code
    // 如果有子組件
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      let data
      // 獲取元素屬性 props
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData(el, state)
      }
      // 獲取元素子節(jié)點(diǎ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)
    }
    // 返回上面作為 with 作用域執(zhí)行的內(nèi)容
    return code
  }
}

每一種類(lèi)型調(diào)用的生成函數(shù)就不一一列舉了,總的來(lái)說(shuō)最后創(chuàng)建出來(lái)的 vnode 節(jié)點(diǎn)類(lèi)型無(wú)非就三種,元素節(jié)點(diǎn)、文本節(jié)點(diǎn)、注釋節(jié)點(diǎn)

自定義的 render

先舉個(gè)例子吧,三種情況如下

// 1. test.vue
<template>
    <h1>我是沐華</h1>
</template>
<script>
  export default {}
</script>
// 2. test.vue
<script>
  export default {
    render(h){
      return h('h1',{},'我是沐華')
    }
  }
</script>
// 3. test.js
export default {
  render(h){
    return h('h1',{},'我是沐華')
  }
}

上面三種,最后渲染的出來(lái)的就是完全一模一樣的,因?yàn)檫@個(gè) h 就是上面模板編譯后的那個(gè) _c

這時(shí)有人可能就會(huì)問(wèn),為什么要自己寫(xiě)呢,不是有模板編譯自動(dòng)生成嗎?

這個(gè)問(wèn)題問(wèn)得好!自己寫(xiě)肯定是有好處的

  1. 自己把 vnode 給寫(xiě)了,就會(huì)直接跳過(guò)了模板編譯,不用去解析模板里的動(dòng)態(tài)屬性、事件、指令等等了,所以性能上會(huì)有那么一丟丟提升。這一點(diǎn)在下面的渲染的優(yōu)先級(jí)上就有體現(xiàn)
  2. 還有一些情況,能讓我們代碼寫(xiě)法的更加靈活,更加方便簡(jiǎn)潔,不會(huì)冗余

比如 Element-UI 里面的組件源碼里就有大量直接寫(xiě) render 函數(shù)

接下來(lái)分別看下這兩點(diǎn)是如何體現(xiàn)的

1. 渲染優(yōu)先級(jí)

先看一下在官網(wǎng)的生命周期里,關(guān)于模板編譯的部分

image

如圖可以知道,如果有 template,就不會(huì)管 el 了,所以 template 比 el 的優(yōu)先級(jí)更高,比如

那我們自己寫(xiě)了 render 呢?

<div id='app'>
    <p>{{ name }}</p>
</div>
<script>
    new Vue({
        el:'#app',
        data:{ name:'沐華' },
        template:'<div>掘金</div>',
        render(h){
            return h('div', {}, '好好學(xué)習(xí),天天向上')
        }
    })
</script>

這個(gè)代碼執(zhí)行后頁(yè)面渲染出來(lái)只有 <div>好好學(xué)習(xí),天天向上</div>

可以得出 render 函數(shù)的優(yōu)先級(jí)更高

因?yàn)椴还苁?el 掛載的,還是 emplate 最后都會(huì)被編譯成 render 函數(shù),而如果已經(jīng)有了 render 函數(shù)了,就跳過(guò)前面的編譯了

這一點(diǎn)在源碼里也有體現(xiàn)

在源碼中找到答案:dist/vue.js - 11927行

  Vue.prototype.$mount = function ( el, hydrating ) {
    el = el && query(el);
    var options = this.$options;
    // 如果沒(méi)有 render 
    if (!options.render) {
      var template = options.template;
      // 再判斷,如果有 template
      if (template) {
        if (typeof template === 'string') {
          if (template.charAt(0) === '#') {
            template = idToTemplate(template);
          }
        } else if (template.nodeType) {
          template = template.innerHTML;
        } else {
          return this
        }
      // 再判斷,如果有 el
      } else if (el) {
        template = getOuterHTML(el);
      }
    }
    return mount.call(this, el, hydrating)
  };

2. 更靈活的寫(xiě)法

比如說(shuō)我們需要寫(xiě)很多 if 判斷的時(shí)候

<template>
    <h1 v-if="level === 1">
      <a href="xxx">
        <slot></slot>
      </a>
    </h1>
    <h2 v-else-if="level === 2">
      <a href="xxx">
        <slot></slot>
      </a>
    </h2>
    <h3 v-else-if="level === 3">
      <a href="xxx">
        <slot></slot>
      </a>
    </h3>
</template>
<script>
  export default {
    props:['level']
  }
</script>

不知道你有沒(méi)有寫(xiě)過(guò)類(lèi)似上面這樣的代碼呢?

我們換一種方式來(lái)寫(xiě)出和上面一模一樣的代碼看看,直接寫(xiě) render

<script>
  export default {
    props:['level'],
    render(h){
      return h('h' + this.level, this.$slots.default())
    }
  }
</script>

搞定!就這!就這?

沒(méi)錯(cuò),就這!

或者下面這樣,多次調(diào)用的時(shí)候就很方便

<script>
  export default {
    props:['level'],
    render(h){
      const tag = 'h' + this.level
      return (<tag>{this.$slots.default()}</tag>)
    }
  }
</script>

補(bǔ)充

如果想知道更多格式的模板編譯出來(lái)是什么樣的,可以這樣

Vue2 的模板編譯可以安裝 vue-template-compiler

Vue3 的模板編譯可以點(diǎn)這里

然后自行測(cè)試

另外在 Vue3 里模板編譯部分有一些修改,可以點(diǎn)擊下面鏈接,看下 深入淺出虛擬 DOM 和 Diff 算法,里面有介紹

往期精彩

結(jié)語(yǔ)

如果本文對(duì)你有一丁點(diǎn)幫助,點(diǎn)個(gè)贊支持一下吧,感謝感謝

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容