Vue 中的 Render 全面詳解 (渲染函數 & JSX)

相信大家都或多或少的在 code 中見過 或使用過 Render,如果你對它還是一臉懵逼,那就快上車!今天就帶你來盤它。



一、Render 的資料簡介

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

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

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

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

二、與 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 }
      }, '保存當前頁')
    ])
  ])
},

或者這樣的:

renderContent (createElement, { node, data, store }) {
	return createElement('span', [
		// 顯示樹的節點信息
		createElement('span', node.label)
		// ......
	])
}

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

2.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.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 所要求的。

有點意思~ 其實它就是 createElement,接下來讓我們來走近一點點,來深入的瞭解它吧~

三、與 Render 的約會

3.1 createElement 參數

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

  • 官方文檔
// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一個 HTML 標籤名、組件選項對象,或者
  // resolve 了上述任何一種的一個 async 函數。必填項。
  'div',

  // {Object}
  // 一個與模板中屬性對應的數據對象。可選。
  {
    // (詳情見下一節-3.2 深入數據對象)
  },

  // {String | Array}
  // 子級虛擬節點 (VNodes),由 `createElement()` 構建而成,
  // 也可以使用字符串來生成“文本虛擬節點”。可選。
  [
    '先寫一些文字',
    createElement('h1', '一則頭條'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)

3.2 深入數據對象

{
  // 與 `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
}

3.3 舉個小栗子

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

3.4 約束

它也是有小脾氣的~ 要記得這個約束喲~

  • 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 的小個性

4.1 v-if 和 v-for

只要在原生的 JavaScript 中可以輕鬆完成的操作,Vue 的渲染函數就不會提供專有的替代方法。比如,在模板中使用的 v-ifv-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/elsemap 來重寫:

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.')
  }
}

4.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 相比,這可以讓你更好地控制交互細節。

4.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.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)
        }
      }
    })
  ])
}

五、實戰

Element 中的 Tree

// 樹節點的內容區的渲染回調
renderContent(h, { node, data, store }) {
    let aa = () => {
        console.log(data)
    }
    return  h('span', [
        h('span', {
            class: "custom-tree-node"
        }, [
            h('i', { class: "icon-folder" }), h('span', { props: { title: node.label }, class: "text ellipsis" }, node.label),
            h('el-popover', {
                props: {
                    placement: "bottom",
                    title: "",
                    width: "61",
                    popperClass: "option-group-popover",
                    trigger: "hover"
                }
            }, [
                h('ul', { class: "option-group" }, [
                    h('li', {
                        class: "pointer-text",
                        on: {
                            click: aa
                        }
                    }, '編輯'),
                    h('li', { class: "pointer-text" }, '刪除'),
                    h('li', { class: "pointer-text" }, '添加')
                ]),
                h('i', { slot: "reference", class: "el-icon-more fr more-icon",
                    on: {
                        click: (e) => {
                            e.stopPropagation();
                        }
                    }
                })
            ])
        ])
    ])
},

六、擴展-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>
    )
  }
})

碼字不易,覺得有幫助的小夥伴點個贊支持下~


在這裏插入圖片描述

掃描上方二維碼關注我的訂閱號~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章