keep-alive

  • 主要用于保留组件状态或避免重新渲染
  • <keep-alive>包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
  • <transition>相似,<keep-alive>是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。
  • 当组件在<keep-alive>内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。
  • 在 2.2.0 及其更高版本中,activated 和 deactivated 将会作用在<keep-alive>的直接子节点及其所有子孙节点
  • <keep-alive>只用在其一个直属的子组件被切换的情形,如果有多个子元素同时存在,则不会工作

include 和 exclude

  • 允许组件有条件地缓存,二者都可以用逗号分隔字符串、正则表达式、一个数组来表示
  • 首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称(父组件 components 选项的键值)
  • 匿名组件不能被匹配
  • 不会再函数式组件中工作,因为它们没有缓存实例

max

最多可以缓存多少组件实例,一旦达到这个数字,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁

源码剖析

// src/core/components/keep-alive.js
export default {
    name: 'keep-alive',
    abstract: true, // 判断当前组件虚拟 dom 是否渲染成真实 dom 的关键
    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.cache 对象数据
        this.$watch('include', valu => {
            pruneCache(this, name => matched(val, name));
        });
        this.$watch('exclude', valu => {
            pruneCache(this, name => !matched(val, name));
        });
    },
    render() {
        const slot = this.$slots.default;
        const vnode = getFirstComponentChild(slot); // 找到第一个子组件对象
        const componentOptions = vnode && vnode.componentOptions;
        if (componentOptions) {
            // 存在组件参数
            const name = getComponentName(componentOptions);
            const { include, exclude } = this;
            if (
                // 条件匹配
                (include && (!name || !matches(include, name))) ||
                (exclude && name && matches(exclude, name))
            ) {
                return vnode;
            }

            const { cache, keys } = this;
            // 定义组件的缓存 key
            const key =
                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); // 调整 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.keyAlive = true; // 渲染和执行被包裹组件的钩子函数需要用到
        }
        return vnode || (slot && slot[0]);
    },
};

function pruneCacheEntry(
    cache: VNodeCache,
    key: string,
    keys: Array<string>,
    current?: VNode
) {
    const cache = cache[key];
    if (cached && (!current || cached.tag !== current.tag)) {
        cached.componentInstance.$destroyed(); // 执行组件的 destroy 钩子函数
    }
    cache[key] = null;
    remove(keys, key);
}

function pruneCache(keepAliveInstance: any, filter: Function) {
    const { cache, keys, _vnode } = keepAliveInstance;
    for (const key in cache) {
        const cacheNode = cache[key];
        if (cachedNode) {
            const name = getComponentName(cacheNode.componnentOptions);
            if (name && !filter(name)) {
                pruneCacheEntry(cache, key, keys, _vnode);
            }
        }
    }
}

流程解析

  • 获取<keep-alive>包裹着的第一个子组件对象及其组件名
  • 根据设定的黑白名单进行条件匹配,决定是否缓存,不匹配则直接返回组件实例,否则执行下一步
  • 根据组件 id 和 tag 生成缓存 key,并在缓存对象中查找是否已缓存过该组件实例,如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键),否则执行下一步(LRU:页面置换算法,将最近最久未使用的页面予以淘汰)
  • 在 this.cache 对象中存储该组件实例并保存 key 值,之后检查缓存的实例数量是否超过 max 设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(即下标为 0 的那个 key)
  • 最后,将该组件实例的 keepAlive 属性值设置为 true

渲染

vue 渲染的整个过程

vue 渲染的整个过程

vue 的渲染是从图中的 render 阶段开始,但 keep-alive 的渲染是在 patch 阶段,这是构建组件树(虚拟 DOM 树),并将 VNode 转换成真正 DOM 节点的过程

keep-alive 组件的渲染

// src/core/instance/lifecycle.js
export function initLifecycle(vm) {
    const options = vm.$options;
    // 找到第一个非 abstract 父组件实例
    let parent = options.parent;
    if (parent && !options.abstract) {
        while (parent.$options.abstract && parent.$parent) {
            parent = parent.$parent;
        }
        parent.$children.push(vm);
    }
    vm.$parent = parent;
}

vue 在初始化生命周期的时候,为组件实例建立父子关系时,会根据 abstract 属性决定是否忽略某个组件。在<keep-alive>中,设置了abstract: true,那么 vue 就会跳过该组件实例,因此最后构建的组件树中就不包含<keep-alive>组件及相关的节点

在 patch 阶段,会执行 createComponent 函数,将被<keep-alive>包裹的组件插入父元素中

  • 首次加载被包裹组件时,由 keey-alive.js 中的 render 函数可知,vnode.componentInstance 的值为 undefined,keepAlive 的值为 true,因为<keep-alive>组件作为父组件,它的 render 函数会先于被包裹组件执行,因此只执行到i(vnode, false),后面的逻辑不执行
  • 再次访问被包裹组件时,vnode.componentInstance 为已经缓存的组件实例,那么会执行insert(parentElem, vnode.elem, refElem)逻辑,这样就直接把上一次的 DOM 插入到父元素中
// src/core/vdom/patch.js
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(parentElem, vnode.elem, refElem); // 将缓存的 DOM(vnode.elm) 插入父元素中
            if (isTrue(isReactivated)) {
                reactivateComponent(
                    vnode,
                    insertedVnodeQueue,
                    parentEle,
                    refElm
                );
            }
            return true;
        }
    }
}

钩子函数

一般的组件每次加载都会有完整的生命周期,因为被缓存的组件实例会被设置 keepAlive=true,同时在初始化组件钩子函数中有如下逻辑判断,因此,当 vnode.componentInstance 和 keepAlive 同时为 true 时,不再进入$mounte 过程,mounted 之前的所有钩子函数都不再执行

// src/core/vdom/create-component.js
const componentVNodeHooks = {
    init(vnode, hydrating) {
        if (
            vnode.componentInstance &&
            !vnode.componentInstance._isDestroyed &&
            vnode.data.keepAlive
        ) {
            // keep-alive components, treat as a patch
            const mountedNode = vnode;
            componentVNodeHooks.prepatch(mountedNode, mountedNode);
        } else {
            const child = (vnode.componentInstance =
                createComponentInstanceForVnode(vnode, activeInstance));
        }
    },
};

activated 和 deactivated

  • activated:在 patch 的阶段,最后会执行 invokeInserHook 函数,而这个函数就是去调用组件实例自身的 insert 钩子
  • deactivated:也是一样的原理,在组件实例(VNode)的 destroy 钩子函数中调用 deactivateChildComponent 函数。
// src/core/vdom/patch.js
function invokeInsertHook(vnode, queue, initial) {
    if (isTrue(initial) && isDef(vnode.parent)) {
        vnode.parent.data.pendingInsert = queue;
    } else {
        for (let i = 0; i < queue.length; i++) {
            queue[i].data.hook.insert(queue[i]);
        }
    }
}

在 insert 钩子里面,调用了 activateChildComponent 函数递归地区执行所有子组件的 activated 钩子函数

// src/cord/vdom/create-component.js
const componentVNodeHooks = {
    insert(vnode) {
        const { context, componentInstance } = vnode;
        if (!componentInstance._isMounted) {
            componentInstance._isMounted = true;
            callHook(componentInstance, 'mounted');
        }
        if (vnode.data.keepAlive) {
            if (context._isMounted) {
                queueActivatedComponent(componentInstance);
            } else {
                activateChildComponent(componentInstance, true);
            }
        }
    },
};
// src/core/instance/lifecycle.js
export function activateChildComponent(vm, direct) {
    if (direct) {
        vm._directInactive = false;
        if (isInInactiveTree(vm)) {
            return;
        }
    } else if (vm._directInactive) {
        return;
    }
    if (vm._inactive || vm._inactive === null) {
        vm._inactive = false;
        for (let i = 0; i < vm.$children.length; i++) {
            activateChildComponent(vm.$children[i]);
        }
        callHook(vm, 'activated');
    }
}
Last Updated:
Contributors: af