Vue.js render函數那些事兒

大多時候,我會使用template, vue單文件去渲染組件。雖然知道Vue中有個render函數,但卻不多在項目中去主動使用它。使用最多的地方是在使用一些UI框架的時候,好比iview table中的按鈕操做,會使用到render函數。另外平時在閱讀一些Vue UI框架源碼的時候,也時常能遇到使用render函數的地方,這也激發了本身研究學習的慾望。若是你也感興趣,那就繼續閱讀吧。vue

在本文中,會有以下內容:node

  • 什麼是Vue render函數
  • Vue編譯器如何處理render函數
  • 建立一個組件
  • 在render函數中使用指令
  • Vue渲染函數中的事件綁定
  • 模板覆蓋的實際用例

讓咱們開始吧!數組

什麼是Vue render函數

Vue.js模板功能強大,幾乎能夠知足咱們在應用程序中所需的一切。可是,有一些場景下,好比基於輸入或插槽值建立動態組件,render函數能夠更好地知足這些用例。瀏覽器

那些來自React世界的開發者可能對render函數很是熟悉。一般在JSX中使用它們來構建React組件。雖然Vue渲染函數也能夠用JSX編寫,但咱們將繼續使用原始JS,有助於咱們能夠更輕鬆地瞭解Vue組件系統的基礎。。babel

每一個Vue組件都實現了一個render函數。大多數時候,該函數將由Vue編譯器建立。當咱們在組件上指定模板時,該模板的內容將由Vue編譯器處理,編譯器最終將返回render函數。渲染函數本質上返回一個虛擬DOM節點,該節點將被Vue在瀏覽器DOM中渲染。app

如今又引出了虛擬DOM的概念,"虛擬DOM究竟是什麼?"框架

虛擬文檔對象模型(或"DOM")容許Vue在更新瀏覽器以前在其內存中渲染組件。這使一切變得更快,同時也避免了DOM從新渲染的高昂成本。由於每一個DOM節點對象包含不少屬性和方法,所以使用虛擬DOM預先在內存進行操做,能夠省去不少瀏覽器直接建立DOM節點對象的開銷。iview

Vue更新瀏覽器DOM時,會將更新的虛擬DOM與上一個虛擬DOM進行比較,並僅使用已修改的部分更新實際DOM。這意味着更少的元素更改,從而提升了性能。Render函數返回虛擬DOM節點,在Vue生態系統中一般稱爲VNode,該接口是容許Vue在瀏覽器DOM中寫入這些對象的接口。它們包含使用Vue所需的全部信息。 Vue的下一版本將包含一個全新的虛擬DOM實現,該實現將比目前更快。 React,Riot,Inferno等許多其餘框架也使用虛擬DOM的概念。dom

咱們能夠在任何Vue組件中實現Vue render函數。一樣,因爲Vue的數據響應性,每當組件的數據獲得更新時,都會再次調用render函數。函數

這是一個簡單的示例,說明如何直接使用組件中的render函數去渲染h1標籤:

new Vue({
  el: '#app',
  render(createElement) {
    return createElement('h1', 'Hello world');
  }
});

有一些內置組件能夠利用渲染函數的功能,例如transitionkeep-alive。這些組件直接在渲染函數中操縱VNode。若是Vue沒有提供這個函數特性,這些功能將沒法實現。

Vue編譯器如何處理render函數?

大多數時候,Vue渲染函數將在項目構建期間由Vue編譯器進行編譯(例如,使用Webpack)。所以,編譯器不會最終出如今您的生產代碼中,從而減少了包的體積。這就是爲何當您使用"單個文件組件"時,除非咱們確實須要/想要,不然您實際上不須要使用render函數。

可是,若是咱們想在代碼中使用編譯器,則可使用帶有編譯器的Vue版本。簡而言之,咱們正在使用Vue編譯器來編譯自定義模板。假設咱們在作一個電商項目,那麼能夠將其注入購物車,從而能夠擁有更多的控制。

咱們編寫了一個實現自定義渲染功能的組件,該功能可獲取用戶建立的模板並替換咱們的默認模板。

這是一個如何使用編譯器將模板字符串編譯爲渲染函數的快速示例:

const template = `
<ul>
  <li v-for="item in items">
   {{ item }}
  </li>
</ul>`;

const compiledTemplate = Vue.compile(template);

new Vue({
  el: '#app',
  data() {
    return {
      items: ['Item1', 'Item2']
    }
  },
  render(createElement) {
    return compiledTemplate.render.call(this, createElement);
  }
});

如您所見,編譯器將返回一個對象(compiledTemplate),其中包含準備使用的render函數。

建立一個組件

具備渲染功能的組件沒有模板標記或屬性。相反,他們定義了一個稱爲render的函數,該函數接收一個createElement(renderElement:String | Component,define:Object,children:String | Array)參數(因爲某種緣由,一般別名爲h,歸咎於JSX)並返回使用該函數建立的元素。其餘一切保持不變。

export default {
  data() {
    return {
      isRed: true
    }
  },

  /*
   * 和下邊使用template相同
   * <template>
   *   <div :class="{'is-red': isRed}">
   *     <p>Example Text</p>
   *   </div>
   * </template>
   */
  render(h) {
    return h('div', {
      'class': {
        'is-red': this.isRed
      }
    }, [
      h('p', 'Example Text')
    ])
  }
}

在render函數中使用指令

Vue模板具備各類便捷功能,以便向模板添加基本邏輯和綁定功能。渲染函數沒法訪問它們。取而代之的是,它們必須以純Javascript實現,對於大多數指令而言,這是至關簡單的。

v-if

這個很簡單。除了使用v-if以外,還能夠在createElement中調用普通的if(expr)語句。

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

這些均可以在渲染函數中用JavaScript的if/else 和 map來重寫:

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

v-for

v-for可使用for-of,Array.map,Array.filter等多種迭代方法中的任何一種來實現。咱們能夠將它們以很是有趣的方式組合在一塊兒,以實現過濾或分割數據。

例如,咱們能夠替換

<template>
  <ul>
    <li v-for="item of list">
      
    </li>
  </ul>
</template>

render(h) {
  return h('ul', this.list.map(item => h('li', item.name)));
}

v-model

要記住的一件事是,v-model就是綁定屬性爲value(還能夠是其餘屬性),只要觸發input事件並設置data屬性的簡寫形式。不幸的是,渲染函數沒有這麼簡寫。咱們必須本身實現它,以下所示。

render(h) {
  return h('input', {
    domProps: {
      value: this.myBoundProperty
    },
    on: {
      input: e => {
        this.myBoundProperty = e.target.value
      }
    }
  })
}

等效於:

<template>
  <input :value="myBoundProperty" @input="myBoundProperty = $event.target.value"/>
</template>

<template>
  <input v-model="myBoundProperty"/>
</template>

v-bind

屬性和屬性綁定以attrs,props和domProps(相似於value和innerHTML之類)的形式放置在元素定義中。

render(h) {
  return h('div', {
    attrs: {
      // <div :id="myCustomId">
      id: this.myCustomId
    },

    props: {
      // <div :someProp="someValue">
      someProp: this.someValue
    },

    domProps: {
       // <div :value="someValue">
      value: this.someValue
    }
  });
}

附帶說明一下,類和樣式綁定直接在定義的根進行處理,而不是做爲attrs,props或domProps處理。

render(h) {
  return h('div', {
    // "class" 是JS中的保留字, 因此須要引號包起來.
    'class': {
      myClass: true,
      theirClass: false
    },

    style: {
      backgroundColor: 'green'
    }
  });
}

v-on

事件處理程序也能夠直接在on(或nativeOn,與組件的v-on.native效果相同)中直接添加到元素定義中。

render(h) {
  return h('div', {
    on: {
      click(e) {
        console.log('我點擊了!')
      }
    }
  });
}
  1. 修飾符能夠在處理程序內部實現:
  • .stop -> e.stopPropagation()
  • .prevent -> e.preventDefault()
  • .self -> if (e.target !== e.currentTarget) return
  1. 鍵盤修飾符
  • .[TARGET_KEY_CODE] -> if (event.keyCode !== TARGET_KEY_CODE) return
  • .[MODIFIER] -> if (!event.MODIFIERKey) return

插槽

能夠經過this.$slots做爲createElement()節點的數組來訪問插槽。

<div id="app">
<myslot>
    <div slot="slot1">this is slot1</div>
    <div slot="slot2">This is slot2 </div>
</myslot>

<script>
var app = new Vue({
  el: '#app'
})
</script>
Vue.component('my-slot', {
  render: function(h) {
    const child = h('div', {
      domProps: {innerHTML: 'this is child'}
    });

    return h(
      'div',
      [
        child,
        this.$slots.slot1,
        this.$slots.slot2
      ]
    )
  }
})

做用域插槽存儲在this.$scopedSlots[scope](props:object)中,做爲返回createElement()節點數組的函數。

<div id="app">
<myslot>
    <template scope="props">
        <div>{{props.text}}</div>
    </template>
</myslot>

<script>
var app = new Vue({
  el: '#app'
})
</script>
Vue.component('my-slot', {
  render: function(h) {
    const child = h('div', {
      domProps: {innerHTML: 'this is child'}
    });

    return h(
      'div',
      [
        child,
        this.$scopedSlots.default({
          text: 'hello scope'
        })
      ]
    )
  }
})

Vue渲染函數中的事件綁定

createElement函數能夠接收稱爲數據對象的參數。該對象能夠具備多個屬性,這些屬性與咱們在標準模板中使用的v-bind:on等指令等效。這是帶有按鈕的簡單計數器組件的示例,該按鈕能夠增長點擊次數。

new Vue({
  el: '#app',
  data() {
    return {
      clickCount: 0,
    }
  },
  methods: {
    onClick() {
      this.clickCount += 1;
    }
  },
  render(h) {
    const button = h('button', {
      on: {
        click: this.onClick
      }
    }, 'Click me');
    
    const counter = h('span', [
      'Number of clicks:',
      this.clickCount
    ]);
    
    return h('div', [
      button, counter
    ])
  }
});

可是數據對象不限於事件綁定!咱們也能夠像使用v-bind:class指令同樣將CSS類應用於元素。

new Vue({
  el: '#app',
  data() {
    return {
      clickCount: 0,
    }
  },
  computed: {
    backgroundColor() {
      return {
        'pink': this.clickCount%2 === 0,
        'green': this.clickCount%2 !== 0, 
      };
    }
  },
  methods: {
    onClick() {
      this.clickCount += 1;
    }
  },
  render(h) {
    const button = h('button', {
      on: {
        click: this.onClick
      }
    }, 'Click me');
    
    const counter = h('span', {
      'class': this.backgroundColor,
    }, [
      'Number of clicks:',
      this.clickCount
    ]);
    
    return h('div', [
      button, counter
    ])
  }
});

模板覆蓋的實際用例

我認爲了解Vue的幕後工做很是有趣。要知道是否可以最有效地使用工具,惟一的方法是確切地瞭解它的工做方式。

這並非說咱們應該開始將全部模板都轉換爲render函數,可是有時它們能夠派上用場,因此咱們至少應該知道如何使用它們。

在上面的示例中,我展現瞭如何在組件中使用自定義render函數,該函數容許咱們的某些組件可重寫。

首先,讓咱們建立初始模板。

<div id="app">
  <heading>
    <span>Heading title is: {{ title }}</span>
  </heading>
</div>

在將要安裝Vue應用程序的div內,咱們編寫一個自定義模板。接下來,咱們但願模板覆蓋咱們將要建立的標題組件的默認版本。

咱們要作的第一件事是遍歷自定義模板,並使用Vue編譯器對其進行預編譯

const templates = [];
const templatesContainer = document.getElementById('app');

// 咱們遍歷每一個自定義模板並對其進行預編譯
for (var i = 0; i < templatesContainer.children.length; i++) {
  const template = templatesContainer.children.item(i);
  templates.push({
    name: template.nodeName.toLowerCase(),
    renderFunction: Vue.compile(template.innerHTML),
  });
}

而後,讓咱們建立標題組件:

Vue.component('heading', {
  props: ['title'],
  template: `
    <overridable name="heading">
      <h1>
        {{ title }}
      </h1>
    </overridable>`
});

如今,它只是一個簡單的組件,具備一個名爲title的屬性。默認模板將渲染帶有標題的h1。咱們將用隨後建立的overridable組件包裝該組件。

這是咱們將使用自定義渲染功能的地方。

Vue.component('overridable', {
  props: ['name'],
  render(createElement) {
    // 咱們使用在初始化應用程序時建立的模板數組
    const template = templates.find(x => x.name === this.name);

    // 當沒有自定義模板時,咱們將使用默認插槽返回默認內容。
    if (!template) {
      return this.$slots.default[0];
    }
    
    // 使用預編譯的render函數
    return template.renderFunction.render.call(this.$parent, createElement);
  }
});

而後,讓咱們掛載Vue應用程序:

new Vue({
  el: '#app',
  template: `<heading title="Hello world"></heading>`
});

在此示例中,咱們能夠看到使用了默認模板,所以它是一個標準的h1標籤。

若是將自定義模板添加到div#app內,則會看到標題組件會被渲染成咱們指定的自定義模板。

最後

若是使用render函數建立組件,讓你感受很是繁瑣。那麼還能夠嘗試babel-plugin-transform-vue-jsx插件,就能夠像在React中那樣輕便(屬性細節略有不一樣,具體參考插件文檔)。

整體而言,使用render函數很是有趣,而且在v3.0中也派上了用場。 Vue渲染函數是Vue自己的基本組成部分,所以,我真的認爲花一些時間並完全理解該概念(特別是長期使用該框架)頗有價值。隨着Vue.js的發展和效率的提升,咱們平時積累的這些底層基礎知識也有助於咱們的發展。

換句話說,瞭解Vue render函數只是你技術進步中的一小步,但很重要。 :)
掃碼_搜索聯合傳播樣式-標準色版.png

相關文章
相關標籤/搜索