likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 15

阅读本章节前,建议先查阅 [[Vue 源码]keep-alive 组件逻辑分析(中)]([Vue 源码] keep-alive 组件逻辑分析(上))。 本章节中主要分析了抽象组件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 parent
  let parent = options.parent
  if (parent && !options.abstract) {
    // 如果父组件有 abstract 属性,则一直往上找,直到不是抽象组件为止
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._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.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },
}

export function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  // 更新旧的状态

  // resolve slots + force update if has children
  if (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.componentOptions
  if (componentOptions) {
    // check pattern
    // 拿到第一个子组件 vnode 的 name 属性
    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
    }

    const { cache, keys } = this
    const 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.key
    if (cache[key]) {
      // 命中缓存,
      vnode.componentInstance = cache[key].componentInstance
      // make current key freshest
      // 删除 key 之后在重新添加,最近使用到的缓存会放在数组后面,这是 lru 算法思想,后面详细分析
      remove(keys, key)
      keys.push(key)
    } else {
      // 初次渲染,缓存 vnode
      cache[key] = vnode
      keys.push(key)
      // prune oldest entry
      if (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.data
  if (isDef(i)) {
    // isReactivated 用来判断组件是否存在缓存 , 再次渲染时 vnode 中存在 componentInstance 属性并且 vnode.data.keepAlive 为 true ,因此 isReactivated 为 true
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      // 执行组件初始化的内部钩子 init
      i(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 patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      // 将组件实例复制给虚拟 DOM 的 componentInstance 属性
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      // 创建组件实例之后进行挂载
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },
}

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