前面的話
Vue 推薦在絕大多數情況下使用 template 來創建HTML。然而在一些場景中,真的需要 JavaScript 的完全編程的能力,這就是 render 函數,它比 template 更接近編譯器。本文將詳細介紹Vue渲染函數
?
引入
下面是一個例子,如果要實現類似下面的效果。其中,H標簽可替換
<h1><a name="hello-world" href="#hello-world">Hello world!</a> </h1>
在 HTML 層,像下面這樣定義來組件接口:
<anchored-heading :level="1">Hello world!</anchored-heading>
當開始寫一個通過 level
prop 動態生成 heading 標簽的組件,可能很快想到這樣實現:
<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>
雖然模板在大多數組件中都非常好用,但是在這里它就不是很簡潔的了。那么,來嘗試使用 render
函數重寫上面的例子:
<div id="example"><anchored-heading :level="2"><a name="hello-world" href="#hello-world">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
屬性向組件中傳遞內容時,比如 anchored-heading
中的 Hello world!
,這些子元素被存儲在組件實例中的 $slots.default
中
?
虛擬DOM
在深入渲染函數之前,了解一些瀏覽器的工作原理是很重要的。以下面這段 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
發生了改變。
【虛擬DOM】
Vue 通過建立一個虛擬 DOM 對真實 DOM 發生的變化保持追蹤
return createElement('h1', this.blogTitle)
createElement
到底會返回什么呢?其實不是一個實際的 DOM 元素。它更準確的名字可能是 createNodeDescription
,因為它所包含的信息會告訴 Vue 頁面上需要渲染什么樣的節點,及其子節點。我們把這樣的節點描述為“虛擬節點 (Virtual DOM)”,也常簡寫它為“VNode”。“虛擬 DOM”是我們對由 Vue 組件樹建立起來的整個 VNode 樹的稱呼
?
createElement
接下來需要熟悉的是如何在 createElement
函數中生成模板。這里是 createElement
接受的參數:
// @returns {VNode} createElement(// {String | Object | Function}// 一個 HTML 標簽字符串,組件選項對象,或者一個返回值類型為 String/Object 的函數,必要參數'div',// {Object}// 一個包含模板相關屬性的數據對象// 這樣,可以在 template 中使用這些屬性。可選參數。 { },// {String | Array}// 子節點 (VNodes),由 `createElement()` 構建而成,// 或簡單的使用字符串來生成“文本節點”。可選參數。 ['先寫一些文字',createElement('h1', '一則頭條'),createElement(MyComponent, {props: {someProp: 'foobar'}})] )
【深入data對象】
正如在模板語法中,v-bind:class
和 v-bind:style
,會被特別對待一樣,在 VNode 數據對象中,下列屬性名是級別最高的字段。該對象也允許綁定普通的 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'},// 事件監聽器基于 `on`// 所以不再支持如 `v-on:keyup.enter` 修飾器// 需要手動匹配 keyCode。 on: {click: this.clickHandler},// 僅對于組件,用于監聽原生事件,而不是組件內部使用 `vm.$emit` 觸發的事件。 nativeOn: {click: this.nativeClickHandler},// 自定義指令。注意事項:不能對綁定的舊值設值// Vue 會持續追蹤 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' }
【完整示例】
有了這些知識,現在可以完成最開始想實現的組件:
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 idvar 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]) }
如果真的需要重復很多次的元素/組件,可以使用工廠函數來實現。例如,下面這個例子 render 函數完美有效地渲染了 20 個重復的段落:
render: function (createElement) {return createElement('div',Array.apply(null, { length: 20 }).map(function () {return createElement('p', 'hi')})) }
?
JS代替模板
【v-if和v-for】
由于使用原生的 JavaScript 來實現某些東西很簡單,Vue 的 render 函數沒有提供專用的 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 函數中被 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】
render 函數中沒有與 v-model
相應的 api,必須自己來實現相應的邏輯:
render: function (createElement) {var self = thisreturn createElement('input', {domProps: {value: self.value},on: {input: function (event) {self.value = event.target.valueself.$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 }
對于其他的修飾符,前綴不是很重要,因為可以直接在事件處理函數中使用事件方法:
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) {// 如果觸發事件的元素不是事件綁定的元素// 則返回if (event.target !== event.currentTarget) return// 如果按下去的不是 enter 鍵或者// 沒有同時按下 shift 鍵// 則返回if (!event.shiftKey || event.keyCode !== 13) return// 阻止 事件冒泡 event.stopPropagation()// 阻止該元素默認的 keyup 事件 event.preventDefault()// ... } }
【插槽】
可以從 this.$slots
獲取 VNodes 列表中的靜態內容:
render: function (createElement) {// `<div><slot></slot></div>`return createElement('div', this.$slots.default) }
還可以從 this.$scopedSlots
中獲得能用作函數的作用域插槽,這個函數返回 VNodes:
render: function (createElement) {// `<div><slot :text="msg"></slot></div>`return createElement('div', [this.$scopedSlots.default({text: this.msg})]) }
如果要用渲染函數向子組件中傳遞作用域插槽,可以利用 VNode 數據中的 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
函數,可能會覺得痛苦
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 生態系統中的一個通用慣例,實際上也是 JSX 所要求的,如果在作用域中 h
失去作用,在應用中會觸發報錯
?
函數式組件
之前創建的錨點標題組件是比較簡單,沒有管理或者監聽任何傳遞給它的狀態,也沒有生命周期方法。它只是一個接收參數的函數。在這個例子中,我們標記組件為 functional
,這意味它是無狀態 (沒有 data
),無實例 (沒有 this
上下文)
一個 函數式組件 就像這樣:
Vue.component('my-component', {functional: true,// 為了彌補缺少的實例// 提供第二個參數作為上下文render: function (createElement, context) {// ... },// Props 可選 props: {// ... } })
[注意]在 2.3.0 之前的版本中,如果一個函數式組件想要接受 props,則 props
選項是必須的。在 2.3.0 或以上的版本中,你可以省略 props
選項,所有組件上的屬性都會被自動解析為 props
組件需要的一切都是通過上下文傳遞,包括:
props:提供 props 的對象 children: VNode 子節點的數組 slots: slots 對象 data:傳遞給組件的 data 對象 parent:對父組件的引用 listeners: (2.3.0+) 一個包含了組件上所注冊的 v-on 偵聽器的對象。這只是一個指向 data.on 的別名。 injections: (2.3.0+) 如果使用了 inject 選項,則該對象包含了應當被注入的屬性。
在添加 functional: true
之后,錨點標題組件的 render 函數之間簡單更新增加 context
參數,this.$slots.default
更新為 context.children
,之后this.level
更新為 context.props.level
。
因為函數式組件只是一個函數,所以渲染開銷也低很多。然而,對持久化實例的缺乏也意味著函數式組件不會出現在 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.itemsif (items.length === 0) return EmptyListif (typeof items[0] === 'object') return TableListif (context.props.isOrdered) return OrderedListreturn UnorderedList}return createElement(appropriateListComponent(),context.data,context.children)},props: {items: {type: Array,required: true},isOrdered: Boolean} })
【slots()和children對比】
為什么同時需要 slots()
和 children
。slots().default
不是和 children
類似的嗎?在一些場景中,是這樣,但是如果是函數式組件和下面這樣的 children 呢?
<my-functional-component><p slot="foo">first</p><p>second</p>
</my-functional-component>
對于這個組件,children
會給兩個段落標簽,而 slots().default
只會傳遞第二個匿名段落標簽,slots().foo
會傳遞第一個具名段落標簽。同時擁有 children
和 slots()
,因此可以選擇讓組件通過 slot()
系統分發或者簡單的通過 children
接收,讓其他組件去處理
?
模板編譯
Vue 的模板實際是編譯成了 render 函數。這是一個實現細節,通常不需要關心。下面是一個使用 Vue.compile
來實時編譯模板字符串的簡單 demo:
<div><header><h1>I'm a template!</h1></header><p v-if="message">{{ message }}</p><p v-else>No message.</p>
</div>
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!")])])} }
?