Vue2.0中一共有五個內置組件:動態渲染組件的component、用於過渡動畫的transition-group與transition、緩存組件的keep-alive、內容分發插槽的slot。
component組件配合is屬性在編譯的過程當中被替換成具體的組件,而slot組件已經在上一篇文章中加以描述,所以本章主要闡述剩餘的三個內置組件。
css
<keep-alive> 包裹動態組件時,會緩存不活動的組件實例,而不是銷燬它們。<keep-alive> 是一個抽象組件:它自身不會渲染一個 DOM 元素,也不會出如今父組件鏈中。該組件要求同時只有一個子元素被渲染。前端
KeepAlive 組件源碼以下所示:
node
{
name: 'keep-alive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created () {
this.cache = Object.create(null)
this.keys = []
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key)
} else {
cache[key] = vnode
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
複製代碼
KeepAlive 組件的邏輯相對比較簡單,根據傳入的 include 與 exclude 規則來決定是否緩存子組件,根據傳入的 max 參數來決定最多緩存多少組件。
從 render 函數中能夠看出,若是子組件是緩存對象 cache 的屬性,則直接返回該子組件的VNode,若是不是,則添加到緩存對象上,並將緩存的VNode的 data.keepAlive 屬性置爲 true。
這裏有兩點須要注意:keepAlive組件的 abstract 屬性爲 true、被緩存的子組件 vnode.data.keepAlive 屬性爲 true。
react
當 abstract 屬性爲 true 時,表示該組件爲抽象組件:組件自己不會被渲染成DOM元素、不會出如今父組件鏈中。
在完成一系列初始化的過程當中,會調用 initLifecycle 方法:
web
function initLifecycle(vm) {
const options = vm.$options
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
/* ... */
}
複製代碼
由上可知,在 options.abstract 爲 true 時,組件實例創建父子關係的時候會被忽略。
算法
在 patch 的過程當中會調用 createComponent 方法:
數組
function createComponent(vnode,insertedVnodeQueue,parentElm,refElm){
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
複製代碼
在本系列第七篇文章《組件》中,詳細分析過 createComponent 方法,當時沒考慮 keepAlive 值爲 true的狀況,在這裏重點介紹。
在首次渲染時 vnode.componentInstance 的值爲空,所以不論 keepAlive 是否爲空,isReactivated 值老是 false。再次渲染時,若 keepAlive 值爲 true 則isReactivated 爲true。
瀏覽器
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false)
}
複製代碼
鉤子函數 init 在 keepAlive 值爲 false 時的功能是調取組件的構造函數生成組件構造實例。
緩存
init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
const mountedNode = vnode
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
/* 省略... */
}
}
複製代碼
當 keepAlive 值爲 true 時,會調用 prepatch 方法,該方法不會再執行組件的 mount 過程,而是直接調用 updateChildComponent 方法更新子組件,這也是被 keepAlive 包裹的組件在有緩存的時候就不會再執行組件的 created、mounted 等鉤子函數的緣由。
app
function prepatch (oldVnode, vnode) {
var options = vnode.componentOptions;
var child = vnode.componentInstance = oldVnode.componentInstance;
updateChildComponent(child,options.propsData,options.listeners,
vnode,options.children);
}
複製代碼
在 createComponent 函數最後,若是組件再次渲染且 keepAlive 爲 true 時,會調用 reactivateComponent 函數,該函數將緩存的DOM元素直接插入到目標位置。
function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
/* 省略對 transition 動畫不觸發的問題的處理*/
insert(parentElm, vnode.elm, refElm);
}
複製代碼
Vue 提供了 transition 的封裝組件,在下列情形中,能夠給任何元素和組件添加進入/離開過渡
一、條件渲染 (使用 v-if)。
二、條件展現 (使用 v-show)。
三、動態組件。
四、組件根節點。
當插入或刪除包含在 transition 組件中的元素時,Vue 將會作如下處理:
一、自動嗅探目標元素是否應用了 CSS 過渡或動畫,若是是,在恰當的時機添加/刪除 CSS 類名。
二、若是過渡組件提供了 JavaScript 鉤子函數,這些鉤子函數將在恰當的時機被調用。
三、若是沒有找到 JavaScript 鉤子而且也沒有檢測到 CSS 過渡/動畫,DOM 操做 (插入/刪除) 在下一幀中當即執行。
Transition 組件的定義在 /src/platforms/web/runtime/components/transition.js 文件中,精簡代碼以下:
export default {
name: 'transition',
props: transitionProps,
abstract: true,
render (h) {
let children = this.$slots.default
if (!children) {return}
children = children.filter(isNotTextNode)
if (!children.length) {return}
/* 省略多個子元素警告 */
const mode = this.mode
/* 省略無效模式警告 */
const rawChild = children[0]
if (hasParentTransition(this.$vnode)) {return rawChild}
const child = getRealChild(rawChild)
if (!child) {return rawChild}
if (this._leaving){return placeholder(h, rawChild)}
/* 省略獲取id與key代碼 */
const data = (child.data || (child.data = {})).transition = extractTransitionData(this)
const oldRawChild = this._vnode
const oldChild = getRealChild(oldRawChild)
if (child.data.directives && child.data.directives.some(isVShowDirective)) {
child.data.show = true
}
/* 省略多元素過渡模式處理代碼 */
return rawChild
}
}
複製代碼
Transition 組件與 KeepAlive 組件同樣是抽象函數,在該組件定義中比較重要的就是 render 函數,其做用就是渲染生成VNode。
在該渲染函數中有三個功能比較重要:
一、將 Transition 組件上的參數賦值到 child.data.transition 上。
二、若是 Transition 組件上使用 v-show 指令,則將 child.data.show 設爲 true。
三、設置多元素過渡的模式。
Vue 提供了兩種過渡模式,默認同時生效。
in-out:新元素先進行過渡,完成以後當前元素過渡離開。
out-in:當前元素先進行過渡,完成以後新元素過渡進入。
在 render 函數中相關代碼以下所示:
const oldData = oldChild.data.transition = extend({}, data)
if (mode === 'out-in') {
this._leaving = true
mergeVNodeHook(oldData, 'afterLeave', () => {
this._leaving = false
this.$forceUpdate()
})
return placeholder(h, rawChild)
} else if (mode === 'in-out') {
if (isAsyncPlaceholder(child)) {
return oldRawChild
}
let delayedLeave
const performLeave = () => { delayedLeave() }
mergeVNodeHook(data, 'afterEnter', performLeave)
mergeVNodeHook(data, 'enterCancelled', performLeave)
mergeVNodeHook(oldData,'delayLeave',leave=>{delayedLeave=leave})
}
複製代碼
從上述代碼可知:當過渡模式爲 out-in,在切換元素時,當前元素徹底 leave 後纔會加載新元素。當過渡模式爲 in-out,當前元素延時到新元素 enter 後再 leave。
過渡相關的邏輯在 /src/platforms/web/runtime/modules/transition.js 文件中實現,Vue會將相關邏輯插入到 patch 的生命週期中去處理。
export default inBrowser ? {
create: _enter,
activate: _enter,
remove (vnode, rm) {
if (vnode.data.show !== true) {
leave(vnode, rm)
} else {
rm()
}
}
} : {}
function _enter (_,vnode) {
if (vnode.data.show !== true) {
enter(vnode)
}
}
複製代碼
能夠看出過渡的邏輯本質上就是在元素插入時調用 enter 函數,在元素移除時調用 leave 函數。
由於在使用 v-show 指令時元素始終會被渲染並保留在 DOM 中,只是簡單地切換元素的 CSS 屬性 display。因此會對使用 v-show 指令的狀況進行特殊處理,在下一小結闡述具體處理過程。
整體來看 enter 函數與 leave 函數幾乎是一個鏡像過程,下面僅分析 enter 函數。
function enter (vnode, toggleDisplay) {
const el = vnode.elm
/* 省略... */
const expectsCSS = css !== false && !isIE9
const userWantsControl = getHookArgumentsLength(enterHook)
/* 省略 cb 函數實現 */
/* 合併 insert 鉤子函數 */
if (!vnode.data.show) {
mergeVNodeHook(vnode, 'insert', () => {
const parent = el.parentNode
const pendingNode = parent && parent._pending && parent._pending[vnode.key]
if (pendingNode &&
pendingNode.tag === vnode.tag &&
pendingNode.elm._leaveCb
) {
pendingNode.elm._leaveCb()
}
enterHook && enterHook(el, cb)
})
}
/* 開始執行過渡動畫 */
beforeEnterHook && beforeEnterHook(el)
if (expectsCSS) {
addTransitionClass(el, startClass)
addTransitionClass(el, activeClass)
nextFrame(/* 省略... */)
}
/* 省略使用 v-show 指令的狀況 */
if (!expectsCSS && !userWantsControl) {
cb()
}
}
複製代碼
enter 函數看上去很複雜,其核心代碼是開始執行過渡動畫的部分。首先執行 beforeEnterHook 鉤子函數,若使用 css 過渡類,則接着執行:
addTransitionClass(el, startClass)
addTransitionClass(el, activeClass)
複製代碼
addTransitionClass 函數做用就是給元素添加樣式,而後執行 nextFrame 函數。
function nextFrame (fn: Function) {
raf(() => {
raf(fn)
})
}
const raf = inBrowser
? window.requestAnimationFrame
? window.requestAnimationFrame.bind(window)
: setTimeout
複製代碼
nextFrame 函數在支持 requestAnimationFrame 方法的瀏覽器中使用該方法,參數 fn 會在下一幀執行。若是不支持則使用 setTimeout 代替。
nextFrame(() => {
removeTransitionClass(el, startClass)
if (!cb.cancelled) {
addTransitionClass(el, toClass)
if (!userWantsControl) {
if (isValidDuration(explicitEnterDuration)) {
setTimeout(cb, explicitEnterDuration)
} else {
whenTransitionEnds(el, type, cb)
}
}
}
})
複製代碼
在下一幀時,首先移除 startClass 樣式,而後判斷過渡是否被取消。若是沒有取消,添加 toClass 樣式,而後根據是否經過 enterHook 鉤子函數控制動畫來決定 cb 函數的執行時機。
const cb = el._enterCb = once(() => {
if (expectsCSS) {
removeTransitionClass(el, toClass)
removeTransitionClass(el, activeClass)
}
if (cb.cancelled) {
if (expectsCSS) {
removeTransitionClass(el, startClass)
}
enterCancelledHook && enterCancelledHook(el)
} else {
afterEnterHook && afterEnterHook(el)
}
el._enterCb = null
})
複製代碼
cb 函數首先移除 toClass 與 activeClass 樣式,若是過渡被取消則先移除 startClass 樣式,再執行 enterCancelledHook 鉤子函數。若是過渡沒有被取消,則調用 afterEnterHook 鉤子函數。
對於在 Transition 組件上使用 v-show 指令的狀況,在 v-show 指令的實現中有特殊處理。相關代碼在 /src/platforms/web/runtime/directives/show.js 文件中。
export default {
bind (el, { value }, vnode) {
/* ... */
const transition = vnode.data && vnode.data.transition
const originalDisplay = el.__vOriginalDisplay =
el.style.display === 'none' ? '' : el.style.display
if (value && transition) {
vnode.data.show = true
enter(vnode, () => {
el.style.display = originalDisplay
})
}
/* ... */
},
update (el, { value, oldValue }, vnode) {
/* ... */
const transition = vnode.data && vnode.data.transition
if (transition) {
vnode.data.show = true
if (value) {
enter(vnode, () => {
el.style.display = el.__vOriginalDisplay
})
} else {
leave(vnode, () => {
el.style.display = 'none'
})
}
}
},
/* 省略... */
複製代碼
能夠看到在 v-show 指令的實現中,若在 Transition 組件上使用則調用 enter 與 leave 函數,與 patch 生命週期調用這兩個函數不一樣的會額外的傳入第二個參數。
在 enter 與 leave 函數也有對應的處理,以保證在DOM元素沒有新增和移除的狀況下實現過渡效果。
if (vnode.data.show) {
toggleDisplay && toggleDisplay()
enterHook && enterHook(el, cb)
}
複製代碼
Vue 使用 <transition-group> 組件完成列表過渡效果,該組件有如下幾個特色:
一、該組件不是抽象組件,會以一個真實元素呈現,默認是 <span>,能夠經過 tag參數 指定。
二、過渡模式不可用。
三、內部元素老是須要提供惟一的 key 屬性值。
四、CSS 過渡的類將會應用在內部的元素中,而不是這個組/容器自己。
TransitionGroup 組件定義在 /src/platforms/web/runtime/components/transition-group.js 文件中,精簡代碼以下:
const props = extend({
tag,
moveClass
}, transitionProps)
delete props.mode
export default {
props,
beforeMount () { /* 省略具體實現 */ },
render (h) { /* 省略具體實現 */ },
updated () { /* 省略具體實現 */ },
methods: {
hasMove (el, moveClass){ /* 省略具體實現 */ }
}
}
複製代碼
TransitionGroup 組件有兩種過渡效果:基本過渡效果、平滑過渡效果,後者經過 v-move 特性來實現。
在源碼實現中,基本過渡效果由組件的 render 函數完成,當數據發生變化時的平滑過渡效果由 updated 生命週期鉤子函數完成。
TransitionGroup 組件 render 方法的完整代碼以下所示:
render (h) {
const tag = this.tag || this.$vnode.data.tag || 'span'
const map = Object.create(null)
const prevChildren = this.prevChildren = this.children
const rawChildren = this.$slots.default || []
const children = this.children = []
const transitionData = extractTransitionData(this)
for (let i = 0; i < rawChildren.length; i++) {
const c = rawChildren[i]
if (c.tag) {
if (c.key != null && String(c.key).indexOf('__vlist') !== 0) {
children.push(c)
map[c.key] = c
;(c.data || (c.data = {})).transition = transitionData
} else if (process.env.NODE_ENV !== 'production') {
const opts = c.componentOptions
const name = opts ? (opts.Ctor.options.name || opts.tag || '') : c.tag
warn(`<transition-group> children must be keyed: <${name}>`)
}
}
}
if (prevChildren) {
const kept = []
const removed = []
for (let i = 0; i < prevChildren.length; i++) {
const c = prevChildren[i]
c.data.transition = transitionData
c.data.pos = c.elm.getBoundingClientRect()
if (map[c.key]) {
kept.push(c)
} else {
removed.push(c)
}
}
this.kept = h(tag, null, kept)
this.removed = removed
}
return h(tag, null, children)
}
複製代碼
render 函數的本質功能就是生成VNode,其中該函數的參數 h 爲用來生成VNode的 createElement 函數。
函數中首先聲明的幾個變量具體含義以下所示:
一、tag:TransitionGroup 組件最終渲染的元素類型,默認是 span。
二、map:存儲原始子節點 key 與值的對象。
三、prevChildren:存儲上一次的子節點數組。
四、rawChildren:原始子節點數組。
五、children:當前子節點數組。
五、transitionData:TransitionGroup 組件上提取的過渡參數。
緊接着的 for 循環是處理原始子節點的,由於 TransitionGroup 組件要求全部子節點都顯式提供 key 值,若是沒有提供 key 值在開發環境下會報錯。
if (c.key != null && String(c.key).indexOf('__vlist') !== 0)
複製代碼
判斷是否顯式提供 key 值的條件語句之因此這樣寫,是由於在 for 循環的渲染過程當中,在沒有提供 key 值的狀況下,會自動加上 __vlist 爲開頭的字符串做爲 key 值。
這個 for 循環還有一個重要的功能是將組件過渡參數賦值給子組件的 data.transition 屬性,在上一節講述 Transition 組件時有講過,在元素進入和移除時會根據這個屬性來顯示相應的過渡效果。
最後處理改變前的子節點,調用了原生 DOM 的 getBoundingClientRect 方法獲取到原生 DOM 的位置信息,記錄到 vnode.data.pos 中。而後將存在的節點放入 kept 中,將刪除的節點放入 removed 中。最後返回由 createElement 函數生成的VNode。
TransitionGroup 組件的 render 方法因爲將過渡信息下沉到子節點上,是能夠實現基本的子節點添加刪除的過渡效果的。因爲插入和刪除操做與須要移動的元素沒有過渡效果控制的關聯,因此並無平滑過渡的效果。
當數據改變時會調用 updated 生命週期鉤子,TransitionGroup 組件當子節點添加與刪除的平滑過渡效果在該鉤子函數中實現。
updated () {
const children = this.prevChildren
const moveClass = this.moveClass || ((this.name || 'v') + '-move')
if (!children.length || !this.hasMove(children[0].elm, moveClass)) {
return
}
children.forEach(callPendingCbs)
children.forEach(recordPosition)
children.forEach(applyTranslation)
this._reflow = document.body.offsetHeight
children.forEach((c) => {
if (c.data.moved) {
const el = c.elm
const s = el.style
addTransitionClass(el, moveClass)
s.transform = s.WebkitTransform = s.transitionDuration = ''
el.addEventListener(transitionEndEvent, el._moveCb = function cb (e) {
if (e && e.target !== el) {
return
}
if (!e || /transform$/.test(e.propertyName)) {
el.removeEventListener(transitionEndEvent, cb)
el._moveCb = null
removeTransitionClass(el, moveClass)
}
})
}
})
}
複製代碼
updated 函數首先使用 hasMove 方法判斷子節點是否認義了 move 相關的動畫樣式,接着對子節點進行預處理:
一、callPendingCbs:在前一個過渡動畫沒執行完又再次執行到該方法的時候,會提早執行 _moveCb 和 _enterCb。
二、recordPosition:記錄節點的新位置,賦值給 data.newPos 屬性。
三、applyTranslation:先計算節點新位置和舊位置的差值,把須要移動的節點的位置又偏移到以前的舊位置,目的是爲了作 move 緩動作準備。
接着經過讀取 document.body.offsetHeight 強制觸發瀏覽器重繪。
而後遍歷子節點,先給子節點添加 moveClass,接着把子節點的 style.transform 設置爲空,因爲以前使用 applyTranslation 方法將子節點偏移到舊位置,此時會按照設置的過渡效果偏移到當前位置,進而實現平滑過渡的效果。
最後監聽 transitionEndEvent 過渡結束的事件,作一些清理的操做。
Vue 中虛擬 DOM 的子元素更新算法是不穩定的,它不能保證被移除元素的相對位置。Vue 在 beforeMount 生命週期鉤子函數中對這種狀況進行了處理。
beforeMount () {
const update = this._update
this._update = (vnode, hydrating) => {
const restoreActiveInstance = setActiveInstance(this)
this.__patch__(this._vnode, this.kept, false, true)
this._vnode = this.kept
restoreActiveInstance()
update.call(this, vnode, hydrating)
}
}
複製代碼
在 beforeMount 函數中,首先重寫了 _update 方法,_update 方法自己的做用是根據VNode生成真實DOM的。重寫後的 _update 方法主要有兩步:首先移除須要移除的 vnode,同時觸發它們的 leaving 過渡;而後須要把插入和移動的節點達到它們的最終態,同時還要保證移除的節點保留在應該的位置。
Vue 經過這兩步處理來解決子元素更新算法是不穩定的問題,做者在 TransitionGroup 組件實現的文件中也有詳細的註釋說明。
KeepAlive 組件不渲染真實DOM節點,會將緩存的子組件放入 cache 數組中,並將被緩存子組件的 data.keepAlive 屬性置爲 true。若是須要再次渲染被緩存的子組件,則直接返回該子組件的VNode,而組件的 created、mounted 等鉤子函數不會再執行。
Transition 組件的 render 函數會將組件上的參數賦值到 child.data.transition 上,而後在 patch 的過程當中會調用 enter 與 leave 函數完成相關過渡效果。在使用 v-show 指令時,DOM元素並無新增和刪除,Vue 對這種狀況進行了特別處理,保證在DOM元素沒有新增和移除的狀況下實現過渡效果。
TransitionGroup 組件的基本過渡效果跟 Transition 組件實現效果同樣。修改列表數據的時候,若是是添加或者刪除數據,則會觸發相應元素自己的過渡動畫。平滑過渡效果本質上就是先將元素移動到舊位置,而後再根據定義的過渡效果將其移動到新位置。
歡迎關注公衆號:前端桃花源,互相交流學習!