淺析Vue中的Render函數 (渲染函數 & JSX)


一、render簡介

  Render 函數是 Vue2.x 新增的一個函數、主要用來提升節點的性能,它是基於 JavaScript 計算。使用 Render 函數將 Template 里面的節點解析成虛擬的 Dom 。

Vue 推薦在絕大多數情況下使用模板來創建你的 HTML。然而在一些場景中,你真的需要 JavaScript 的完全編程的能力。這時你可以用渲染函數,它比模板更接近編譯器。

  簡單的說,在 Vue 中我們使用模板 HTML 語法組建頁面,而使用 Render 函數我們可以用 Js 語言來構建 DOM。

  因為 Vue 是虛擬 DOM,所以在拿到 Template 模板時也要轉譯成 VNode 的函數,而用 Render 函數構建 DOM,Vue 就免去了轉譯的過程。

二、render詳解

  我們在使用一些插件時,可以看到render語法

// IView
render:(h, params)=>{ return h('div', {style:{width:'100px',height:'100px',background:'#ccc'}}, '地方') } // Element
<el-table-column :render-header="setHeader"></el-table-column> setHeader (h) { return h('span', [ h('span', { style: 'line-height: 40px;' }, '備注'), h('el-button', { props: { type: 'primary', size: 'medium', disabled: this.isDisable || !this.tableData.length }, on: { click: this.save } }, '保存當前頁') ]) ]) },

  那它的真身到底是什么樣的呢?這還要從它的身世說起。

1、節點和樹

  在深入渲染函數之前,了解一些瀏覽器的工作原理是很重要的。以下面這段 HTML 為例:

<div>
  <h1>My title</h1> Some text content <!-- TODO: Add tagline -->
</div>

  當瀏覽器讀到這些代碼時,它會建立一個DOM 節點樹來保持追蹤所有內容,如同你會畫一張家譜樹來追蹤家庭成員的發展一樣。上述 HTML 對應的 DOM 節點樹如下圖所示:

  每個元素都是一個節點,每段文字也是一個節點,甚至注釋也都是節點。一個節點就是頁面的一個部分,就像家譜樹一樣,每個節點都可以有孩子節點 (也就是說每個部分可以包含其它的一些部分)。

  高效地更新所有這些節點會是比較困難的,不過所幸你不必手動完成這個工作。你只需要告訴 Vue 你希望頁面上的 HTML 是什么,這可以是在一個模板里:

<h1>{{ blogTitle }}</h1>

  或者一個渲染函數里:

render: function (createElement) { return createElement('h1', this.blogTitle) }

  在這兩種情況下,Vue 都會自動保持頁面的更新,即便 blogTitle 發生了改變。

2、虛擬 DOM

  Vue 通過建立一個虛擬 DOM 來追蹤自己要如何改變真實 DOM。請仔細看這行代碼:

return createElement('h1', this.blogTitle)

  createElement到底會返回什么呢?其實不是一個實際的DOM 元素。它更准確的名字可能是 createNodeDescription,因為它所包含的信息會告訴 Vue 頁面上需要渲染什么樣的節點,包括及其子節點的描述信息。我們把這樣的節點描述為“虛擬節點 (virtual node)”,也常簡寫它為“VNode”。“虛擬 DOM”是我們對由 Vue 組件樹建立起來的整個 VNode 樹的稱呼。

  注:當使用render函數描述虛擬 DOM 時,vue 提供一個函數,這個函數是就構建虛擬 DOM 所需要的工具。官網上給他起了個名字叫 createElement。還有約定的簡寫叫 h,將 h 作為 createElement 的別名是 Vue 生態系統中的一個通用慣例,實際上也是 JSX 所要求的。

3、createElement 參數

createElement(TagName,Option,Content)接受三個參數
createElement(" 定義的元素 ",{ 元素的性質 }," 元素的內容"/[元素的內容])

  官方文檔

// @returns {VNode}
createElement( // {String | Object | Function} // 一個 HTML 標簽名、組件選項對象,或者 // resolve 了上述任何一種的一個 async 函數。必填項。
  'div', // {Object} // 一個與模板中屬性對應的數據對象。可選。
 { // (詳情見下一節:深入數據對象)
 }, // {String | Array} // 子級虛擬節點 (VNodes),由 `createElement()` 構建而成, // 也可以使用字符串來生成“文本虛擬節點”。可選。
 [ '先寫一些文字', createElement('h1', '一則頭條'), createElement(MyComponent, { props: { someProp: 'foobar' } }) ] )

4、深入數據對象

{ // 與 `v-bind:class` 的 API 相同, // 接受一個字符串、對象或字符串和對象組成的數組
  'class': { foo: true, bar: false }, // 與 `v-bind:style` 的 API 相同, // 接受一個字符串、對象,或對象組成的數組
 style: { color: 'red', fontSize: '14px' }, // 普通的 HTML 特性
 attrs: { id: 'foo' }, // 組件 prop
 props: { myProp: 'bar' }, // DOM 屬性
 domProps: { innerHTML: 'baz' }, // 事件監聽器在 `on` 屬性內, // 但不再支持如 `v-on:keyup.enter` 這樣的修飾器。 // 需要在處理函數中手動檢查 keyCode。
 on: { click: this.clickHandler }, // 僅用於組件,用於監聽原生事件,而不是組件內部使用 // `vm.$emit` 觸發的事件。
 nativeOn: { click: this.nativeClickHandler }, // 自定義指令。注意,你無法對 `binding` 中的 `oldValue` // 賦值,因為 Vue 已經自動為你進行了同步。
 directives: [ { name: 'my-custom-directive', value: '2', expression: '1 + 1', arg: 'foo', modifiers: { bar: true } } ], // 作用域插槽的格式為 // { name: props => VNode | Array<VNode> }
 scopedSlots: { default: props => createElement('span', props.text) }, // 如果組件是其它組件的子組件,需為插槽指定名稱
  slot: 'name-of-slot', // 其它特殊頂層屬性
  key: 'myKey', ref: 'myRef', // 如果你在渲染函數中給多個元素都應用了相同的 ref 名, // 那么 `$refs.myRef` 會變成一個數組。
  refInFor: true }

  比如下面這樣

render:(h) => { return h('div',{    //給div綁定value屬性
 props: { value:'' },    //給div綁定樣式
   style:{      width:'30px'    },     //給div綁定點擊事件  
 on: { click: () => { console.log('點擊事件') } }, }) }

5、約束

  VNode 必須唯一:組件樹中的所有 VNode 必須是唯一的。這意味着,下面的渲染函數是不合法的:

render: function (createElement) { var myParagraphVNode = createElement('p', 'hi') return createElement('div', [ // 錯誤 - 重復的 VNode
 myParagraphVNode, myParagraphVNode ]) }

  如果你真的需要重復很多次的元素/組件,你可以使用工廠函數來實現。例如,下面這渲染函數用完全合法的方式渲染了 20 個相同的段落:

render: function (createElement) { return createElement('div', Array.apply(null, { length: 20 }).map(function () { return createElement('p', 'hi') }) ) }

三、render個性

1、v-if 和 v-for

  只要在原生的 JavaScript 中可以輕松完成的操作,Vue 的渲染函數就不會提供專有的替代方法。比如,在模板中使用的 v-if 和 v-for

<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 (createElement) { if (this.items.length) { return createElement('ul', this.items.map(function (item) { return createElement('li', item.name) })) } else { return createElement('p', 'No items found.') } }

2、v-model

  渲染函數中沒有與 v-model 的直接對應——你必須自己實現相應的邏輯:

    props: ['value'], render: function (createElement) { var self = this
      return createElement('input', { domProps: { value: self.value }, on: { input: function (event) { self.$emit('input', event.target.value) } } }) }

  這就是深入底層的代價,但與 v-model 相比,這可以讓你更好地控制交互細節。

3、事件 & 按鍵修飾符

  對於 .passive.capture 和 .once 這些事件修飾符, Vue 提供了相應的前綴可以用於 on

修飾符 前綴
.passive &
.capture !
.once ~
.capture.once.once.capture ~!
 on: { '!click': this.doThisInCapturingMode, '~keyup': this.doThisOnce, '~!mouseover': this.doThisOnceInCapturingMode }

  對於所有其它的修飾符,私有前綴都不是必須的,因為你可以在事件處理函數中使用事件方法:

修飾符 處理函數中的等價操作
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.currentTarget) return
按鍵:.enter.13 if (event.keyCode !== 13) return (對於別的按鍵修飾符來說,可將 13 改為另一個按鍵碼)
修飾鍵:.ctrl.alt.shift.meta if (!event.ctrlKey) return (將 ctrlKey 分別修改為 altKeyshiftKey 或者 metaKey)

   這里是一個使用所有修飾符的例子:

on: { keyup: function (event) { // 如果觸發事件的元素不是事件綁定的元素 // 則返回
    if (event.target !== event.currentTarget) return
    // 如果按下去的不是 enter 鍵或者 // 沒有同時按下 shift 鍵 // 則返回
    if (!event.shiftKey || event.keyCode !== 13) return
    // 阻止 事件冒泡
    event.stopPropagation() // 阻止該元素默認的 keyup 事件
    event.preventDefault() // ...
 } }

4、插槽

  你可以通過 this.$slots 訪問靜態插槽的內容,每個插槽都是一個 VNode 數組:

render: function (createElement) { // `<div><slot></slot></div>`
  return createElement('div', this.$slots.default) }

  也可以通過 this.$scopedSlots 訪問作用域插槽,每個作用域插槽都是一個返回若干 VNode 的函數:

props: ['message'], render: function (createElement) { // `<div><slot :text="message"></slot></div>`
  return createElement('div', [ this.$scopedSlots.default({ text: this.message }) ]) }

  如果要用渲染函數向子組件中傳遞作用域插槽,可以利用 VNode 數據對象中的 scopedSlots 字段:

render: function (createElement) { return createElement('div', [ createElement('child', { // 在數據對象中傳遞 `scopedSlots` // 格式為 { name: props => VNode | Array<VNode> }
 scopedSlots: { default: function (props) { return createElement('span', props.text) } } }) ]) }

四、擴展JSX

  如果你寫了很多 render 函數,可能會覺得下面這樣的代碼寫起來很痛苦:

createElement( 'anchored-heading', { props: { level: 1 } }, [ createElement('span', 'Hello'), ' world!' ] )

  特別是對應的模板如此簡單的情況下:

<anchored-heading :level="1">
  <span>Hello</span> world!
</anchored-heading>

  這就是為什么會有一個 Babel 插件,用於在 Vue 中使用 JSX 語法,它可以讓我們回到更接近於模板的語法上。

import AnchoredHeading from './AnchoredHeading.vue'
 
new Vue({ el: '#demo', render: function (h) { return ( <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading> ) } })

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM