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

抽象组件

Vue 提供的内置组件都有一个描述组件类型的选项,这个选项就是 {abstract: true } , 该选项表明该组件是抽象组件。那么什么是抽象组件,为什么要进行区分。

  • 抽象组件没有真实的节点,在渲染是不会被解析渲染成真实的 DOM 接待你,而只是作为中间的数据过渡层处理,在 keep-alive 中是对组件缓存进行处理。
  • 在子组件进行初始化是,会将父组件实例挂载到自身选项的 parent 属性上, 在 initLifeCycle 过程中,会反向拿到 parent 上父组件的 vnode ,并为其 $children 属性添加该子组件的 vnode , 如果在反向查找父组件的过程中,父组件拥有 abstract 属性,即可判断该组件是抽象组件,此时利用 parent 链条继续往上找,直到组件不是抽象组件为止。
export function initLifecycle (vm: Component) {const options = vm.$options// locate first non-abstract parentlet parent = options.parentif (parent && !options.abstract) {// 如果父组件有 abstract 属性,则一直往上找,直到不是抽象组件为止while (parent.$options.abstract && parent.$parent) {parent = parent.$parent}parent.$children.push(vm)}vm.$parent = parentvm.$root = parent ? parent.$root : vmvm.$children = []vm.$refs = {}vm._watcher = nullvm._inactive = nullvm._directInactive = falsevm._isMounted = falsevm._isDestroyed = falsevm._isBeingDestroyed = false
} 

父子组件之间建立的这种联系,是父子组件之间通信的基础

重新渲染组件

再次渲染的流程从数据发生改变说起,动态组件中数据发生变化会引起依赖派发更新的过程。当数据发生改变时,收集过的依赖会进行派发更新操作。

其中,父组件中负责实例挂载的过程做为依赖会被执行,即执行父组件的 vm._update(vm._render(), hydrating) 其中 _render 函数会根据数据变化为组件生成新的虚拟 DOM 节点, 而 _update 最终会为新的虚拟 DOM 节点生成真实的节点,而在生成真实节点的过程中,会利用 diff 算法对新旧的虚拟 DOM 节点进行对比,使之尽可能少的改变真实节点。

patch 是新旧虚拟 DOM 对比的过程,而 patchVnode 是其中核心步骤,在这里主要关注对子组件执行 prePatch 钩子的过程

function patchVnode (oldVnode,vnode,insertedVnodeQueue,ownerArray,index,removeOnly
) {// ...if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {i(oldVnode, vnode)}
} 

在执行 prePatch 钩子是会拿到新旧组件的实例并执行 updateChildComponent 函数, 而 updateChildComponent 会对针对新的组件实例对就实例进行状态的更新,包括 props listeners 等,最终会调用 vue 提供的全局 vm.$forceUpdate() 方法进行实例的重新渲染。

const componentVNodeHooks = {prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {// 新组件的实例const options = vnode.componentOptions// 旧组件实例const child = vnode.componentInstance = oldVnode.componentInstanceupdateChildComponent(child,options.propsData, // updated propsoptions.listeners, // updated listenersvnode, // new parent vnodeoptions.children // new children)},
}

export function updateChildComponent (vm: Component,propsData: ?Object,listeners: ?Object,parentVnode: MountedComponentVNode,renderChildren: ?Array<VNode>
) {// 更新旧的状态// resolve slots + force update if has childrenif (needsForceUpdate) {vm.$slots = resolveSlots(renderChildren, parentVnode.context)// 强制实例重新渲染vm.$forceUpdate()}if (process.env.NODE_ENV !== 'production') {isUpdatingChildComponent = false}
} 

先看看 $forceUpdate 做了什么操作。 $forceUpdatevue 对外暴露的一个 api ,该 api 能够使 vue 实例重新渲染,本质上是执行实例所收集的依赖,对于有 keep-alive 包裹的动态组件而言,是执行 keep-alivevm._update(vm._render(), hydrating) 过程

Vue.prototype.$forceUpdate = function () {var vm = this;if (vm._watcher) {vm._watcher.update();}
}; 

重用缓存组件

由于 vm.$forceUpdate 会强迫 keep-alive 组件进行重新渲染,因此 keep-alive 组件会再次执行 render 过程,由于在第一渲染时,对虚拟 DOM 进行了缓存,所以再次执行时会从 cache 对象中找到缓存的组件。

// 渲染函数
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-aliverender 函数时,由于 cache 对象中存储了虚拟 DOM 读喜庆,所以直接通过 cache[key] 取出缓存的组件实例并赋值给 vnodecomponentInstance 属性。

真实节点的替换

在执行 keep-alive 组件的 _render 过程之后,接下来时 _update 过程产生真实的节点,由于 keep-alive 下面存在子组件,所以 _update 过程会调用 createComponent 递归创建子组件爱你的 vnode , 由于在初次渲染时已经存在缓存,看下再次渲染和初次渲染有哪些不同

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {let i = vnode.dataif (isDef(i)) {// isReactivated 用来判断组件是否存在缓存 , 再次渲染时 vnode 中存在 componentInstance 属性并且 vnode.data.keepAlive 为 true ,因此 isReactivated 为 trueconst 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}}
} 

createComponent 方法中,依旧会执行组件的初始化过程, 也就是组件的 init 钩子函数,但是由于这个过程已经存在缓存了,所以执行过程与第一次初始化不完全相同

const componentVNodeHooks = {init (vnode: VNodeWithData, hydrating: boolean): ?boolean {if (vnode.componentInstance &&!vnode.componentInstance._isDestroyed &&vnode.data.keepAlive) {// 已经存在缓存时,执行 prePatch 钩子函数// 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)}},
} 

显然,因为存在 keepAlive 标志,所以子组件不在走挂载流程,只是执行 prePatch 钩子对组件的状态进行更新,很好的利用了缓存 vnode 之间保留的真实节点进行节点的替换。

最后

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



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

猜你喜欢

转载自blog.csdn.net/Android062005/article/details/129364306