溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Vue渲染函數(shù)詳解

發(fā)布時間:2020-08-27 09:08:36 來源:腳本之家 閱讀:141 作者:小火柴的藍(lán)色理想 欄目:web開發(fā)

前面的話

Vue 推薦在絕大多數(shù)情況下使用 template 來創(chuàng)建HTML。然而在一些場景中,真的需要 JavaScript 的完全編程的能力,這就是 render 函數(shù),它比 template 更接近編譯器。本文將詳細(xì)介紹Vue渲染函數(shù)

引入

下面是一個例子,如果要實(shí)現(xiàn)類似下面的效果。其中,H標(biāo)簽可替換

<h2>
 <a name="hello-world" href="#hello-world" rel="external nofollow" rel="external nofollow" >
 Hello world!
 </a>
</h2>

在 HTML 層,像下面這樣定義來組件接口:

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

當(dāng)開始寫一個通過 level prop 動態(tài)生成 heading 標(biāo)簽的組件,可能很快想到這樣實(shí)現(xiàn):

<script type="text/x-template" id="anchored-heading-template">
 <h2 v-if="level === 1">
 <slot></slot>
 </h2>
 <h3 v-else-if="level === 2">
 <slot></slot>
 </h3>
 <h4 v-else-if="level === 3">
 <slot></slot>
 </h4>
 <h5 v-else-if="level === 4">
 <slot></slot>
 </h5>
 <h6 v-else-if="level === 5">
 <slot></slot>
 </h6>
 <h7 v-else-if="level === 6">
 <slot></slot>
 </h7>
</script>

JS代碼如下

Vue.component('anchored-heading', {
 template: '#anchored-heading-template',
 props: {
 level: {
  type: Number,
  required: true
 }
 }
})

在這種場景中使用 template 并不是最好的選擇:首先代碼冗長,為了在不同級別的標(biāo)題中插入錨點(diǎn)元素,需要重復(fù)地使用 <slot></slot>

雖然模板在大多數(shù)組件中都非常好用,但是在這里它就不是很簡潔的了。那么,來嘗試使用 render 函數(shù)重寫上面的例子:

<div id="example">
 <anchored-heading :level="2"><a name="hello-world" href="#hello-world" rel="external nofollow" rel="external nofollow" >Hello world!</a></anchored-heading>
</div>
<script src="vue.js"></script>
<script>
Vue.component('anchored-heading', {
 render: function (createElement) {
 return createElement(
  'h' + this.level, // tag name 標(biāo)簽名稱
  this.$slots.default // 子組件中的陣列
 )
 },
 props: {
 level: {
  type: Number,
  required: true
 }
 }
}) 
new Vue({
 el: '#example'
})
</script>

這樣的代碼精簡很多,但是需要非常熟悉 Vue 的實(shí)例屬性。在這個例子中,需要知道當(dāng)不使用 slot 屬性向組件中傳遞內(nèi)容時,比如 anchored-heading 中的 Hello world!,這些子元素被存儲在組件實(shí)例中的 $slots.default中

虛擬DOM

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

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

當(dāng)瀏覽器讀到這些代碼時,它會建立一個“DOM 節(jié)點(diǎn)”樹來保持追蹤,如同會畫一張家譜樹來追蹤家庭成員的發(fā)展一樣。HTML 的 DOM 節(jié)點(diǎn)樹如下圖所示:

Vue渲染函數(shù)詳解

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

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

<h2>{{ blogTitle }}</h2>

或者一個渲染函數(shù)里:

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

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

【虛擬DOM】

Vue 通過建立一個虛擬 DOM 對真實(shí) DOM 發(fā)生的變化保持追蹤

return createElement('h2', this.blogTitle)
createElement 到底會返回什么呢?其實(shí)不是一個實(shí)際的 DOM 元素。它更準(zhǔn)確的名字可能是 createNodeDescription,因?yàn)樗男畔嬖V Vue 頁面上需要渲染什么樣的節(jié)點(diǎn),及其子節(jié)點(diǎn)。我們把這樣的節(jié)點(diǎn)描述為“虛擬節(jié)點(diǎn) (Virtual DOM)”,也常簡寫它為“VNode”?!疤摂M DOM”是我們對由 Vue 組件樹建立起來的整個 VNode 樹的稱呼

createElement

接下來需要熟悉的是如何在 createElement 函數(shù)中生成模板。這里是 createElement 接受的參數(shù):

// @returns {VNode}
createElement(
 // {String | Object | Function}
 // 一個 HTML 標(biāo)簽字符串,組件選項(xiàng)對象,或者一個返回值類型為 String/Object 的函數(shù),必要參數(shù)
 'div',
 // {Object}
 // 一個包含模板相關(guān)屬性的數(shù)據(jù)對象
 // 這樣,可以在 template 中使用這些屬性??蛇x參數(shù)。
 {
 },
 // {String | Array}
 // 子節(jié)點(diǎn) (VNodes),由 `createElement()` 構(gòu)建而成,
 // 或簡單的使用字符串來生成“文本節(jié)點(diǎn)”??蛇x參數(shù)。
 [
 '先寫一些文字',
 createElement('h2', '一則頭條'),
 createElement(MyComponent, {
  props: {
  someProp: 'foobar'
  }
 })
 ]
)

【深入data對象】

正如在模板語法中,v-bind:class 和 v-bind:style ,會被特別對待一樣,在 VNode 數(shù)據(jù)對象中,下列屬性名是級別最高的字段。該對象也允許綁定普通的 HTML 特性,就像 DOM 屬性一樣,比如 innerHTML (這會取代 v-html 指令)

{
 // 和`v-bind:class`一樣的 API
 'class': {
 foo: true,
 bar: false
 },
 // 和`v-bind:style`一樣的 API
 style: {
 color: 'red',
 fontSize: '14px'
 },
 // 正常的 HTML 特性
 attrs: {
 id: 'foo'
 },
 // 組件 props
 props: {
 myProp: 'bar'
 },
 // DOM 屬性
 domProps: {
 innerHTML: 'baz'
 },
 // 事件監(jiān)聽器基于 `on`
 // 所以不再支持如 `v-on:keyup.enter` 修飾器
 // 需要手動匹配 keyCode。
 on: {
 click: this.clickHandler
 },
 // 僅對于組件,用于監(jiān)聽原生事件,而不是組件內(nèi)部使用 `vm.$emit` 觸發(fā)的事件。
 nativeOn: {
 click: this.nativeClickHandler
 },
 // 自定義指令。注意事項(xiàng):不能對綁定的舊值設(shè)值
 // Vue 會持續(xù)追蹤
 directives: [
 {
  name: 'my-custom-directive',
  value: '2',
  expression: '1 + 1',
  arg: 'foo',
  modifiers: {
  bar: true
  }
 }
 ],
 // Scoped slots in the form of
 // { name: props => VNode | Array<VNode> }
 scopedSlots: {
 default: props => createElement('span', props.text)
 },
 // 如果組件是其他組件的子組件,需為插槽指定名稱
 slot: 'name-of-slot',
 // 其他特殊頂層屬性
 key: 'myKey',
 ref: 'myRef'
}

【完整示例】

有了這些知識,現(xiàn)在可以完成最開始想實(shí)現(xiàn)的組件:

var getChildrenTextContent = function (children) {
 return children.map(function (node) {
 return node.children
  ? getChildrenTextContent(node.children)
  : node.text
 }).join('')
}
Vue.component('anchored-heading', {
 render: function (createElement) {
 // create kebabCase id
 var headingId = getChildrenTextContent(this.$slots.default)
  .toLowerCase()
  .replace(/\W+/g, '-')
  .replace(/(^\-|\-$)/g, '')
 return createElement(
  'h' + this.level,
  [
  createElement('a', {
   attrs: {
   name: headingId,
   href: '#' + headingId
   }
  }, this.$slots.default)
  ]
 )
 },
 props: {
 level: {
  type: Number,
  required: true
 }
 }
})

【約束】

組件樹中的所有 VNodes 必須是唯一的。這意味著,下面的 render function 是無效的:

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

如果真的需要重復(fù)很多次的元素/組件,可以使用工廠函數(shù)來實(shí)現(xiàn)。例如,下面這個例子 render 函數(shù)完美有效地渲染了 20 個重復(fù)的段落:

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

JS代替模板

【v-if和v-for】

由于使用原生的 JavaScript 來實(shí)現(xiàn)某些東西很簡單,Vue 的 render 函數(shù)沒有提供專用的 API。比如,template 中的 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>

這些都會在 render 函數(shù)中被 JavaScript 的 if/else 和 map 重寫:

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

【v-model】

ender 函數(shù)中沒有與 v-model 相應(yīng)的 api,必須自己來實(shí)現(xiàn)相應(yīng)的邏輯:

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

這就是深入底層要付出的,盡管麻煩了一些,但相對于 v-model 來說,可以更靈活地控制

【事件&按鍵修飾符】

對于 .passive、.capture 和 .once事件修飾符,Vue 提供了相應(yīng)的前綴可以用于 on:

Modifier(s)  Prefix
.passive    &
.capture    !
.once      ~
.capture.once   or
.once.capture ~!

下面是一個例子

on: {
 '!click': this.doThisInCapturingMode,
 '~keyup': this.doThisOnce,
 `~!mouseover`: this.doThisOnceInCapturingMode
}

對于其他的修飾符,前綴不是很重要,因?yàn)榭梢灾苯釉谑录幚砗瘮?shù)中使用事件方法:

Modifier(s)  Equivalent in Handler
.stop  event.stopPropagation()
.prevent  event.preventDefault()
.self  if (event.target !== event.currentTarget) return
Keys:
.enter, .13  if (event.keyCode !== 13) return (...)
Modifiers Keys:
.ctrl, .alt, .shift, .meta  if (!event.ctrlKey) return (...)

下面是一個使用所有修飾符的例子:

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

【插槽】

可以從 this.$slots 獲取 VNodes 列表中的靜態(tài)內(nèi)容:

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

還可以從 this.$scopedSlots 中獲得能用作函數(shù)的作用域插槽,這個函數(shù)返回 VNodes:

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

如果要用渲染函數(shù)向子組件中傳遞作用域插槽,可以利用 VNode 數(shù)據(jù)中的 scopedSlots 域:

render (createElement) {
 return createElement('div', [
 createElement('child', {
  // pass `scopedSlots` in the data object
  // in the form of { name: props => VNode | Array<VNode> }
  scopedSlots: {
  default: function (props) {
   return createElement('span', props.text)
  }
  }
 })
 ])
}

JSX

如果寫了很多 render 函數(shù),可能會覺得痛苦

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 (h) {
 return (
  <AnchoredHeading level={1}>
  <span>Hello</span> world!
  </AnchoredHeading>
 )
 }
})

[注意]將 h 作為 createElement 的別名是 Vue 生態(tài)系統(tǒng)中的一個通用慣例,實(shí)際上也是 JSX 所要求的,如果在作用域中 h 失去作用,在應(yīng)用中會觸發(fā)報錯

函數(shù)式組件

之前創(chuàng)建的錨點(diǎn)標(biāo)題組件是比較簡單,沒有管理或者監(jiān)聽任何傳遞給它的狀態(tài),也沒有生命周期方法。它只是一個接收參數(shù)的函數(shù)。

在這個例子中,我們標(biāo)記組件為 functional,這意味它是無狀態(tài) (沒有 data),無實(shí)例 (沒有 this 上下文)

一個 函數(shù)式組件 就像這樣:

Vue.component('my-component', {
 functional: true,
 // 為了彌補(bǔ)缺少的實(shí)例
 // 提供第二個參數(shù)作為上下文
 render: function (createElement, context) {
 // ...
 },
 // Props 可選
 props: {
 // ...
 }
})

[注意]在 2.3.0 之前的版本中,如果一個函數(shù)式組件想要接受 props,則 props 選項(xiàng)是必須的。在 2.3.0 或以上的版本中,你可以省略 props 選項(xiàng),所有組件上的屬性都會被自動解析為 props

組件需要的一切都是通過上下文傳遞,包括:

props:提供 props 的對象
children: VNode 子節(jié)點(diǎn)的數(shù)組
slots: slots 對象
data:傳遞給組件的 data 對象
parent:對父組件的引用
listeners: (2.3.0+) 一個包含了組件上所注冊的 v-on 偵聽器的對象。這只是一個指向 data.on 的別名。
injections: (2.3.0+) 如果使用了 inject 選項(xiàng),則該對象包含了應(yīng)當(dāng)被注入的屬性。

在添加 functional: true 之后,錨點(diǎn)標(biāo)題組件的 render 函數(shù)之間簡單更新增加 context 參數(shù),this.$slots.default 更新為 context.children,之后this.level 更新為 context.props.level。

因?yàn)楹瘮?shù)式組件只是一個函數(shù),所以渲染開銷也低很多。然而,對持久化實(shí)例的缺乏也意味著函數(shù)式組件不會出現(xiàn)在 Vue devtools 的組件樹里。

在作為包裝組件時它們也同樣非常有用,比如,當(dāng)需要做這些時:

1、程序化地在多個組件中選擇一個

2、在將 children, props, data 傳遞給子組件之前操作它們

下面是一個依賴傳入 props 的值的 smart-list 組件例子,它能代表更多具體的組件:

var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }
Vue.component('smart-list', {
 functional: true,
 render: function (createElement, context) {
 function appropriateListComponent () {
  var items = context.props.items
  if (items.length === 0)   return EmptyList
  if (typeof items[0] === 'object') return TableList
  if (context.props.isOrdered)  return OrderedList
  return UnorderedList
 }
 return createElement(
  appropriateListComponent(),
  context.data,
  context.children
 )
 },
 props: {
 items: {
  type: Array,
  required: true
 },
 isOrdered: Boolean
 }
})

【slots()和children對比】

為什么同時需要 slots() 和 children。slots().default 不是和 children 類似的嗎?在一些場景中,是這樣,但是如果是函數(shù)式組件和下面這樣的 children 呢?

<my-functional-component>
 <p slot="foo">
 first
 </p>
 <p>second</p>
</my-functional-component>

對于這個組件,children 會給兩個段落標(biāo)簽,而 slots().default 只會傳遞第二個匿名段落標(biāo)簽,slots().foo 會傳遞第一個具名段落標(biāo)簽。同時擁有 children 和 slots() ,因此可以選擇讓組件通過 slot() 系統(tǒng)分發(fā)或者簡單的通過 children 接收,讓其他組件去處理

模板編譯

Vue 的模板實(shí)際是編譯成了 render 函數(shù)。這是一個實(shí)現(xiàn)細(xì)節(jié),通常不需要關(guān)心。下面是一個使用 Vue.compile 來實(shí)時編譯模板字符串的簡單 demo:

<my-functional-component>
 <p slot="foo">
 first
 </p>
 <p>second</p>
</my-functional-component>

render:

function anonymous(
) {
 with(this){return _c('div',[_m(0),(message)?_c('p',[_v(_s(message))]):_c('p',[_v("No message.")])])}
}

staticRenderFns:

_m(0): function anonymous(
) {
 with(this){return _c('header',[_c('h2',[_v("I'm a template!")])])}
}

以上這篇Vue渲染函數(shù)詳解就是小編分享給大家的全部內(nèi)容了,希望能給大家一個參考,也希望大家多多支持億速云。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI