本文目錄:
1. event
2. v-model
3. slot
4. keep-alive
5. transition
6. transition-group
[event]
export function eventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
Vue.prototype.$once = function (event: string, fn: Function): Component {
const vm: Component = this
function on () {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// all
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// specific event
const cbs = vm._events[event]
if (!cbs) {
return vm
}
if (!fn) {
vm._events[event] = null
return vm
}
// specific handler
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
return vm
}
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (process.env.NODE_ENV !== 'production') {
const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
tip(
`Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName(vm)} but the handler is registered for "${event}". ` +
`Note that HTML attributes are case-insensitive and you cannot use ` +
`v-on to listen to camelCase events when using in-DOM templates. ` +
`You should probably use "${hyphenate(event)}" instead of "${event}".`
)
}
}
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
}
先通過正則把節(jié)點上的元素都解析出來脓钾,并對事件是原生事件還是自定義事件加以區(qū)分。然后把所有的事件用 vm._events 存儲起來. on 就是往_events 里push腿短,off就是對_events的元素進行刪除仅淑,once就是兩者結(jié)合一下称勋。
[v-model]
v-model其實是一種語法糖,其本質(zhì)利用了父子組件的通信完成的漓糙。
[slot]
普通插槽是在父組件編譯和渲染階段生成 vnodes铣缠,所以數(shù)據(jù)的作用域是父組件實例,子組件渲染的時候直接拿到這些渲染好的 vnodes昆禽。而對于作用域插槽蝗蛙,父組件在編譯和渲染階段并不會直接生成 vnodes,而是在父節(jié)點 vnode 的 data 中保留一個 scopedSlots 對象醉鳖,存儲著不同名稱的插槽以及它們對應的渲染函數(shù)捡硅,只有在編譯和渲染子組件階段才會執(zhí)行這個渲染函數(shù)生成 vnodes,由于是在子組件環(huán)境執(zhí)行的盗棵,所以對應的數(shù)據(jù)作用域是子組件實例壮韭。
[keep-alive]
/* @flow */
import { isRegExp, remove } from 'shared/util'
import { getFirstComponentChild } from 'core/vdom/helpers/index'
type VNodeCache = { [key: string]: ?VNode };
function getComponentName (opts: ?VNodeComponentOptions): ?string {
return opts && (opts.Ctor.options.name || opts.tag)
}
function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
return pattern.test(name)
}
/* istanbul ignore next */
return false
}
function pruneCache (keepAliveInstance: any, filter: Function) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const cachedNode: ?VNode = cache[key]
if (cachedNode) {
const name: ?string = getComponentName(cachedNode.componentOptions)
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key]
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
const patternTypes: Array<Function> = [String, RegExp, Array]
export default {
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) {
// check pattern
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
cache[key] = vnode
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
可以看到這個組件設(shè)置了一個內(nèi)置屬性abstract。
雖然文檔上沒有提及纹因,但是可以在src\core\instance\lifecycle.js看到
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
也就是說如果這個組件是抽象組件喷屋,那么這個組件不會建立子組件。
<keep-alive> 直接實現(xiàn)了 render 函數(shù)瞭恰,而不是我們常規(guī)模板的方式屯曹,執(zhí)行 <keep-alive> 組件渲染的時候,就會執(zhí)行到這個 render 函數(shù)惊畏,接下來我們分析一下它的實現(xiàn):
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
cache[key] = vnode
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
export function getFirstComponentChild (children: ?Array<VNode>): ?VNode {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
const c = children[i]
if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
return c
}
}
}
}
可以看到keep-alive組件值對獲取到第一個子組件生效恶耽,因此它的使用場景一般是路由和動態(tài)組件。并且緩存的組件也接受include和exclude的限制颜启。
另外可以看到這里之前緩存過的話就用之前緩存過的實例偷俭,否則僅僅就是做了緩存而已。
另外缰盏,之前提到了涌萤,keep-alive是沒有確立父子關(guān)系的,那么能更新的原因是因為在diff之前執(zhí)行了prepatch函數(shù)口猜,由于keep-alive本身是支持插槽的形葬,那么它就會執(zhí)行forceupdate方法,這就是他能渲染包裹的組件的原因暮的。
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
},
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
},
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
}
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 /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
從上面的代碼可以看到如果是keep-alive組件,那么不會執(zhí)行mount方法了淌实,直接通過createComponent 去渲染冻辩,同時通過reactivateComponent執(zhí)行keep-alive的生命周期回調(diào)函數(shù)actived和deactived.
transition && transition-group
自動嗅探目標元素是否應用了 CSS 過渡或動畫猖腕,如果是,在恰當?shù)臅r機添加/刪除 CSS 類名恨闪。
如果過渡組件提供了 JavaScript 鉤子函數(shù)倘感,這些鉤子函數(shù)將在恰當?shù)臅r機被調(diào)用。
如果沒有找到 JavaScript 鉤子并且也沒有檢測到 CSS 過渡/動畫咙咽,DOM 操作 (插入/刪除) 在下一幀中立即執(zhí)行老玛。
所以真正執(zhí)行動畫的是我們寫的 CSS 或者是 JavaScript 鉤子函數(shù),而 Vue 的 <transition> 只是幫我們很好地管理了這些 CSS 的添加/刪除钧敞,以及鉤子函數(shù)的執(zhí)行時機蜡豹。
總結(jié)
1. event
先通過正則把節(jié)點上的元素都解析出來,并對事件是原生事件還是自定義事件加以區(qū)分溉苛。然后把所有的事件用 vm._events 存儲起來. on 就是往_events 里push镜廉,off就是對_events的元素進行刪除,once就是兩者結(jié)合一下愚战。
2. v-model
v-model其實是一種語法糖娇唯,其本質(zhì)利用了父子組件的通信完成的。
3. slot
普通插槽是在父組件編譯和渲染階段生成 vnodes寂玲,所以數(shù)據(jù)的作用域是父組件實例塔插,子組件渲染的時候直接拿到這些渲染好的 vnodes。而對于作用域插槽拓哟,父組件在編譯和渲染階段并不會直接生成 vnodes想许,而是在父節(jié)點 vnode 的 data 中保留一個 scopedSlots 對象,存儲著不同名稱的插槽以及它們對應的渲染函數(shù)彰檬,只有在編譯和渲染子組件階段才會執(zhí)行這個渲染函數(shù)生成 vnodes伸刃,由于是在子組件環(huán)境執(zhí)行的,所以對應的數(shù)據(jù)作用域是子組件實例逢倍。
4. keep-alive
vue內(nèi)部對keep-alive做了特殊處理捧颅,在執(zhí)行prepatch階段會重新渲染緩存的組件,沒有重新生成一個vue實例较雕,因此也沒有標準組件的生命周期碉哑。
transition && transition-group
自動嗅探目標元素是否應用了 CSS 過渡或動畫,如果是亮蒋,在恰當?shù)臅r機添加/刪除 CSS 類名扣典。
如果過渡組件提供了 JavaScript 鉤子函數(shù),這些鉤子函數(shù)將在恰當?shù)臅r機被調(diào)用慎玖。
如果沒有找到 JavaScript 鉤子并且也沒有檢測到 CSS 過渡/動畫贮尖,DOM 操作 (插入/刪除) 在下一幀中立即執(zhí)行。
所以真正執(zhí)行動畫的是我們寫的 CSS 或者是 JavaScript 鉤子函數(shù)趁怔,而 Vue 的 <transition> 只是幫我們很好地管理了這些 CSS 的添加/刪除湿硝,以及鉤子函數(shù)的執(zhí)行時機薪前。
transition && transition-group