Vue源码解析篇 (二)keep-alive源码解析

keep-alive是Vue.js的一个内置组件。它能够不活动的组件实例保存在内存中,我们来探究一下它的源码实现。 首先回顾下使用方法 举个栗子 export default { data () { return { isShow: true } }, methods: { handleClick () { this.isShow = !this.isShow; } } } 在点击按钮时,两个组件会发生切换,但是这时候这两个组件的状态会被缓存起来,比如:组件中都有一个input标签,那么input标签中的内容不会因为组件的切换而消失。 属性支持 keep-alive组件提供了include与exclude两个属性来允许组件有条件地进行缓存,二者都可以用逗号分隔字符串、正则表达式或一个数组来表示。 举个例子: 缓存name为a的组件。 排除缓存name为a的组件。 当然 props 还定义了 max,该配置允许我们指定缓存大小。 keep-alive 源码实现 说完了keep-alive组件的使用,我们从源码角度看一下keep-alive组件究竟是如何实现组件的缓存的呢? 创建和销毁阶段 首先看看 keep-alive 的创建和销毁阶段做了什么事情: created () { /* 缓存对象 */ this.cache = Object.create(null) }, destroyed () { for (const key in this.cache) { pruneCacheEntry(this.cache[key]) } }, 在 keep-alive 的创建阶段: created钩子会创建一个cache对象,用来保存vnode节点。 在销毁阶段:destroyed 钩子则会调用pruneCacheEntry方法清除cache缓存中的所有组件实例。 pruneCacheEntry 方法的源码实现 /* 销毁vnode对应的组件实例(Vue实例) */ function pruneCacheEntry (vnode: ?VNode) { if (vnode) { vnode.componentInstance.$destroy() } } 因为keep-alive会将组件保存在内存中,并不会销毁以及重新创建,所以不会重新调用组件的created等方法,因此keep-alive提供了两个生命钩子,分别是activated与deactivated。用这两个生命钩子得知当前组件是否处于活动状态。(稍后会看源码如何实现) 渲染阶段 render () { /* 得到slot插槽中的第一个组件 */ const vnode: VNode = getFirstComponentChild(this.$slots.default) const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions if (componentOptions) { // 获取组件名称,优先获取组件的name字段,否则是组件的tag const name: ?string = getComponentName(componentOptions) // 不需要缓存,则返回 vnode if (name && ( (this.include && !matches(this.include, name)) || (this.exclude && matches(this.exclude, name)) )) { return vnode } const key: ?string = vnode.key == null ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key if (this.cache[key]) { // 有缓存则取缓存的组件实例 vnode.componentInstance = this.cache[key].componentInstance } else { // 无缓存则创建缓存 this.cache[key] = vnode // 创建缓存时 // 如果配置了 max 并且缓存的长度超过了 this.max // 则从缓存中删除第一个 if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(this.cache, keys[0], keys, this._vnode) } } // keepAlive标记 vnode.data.keepAlive = true } return vnode } render 做了以下事情: 通过getFirstComponentChild获取第一个子组件,获取该组件的name(存在组件名则直接使用组件名,否则会使用tag) 将name通过include与exclude属性进行匹配,匹配不成功(说明不需要缓存)则直接返回vnode 匹配成功则尝试获取缓存的组件实例 若没有缓存该组件,则缓存该组件 缓存超过最大值会删掉第一个缓存 name 匹配的方法(校验是逗号分隔的字符串还是正则) /* 检测name是否匹配 */ function matches (pattern: string | RegExp, name: string): boolean { if (typeof pattern === 'string') { /* 字符串情况,如a,b,c */ return pattern.split(',').indexOf(name) > -1 } else if (isRegExp(pattern)) { /* 正则 */ return pattern.test(name) } /* istanbul ignore next */ return false } 如果在中途有对 include 和 exclude 进行修改该怎么办呢? 作者通过 watch 来监听 include 和 exclude,在其改变时调用 pruneCache 以修改 cache 缓存中的缓存数据。 watch: { /* 监视include以及exclude,在被修改的时候对cache进行修正 */ include (val: string | RegExp) { pruneCache(this.cache, this._vnode, name => matches(val, name)) }, exclude (val: string | RegExp) { pruneCache(this.cache, this._vnode, name => !matches(val, name)) } }, 那么 pruneCache 做了什么? // 修补 cache function pruneCache (cache: VNodeCache, current: VNode, filter: Function) { for (const key in cache) { // 尝试获取 cache中的vnode const cachedNode: ?VNode = cache[key] if (cachedNode) { const name: ?string = getComponentName(cachedNode.componentOptions) if (name && !filter(name)) { // 重新筛选组件 if (cachedNode !== current) { // 不在当前 _vnode 中 pruneCacheEntry(cachedNode) // 调用组件实例的 销毁方法 } cache[key] = null // 移除该缓存 } } } } pruneCache方法 遍历cache中的所有项,如果不符合规则则会销毁该节点并移除该缓存 进阶 再回顾下源码,在 src/core/components/keep-alive.js 中 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) { 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]) } } 现在不加注释也应该大部分都能看懂了? 顺便提下 abstract 这个属性,若 abstract 为 true,则表示组件是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。 那么为什么在组件有缓存的时候不会再次执行组件的 created、mounted 等钩子函数呢? const componentVNodeHooks = { init (vnode: VNodeWithData, hydrating: boolean): ?boolean { // 进入这段逻辑 if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { const mountedNode: any = vnode componentVNodeHooks.prepatch(mountedNode, mountedNode) } else { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) child.$mount(hydrating ? vnode.elm : undefined, hydrating) } }, // ... } 看上面了代码, 满足 vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive 的逻辑就不会执行$mount的操作,而是执行prepatch。 那么 prepatch 究竟做了什么? // 不重要内容都省略... prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) { // 会执行这个方法 updateChildComponent(//...) }, // ... 其中主要是执行了 updateChildComponent 函数。 function updateChildComponent ( vm: Component, propsData: ?Object, listeners: ?Object, parentVnode: MountedComponentVNode, renderChildren: ?Array ) { const hasChildren = !!( renderChildren || vm.$options._renderChildren || parentVnode.data.scopedSlots || vm.$scopedSlots !== emptyObject ) // ... if (hasChildren) { vm.$slots = resolveSlots(renderChildren, parentVnode.context) vm.$forceUpdate() } } keep-alive 组件本质上是通过 slot 实现的,所以它执行 prepatch 的时候,hasChildren = true,会触发组件的 $forceUpdate 逻辑,也就是重新执行 keep-alive 的 render 方法 然鹅,根据上面讲的 render 方法源码,就会去找缓存咯。 那么, 的实现原理就介绍完了 最后 原创不易点个赞呗 欢迎关注公众号「前端进阶课」认真学前端,一起进阶。

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):