[Vue 源码] keep-alive 组件逻辑分析(上)

keep-alive

基本用法

keep 的使用只需要在动态组件的最外层添加标签即可

<div id="app"><button @click="changeTabs('child1')">child1</button><button @click="changeTabs('child2')">child2</button><keep-alive><component :is="chooseTabs"></component></keep-alive>
</div> 
var child1 = {template: '<div><button @click="add">add</button><p>{{num}}</p></div>',data() {return {num: 1}},methods: {add() {this.num++}},
}
var child2 = {template: '<div>child2</div>'
}
var vm = new Vue({el: '#app',components: {child1,child2,},data() {return {chooseTabs: 'child1',}},methods: {changeTabs(tab) {this.chooseTabs = tab;}}
}) 

当动态组件在 child1 和 child2 之间来回切换时,第二次以后在切到 child1 组件, child1 保留着原来的数据状态。

从模版编译到虚拟 DOM

内置组件和普通组件在编译过程中没有区别,不管是哪只组件还是用户定义组件,本质上组件在模版编译成 render 函数的处理方式是一致的。这里不在分析 render 函数的生成过程。

在拿到 render 函数之后,开始生成虚拟 DOM , 由于 keep-alive 是组件,所以会调用 createComponent 函数去创建子组件的虚拟 DOM , 在 createComponent 环节中,和创建普通组件不同之处在于, keep-alive 的虚拟 DOM 会去除多余的属性,除了 slot 属性之外(slot 属性也在 2.6 版本之后被废弃),其他属性都没有意义。在 keep-alive 组件的虚拟 DOM 上,存在一个 abstract 属性作为抽象组件的标志。

// 创建子组件Vnode过程
function createComponent(Ctordata,context,children,tag) {// abstract是内置组件(抽象组件)的标志if (isTrue(Ctor.options.abstract)) {// 只保留slot属性,其他标签属性都被移除,在vnode对象上不再存在var slot = data.slot;data = {};if (slot) {data.slot = slot;}}
} 

初次渲染

keep-alive 之所以特别,是因为他不会重复渲染相同的组件,只会利用初次渲染保留的缓存去更新节点,为了更全面的了解 keep-alive 的实现原理,我们冲他的首次渲染开始分析。

流程分析

和普通组件相同的是, Vue 会拿到前面生成的虚拟 DOM 对象,执行真实节点的创建过程,也就是 patch 过程,在创建节点过程中, keep-alive 的虚拟 DOM 会被认为是一个组件的 vnode ,因此会进入 createComponent 函数,在该函数中对 keep-alive 组件进行初始化和实例化。

 function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {let i = vnode.dataif (isDef(i)) {// isReactivated 用来判断组件是否缓存const isReactivated = isDef(vnode.componentInstance) && i.keepAliveif (isDef(i = i.hook) && isDef(i = i.init)) {// 执行组件初始化的内部钩子 initi(vnode, false /* hydrating */)}if (isDef(vnode.componentInstance)) {// 其中一个作用就是保留真实 DOM 的虚拟 DOM 中initComponent(vnode, insertedVnodeQueue)insert(parentElm, vnode.elm, refElm)if (isTrue(isReactivated)) {reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)}return true}}
} 

keep-alive 组件会先调用内部的 init 钩子进行初始化操作,

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 {// 将组件实例复制给虚拟 DOM 的 componentInstance 属性const child = vnode.componentInstance = createComponentInstanceForVnode(vnode,activeInstance)// 创建组件实例之后进行挂载child.$mount(hydrating ? vnode.elm : undefined, hydrating)}},prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {// ...},insert (vnode: MountedComponentVNode) {// ...},destroy (vnode: MountedComponentVNode) {//...}
} 

在第一次执行,组件的 vnode 对象中没有 componentInstance 属性, vnode.data.keepAlive 也没有值,所以会调用 createComponentInstanceForVnode 方法进行组件实例化并将组件实例复制给 vnode 的 componentInstance 属性,最终执行组件实例的 $mount 方法进行实例挂载。

createComponentInstanceForVnode 就是组件实例化的过程,这一过程前面已经分析过了,主要包含选项合并 、 初始化事件 、 生命周期等初始化操作。

内置组件选项

在使用组件的时候经常利用对象的形式定义组件选项,包括 datamethodcomputed 等,并在父组件或者全局中进行注册,来看一下 keep-alive 的具体选项

export default {name: 'keep-alive',abstract: true,// keep-alive 组件允许的 props 值props: {include: patternTypes,// 需要进行缓存的组件名称exclude: patternTypes,// 不需要进行缓存的组件名称max: [String, Number] // 最大缓存数量,超过该数量时, 依据 lru 算法进行替换},created () {// 缓存组件 vnodethis.cache = Object.create(null)// 缓存组件名称this.keys = []},destroyed () {for (const key in this.cache) {pruneCacheEntry(this.cache, key, this.keys)}},mounted () {// 对于动态的 include 和 exclude , 需要进行监听this.$watch('include', val => {pruneCache(this, name => matches(val, name))})this.$watch('exclude', val => {pruneCache(this, name => !matches(val, name))})},// 渲染函数render () {// 拿到 keep-alive 下插槽的值const slot = this.$slots.default// 获取第一个 vnode 节点const vnode: VNode = getFirstComponentChild(slot)// 拿到第一个组件实例const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptionsif (componentOptions) {// check pattern// 拿到第一个子组件 vnode 的 name 属性const name: ?string = getComponentName(componentOptions)const { include, exclude } = thisif (// 判断子组件是否需要进行缓存,不需要缓存时直接返回 vnode 对象// 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.keyif (cache[key]) {// 命中缓存,vnode.componentInstance = cache[key].componentInstance// make current key freshest// 删除 key 之后在重新添加,最近使用到的缓存会放在数组后面,这是 lru 算法思想,后面详细分析remove(keys, key)keys.push(key)} else {// 初次渲染,缓存 vnodecache[key] = vnodekeys.push(key)// prune oldest entryif (this.max && keys.length > parseInt(this.max)) {pruneCacheEntry(cache, keys[0], keys, this._vnode)}}// 为缓存组件加上标志vnode.data.keepAlive = true}return vnode || (slot && slot[0])}
} 

keep-alive 组件和普通组件在选项上是类似的, keep-alive 组件使用了 render 函数而不是 template 模版。 keep-alive 组件本质上只是存缓存和取缓存的过程,并没有实际的节点渲染。

缓存 VNode

keep-alive 组件在实例化之后会进行组件的挂载,而挂载过程又回到了 vm._rendervm_update 的过程。 由于 keep-alive 拥有 render 函数,所以我们直接分析 render 函数的实现

获取 keep-alive 组件插槽的内容

首先是通过 getFirstComponentChild 方法获取 keep-alive 下插槽的内容,也就是 keep-alive 组件需要渲染的子组件,

export function getFirstComponentChild (children: ?Array<VNode>): ?VNode {if (Array.isArray(children)) {for (let i = 0; i < children.length; i++) {const c = children[i]// 组件实例存在,则返回,理论上返回第一个组件的 vnodeif (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {return c}}}
} 

缓存组件

拿到子组件实例后,需要判断是否满足缓存的匹配条件,匹配条件可以使用数组、字符串、正则

const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (// 判断子组件是否需要进行缓存,不需要缓存时直接返回 vnode 对象// not included(include && (!name || !matches(include, name))) ||// excluded(exclude && name && matches(exclude, name))
) {return vnode
}

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
} 

如果组件不满足缓存的要求,则会之间返回组件的 vnode ,然后进入挂载流程

render 函数关键的一步就是缓存 vnode ,由于第一次执行 render 函数,选项中的 cachekeys 都没有数据,无法命中缓存,因此,在第一次渲染 keep-alive 组件是,会将需要渲染的子组件 vnode 进行缓存。将已经缓存的 vnode 搭上标志,并将子组件的 vnode 进行返回, vnode.data.keepAlive = true

真实节点的保存

再回到 crateComponent 的逻辑,在 createComponent 方法中,首先会执行 keep-alive 组件的初始化流程,也包括了子组件的挂载,之后在 createComponent 方法中拿到了 keep-alive 组件的实例,接下来重要的一步就是将真实 DOM 保存在 vnode 中。

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {let i = vnode.dataif (isDef(i)) {// isReactivated 用来判断组件是否存在缓存const isReactivated = isDef(vnode.componentInstance) && i.keepAliveif (isDef(i = i.hook) && isDef(i = i.init)) {// 执行组件初始化的内部钩子 initi(vnode, false /* hydrating */)}if (isDef(vnode.componentInstance)) {// 其中一个作用就是保留真实 DOM 的虚拟 DOM 中initComponent(vnode, insertedVnodeQueue)insert(parentElm, vnode.elm, refElm)if (isTrue(isReactivated)) {reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)}return true}}
}

function initComponent (vnode, insertedVnodeQueue) {if (isDef(vnode.data.pendingInsert)) {insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)vnode.data.pendingInsert = null}// 将真实 DOM 保存到 vnode 中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)}
} 

在进行组件缓存是,需要将组件的真实 DOM 节点保存到 vnode 对象上,而保存大量的 DOM 元素会非常耗费性能,因此我们需要严格控制缓存组件的数量,另外在缓存策略上也需要做优化。

keep-alive 组件的初次渲染做一个总结:内置的 keep-alive 组件,在子组件第一次进行渲染是将 vnode 和真实 DOM 进行了缓存

最后

整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值