您好,登錄后才能下訂單哦!
前面的話
Vue 推薦在絕大多數情況下使用 template 來創建HTML。然而在一些場景中,真的需要 JavaScript 的完全編程的能力,這就是 render 函數,它比 template 更接近編譯器。本文將詳細介紹Vue渲染函數
引入
下面是一個例子,如果要實現類似下面的效果。其中,H標簽可替換
<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>
當開始寫一個通過 level prop 動態生成 heading 標簽的組件,可能很快想到這樣實現:
<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 并不是最好的選擇:首先代碼冗長,為了在不同級別的標題中插入錨點元素,需要重復地使用 <slot></slot>
雖然模板在大多數組件中都非常好用,但是在這里它就不是很簡潔的了。那么,來嘗試使用 render 函數重寫上面的例子:
<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 屬性向組件中傳遞內容時,比如 anchored-heading 中的 Hello world!,這些子元素被存儲在組件實例中的 $slots.default中
虛擬DOM
在深入渲染函數之前,了解一些瀏覽器的工作原理是很重要的。以下面這段 HTML 為例:
<div> <h2>My title</h2> Some text content <!-- TODO: Add tagline --> </div>
當瀏覽器讀到這些代碼時,它會建立一個“DOM 節點”樹來保持追蹤,如同會畫一張家譜樹來追蹤家庭成員的發展一樣。HTML 的 DOM 節點樹如下圖所示:
每個元素都是一個節點。每段文字也是一個節點。甚至注釋也都是節點。一個節點就是頁面的一個部分。就像家譜樹一樣,每個節點都可以有子節點 (也就是說每個部分可以包含其它的一些部分)
高效的更新所有這些節點會是比較困難的,不過所幸不必再手動完成這個工作了。只需要告訴 Vue 希望頁面上的 HTML 是什么,這可以是在一個模板里:
<h2>{{ blogTitle }}</h2>
或者一個渲染函數里:
render: function (createElement) { return createElement('h2', this.blogTitle) }
在這兩種情況下,Vue 都會自動保持頁面的更新,即便 blogTitle 發生了改變。
【虛擬DOM】
Vue 通過建立一個虛擬 DOM 對真實 DOM 發生的變化保持追蹤
return createElement('h2', 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('h2', '一則頭條'), 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 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 ]) }
如果真的需要重復很多次的元素/組件,可以使用工廠函數來實現。例如,下面這個例子 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】
ender 函數中沒有與 v-model 相應的 api,必須自己來實現相應的邏輯:
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 }
對于其他的修飾符,前綴不是很重要,因為可以直接在事件處理函數中使用事件方法:
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.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 類似的嗎?在一些場景中,是這樣,但是如果是函數式組件和下面這樣的 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:
<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渲染函數詳解就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。