内置组件
定义在src/core/components/keep-alive.js中
export default { // 一个对象name: 'keep-alive',abstract: true, // 抽象组件props: {// 字符串或者表达式include: patternTypes, // 只有匹配的组件会被缓存exclude: patternTypes, // 任何匹配的组件都不会被缓存max: [String, Number] // 缓存的大小// 缓存的是vnode对象,也会缓存DOM,当缓存很多时会比较占用内存,所以max允许配置指定缓存大小},methods: {cacheVNode() {const { cache, keys, vnodeToCache, keyToCache } = thisif (vnodeToCache) {const { tag, componentInstance, componentOptions } = vnodeToCachecache[keyToCache] = {name: getComponentName(componentOptions),tag,componentInstance,}keys.push(keyToCache)// prune oldest entryif (this.max && keys.length > parseInt(this.max)) {pruneCacheEntry(cache, keys[0], keys, this._vnode)}this.vnodeToCache = null}}},created () {// 缓存已经创建过的vnodethis.cache = Object.create(null)this.keys = []},destroyed () {for (const key in this.cache) {pruneCacheEntry(this.cache, key, this.keys)}},mounted () {this.cacheVNode()this.$watch('include', val => {pruneCache(this, name => matches(val, name))})this.$watch('exclude', val => {pruneCache(this, name => !matches(val, name))})},updated () {this.cacheVNode()},// 实现了render函数来执行组件渲染render () {// 获取第一个元素的vnode// 在<keep-alive>内部写DOM,可以先获取到它的默认插槽,然后在获取到它的第一个子节点// <keep-alive>只处理第一个子元素,所以一般和它搭配使用的有component动态组件或者是router-viewconst slot = this.$slots.defaultconst vnode: VNode = getFirstComponentChild(slot)const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptionsif (componentOptions) {// check pattern// 判断当前组件的名称和include、exclude的关系const name: ?string = getComponentName(componentOptions)const { include, exclude } = this// 传的include和exclude可以是数组、字符串、正则表达式三种中任意一种// 如果组件名满足了配置include且不匹配或者是配置了exclude且匹配就直接返回这个组件的vnode,否则走下一步缓存if (// not included(include && (!name || !matches(include, name))) ||// excluded(exclude && name && matches(exclude, name))) {return vnode}const { cache, keys } = thisconst 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// 命中缓存-直接从缓存中拿vnode的组件实例,并且重新调整key的顺序放在最后一个if (cache[key]) {vnode.componentInstance = cache[key].componentInstance// make current key freshestremove(keys, key)keys.push(key)} else {// delay setting the cache until update 将缓存设置延迟到更新this.vnodeToCache = vnodethis.keyToCache = key}vnode.data.keepAlive = true}return vnode || (slot && slot[0])}}// 做匹配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}// 缓存vnodecacheVNode() {const { cache, keys, vnodeToCache, keyToCache } = thisif (vnodeToCache) {const { tag, componentInstance, componentOptions } = vnodeToCachecache[keyToCache] = {name: getComponentName(componentOptions),tag,componentInstance,}keys.push(keyToCache)// prune oldest entry// 配置了max,并且缓存的长度超过了this.max,还要从缓存中删除第一个if (this.max && keys.length > parseInt(this.max)) {pruneCacheEntry(cache, keys[0], keys, this._vnode)}this.vnodeToCache = null}}// 删除缓存入口function pruneCacheEntry (cache: CacheEntryMap,key: string,keys: Array<string>,current?: VNode) {const entry: ?CacheEntry = cache[key]// 如果要删除的缓存的组件tag不是当前渲染组件tagif (entry && (!current || entry.tag !== current.tag)) {// 执行删除缓存的组件实例的$destroy方法entry.componentInstance.$destroy()}cache[key] = nullremove(keys, key)}
// locate first non-abstract parentvar parent = options.parent;if (parent && !options.abstract) {while (parent.$options.abstract && parent.$parent) {parent = parent.$parent;}parent.$children.push(vm);}vm.$parent = parent;
注意,
// 观测变化执行pruneCache函数this.$watch('include', val => {pruneCache(this, name => matches(val, name))})this.$watch('exclude', val => {pruneCache(this, name => !matches(val, name))})function pruneCache (keepAliveInstance: any, filter: Function) {const { cache, keys, _vnode } = keepAliveInstance// 对cache做遍历for (const key in cache) {const entry: ?CacheEntry = cache[key]// 发现缓存的节点名称和新的规则没有匹配上时if (entry) {const name: ?string = entry.nameif (name && !filter(name)) {// 把这个缓存节点从缓存中摘除pruneCacheEntry(cache, key, keys, _vnode)}}}}
组件渲染
了解了
例子
let A = {template: '<div class="a">' +'<p>A Comp</p>' +'</div>',name: 'A'}let B = {template: '<div class="b">' +'<p>B Comp</p>' +'</div>',name: 'B'}let vm = new Vue({el: '#app',template: '<div>' +'<keep-alive>' +'<component :is="currentComp">' +'</component>' +'</keep-alive>' +'<button @click="change">switch</button>' +'</div>',data: {currentComp: 'A'},methods: {change() {this.currentComp = this.currentComp === 'A' ? 'B' : 'A'}},components: {A,B}})
首次渲染
Vue 的渲染最后都会到 patch 过程,而组件的 patch 过程会执行 createComponent 方法,它的定义在 src/core/vdom/patch.js 中
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {let i = vnode.dataif (isDef(i)) {// 第一次渲染时vnode.componentInstance为undefined和vnode.data.keepAlive为true// 因为它的父组件 <keep-alive> 的 render 函数会先执行,那么该 vnode 缓存到内存中,并且设置 vnode.data.keepAlive 为 trueconst isReactivated = isDef(vnode.componentInstance) && i.keepAlive // false// 走正常的init钩子函数执行组件的mountif (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)) {// 当vnode已经执行完patch后执行initComponent函数initComponent(vnode, insertedVnodeQueue)insert(parentElm, vnode.elm, refElm)if (isTrue(isReactivated)) {reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)}return true}}}
initComponent
function initComponent (vnode, insertedVnodeQueue) {if (isDef(vnode.data.pendingInsert)) {insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)vnode.data.pendingInsert = null}// 缓存了vnode创建生成的DOM节点vnode.elm = vnode.componentInstance.$elif (isPatchable(vnode)) {invokeCreateHooks(vnode, insertedVnodeQueue)setScope(vnode)} else {// empty component root.// skip all element-related modules except for ref (#3455)registerRef(vnode)// make sure to invoke the insert hookinsertedVnodeQueue.push(vnode)}}
对于首次渲染而言,除了在
对于例子,初始化渲染 A 组件以及第一次点击 switch 渲染 B 组件,都是首次渲染
缓存渲染
当从 B 组件再次点击 switch 切换到 A 组件,就会命中缓存渲染
当数据发送变化,在 patch 的过程中会执行 patchVnode 的逻辑,它会对比新旧 vnode 节点,甚至对比它们的子节点去做更新逻辑,但是对于组件 vnode 而言,是没有 children 的,那么对于
原来 patchVnode 在做各种 diff 之前,会先执行 prepatch 的钩子函数,
定义在 src/core/vdom/create-component 中
const componentVNodeHooks = {// ...prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {const options = vnode.componentOptionsconst child = vnode.componentInstance = oldVnode.componentInstanceupdateChildComponent(child,options.propsData, // updated propsoptions.listeners, // updated listenersvnode, // new parent vnodeoptions.children // new children)},// ...}
updateChildComponent定义在src/core/instance/lifecycle.js中
// 更新组件实例的一些属性export function updateChildComponent (vm: Component,propsData: ?Object,listeners: ?Object,parentVnode: MountedComponentVNode,renderChildren: ?Array<VNode>) {// ...// Any static slot children from the parent may have changed during parent's// update. Dynamic scoped slots may also have changed. In such cases, a forced// update is necessary to ensure correctness.const needsForceUpdate = !!(renderChildren || // has new static slotsvm.$options._renderChildren || // has old static slotshasDynamicScopedSlot)// ...// resolve slots + force update if has childrenif (needsForceUpdate) {vm.$slots = resolveSlots(renderChildren, parentVnode.context)vm.$forceUpdate()}// ...}
由于
在例子中就是缓存的 A 组件,接着又会执行 patch 过程,再次执行到 createComponent 方法
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {let i = vnode.dataif (isDef(i)) {const isReactivated = isDef(vnode.componentInstance) && i.keepAlive // trueif (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}}}
在执行 init 钩子函数的时候不会再执行组件的mount过程了,相关逻辑在src/core/vdom/create-component.js中
const componentVNodeHooks = {init (vnode: VNodeWithData, hydrating: boolean): ?boolean {if (vnode.componentInstance &&!vnode.componentInstance._isDestroyed &&vnode.data.keepAlive) {// kept-alive components, treat as a patchconst mountedNode: any = vnode // work around flowcomponentVNodeHooks.prepatch(mountedNode, mountedNode)} else {const child = vnode.componentInstance = createComponentInstanceForVnode(vnode,activeInstance)child.$mount(hydrating ? vnode.elm : undefined, hydrating)}},}
这也就是被
reactivateComponent
function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {let i// hack for #4339: a reactivated component with inner transition// does not trigger because the inner node's created hooks are not called// again. It's not ideal to involve module-specific logic in here but// there doesn't seem to be a better way to do it.// 解决对 reactived 组件 transition 动画不触发的问题let innerNode = vnodewhile (innerNode.componentInstance) {innerNode = innerNode.componentInstance._vnodeif (isDef(i = innerNode.data) && isDef(i = i.transition)) {for (i = 0; i < cbs.activate.length; ++i) {cbs.activate[i](emptyNode, innerNode)}insertedVnodeQueue.push(innerNode)break}}// unlike a newly created component,// a reactivated keep-alive component doesn't insert itself// 把缓存的DOM对象直接插入到目标元素中,这样就完成了数据更新的情况下渲染的过程insert(parentElm, vnode.elm, refElm)}
生命周期
组件一旦被
在渲染的最后一步,会执行 invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) 函数执行 vnode 的 insert 钩子函数,它的定义在 src/core/vdom/create-component.js 中
const componentVNodeHooks = {// ...insert (vnode: MountedComponentVNode) {const { context, componentInstance } = vnodeif (!componentInstance._isMounted) {componentInstance._isMounted = truecallHook(componentInstance, 'mounted')}if (vnode.data.keepAlive) {if (context._isMounted) { // 判断被<keep-alive>包裹的组件已经mounted// 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 */)}}},// ...}
activateChildComponent
export function activateChildComponent (vm: Component, direct?: boolean) {if (direct) {vm._directInactive = falseif (isInInactiveTree(vm)) {return}} else if (vm._directInactive) {return}if (vm._inactive || vm._inactive === null) {vm._inactive = false// 递归执行它的所有子组件的activated钩子函数for (let i = 0; i < vm.$children.length; i++) {activateChildComponent(vm.$children[i])}// 执行组件的activated钩子函数callHook(vm, 'activated')}}
queueActivatedComponent定义在 src/core/observer/scheduler.js 中
/*** Queue a kept-alive component that was activated during patch.* The queue will be processed after the entire tree has been patched.*/export function queueActivatedComponent (vm: Component) {// setting _inactive to false here so that a render function can// rely on checking whether it's in an inactive tree (e.g. router-view)vm._inactive = false// 把当前vm实例添加到activatedChildren数组中,等所有的渲染完毕后在nextTick后会执行flushSchedulerQueueactivatedChildren.push(vm)}
flushSchedulerQueue
*** Flush both queues and run the watchers.*/function flushSchedulerQueue () {// ...// keep copies of post queues before resetting stateconst activatedQueue = activatedChildren.slice()const updatedQueue = queue.slice()resetSchedulerState()// call component updated and activated hooks// 遍历所有的activatedChildrencallActivatedHooks(activatedQueue)// ...}function callActivatedHooks (queue) {// 执行activateChildComponent方法,通过队列的方式就是把整个activated时机延后了for (let i = 0; i < queue.length; i++) {queue[i]._inactive = trueactivateChildComponent(queue[i], true /* true */)}}
有 activated 钩子函数,也就有对应的 deactivated 钩子函数,它是发生在 vnode 的 destory 钩子函数,定义在 src/core/vdom/create-component.js 中
const componentVNodeHooks = {// ...destroy (vnode: MountedComponentVNode) {const { componentInstance } = vnodeif (!componentInstance._isDestroyed) {if (!vnode.data.keepAlive) {componentInstance.$destroy()} else {deactivateChildComponent(componentInstance, true /* direct */)}}}}
对于
export function deactivateChildComponent (vm: Component, direct?: boolean) {if (direct) {vm._directInactive = trueif (isInInactiveTree(vm)) {return}}if (!vm._inactive) {vm._inactive = true// 递归执行它的所有子组件的deactivated钩子函数for (let i = 0; i < vm.$children.length; i++) {deactivateChildComponent(vm.$children[i])}// 执行组件的deactivated钩子函数callHook(vm, 'deactivated')}}
另外还知道了
