Vue.js 渲染函数 JSX
基礎(chǔ)
Vue 推薦在絕大多數(shù)情況下使用 template 來創(chuàng)建你的 HTML。然而在一些場景中,你真的需要 JavaScript 的完全編程的能力,這時(shí)你可以用?render 函數(shù),它比 template 更接近編譯器。
讓我們深入一個(gè)簡單的例子,這個(gè)例子里?render?函數(shù)很實(shí)用。假設(shè)我們要生成錨點(diǎn)標(biāo)題 (anchored headings):
| <h1><a name="hello-world" href="#hello-world">Hello world!</a> </h1> |
對于上面的 HTML,我們決定這樣定義組件接口:
| <anchored-heading :level="1">Hello world!</anchored-heading> |
當(dāng)我們開始寫一個(gè)只能通過?level?prop 動(dòng)態(tài)生成 heading 標(biāo)簽的組件時(shí),你可能很快想到這樣實(shí)現(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> |
| Vue.component('anchored-heading', {template: '#anchored-heading-template',props: {level: {type: Number,required: true}} }) |
在這種場景中使用 template 并不是最好的選擇:首先代碼冗長,為了在不同級(jí)別的標(biāo)題中插入錨點(diǎn)元素,我們需要重復(fù)地使用?<slot></slot>。
雖然模板在大多數(shù)組件中都非常好用,但是在這里它就不是很簡潔的了。那么,我們來嘗試使用?render?函數(shù)重寫上面的例子:
| Vue.component('anchored-heading', {render: function (createElement) {return createElement('h' + this.level, // 標(biāo)簽名稱this.$slots.default // 子元素?cái)?shù)組)},props: {level: {type: Number,required: true}} }) |
簡單清晰很多!簡單來說,這樣代碼精簡很多,但是需要非常熟悉 Vue 的實(shí)例屬性。在這個(gè)例子中,你需要知道,向組件中傳遞不帶?slot?特性的子元素時(shí),比如?anchored-heading?中的?Hello world!,這些子元素被存儲(chǔ)在組件實(shí)例中的?$slots.default?中。如果你還不了解,在深入 render 函數(shù)之前推薦閱讀實(shí)例屬性 API。
節(jié)點(diǎn)、樹以及虛擬 DOM
在深入渲染函數(shù)之前,了解一些瀏覽器的工作原理是很重要的。以下面這段 HTML 為例:
| <div><h1>My title</h1>Some text content<!-- TODO: 添加標(biāo)簽行 --> </div> |
當(dāng)瀏覽器讀到這些代碼時(shí),它會(huì)建立一個(gè)“DOM 節(jié)點(diǎn)”樹來保持追蹤,如同你會(huì)畫一張家譜樹來追蹤家庭成員的發(fā)展一樣。
HTML 的 DOM 節(jié)點(diǎn)樹如下圖所示:
每個(gè)元素都是一個(gè)節(jié)點(diǎn)。每片文字也是一個(gè)節(jié)點(diǎn)。甚至注釋也都是節(jié)點(diǎn)。一個(gè)節(jié)點(diǎn)就是頁面的一個(gè)部分。就像家譜樹一樣,每個(gè)節(jié)點(diǎn)都可以有孩子節(jié)點(diǎn) (也就是說每個(gè)部分可以包含其它的一些部分)。
高效的更新所有這些節(jié)點(diǎn)會(huì)是比較困難的,不過所幸你不必再手動(dòng)完成這個(gè)工作了。你只需要告訴 Vue 你希望頁面上的 HTML 是什么,這可以是在一個(gè)模板里:
| <h1>{{ blogTitle }}</h1> |
或者一個(gè)渲染函數(shù)里:
| render: function (createElement) {return createElement('h1', this.blogTitle) } |
在這兩種情況下,Vue 都會(huì)自動(dòng)保持頁面的更新,即便?blogTitle?發(fā)生了改變。
虛擬 DOM
Vue 通過建立一個(gè)虛擬 DOM?對真實(shí) DOM 發(fā)生的變化保持追蹤。請仔細(xì)看這行代碼:
| return createElement('h1', this.blogTitle) |
createElement?到底會(huì)返回什么呢?其實(shí)不是一個(gè)實(shí)際的?DOM 元素。它更準(zhǔn)確的名字可能是?createNodeDescription,因?yàn)樗男畔?huì)告訴 Vue 頁面上需要渲染什么樣的節(jié)點(diǎn),及其子節(jié)點(diǎn)。我們把這樣的節(jié)點(diǎn)描述為“虛擬節(jié)點(diǎn) (Virtual Node)”,也常簡寫它為“VNode”。“虛擬 DOM”是我們對由 Vue 組件樹建立起來的整個(gè) VNode 樹的稱呼。
createElement?參數(shù)
接下來你需要熟悉的是如何在?createElement?函數(shù)中生成模板。這里是?createElement?接受的參數(shù):
| // @returns {VNode} createElement(// {String | Object | Function}// 一個(gè) HTML 標(biāo)簽字符串,組件選項(xiàng)對象,或者// 解析上述任何一種的一個(gè) async 異步函數(shù)。必需參數(shù)。'div',// {Object}// 一個(gè)包含模板相關(guān)屬性的數(shù)據(jù)對象// 你可以在 template 中使用這些特性。可選參數(shù)。{// (詳情見下一節(jié))},// {String | Array}// 子虛擬節(jié)點(diǎn) (VNodes),由 `createElement()` 構(gòu)建而成,// 也可以使用字符串來生成“文本虛擬節(jié)點(diǎn)”。可選參數(shù)。['先寫一些文字',createElement('h1', '一則頭條'),createElement(MyComponent, {props: {someProp: 'foobar'}})] ) |
深入 data 對象
有一點(diǎn)要注意:正如在模板語法中,v-bind:class?和?v-bind:style,會(huì)被特別對待一樣,在 VNode 數(shù)據(jù)對象中,下列屬性名是級(jí)別最高的字段。該對象也允許你綁定普通的 HTML 特性,就像 DOM 屬性一樣,比如?innerHTML?(這會(huì)取代?v-html?指令)。
| {// 和`v-bind:class`一樣的 API// 接收一個(gè)字符串、對象或字符串和對象組成的數(shù)組'class': {foo: true,bar: false},// 和`v-bind:style`一樣的 API// 接收一個(gè)字符串、對象或?qū)ο蠼M成的數(shù)組style: {color: 'red',fontSize: '14px'},// 普通的 HTML 特性attrs: {id: 'foo'},// 組件 propsprops: {myProp: 'bar'},// DOM 屬性domProps: {innerHTML: 'baz'},// 事件監(jiān)聽器基于 `on`// 所以不再支持如 `v-on:keyup.enter` 修飾器// 需要手動(dòng)匹配 keyCode。on: {click: this.clickHandler},// 僅用于組件,用于監(jiān)聽原生事件,而不是組件內(nèi)部使用// `vm.$emit` 觸發(fā)的事件。nativeOn: {click: this.nativeClickHandler},// 自定義指令。注意,你無法對 `binding` 中的 `oldValue`// 賦值,因?yàn)?Vue 已經(jīng)自動(dòng)為你進(jìn)行了同步。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',// 如果你在渲染函數(shù)中向多個(gè)元素都應(yīng)用了相同的 ref 名,// 那么 `$refs.myRef` 會(huì)變成一個(gè)數(shù)組。refInFor: true } |
完整示例
有了這些知識(shí),我們現(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) {// 創(chuàng)建 kebab-case 風(fēng)格的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 必須唯一
組件樹中的所有 VNodes 必須是唯一的。這意味著,下面的 render function 是無效的:
| render: function (createElement) {var myParagraphVNode = createElement('p', 'hi')return createElement('div', [// 錯(cuò)誤-重復(fù)的 VNodesmyParagraphVNode, myParagraphVNode]) } |
如果你真的需要重復(fù)很多次的元素/組件,你可以使用工廠函數(shù)來實(shí)現(xiàn)。例如,下面這個(gè)例子 render 函數(shù)完美有效地渲染了 20 個(gè)相同的段落:
| render: function (createElement) {return createElement('div',Array.apply(null, { length: 20 }).map(function () {return createElement('p', 'hi')})) } |
使用 JavaScript 代替模板功能
v-if?和?v-for
只要在原生的 JavaScript 中可以輕松完成的操作,Vue 的 render 函數(shù)就不會(huì)提供專有的替代方法。比如,在 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> |
這些都會(huì)在 render 函數(shù)中被 JavaScript 的?if/else?和?map?重寫:
| 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.')} } |
v-model
render 函數(shù)中沒有與?v-model?的直接對應(yīng) - 你必須自己實(shí)現(xiàn)相應(yīng)的邏輯:
| props: ['value'], render: function (createElement) {var self = thisreturn createElement('input', {domProps: {value: self.value},on: {input: function (event) {self.$emit('input', event.target.value)}}}) } |
這就是深入底層的代價(jià),但與?v-model?相比,這可以讓你更好地控制交互細(xì)節(jié)。
事件 & 按鍵修飾符
對于?.passive、.capture?和?.once事件修飾符, Vue 提供了相應(yīng)的前綴可以用于?on:
| .passive | & |
| .capture | ! |
| .once | ~ |
| .capture.once?or .once.capture | ~! |
例如:
| on: {'!click': this.doThisInCapturingMode,'~keyup': this.doThisOnce,'~!mouseover': this.doThisOnceInCapturingMode } |
對于其他的修飾符,前綴不是很重要,因?yàn)槟憧梢栽谑录幚砗瘮?shù)中使用事件方法:
| .stop | event.stopPropagation() |
| .prevent | event.preventDefault() |
| .self | if (event.target !== event.currentTarget) return |
| Keys: .enter,?.13 | if (event.keyCode !== 13) return?(change?13?to?another key code?for other key modifiers) |
| Modifiers Keys: .ctrl,?.alt,?.shift,?.meta | if (!event.ctrlKey) return?(change?ctrlKey?to?altKey,?shiftKey, or?metaKey, respectively) |
這里是一個(gè)使用所有修飾符的例子:
| on: {keyup: function (event) {// 如果觸發(fā)事件的元素不是事件綁定的元素// 則返回if (event.target !== event.currentTarget) return// 如果按下去的不是 enter 鍵或者// 沒有同時(shí)按下 shift 鍵// 則返回if (!event.shiftKey || event.keyCode !== 13) return// 阻止?事件冒泡event.stopPropagation()// 阻止該元素默認(rèn)的 keyup 事件event.preventDefault()// ...} } |
插槽
你可以通過?this.$slots?訪問靜態(tài)插槽的內(nèi)容,得到的是一個(gè) VNodes 數(shù)組:
| render: function (createElement) {// `<div><slot></slot></div>`return createElement('div', this.$slots.default) } |
也可以通過?this.$scopedSlots?訪問作用域插槽,得到的是一個(gè)返回 VNodes 的函數(shù):
| props: ['message'], render: function (createElement) {// `<div><slot :text="message"></slot></div>`return createElement('div', [this.$scopedSlots.default({text: this.message})]) } |
如果要用渲染函數(shù)向子組件中傳遞作用域插槽,可以利用 VNode 數(shù)據(jù)對象中的?scopedSlots?域:
| render: function (createElement) {return createElement('div', [createElement('child', {// 在數(shù)據(jù)對象中傳遞 `scopedSlots`// 格式:{ name: props => VNode | Array<VNode> }scopedSlots: {default: function (props) {return createElement('span', props.text)}}})]) } |
JSX
如果你寫了很多?render?函數(shù),可能會(huì)覺得下面這樣的代碼寫起來很痛苦:
| createElement('anchored-heading', {props: {level: 1}}, [createElement('span', 'Hello'),' world!'] ) |
特別是模板如此簡單的情況下:
| <anchored-heading :level="1"><span>Hello</span> world! </anchored-heading> |
這就是為什么會(huì)有一個(gè)?Babel 插件,用于在 Vue 中使用 JSX 語法,它可以讓我們回到更接近于模板的語法上。
| import AnchoredHeading from './AnchoredHeading.vue'new Vue({el: '#demo',render: function (h) {return (<AnchoredHeading level={1}><span>Hello</span> world!</AnchoredHeading>)} }) |
將?h?作為?createElement?的別名是 Vue 生態(tài)系統(tǒng)中的一個(gè)通用慣例,實(shí)際上也是 JSX 所要求的,如果在作用域中?h?失去作用,在應(yīng)用中會(huì)觸發(fā)報(bào)錯(cuò)。
更多關(guān)于 JSX 映射到 JavaScript,閱讀?使用文檔。
函數(shù)式組件
之前創(chuàng)建的錨點(diǎn)標(biāo)題組件是比較簡單,沒有管理或者監(jiān)聽任何傳遞給他的狀態(tài),也沒有生命周期方法。它只是一個(gè)接收參數(shù)的函數(shù)。
在這個(gè)例子中,我們標(biāo)記組件為?functional,這意味它是無狀態(tài) (沒有響應(yīng)式數(shù)據(jù)),無實(shí)例 (沒有?this?上下文)。
一個(gè)函數(shù)式組件就像這樣:
| Vue.component('my-component', {functional: true,// Props 可選props: {// ...},// 為了彌補(bǔ)缺少的實(shí)例// 提供第二個(gè)參數(shù)作為上下文render: function (createElement, context) {// ...} }) |
注意:在 2.3.0 之前的版本中,如果一個(gè)函數(shù)式組件想要接受 props,則?props選項(xiàng)是必須的。在 2.3.0 或以上的版本中,你可以省略?props?選項(xiàng),所有組件上的特性都會(huì)被自動(dòng)解析為 props。
在 2.5.0 及以上版本中,如果你使用了單文件組件,那么基于模板的函數(shù)式組件可以這樣聲明:
| <template functional> </template> |
組件需要的一切都是通過上下文傳遞,包括:
- props:提供所有 prop 的對象
- children: VNode 子節(jié)點(diǎn)的數(shù)組
- slots: 返回所有插槽的對象的函數(shù)
- data:傳遞給組件的數(shù)據(jù)對象,作為?createElement?的第二個(gè)參數(shù)傳入組件
- parent:對父組件的引用
- listeners: (2.3.0+) 一個(gè)包含了所有在父組件上注冊的事件偵聽器的對象。這只是一個(gè)指向?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ù)式組件只是一個(gè)函數(shù),所以渲染開銷也低很多。然而,對持久化實(shí)例的缺乏也意味著函數(shù)式組件不會(huì)出現(xiàn)在?Vue devtools?的組件樹里。
在作為包裝組件時(shí)它們也同樣非常有用,比如,當(dāng)你需要做這些時(shí):
- 程序化地在多個(gè)組件中選擇一個(gè)
- 在將 children, props, data 傳遞給子組件之前操作它們。
下面是一個(gè)依賴傳入 props 的值的?smart-list?組件例子,它能代表更多具體的組件:
| var EmptyList = { /* ... */ } var TableList = { /* ... */ } var OrderedList = { /* ... */ } var UnorderedList = { /* ... */ }Vue.component('smart-list', {functional: true,props: {items: {type: Array,required: true},isOrdered: Boolean},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)} }) |
向子元素或子組件傳遞特性和事件
在普通組件中,沒有被定義為 prop 的特性會(huì)自動(dòng)添加到組件的根元素上,將現(xiàn)有的同名特性替換或與其智能合并。
然而函數(shù)式組件要求你顯式定義該行為:
| Vue.component('my-functional-button', {functional: true,render: function (createElement, context) {// 完全透明的傳入任何特性、事件監(jiān)聽器、子結(jié)點(diǎn)等。return createElement('button', context.data, context.children)} }) |
向?createElement?通過傳入?context.data?作為第二個(gè)參數(shù),我們就把?my-functional-button?上面所有的特性和事件監(jiān)聽器都傳遞下去了。事實(shí)上這是非常透明的,那些事件甚至并不要求?.native?修飾符。
如果你使用基于模板的函數(shù)式組件,那么你還需要手動(dòng)添加特性和監(jiān)聽器。因?yàn)槲覀兛梢栽L問到其獨(dú)立的上下文內(nèi)容,所以我們可以使用?data.attrs?傳遞任何 HTML 特性,也可以使用?listeners?(即?data.on?的別名)?傳遞任何事件監(jiān)聽器。
| <template functional><buttonclass="btn btn-primary"v-bind="data.attrs"v-on="listeners"><slot/></button> </template> |
slots()?和?children?對比
你可能想知道為什么同時(shí)需要?slots()?和?children。slots().default?不是和?children?類似的嗎?在一些場景中,是這樣,但是如果是函數(shù)式組件和下面這樣的 children 呢?
| <my-functional-component><p slot="foo">first</p><p>second</p> </my-functional-component> |
對于這個(gè)組件,children?會(huì)給你兩個(gè)段落標(biāo)簽,而?slots().default?只會(huì)傳遞第二個(gè)匿名段落標(biāo)簽,slots().foo?會(huì)傳遞第一個(gè)具名段落標(biāo)簽。同時(shí)擁有?children和?slots()?,因此你可以選擇讓組件通過?slot()?系統(tǒng)分發(fā)或者簡單的通過?children?接收,讓其他組件去處理。
模板編譯
你可能有興趣知道,Vue 的模板實(shí)際是編譯成了 render 函數(shù)。這是一個(gè)實(shí)現(xiàn)細(xì)節(jié),通常不需要關(guān)心,但如果你想看看模板的功能是怎樣被編譯的,你會(huì)發(fā)現(xiàn)會(huì)非常有趣。下面是一個(gè)使用?Vue.compile?來實(shí)時(shí)編譯模板字符串的簡單 demo:
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!")])])} }from:?https://cn.vuejs.org/v2/guide/render-function.html?
總結(jié)
以上是生活随笔為你收集整理的Vue.js 渲染函数 JSX的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Vue.js 自定义指令
- 下一篇: Vue + webpack 项目实践