爲啥研究這個?在以前開發組件庫的過程當中,遇到了許多遺留的問題,包括數據模板渲染、組件按需加載、引入自定義組件插槽等等,因此爲了修復和避免這些問題,學習一波更接近編譯器的編寫方式,看看如何經過這種徹底編程方式來解決一波這些問題~固然這裏只是一些最基本的使用和探索,由於官網例子太少了,只能一個個本身搭=。=javascript
Vue 推薦在絕大多數狀況下使用 template 來建立你的 HTML。然而在一些場景中,你真的須要 JavaScript 的徹底編程的能力,這就是render 函數,它比 template 更接近編譯器。(從官網複製的,慌得一批,其實簡單來講就是以函數的方式寫HTML,可控性更強一些~)css
固然,官網已經給出了一個使用template來編寫的不方便的demo,因此在這裏就不反覆提起了,初次使用或者有興趣的大佬能夠直接戳這個連接瞭解一下~Vue Renderhtml
瞭解基本概念的客官能夠直接下拉到實例,實例已上傳githubvue
slot
屬性的用法scopedSlots
的用法DOM 就是瀏覽器解析 HTML 得來的一個樹形邏輯對象。java
用 Object 來表明一個節點,這個節點叫作虛擬節點( Virtual Node )簡寫爲 VNode,由 VNode 樹組成虛擬DOM。node
Web 頁面的大多數操做和邏輯的本質就是不停地修改 DOM 元素,可是 DOM 操做太慢了,過於頻繁的 DOM 操做可能會致使整個頁面掉幀、卡頓甚至失去響應。仔細想想,不少 DOM 操做是能夠打包(多個操做壓成一個)和合並(一個連續更新操做只保留最終結果)的,同時 JS 引擎的計算速度要快得多,因此爲何不把 DOM 操做先經過JS計算完成後統一來一次大招操做DOM呢,因而就有了虛擬DOM的概念。固然,虛擬DOM操做的核心是Diff算法,也就是比較變化先後Vnode的不一樣,計算出最小的DOM操做來改變DOM,提升性能。webpack
經過`createElement(tag, options, VNodes)`,下面就來介紹這個函數的基本概念。git
簡單來講CreateElement就是用來生成Vnode的函數github
CreateElement 到底會返回什麼呢?其實不是一個實際的 DOM 元素(返回的是Vnode)。它更準確的名字多是 createNodeDescription,由於它所包含的信息會告訴 Vue 頁面上須要渲染什麼樣的節點,及其子節點。web
【Tips】 CreateElement
函數在慣例中一般也寫做h
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一個 HTML 標籤字符串,組件選項對象,或者 解析上述任何一種的一個 async 異步函數,必要參數。
'div',
// {Object}
// 一個包含模板相關屬性的數據對象
// 這樣,您能夠在 template 中使用這些屬性。可選參數。
{
// 詳情見下方
},
// {String | Array}
// 子節點 (VNodes),由 `createElement()` 構建而成,或使用字符串來生成「文本節點」。可選參數。
[
'先寫一些文字',
createElement('h1', '一則頭條'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
複製代碼
【Tips】 文檔中此處說VNodes子節點必須是惟一的,也就是說第三個參數的Array裏不能出現相同指向的VNodes,實際驗證之後,就算寫重複的VNodes,也並不會報錯,估計此處會有些坑,如今還沒踩到,建議按照文檔要求,保持子節點惟一。
如下屬性爲簡單介紹,具體用法和一些 _備註解釋 _能夠參考後面會講到的【包含屬性配置較完整的實例】
{
// 和`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
},
// 自定義指令。注意,你沒法對 `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'
}
複製代碼
這是一個基礎的Demo,包含了
簡單的渲染用法
標籤
props
slot
點擊事件
如下示例Demo均採用單文件組件的方式,工程用vue-cli
搭建的webpack-simple
工程。
組件wii-first
<script>
export default {
name: 'wii-first',
data() {
return {
msg: 0
}
},
props: {
level: {
type: [Number, String],
required: true
}
},
render: function(createElement) {
this.$slots.subtitle = this.$slots.subtitle || []
// this.level = 1時, 等價於
// <h1 class="wii-first">
// 第一個組件, <slot></slot>
// <slot name="subtitle"></slot>,此處是data的值: {{msg}}
// <button @click="clickHandler">點我改變內部data值</button>
// </h1>
return createElement(
'h' + this.level, // tag name 標籤名稱
{
class: 'wii-first'
},
// this.$slots.default, // 子組件中的slot 單個傳遞
// this.$slots.subtitle,
[
'第一個組件, ',
...this.$slots.default, // 默認slots傳遞
...this.$slots.subtitle, // 具名slots傳遞
',此處是data的值: ',
this.msg,
createElement('button', {
on: {
click: this.clickHandler
},
}, '點我改變內部data值')
]
)
},
methods: {
clickHandler() {
this.msg = Math.ceil(Math.random() * 1000)
}
}
}
</script>
複製代碼
【Tips】:CreateElement的第三個參數在文檔中規定組件樹中的全部 VNode 必須是惟一的,也就是說在第三個參數中有兩個指向相同的Vnode是無效的。但通過實踐發現,其實是能夠渲染出來的,在此不推薦這麼寫哦,可能會掉到不可預料的大坑hiahiahia~
引入方式
<template>
<div id="app">
<wii-first level="1">我是標題 <span slot="subtitle">我是subtitle</span></wii-first>
</div>
</template>
<script>
import WiiFirst from './components/first/index.vue'
export default {
name: 'app',
components: {
WiiFirst
},
data() {
return {
}
}
}
</script>
複製代碼
這個Demo主要展現了createElement屬性用法,包含
click.stop
的轉換示例不包含
組件wii-second
export default {
name: 'wii-second',
data() {
return {
myProp: '我是data的值, 只是爲了證實props不是走這兒'
}
},
props: {
},
render: function(createElement) {
// 等價於
// <div id="second" class="wii-second blue-color" style="color: green;" @click="clickHandler">
// 我是第二個組件測試, 點我觸發組件內部click和外部定義的@click.native事件。
// <div>{{myProp}}</div>
// <button @click="buttonClick">觸發emit</button>
// </div>
return createElement(
'div', {
//【class】和`v-bind:class`同樣的 API
// 接收一個字符串、對象或字符串和對象組成的數組
// class: 'wii-second',
// class: {
// 'wii-second': true,
// 'grey-color': true
// },
class: [{
'wii-second': true
}, 'blue-color'],
//【style】和`v-bind:style`同樣的 API
// 接收一個字符串、對象或對象組成的數組
style: {
color: 'green'
},
//【attrs】正常的 HTML 特性, id、title、align等,不支持class,緣由是上面的class優先級最高[僅猜想]
// 等同於DOM的 Attribute
attrs: {
id: 'second',
title: '測試'
},
// 【props】組件 props,若是createElement定義的第一個參數是組件,則生效,此處定義的數據將被傳到組件內部
props: {
myProp: 'bar'
},
// DOM 屬性 如 value, innerHTML, innerText等, 是這個DOM元素做爲對象, 其附加的內容
// 等同於DOM的 Property
// domProps: {
// innerHTML: 'baz'
// },
// 事件監聽器基於 `on`, 用於組件內部的事件監聽
on: {
click: this.clickHandler
},
// 僅對於組件,同props,等同@click.native,用於監聽組件內部原生事件,而不是組件內部使用 `vm.$emit` 觸發的事件。
// nativeOn: {
// click: this.nativeClickHandler
// },
// 若是組件是其餘組件的子組件,需爲插槽指定名稱,見 wii-third 組件
// slot: 'testslot',
// 其餘特殊頂層屬性
// key: 'myKey',
// ref: 'myRef'
}, [
`我是第二個組件測試, 點我觸發組件內部click和外部定義的@click.native事件。`,
createElement('div', `${this.myProp}`),
createElement('button', {
on: {
click: this.buttonClick
}
}, '觸發emit')
]
)
},
methods: {
clickHandler() {
console.log('我點擊了第二個組件,這是組件內部觸發的事件')
},
buttonClick(e) {
e.stopPropagation() // 阻止事件冒泡 等價於 click.stop
console.log('我點擊了第二個組件的button,將會經過emit觸發外部的自定義事件')
this.$emit('on-click-button', e)
}
}
}
複製代碼
引入方式
<template>
<div id="app">
<wii-second @click.native="nativeClick" @on-click-button="clickButton"></wii-second>
</div>
</template>
<script>
import WiiSecond from './components/second/index.vue'
export default {
name: 'app',
components: {
WiiSecond
},
data() {
return {
}
},
methods: {
nativeClick() {
console.log('這是組件外部click.native觸發的事件,第二個組件被點擊了')
},
clickButton() {
console.log('這是組件外部觸發的【emit】事件,第二個組件被點擊了')
}
}
}
</script>
複製代碼
上面例子中用到了e.stopPropagation
這個方法,等價於 template 模板寫法的click.stop
,其餘的事件和按鍵修飾符也有對應的方法,對應狀況以下。
事件修飾符對應的前綴
template事件修飾符 | render寫法前綴 |
---|---|
.passive | & |
.capture | ! |
.once | ~ |
.capture.once 或 .once.capture | ~! |
例如
on: {
'!click': this.doThisInCapturingMode,
'~keyup': this.doThisOnce,
'~!mouseover': this.doThisOnceInCapturingMode
}
複製代碼
其餘事件修飾符,對應的事件處理函數中使用事件方法
template事件修飾符 | 對應的事件方法 |
---|---|
.stop | event.stopPropagation() |
.prevent | event.preventDefault() |
.self | if (event.target !== event.currentTarget) return |
Keys: .enter, .13 | if (event.keyCode !== 13) return (對於其餘的鍵盤事件修飾符,將13換成其餘的鍵盤code就行) |
Modifiers Keys: .ctrl, .alt, .shift, .meta | if (!event.ctrlKey) return (將 ctrlKey 換成 altKey, shiftKey, 或者 metaKey, respectively) |
例如
on: {
keyup: function (event) {
// 若是觸發事件的元素不是事件綁定的元素
// 則返回
if (event.target !== event.currentTarget) return
// 若是按下去的不是 enter 鍵或者
// 沒有同時按下 shift 鍵
// 則返回
if (!event.shiftKey || event.keyCode !== 13) return
// 阻止 事件冒泡
event.stopPropagation()
// 阻止該元素默認的 keyup 事件
event.preventDefault()
// ...
}
}
複製代碼
slot
屬性的用法這個Demo主要展現render中createElement的配置slot屬性用法。
由於此處一直很疑惑什麼狀況下可以用到這個slot屬性,因此就試了一下,僅供參考,具體使用場景須要根據業務邏輯來定
組件wii-third
<script>
export default {
name: 'wii-third',
data() {
return {}
},
components: {
WiiTestSlot: {
name: 'wii-test-slot',
render(createElement) {
this.$slots.testslot = this.$slots.testslot || []
// 等價於
// <div>
// 第三個組件,測試在組件中定義slot, <slot name="testslot"></slot>
// </div>
return createElement(
'div', [
'第三個組件,測試在組件中定義slot, ',
...this.$slots.testslot
]
)
}
},
WiiTestSlotIn: {
name: 'wii-test-slot-in',
render(createElement) {
// 等價於
// <span>我是組件中的slot內容</span>
return createElement(
'span', [
'我是組件中的slot內容'
]
)
}
}
},
props: {
},
render: function(createElement) {
// 等價於
// <div style="margin-top: 15px;">
// <wii-test-slot>
// <wii-test-slot-in slot="testslot"></wii-test-slot-in>
// </wii-test-slot>
// </div>
return createElement(
'div', {
style: {
marginTop: '15px'
}
}, [
createElement(
'wii-test-slot',
//這麼寫不會被渲染到節點中去
// createElement(
// 'wii-test-slot-in',
// {
// slot: 'testslot'
// }
// ),
[
// createElement再放createElement須要放入數組裏面,建議全部的組件的內容都放到數組裏面,統一格式,防止出錯
createElement(
'wii-test-slot-in', {
slot: 'testslot'
}
)
]
)
]
)
},
methods: {
}
}
</script>
複製代碼
【Tips】:若是createElement裏面的第三個參數傳遞的是createElement生成的VNode對象,將不會被渲染到節點中,須要放到數組中才能生效,此處猜想是由於VNode對象不會被直接識別,由於文檔要求是String或者Array。
引入方式
<template>
<div id="app">
<wii-third></wii-third>
</div>
</template>
<script>
import WiiThird from './components/third/index.vue'
export default {
name: 'app',
components: {
WiiThird
},
data() {
return {}
}
}
</script>
複製代碼
scopedSlots
的用法這個Demo主要展現scopedSlots的用法,包括定義和使用。scopedSlots的template用法和解釋參考vue-slot-scope。
組件wii-forth
<script>
export default {
name: 'wii-forth',
data() {
return {}
},
components: {
WiiScoped: {
name: 'wii-scoped',
props: {
message: String
},
render(createElement) {
// 等價於 <div><slot :text="message"></slot></div>
return createElement(
'div', [
this.$scopedSlots.default({
text: this.message
})
]
)
}
}
},
render: function(createElement) {
// 等價於
// <div style="margin-top: 15px;">
// <wii-scoped message="測試scopedSlots,我是傳入的message">
// <span slot-scope="props">{{props.text}}</span>
// </wii-scoped>
// </div>
return createElement(
'div', {
style: {
marginTop: '15px'
}
}, [
createElement('wii-scoped', {
props: {
message: '測試scopedSlots,我是傳入的message'
},
// 傳遞scopedSlots,經過props(自定義名稱)取值
scopedSlots: {
default: function(props) {
return createElement('span', props.text)
}
}
})
]
)
}
}
</script>
複製代碼
引入方法
<template>
<div id="app">
<wii-forth></wii-forth>
</div>
</template>
<script>
import WiiForth from './components/forth/index.vue'
export default {
name: 'app',
components: {
WiiForth
},
data() {
return {}
}
}
</script>
複製代碼
寫了這麼多createElement,眼睛都花了,有的寫起來也挺麻煩的。咱們試試來換個口味,試試JSX的寫法。
工欲善其事,必先利其器。
npm install babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx babel-helper-vue-jsx-merge-props babel-preset-env --save-dev
複製代碼
安裝完成之後,在.babelrc
文件配置"plugins": ["transform-vue-jsx"]
。
將webpack配置文件中的js解析部分改爲test: /\.jsx?$/
表示對jsx的代碼塊進行解析。
這個示例作了以下功能:
父組件wii-jsx
<script type="text/jsx">
import WiiJsxItem from './item.vue'
export default {
name: 'wii-jsx',
components: {
WiiJsxItem
},
data() {
return {
color: 'red'
}
},
props: {
},
render: function (h) {
return (
<div class="wii-jsx"> <wii-jsx-item color={this.color} nativeOnClick={this.clickHandler}> <span>我是wii-jsx-item組件的slot, color經過變量傳入: {this.color}</span> </wii-jsx-item> </div> ) }, methods: { clickHandler() { this.color = this.color == 'red' ? 'blue' : 'red' console.log(`點擊了wii-jsx-item,經過native觸發,改變了顏色爲【${this.color}】`) } } } </script>
複製代碼
子組件wii-jsx-item
該子組件在父組件中被引入,並用JSX的寫法渲染。
export default {
name: 'wii-jsx-item',
data() {
return {}
},
props: {
color: String
},
render: function(createElement) {
// 等價於 <div class="wii-jsx-item"><slot></slot></div>
return createElement(
'div', {
class: 'wii-jsx-item',
style: {
color: this.color
}
},
this.$slots.default
)
},
methods: {
}
}
複製代碼
引入方式
<template>
<div id="app"> <wii-jsx></wii-jsx> </div> </template>
<script>
import WiiJsx from './components/jsx/index.vue'
export default {
name: 'app',
components: {
WiiJsx
},
data() {
return {}
}
}
複製代碼
JSX的主要轉換仍是依靠咱們以前安裝的babel插件,而JSX的事件以及屬性的用法見babel插件的使用說明,這裏麪包含了vue裏面事件和屬性對應的用法說明。
下面來進行最後一個模塊的介紹,函數式組件functional,這個東西的用法就見仁見智了,這裏也沒啥好的方案,只是給出了一些示例,各位大佬若是有一些具體的使用到的地方,闊以指點一下哇~thx~(害羞.jpg)。
官方文檔的定義是functional組件須要的一切都是經過上下文傳遞,包括:
_在添加 _functional: true
以後,組件的render函數會增長第二個參數context(第一個是createElement),數據和節點經過context傳遞。
Tips:
在 2.3.0 以前的版本中,若是一個函數式組件想要接受 props,則props
選項是必須的。在 2.3.0 或以上的版本中,你能夠省略props
選項,全部組件上的屬性都會被自動解析爲 props。
我我的的理解是:
Functional至關於一個純函數同樣,內部不存儲用於在界面上展現的數據,傳入什麼,展現什麼,傳入的是相同的數據,展現的必然是相同的。無實例,無狀態,沒有this上下文,均經過context來控制。
優勢:
由於函數式組件只是一個函數,因此渲染開銷低不少。
使用場景:
接下來就經過兩個組件來看看如何使用的吧,這裏也僅僅只是示例而已,使用的場景仍在探索中,具體的使用場景還須要在開發過程當中根據需求複雜的和性能要求來酌情選擇~
wii-functional
用在動畫的functional這個Demo的做用是在輸入框中輸入字符,對數據列表進行篩選,篩選時加入顯示和消失的動畫。
組件主體
<script>
import Velocity from 'velocity-animate' // 這是一個動畫庫
export default {
name: 'wii-functional',
functional: true, //代表是函數式組件
render: function(createElement, context) {
// context是在functional: true時的參數
let data = {
props: {
tag: 'ul',
css: false
},
on: {
// 進入前事件
beforeEnter: function(el) {
el.style.opacity = 0
el.style.height = 0
},
// 進入事件
enter: function(el, done) {
let delay = el.dataset.index * 150
setTimeout(function() {
Velocity(el, {
opacity: 1,
height: '1.6em'
}, {
complete: done
})
}, delay)
},
// 離開事件
leave: function(el, done) {
let delay = el.dataset.index * 150
setTimeout(function() {
Velocity(el, {
opacity: 0,
height: 0
}, {
complete: done
})
}, delay)
}
}
}
return createElement('transition-group', data, context.children)
}
}
</script>
複製代碼
上面這個組件至關於建立了一個ul-li
標籤組成的vue動畫,經過functional方式包裹到組件外部,能夠做爲通用的動畫。
引入方式
<template>
<div id="app"> <input v-model="query"/> <wii-functional> <li v-for="(item, index) in computedList" :key="item.msg" :data-index="index"> {{item.msg}} </li> </wii-functional> </div> </template> <script> import WiiFunctional from './components/functional/index.vue' export default { name: 'app', components: { WiiFunctional }, data() { return { // 關鍵字 query: '', // 數據列表 list: [{ msg: 'Bruce Lee' }, { msg: 'Jackie Chan' }, { msg: 'Chuck Norris' }, { msg: 'Jet Li' }, { msg: 'Kung Furry' }, { msg: 'Chain Zhang' }, { msg: 'Iris Zhao' }, ] } }, computed:{ computedList: function() { var vm = this // 過濾出符合條件的查詢結果 return this.list.filter(function(item) { return item.msg.toLowerCase().indexOf(vm.query.toLowerCase()) !== -1 }) } }, watch: { computedList(newVal, oldVal) { console.log(newVal) } } } </script> 複製代碼
wii-choose-comp
用在組件切換的functional在這個示例中,經過props來切換加載不一樣的組件,而且在props傳遞給子組件以前操做它,組件內部定義了click.native事件來展現示例。若是對一批組件進行一樣的操做,則能夠用這個functional,相似於加工廠。
固然若是組件須要不一樣的點擊事件或者表現方式也能夠在各個組件內部單獨寫邏輯或者監聽~由於wii-choose-comp
這個外殼本質不過就是個函數而已~
組件主體 wii-choose-comp
<script>
export default {
name: 'wii-choose-comp',
functional: true,
props: { // 2.3.0版本以上也能夠不寫props,會將組件屬性默認綁定成props,爲了統一標準仍是寫上
componentName: String // 組件名
},
render: function(createElement, context) {
// 給組件加上class
context.data.class = [context.props.componentName]
// 在props傳給子組件以前操做它
context.data.props = {
compName: context.props.componentName
}
context.data.nativeOn = {
click() {
alert('我是functional裏面統一的點擊事件')
}
}
return createElement(context.props.componentName, context.data, context.children)
}
}
</script>
複製代碼
切換組件1 wii-comp-one
<script>
export default {
name: 'wii-comp-one',
props: {
compName: String
},
render: function(createElement) {
return createElement('div', [
'我是第一個comp, 我有點擊效果, ',
`個人名字叫${this.compName}, `,
...this.$slots.default
])
}
}
</script>
複製代碼
切換組件2 wii-comp-two
<script>
export default {
name: 'wii-comp-two',
props: {
compName: String
},
render: function(createElement) {
return createElement('div', [
'我是第二個comp, 點我試試唄, ',
`個人名字叫${this.compName}, `,
...this.$slots.default
])
}
}
</script>
複製代碼
引入方式
<template>
<div id="app">
<button @click="changeComponent">點擊切換組件</button>
<wii-choose-comp :component-name="componentName">
<span>我是{{componentName}}的slot</span>
</wii-choose-comp>
</div>
</template>
<script>
import WiiChooseComp from './components/functional/chooseComp.vue'
import WiiCompOne from './components/functional/comp1.vue'
import WiiCompTwo from './components/functional/comp2.vue'
export default {
name: 'app',
components: {
WiiChooseComp,
WiiCompOne,
WiiCompTwo
},
data() {
return {
componentName: 'wii-comp-one'
}
},
methods: {
changeComponent() {
this.componentName = this.componentName == 'wii-comp-one' ? 'wii-comp-two' : 'wii-comp-one'
}
}
}
</script>
複製代碼
【Tips】 須要將待切換的組件所有引入到外層。(不造有沒有更好的辦法?)
以上就是最近對Vue Render的一個探索,由於對於公共組件庫開發來講,須要考慮的問題有不少,因此靈活性要求也更高,若是用Vue Render這種更接近編譯的方式來編寫組件庫,可能會讓邏輯更清晰,雖然不停的建立元素的寫法是挺噁心的哈哈哈哈~~
接下來就是用來進行一下實戰了,在實戰的時候有什麼坑就到時候再慢慢填咯~~