Vue.js 2.x render 渲染函數 & JSX


Vue.js 2.x render

渲染函數 & JSX

Vue絕大多數情況下使用template創建 HTML。但是比如一些重復性比較高的場景,需要運用 JavaScript 的完全編程能力,可以使用render函數。

1. 節點、樹以及虛擬DOM

每個元素都是一個節點。每片文字也是一個節點。甚至注釋也都是節點。
一個節點就是頁面的一個部分。每個節點都可以有子節點。

比如上面的節點樹就表示下面的代碼:

<div>
    <h1>My title</h1>
    Some text content
    <!-- TODO: 添加標簽行 -->
</div>

1.1 虛擬 DOM

Vue 通過建立一個虛擬 DOM 對真實 DOM 發生的變化保持追蹤。
虛擬 DOM 是對Vue組件樹建立起來的整個 VNode(Virtual Node)樹的稱呼。

1.1.1 createElement 參數
// @returns {VNode}
createElement(
    // {String | Object | Function}
    // 一個 HTML 標簽字符串,組件選項對象,或者
    // 解析上述任何一種的一個 async 異步函數
    // 必須參數
    'div',
    
    // {Object}
    // 一個包含模板相關屬性的數據對象
    // 你可以在 template 中使用這些特性
    // 可選參數
    { 
        // 屬性參見 1.1.2 深入 data 對象
    },
    
    // {String | Array}
    // 子虛擬節點(VNodes) 由 createElement() 構建而成
    // 也可以使用字符串來生成“文本虛擬節點”
    // 可選參數
    [
        '文字',
        createElement('h1', '一個標題'),
        createElement(MyComponent, {
            props: {
                someProp: 'foo'
            }
        })
    ]
)
1.1.2 深入 data 對象

在 VNode 數據對象中,下列屬性名是級別最高的字段。該對象也允許綁定普通的 HTML 特性,就像 DOM 屬性一樣,比如innerHTML(這會取代v-html指令)。

{
    // 和 v-bind:calss 一樣的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
    },
    
    // 自定義指令
    // 注意你無法對 binding 中的 oldValue 賦值
    // 因為 Vue 已經自動進行了同步
    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',
    
    // 如果在渲染函數中向多個元素都應用了相同的 ref 名
    // 那么 $refs.myRef 會變成一個數組
    refInfor: true
}
1.1.3 完整示例

一個生成錨點標題的組件:

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) {
    // 創建 kebab-case 風格的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
    }
  }
})

使用效果:

<div id="app">
    <anchored-heading :level="1">first</anchored-heading>
    <anchored-heading :level="2">second</anchored-heading>
    <anchored-heading :level="3">
        third
        <small>thirdSub</small>
    </anchored-heading>
</div>

1.1.4 約束

組件樹中的所有 VNodes 必須是唯一的。

比如這樣寫是錯誤的:

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

如果需要重復多次元素/組件,可以用工廠函數來實現:

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

1.2 使用 JavaScript 代替模板功能

1.2.1 v-if 和 v-for

只要在原生的 JavaScript 中可以輕松完成的操作,Vue的render函數就不會提供專有的替代方法。
比如這里的v-ifv-for

<ul v-if="items.length">
    <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>

使用 render 方法重寫:

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 here.');
    }
}
1.2.2 v-model

render 函數中沒有與 v-model直接對應的方法,需要手動實現:

props: ['value'],
render: function (createElement) {
    var self = this;
    return createElement('input', {
        // DOM屬性
        domProps: {
            value: self.value
        },
        on: {
            input: function(e) {
                self.$emit('input', e.target.value);
            }
        }
    })
}

1.3 事件&按鍵修飾符

對於.passive.capture.once事件修飾符,Vue提供了相應的前綴可以用於on

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

比如:

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

對於其他的修飾符,可以在事件處理函數中使用事件方法:

Modifier(s) Equivalent in Handler
.stop e.stopPropagation()
.prevent e.preventDefault()
.self if (e.target !== e.currentTarget) return;
.enter, .13 e.keyCode === 13

比如:

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

1.4 插槽

可以通過this.$slots訪問靜態插槽內容,得到的是一個 VNodes 數組:

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

也可以通過this.$scopeSlots訪問作用域插槽,得到的是一個 VNodes 的函數:

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

如果要用渲染函數句子向子組件中傳遞作用域插槽,可以利用 VNode 數據對象中的scopedSlots域:

render: function(createElement) {
    return createElement('div', [
        createElement('child', {
            // 在數據對象中傳遞 scopedSlots
            // 格式:{ name: props => VNode | Array<VNode> }
            scopedSlots: {
                default: function(props) {
                    return createElement('span', props.text)
                }
            }
        })
    ])
}

2. JSX

在Vue中使用JSX語法,可以避免書寫繁雜冗余的render代碼,Babel插件用於在Vue中使用JSX語法。

使用文檔

import AnchoredHeading from './AnchoredHeading.vue'

new Vue({
    el: '#app',
    render: function(h) {
        return (
            <AnchoredHeading level={1}>
                <span>Hello</span> world
            </AnchoredHeading>
        )
    }
})

3. 函數式組件

一個函數是組件,意味着它是無狀態(沒有響應式數據),無實例(沒有this上下文)的:

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

在v2.5.0^版本中,單文件組件基於模板的函數式組件可以這樣聲明:

<template functional></template>

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

  • props 提供所有prop對象
  • children VNode子節點的數組
  • slots 返回所有插槽的對象的函數
  • scopedSlots 一個暴露傳入的作用域插槽以及函數形式的普通插槽的對象
  • data 傳遞給組件的數據對象,作為CreateElement的第二個參數傳入組件
  • parent 對父組件的引用
  • listeners 一個包含了所有在父組件上注冊的事件偵聽器的對象,data.on的別名
  • injections 如果使用了inject選項,該對象包含了應當被注入的屬性

之前的錨點標題組件可以改為如下的函數式組件:

...

Vue.component('anchored-heading', {
  functional: true,
  render: function (createElement, context) {
    // 創建 kebab-case 風格的ID
    var headingId = getChildrenTextContent(context.children)
      .toLowerCase()
      .replace(/\W+/g, '-')
      .replace(/(^-|-$)/g, '')

    return createElement(
      'h' + context.props.level,
      [
        createElement('a', {
          attrs: {
            name: headingId,
            href: '#' + headingId
          }
        }, context.children)
      ]
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

3.1 想子組件或子元素傳遞特性和事件

在普通組件中,沒有被定義為 prop 的特性會自動添加到組件的根元素上,將現有的同名特性替換或智能合並。
而函數式組件要求必須顯示定義該行為:

Vue.component('my-functional-button', {
    functional: true,
    render: function(createElement, context) {
        return createElement('button', context.data, context.children)
    }
})

如果使用模板的函數式組件,還需要手動添加特性和監聽器。

<template functional>
  <button
    class="btn btn-primary"
    v-bind="data.attrs"
    v-on="listeners"
  >
    <slot/>
  </button>
</template>

3.2 slots()和children對比

slots()和children對比

總的來說個人理解,slots()可以拿到具名插槽,children拿到所有。

4. 模板編譯

模板編譯

The end
2019-8-13 15:45:59


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM