vue的可複用性和組合
混合
基礎
混合(mixins)是一種分發Vue組件中可複用功能的非常靈活的方式。混合對象可以包含任意組件選項。以組件使用混合對象時,所有混合對象的選項將被混入該組件本身的選項。例如:
var myMixin = { // 定義一個混合對象
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
var Component = Vue.extend({ // 定義一個使用混合對象的組件
mixins: [myMixin]
})
var component = new Component() // => "hello from mixin!"
選項合並
當組件和混合對象含有同名選項時,這些選項將以恰當的方式混合。比如,同名鉤子函數將混合為一個數組,因此都將被調用。另外,混合對象的鉤子將在組件自身鉤子之前調用:
var mixin = {
created: function () {
console.log('混合對象的鉤子被調用')
}
}
new Vue({
mixins: [mixin],
created: function () {
console.log('組件鉤子被調用')
}
})
值為對象的選項,例如methods, components和directives,將被混合為同一個對象。兩個對象鍵名衝突時取組件對象的鍵值對
var mixin = {
methods: {
foo: function () {
console.log('foo')
},
conflicting: function () {
console.log('from mixin')
}
}
}
var vm = new Vue({
mixins: [mixin],
methods: {
bar: function () {
console.log('bar')
},
conflicting: function () {
console.log('from self')
}
}
})
vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"
注意:Vue.extend()也使用同樣的策略進行合並
全局混合
也可以全局注冊混合對象。 注意使用!一旦使用全局混合對象,將會影響到所有之後創建的Vue實例。使用恰當時可以為自定義對象注入處理邏輯
Vue.mixin({ // 為自定義選項 'myOption' 注入一個處理器
created: function () {
var myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
new Vue({
myOption: 'hello!'
}) // => "hello!"
謹慎使用全局混合對象,因為會影響到每個單獨創建的Vue實例(包括第三方模板)。大多數情況下,隻應當應用於自定義選項,就像上麵示例一樣。也可以將其用作Plugins以避免產生重複應用
自定義選項合並策略
自定義選項將使用默認策略,即簡單地覆蓋已有值。如果想讓自定義選項以自定義邏輯合並,可以向Vue.config.optionMergeStrategies添加一個函數:
Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
// return mergedVal
}
對於大多數對象選項,可以使用methods的合並策略:
var strategies = Vue.config.optionMergeStrategies
strategies.myOption = strategies.methods
更多高級的例子可以在 Vuex 的 1.x 混合策略裏找到:
const merge = Vue.config.optionMergeStrategies.computed
Vue.config.optionMergeStrategies.vuex = function (toVal, fromVal) {
if (!toVal) return fromVal
if (!fromVal) return toVal
return {
getters: merge(toVal.getters, fromVal.getters),
state: merge(toVal.state, fromVal.state),
actions: merge(toVal.actions, fromVal.actions)
}
}
自定義指令
簡介
除了默認設置的核心指令(v-model和v-show),Vue也允許注冊自定義指令。注意:在Vue2.0裏,代碼複用的主要形式和抽象是組件——然而,有的情況下,你仍然需要對純DOM元素進行底層操作,這時候就會用到自定義指令,例如聚焦一個input元素。頁麵加載時,元素將獲得焦點(注意:autofocus在移動版Safari上不工作)。事實上,訪問後還沒點擊任何內容input就獲得了焦點。現在讓我們完善這個指令:
const merge = Vue.config.optionMergeStrategies.computed
Vue.config.optionMergeStrategies.vuex = function(toVal, fromVal) {
if (!toVal) return fromVal
if (!fromVal) return toVal
return {
getters: merge(toVal.getters, fromVal.getters),
state: merge(toVal.state, fromVal.state),
actions: merge(toVal.actions, fromVal.actions)
}
}
也可以注冊局部指令,組件中接受一個directives的選項:
directives: {
focus: {
// 指令的定義---
}
}
然後你可以在模板中任何元素上使用新的v-focus屬性:
<input v-focus>
鉤子函數
指令定義函數提供了幾個鉤子函數(可選):
1.bind: 隻調用一次,指令第一次綁定到元素時調用,用這個鉤子函數可以定義一個在綁定時執行一次的初始化動作
2.inserted: 被綁定元素插入父節點時調用(父節點存在即可調用,不必存在於document中)
3.update: 所在組件的VNode更新時調用,但是可能發生在其孩子的VNode更新之前。指令的值可能發生了改變也可能沒有。但是你可以通過比較更新前後的值來忽略不必要的模板更新
4.componentUpdated: 所在組件的VNode及其孩子的VNode全部更新時調用
5.unbind: 隻調用一次, 指令與元素解綁時調用
接下來我們來看一下鉤子函數的參數 (包括 el,binding,vnode,oldVnode
)
鉤子函數參數
鉤子函數被賦予了以下參數:
el: 指令所綁定的元素,可以用來直接操作DOM
binding: 一個對象,包含以下屬性:
1.name: 指令名,不包括v-前綴。
2.value: 指令的綁定值,例如:v-my-directive="1+1", value的值是2
3.oldValue: 指令綁定的前一個值,僅在update和componentUpdated鉤子中可用。無論值是否改變都可用
4.expression: 綁定值的字符串形式。 例如v-my-directive="1+1" , expression的值是"1+1"
5.arg: 傳給指令的參數。例如v-my-directive:foo,arg的值是"foo"
6.modifiers: 一個包含修飾符的對象。例如v-my-directive.foo.bar,修飾符對象modifiers的值是{foo:true, bar:true}
7.vnode: Vue編譯生成的虛擬節點,查閱VNode API了解更多詳情
8.oldVnode: 上一個虛擬節點,僅在update和componentUpdated 子中可用
除el外,其它參數都應是隻讀的,盡量不要修改他們。如果需要在鉤子之間共享數據,建議通過元素的dataset來進行
一個使用了這些參數的自定義鉤子樣例:
<div v-demo:foo.a.b="message"></div>
<script>
Vue.directive('demo', {
bind: function (el, binding, vnode) {
var s = JSON.stringify
el.innerHTML =
'name: ' + s(binding.name) + '<br>' +
'value: ' + s(binding.value) + '<br>' +
'expression: ' + s(binding.expression) + '<br>' +
'argument: ' + s(binding.arg) + '<br>' +
'modifiers: ' + s(binding.modifiers) + '<br>' +
'vnode keys: ' + Object.keys(vnode).join(', ')
}
})
new Vue({
el: '#hook-arguments-example',
data: {
message: 'hello!'
}
})
</script>
函數簡寫
大多數情況下,我們可能想在bind和update鉤子上做重複動作,並且不想關心其它的鉤子函數。可以這樣寫:
Vue.directive('color-swatch', function (el, binding) {
el.style.backgroundColor = binding.value
})
對象字麵量
如果指令需要多個值,可以傳入一個JS對象字麵量。記住:指令函數能夠接受所有合法類型的JavaScript表達式
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
<script>
Vue.directive('demo', function (el, binding) {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
})
</script>
渲染函數 & JSX
基礎
Vue推薦在絕大多數情況下使用template來創建你的HTML。然而在一些場景中,你真的需要JavaScript的完全編程的能力,這就是render函數,它比template更接近編譯器
<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" >
<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並不是最好的選擇:首先代碼冗長,為了在不同級別的標題中插入錨點元素,我們需要重複地使用。雖然模板在大多數組件中都非常好用,但在這裏就不是很簡潔的了。那麼,我們來嚐試使用render函數重寫上麵的例子:
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // tag name 標簽名稱
this.$slots.default // 子組件中的陣列
)
},
props: {
level: {
type: Number,
required: true
}
}
})
這樣代碼精簡很多,但是需要非常熟悉Vue的實例屬性。在這個例子中,你需要知道當你不使用slot屬性向組件中傳遞內容時,比如anchored-heading中的Hello world!,這些子元素被存儲在組件實例中的$slots.default中
節點、樹以及虛擬DOM
在深入渲染函數之前,了解一些瀏覽器的工作原理是很重要的。以下麵這段HTML為例:
<div>
<h1>My title</h1>
Some text content
<!-- TODO: Add tagline -->
</div>
瀏覽器讀到這些代碼時,會建立一個“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函數中生成模板
深入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) {
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必須唯一:組件樹中的所有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 = 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 (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) |
這裏是一個使用所有修飾符的例子:
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的組件樹裏。在作為包裝組件時它們也同樣非常有用,比如,當你需要做這些時:程序化地在多個組件中選擇一個,在將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()和childre對比
你可能想知道為什麼同時需要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!")])])}
}
插件
開發插件
1.添加全局方法或者屬性,如:vue-custom-element
2.添加全局資源:指令/過濾器/過渡等,如vue-touch
3.通過全局mixin方法添加一些組件選項,如:vue-router
4.添加Vue實例方法,通過把它們添加到Vue.prototype上實現。
5.一個庫,提供自己的API,同時提供上麵提到的一個或多個功能,如vue-router
Vue.js的插件應當有一個公開方法install。這個方法的第一個參數是Vue構造器,第二個參數是一個可選的選項對象:
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或屬性
Vue.myGlobalMethod = function () {
// 邏輯...
}
// 2. 添加全局資源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 邏輯...
}
...
})
// 3. 注入組件
Vue.mixin({
created: function () {
// 邏輯...
}
...
})
// 4. 添加實例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 邏輯...
}
}
使用插件
通過全局方法Vue.use()使用插件:
// 調用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)
也可以傳入一個選項對象:
Vue.use(MyPlugin, { someOption: true })
Vue.use會自動阻止注冊相同插件多次,屆時隻會注冊一次該插件
Vue.js官方提供的一些插件(例如vue-router)在檢測到Vue是可訪問的全局變量時會自動調用Vue.use()。然而在例如CommonJS的模塊環境中,你應該始終顯式地調用Vue.use():
// 用 Browserify 或 webpack 提供的 CommonJS 模塊環境時
var Vue = require('vue')
var VueRouter = require('vue-router')
// 不要忘了調用此方法
Vue.use(VueRouter)
過濾器
Vue.js允許你自定義過濾器,可被用作一些常見的文本格式化。過濾器可以用在兩個地方:mustache插值和v-bind表達式(後者從2.1.0+開始支持)。過濾器應該被添加在JavaScript表達式的尾部,由“管道”符指示:
<!-- in mustaches -->
{{ message | capitalize }}
<!-- in v-bind -->
<div v-bind:></div>
過濾器函數總接收表達式的值(之前的操作鏈的結果)作為第一個參數。在這個例子中,capitalize過濾器函數將會收到message的值作為第一個參數
new Vue({
// ...
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
})
過濾器可以串聯:
{{ message | filterA | filterB }}
filterA被定義為接收單個參數的過濾器函數,表達式message的值將作為參數傳入到函數中,然後繼續調用同樣被定義為接收單個參數的過濾器函數filterB,將filterA的結果傳遞到filterB中。過濾器是JavaScript函數,因此可以接收參數:
{{ message | filterA('arg1', arg2) }}
這裏,filterA 被定義為接收三個參數的過濾器函數。其中message的值作為第一個參數,普通字符串'arg1'作為第二個參數,表達式arg2取值後的值作為第三個參數
最後更新:2017-09-14 19:33:03