Vue源码分析之keep-alive(二)

·  阅读 917

这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战

前言

上篇已经看过Vue内置的抽象组件keep-alive内部的具体实现,但是我们可能更关心的是被keep-alive包裹的组件是怎么做处理的。

这篇来看下被keep-alive缓存的组件是具体怎么渲染的,与平时我们写的普通组件有什么不同??

组件创建

在前边看Vue diff的时候已经知道Vue的_update方法是用来把虚拟DOM渲染为真实DOM的,它内部就是执行patch的具体过程。

所有的创建节点最后都会走到createElm函数,包括组件。createElm定义在src/core/vdom/patch.js中。

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
    ...
    
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
    
    ...
}
复制代码

看到创建组件主要是调用了createComponent方法,也定义在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 /* 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
    }
  }
}
复制代码

从代码看到,createComponent函数中定义了一个与缓存组件有关的变量:isReactivated

  • 初次渲染的时候,vnode.componentInstance是undefined

  • 由于<keep-alive>组件会先比被它包裹的组件先执行,所以在执行keep-alive组件的render函数时,会把该组件Vnode缓存到自己定义的cache对象中,并将vnode.data.keepAlive设置为true

  • 所以初次渲染的时候isReactivated为false,会跟普通组件一样走正常的init过程。

组件切换

在分析diff的过程中我们知道对于相同类型的节点会执行patchVnode方法,该方法会去对比新老节点以及它们的子节点(updateChildren)。但是组件Vnode是没有children的(children参数为undefined),那被keep-alive包裹的组件在切换时,怎么更新呢??

image.png

当初在看这个方法的时候只看了后边对新老节点的对比,没有关注代码前边部分。其实代码前边有如下一段:

function patchVnode (
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  
  let i
  const data = vnode.data
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }
  
  ...
}
复制代码

在执行节点对比之前,会先执行prepatch钩子函数,定义在src/core/vdom/create-component.js

prepatch

prepatch函数定义在componentVNodeHooks中:

const componentVNodeHooks = {
  ...
  
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  }
  
  ...
}
复制代码

prepatch钩子函数内部主要是执行了updateChildComponent方法,定义在src/core/instance/lifecycle.js

updateChildComponent

export function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  // determine whether component has slot children
  // we need to do this before overwriting $options._renderChildren.

  // check if there are dynamic scopedSlots (hand-written or compiled but with
  // dynamic slot names). Static scoped slots compiled from template has the
  // "$stable" marker.
  const newScopedSlots = parentVnode.data.scopedSlots
  const oldScopedSlots = vm.$scopedSlots
  const hasDynamicScopedSlot = !!(
    (newScopedSlots && !newScopedSlots.$stable) ||
    (oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
    (newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key)
  )

  // 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 slots
    vm.$options._renderChildren ||  // has old static slots
    hasDynamicScopedSlot
  )

  ...
  
  // resolve slots + force update if has children
  if (needsForceUpdate) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
  }
}
复制代码

keep-alive组件是支持slot的,所以在执行prepatch方法的时候会去执行$forceUpdate逻辑。forceUpdate后边单独看,它的主要作用就是强制触发依赖更新。

这个时候又会去执行<keep-alive>的render函数,如果包裹的组件已经被缓存过,则直接执行vnode.componentInstance = cache[key].componentInstance

此时又回到最开始的createComponent方法,首先执行init方法,定义在src/core/vdom/create-component.js中

init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
  if (
    vnode.componentInstance &&
    !vnode.componentInstance._isDestroyed &&
    vnode.data.keepAlive
  ) {
    // kept-alive components, treat as a patch
    const mountedNode: any = vnode // work around flow
    componentVNodeHooks.prepatch(mountedNode, mountedNode)
  } else {
    const child = vnode.componentInstance = createComponentInstanceForVnode(
      vnode,
      activeInstance
    )
    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  }
}
复制代码

作为缓存组件会直接执行prepatch方法,看一下这个prepatch做了什么:

prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  const options = vnode.componentOptions
  const child = vnode.componentInstance = oldVnode.componentInstance
  updateChildComponent(
    child,
    options.propsData, // updated props
    options.listeners, // updated listeners
    vnode, // new parent vnode
    options.children // new children
  )
}
复制代码

prepatch方法内部是调用了updateChildComponent方法,即调用了$forceUpdate去更新组件。从这里可以看出keep-alive组件在命中缓存以后,在执行createComponent函数时是不会像普通组件一样去执createComponentInstanceForVnode,这也就是为什么缓存组件内部切换时,不会再去调用created,mounted等钩子了

继续看,这时候的isReactivated为true,会执行下边的代码:

if (isDef(vnode.componentInstance)) {
  initComponent(vnode, insertedVnodeQueue)
  insert(parentElm, vnode.elm, refElm)
  if (isTrue(isReactivated)) {
    reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
  }
  return true
}
复制代码

看一下这个reactivateComponent函数做了什么:

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.
  let innerNode = vnode
  while (innerNode.componentInstance) {
    innerNode = innerNode.componentInstance._vnode
    if (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
  insert(parentElm, vnode.elm, refElm)
}
复制代码

从注释可以看到reactivateComponent函数主要做了两件事:

  • 解决reactivated组件中transition不触发的问题(原因就是created钩子不再调用导致的)。

  • 执行insert方法将缓存组件的DOM元素插入到对应的位置。

总结

  • 缓存组件在初次执行时除了会被keep-alive缓存外,与普通组件是一样的。

  • 组件Vnode不存在children,在执行diff之前会先调用prepatch方法,更新组件时会直接调用updateChildComponent

  • 缓存组件在命中缓存后不会再去执行created,mounted等方法,而是直接调用$forceUpdate去更新组件。

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改