Vue渲染函數(shù)詳解
前面的話
Vue 推薦在絕大多數(shù)情況下使用 template 來創(chuàng)建HTML。然而在一些場景中,真的需要 JavaScript 的完全編程的能力,這就是 render 函數(shù),它比 template 更接近編譯器。本文將詳細介紹Vue渲染函數(shù)
引入
下面是一個例子,如果要實現(xiàn)類似下面的效果。其中,H標簽可替換
<h1> <a name="hello-world" href="#hello-world" rel="external nofollow" rel="external nofollow" > Hello world! </a> </h1>
在 HTML 層,像下面這樣定義來組件接口:
<anchored-heading :level="1">Hello world!</anchored-heading>
當開始寫一個通過 level prop 動態(tài)生成 heading 標簽的組件,可能很快想到這樣實現(xiàn):
<script type="text/x-template" id="anchored-heading-template"> <h1 v-if="level === 1"> <slot></slot> </h1> <h2 v-else-if="level === 2"> <slot></slot> </h2> <h3 v-else-if="level === 3"> <slot></slot> </h3> <h4 v-else-if="level === 4"> <slot></slot> </h4> <h5 v-else-if="level === 5"> <slot></slot> </h5> <h6 v-else-if="level === 6"> <slot></slot> </h6> </script>
JS代碼如下
Vue.component('anchored-heading', {
template: '#anchored-heading-template',
props: {
level: {
type: Number,
required: true
}
}
})
在這種場景中使用 template 并不是最好的選擇:首先代碼冗長,為了在不同級別的標題中插入錨點元素,需要重復地使用 <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 標簽名稱
this.$slots.default // 子組件中的陣列
)
},
props: {
level: {
type: Number,
required: true
}
}
})
new Vue({
el: '#example'
})
</script>
這樣的代碼精簡很多,但是需要非常熟悉 Vue 的實例屬性。在這個例子中,需要知道當不使用 slot 屬性向組件中傳遞內(nèi)容時,比如 anchored-heading 中的 Hello world!,這些子元素被存儲在組件實例中的 $slots.default中
虛擬DOM
在深入渲染函數(shù)之前,了解一些瀏覽器的工作原理是很重要的。以下面這段 HTML 為例:
<div> <h1>My title</h1> Some text content <!-- TODO: Add tagline --> </div>
當瀏覽器讀到這些代碼時,它會建立一個“DOM 節(jié)點”樹來保持追蹤,如同會畫一張家譜樹來追蹤家庭成員的發(fā)展一樣。HTML 的 DOM 節(jié)點樹如下圖所示:

每個元素都是一個節(jié)點。每段文字也是一個節(jié)點。甚至注釋也都是節(jié)點。一個節(jié)點就是頁面的一個部分。就像家譜樹一樣,每個節(jié)點都可以有子節(jié)點 (也就是說每個部分可以包含其它的一些部分)
高效的更新所有這些節(jié)點會是比較困難的,不過所幸不必再手動完成這個工作了。只需要告訴 Vue 希望頁面上的 HTML 是什么,這可以是在一個模板里:
<h1>{{ blogTitle }}</h1>
或者一個渲染函數(shù)里:
render: function (createElement) {
return createElement('h1', this.blogTitle)
}
在這兩種情況下,Vue 都會自動保持頁面的更新,即便 blogTitle 發(fā)生了改變。
【虛擬DOM】
Vue 通過建立一個虛擬 DOM 對真實 DOM 發(fā)生的變化保持追蹤
return createElement('h1', this.blogTitle)
createElement 到底會返回什么呢?其實不是一個實際的 DOM 元素。它更準確的名字可能是 createNodeDescription,因為它所包含的信息會告訴 Vue 頁面上需要渲染什么樣的節(jié)點,及其子節(jié)點。我們把這樣的節(jié)點描述為“虛擬節(jié)點 (Virtual DOM)”,也常簡寫它為“VNode”。“虛擬 DOM”是我們對由 Vue 組件樹建立起來的整個 VNode 樹的稱呼
createElement
接下來需要熟悉的是如何在 createElement 函數(shù)中生成模板。這里是 createElement 接受的參數(shù):
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一個 HTML 標簽字符串,組件選項對象,或者一個返回值類型為 String/Object 的函數(shù),必要參數(shù)
'div',
// {Object}
// 一個包含模板相關屬性的數(shù)據(jù)對象
// 這樣,可以在 template 中使用這些屬性??蛇x參數(shù)。
{
},
// {String | Array}
// 子節(jié)點 (VNodes),由 `createElement()` 構(gòu)建而成,
// 或簡單的使用字符串來生成“文本節(jié)點”??蛇x參數(shù)。
[
'先寫一些文字',
createElement('h1', '一則頭條'),
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
},
// 自定義指令。注意事項:不能對綁定的舊值設值
// 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)在可以完成最開始想實現(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', [
// 錯誤-重復的 VNodes
myParagraphVNode, myParagraphVNode
])
}
如果真的需要重復很多次的元素/組件,可以使用工廠函數(shù)來實現(xiàn)。例如,下面這個例子 render 函數(shù)完美有效地渲染了 20 個重復的段落:
render: function (createElement) {
return createElement('div',
Array.apply(null, { length: 20 }).map(function () {
return createElement('p', 'hi')
})
)
}
JS代替模板
【v-if和v-for】
由于使用原生的 JavaScript 來實現(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 相應的 api,必須自己來實現(xiàn)相應的邏輯:
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 提供了相應的前綴可以用于 on:
Modifier(s) Prefix .passive & .capture ! .once ~ .capture.once or .once.capture ~!
下面是一個例子
on: {
'!click': this.doThisInCapturingMode,
'~keyup': this.doThisOnce,
`~!mouseover`: this.doThisOnceInCapturingMode
}
對于其他的修飾符,前綴不是很重要,因為可以直接在事件處理函數(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()
// 阻止該元素默認的 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)中的一個通用慣例,實際上也是 JSX 所要求的,如果在作用域中 h 失去作用,在應用中會觸發(fā)報錯
函數(shù)式組件
之前創(chuàng)建的錨點標題組件是比較簡單,沒有管理或者監(jiān)聽任何傳遞給它的狀態(tài),也沒有生命周期方法。它只是一個接收參數(shù)的函數(shù)。
在這個例子中,我們標記組件為 functional,這意味它是無狀態(tài) (沒有 data),無實例 (沒有 this 上下文)
一個 函數(shù)式組件 就像這樣:
Vue.component('my-component', {
functional: true,
// 為了彌補缺少的實例
// 提供第二個參數(shù)作為上下文
render: function (createElement, context) {
// ...
},
// Props 可選
props: {
// ...
}
})
[注意]在 2.3.0 之前的版本中,如果一個函數(shù)式組件想要接受 props,則 props 選項是必須的。在 2.3.0 或以上的版本中,你可以省略 props 選項,所有組件上的屬性都會被自動解析為 props
組件需要的一切都是通過上下文傳遞,包括:
props:提供 props 的對象 children: VNode 子節(jié)點的數(shù)組 slots: slots 對象 data:傳遞給組件的 data 對象 parent:對父組件的引用 listeners: (2.3.0+) 一個包含了組件上所注冊的 v-on 偵聽器的對象。這只是一個指向 data.on 的別名。 injections: (2.3.0+) 如果使用了 inject 選項,則該對象包含了應當被注入的屬性。
在添加 functional: true 之后,錨點標題組件的 render 函數(shù)之間簡單更新增加 context 參數(shù),this.$slots.default 更新為 context.children,之后this.level 更新為 context.props.level。
因為函數(shù)式組件只是一個函數(shù),所以渲染開銷也低很多。然而,對持久化實例的缺乏也意味著函數(shù)式組件不會出現(xiàn)在 Vue devtools 的組件樹里。
在作為包裝組件時它們也同樣非常有用,比如,當需要做這些時:
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 會給兩個段落標簽,而 slots().default 只會傳遞第二個匿名段落標簽,slots().foo 會傳遞第一個具名段落標簽。同時擁有 children 和 slots() ,因此可以選擇讓組件通過 slot() 系統(tǒng)分發(fā)或者簡單的通過 children 接收,讓其他組件去處理
模板編譯
Vue 的模板實際是編譯成了 render 函數(shù)。這是一個實現(xiàn)細節(jié),通常不需要關心。下面是一個使用 Vue.compile 來實時編譯模板字符串的簡單 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('h1',[_v("I'm a template!")])])}
}
以上這篇Vue渲染函數(shù)詳解就是小編分享給大家的全部內(nèi)容了,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
vue監(jiān)聽頁面滾動到某個高度觸發(fā)事件流程
這篇文章主要介紹了vue監(jiān)聽頁面滾動到某個高度觸發(fā)事件流程,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-04-04
Vue數(shù)據(jù)驅(qū)動模擬實現(xiàn)3
這篇文章主要為大家詳細介紹了Vue數(shù)據(jù)驅(qū)動模擬實現(xiàn),教大家如何在某個對象中,新增某個屬性,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-01-01
vue+el-select?多數(shù)據(jù)分頁搜索組件的實現(xiàn)
本文主要介紹了vue+el-select?多數(shù)據(jù)分頁搜索組件的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2024-12-12
Vue3 + ElementPlus動態(tài)合并數(shù)據(jù)相同的單元格的完整代碼
文章介紹了如何使用ElementPlus的Table組件動態(tài)合并單元格,適用于后臺數(shù)據(jù)返回格式動態(tài)且沒有規(guī)律的場景,包括數(shù)據(jù)處理和按鈕操作,文章還附帶了效果圖和代碼詳解,希望能對大家有所幫助,感興趣的朋友跟隨小編一起看看吧2025-02-02
vue3中reactive的對象清空所引發(fā)的問題解決方案(清空不了和清空之后再去賦值就賦值不了)
在使用reactive定義的變量時,直接賦值會失去響應式,為了清空?filters并確保響應式,可以使用Object.assign({},?filters)或者遍歷對象逐個清除屬性,本文介紹vue3中reactive的對象清空所引發(fā)的問題解決方案(清空不了和清空之后再去賦值就賦值不了),感興趣的朋友一起看看吧2025-02-02
詳細聊聊前端如何實現(xiàn)token無感刷新(refresh_token)
實現(xiàn)token無感刷新對于前端來說是一項非常常用的技術(shù),其本質(zhì)是為了優(yōu)化用戶體驗,下面這篇文章主要給大家介紹了關于前端如何實現(xiàn)token無感刷新(refresh_token)的相關資料,需要的朋友可以參考下2022-11-11

