Vue keep-alive 组件原理

Vue keep-alive 组件原理

组件实现原理

kotlin 复制代码
// src/core/components/keep-alive.js
export default {
  name: 'keep-alive',
  abstract: true,

  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]
  },

  methods: {
    cacheVNode() {
      const { cache, keys, vnodeToCache, keyToCache } = this
      if (vnodeToCache) {
        const { tag, componentInstance, componentOptions } = vnodeToCache
        cache[keyToCache] = {
          name: getComponentName(componentOptions),
          tag,
          componentInstance,
        }
        keys.push(keyToCache)
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
        this.vnodeToCache = null
      }
    }
  },

  created () {
    this.cache = Object.create(null)
    this.keys = []
  },

  destroyed () {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () {
    this.cacheVNode()
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  updated () {
    this.cacheVNode()
  },

  render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        // 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
        // 相同的构造函数可能会注册为不同的本地组件,因此仅 cid 是不够的
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      if (cache[key]) {
        // 如果已经缓存该子组件,获取组件的实例
        vnode.componentInstance = cache[key].componentInstance
        // 更新缓存的key
        remove(keys, key)
        keys.push(key)
      } else {
        // 缓存将要缓存节点以及节点的key值
        this.vnodeToCache = vnode
        this.keyToCache = key
      }
      // 缓存的节点添加keepAlive属性为true
      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}

组件原理

是一个抽象组件,只对包裹的子组件做处理,自身不会渲染DOM元素,也不会出现在组件的父组件链中。那如何实现是一个抽象组件呢?

组件有一个abstract属性,值为true,其包裹的子组件在执行initLifcycle方法时,会判断其父组件是否为抽象组件,如果是抽象组件则忽略与此组件的父子关系,设置更高层级组件为父组件。

php 复制代码
// src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
  const options = vm.$options

  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

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

  // ...
}

组件没有编写组件模板template,而是实现了render方法。在进行组件渲染时,会调用自身的render方法,通过render方法的返回结果决定渲染结果。

kotlin 复制代码
// src/core/components/keep-alive.js
render () {
    // 获取默认插槽
    const slot = this.$slots.default
    // 获取插槽中的第一个节点
    const vnode: VNode = getFirstComponentChild(slot)
    // 获取组件options
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // 获取组件名
      const name: ?string = getComponentName(componentOptions)
      // 校验是否缓存
      const { include, exclude } = this
      // 不缓存的情况
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      // 获取组件的key
      const key: ?string = vnode.key == null
         // 相同的构造函数可能会注册为不同的本地组件,因此仅 cid 是不够的
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      if (cache[key]) {
        // 如果已经缓存该组件节点,获取组件的实例
        vnode.componentInstance = cache[key].componentInstance
        // 更新缓存的key
        remove(keys, key)
        keys.push(key)
      } else {
        // 缓存将要缓存节点以及节点的key值
        this.vnodeToCache = vnode
        this.keyToCache = key
      }
      // 缓存的节点添加keepAlive属性为true
      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }

在render方法中,首先会获取组件的默认插槽,然后调用getFirstComponentChild方法获取默认插槽中的第一个子元素的vnode,此vnode将作为render方法执行的结果被返回。因此如果组件包裹多个元素只会渲染第一个元素。

arduino 复制代码
// 获取默认插槽
const slot = this.$slots.default
// 获取插槽中的第一个元素节点
const vnode: VNode = getFirstComponentChild(slot)

接下来调用getComponentName获取组件名,同时获取组件接收到的include和exclude参数。通过判断组件名是否存在于include和exclude列表中来确定该组件是否需要被缓存。如果该组件不需要被缓存,则直接返回节点,否则接着进行后续的处理。

typescript 复制代码
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
    // not included
    (include && (!name || !matches(include, name))) ||
    // excluded
    (exclude && name && matches(exclude, name))
 ) {
    return vnode
 }
 // match 方法
 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
}

组件在created钩子函数中,定义了cache 和key两个属性。cache中保存着需要被缓存的组件节点vnode,keys保存这组件节点的key。

kotlin 复制代码
// src/core/components/keep-alive.js
created () {
    this.cache = Object.create(null)
    this.keys = []
 }

接着判断节点是否存在于cache中,如果节点已存在于cache中即命中了缓存,会将缓存的节点的实例赋值给vnode.componentInstance,同时更新keys列表中保存的组件节点的key;如果没有命中缓存,则将需要缓存的组件节点以及组件节点的key保存在vnodeToCache和keyToCache中,同时设置vnode.data.keepAlive = true,即标记该节点是被缓存节点,最后将该节点返回。

kotlin 复制代码
const { cache, keys } = this
const key: ?string = vnode.key == null
    // 相同的构造函数可能会注册为不同的本地组件,因此仅 cid 是不够的
    ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
    : vnode.key
if (cache[key]) {
    // 如果已经缓存该子组件,获取组件的实例
    vnode.componentInstance = cache[key].componentInstance
    // 更新缓存的key
    remove(keys, key)
    keys.push(key)
} else {
    // 缓存将要缓存节点以及节点的key值
    this.vnodeToCache = vnode
    this.keyToCache = key
}
// 缓存的节点添加keepAlive属性为true
vnode.data.keepAlive = true

在完成组件挂载后,执行组件的mounted钩子函数,此时vnode.componentInstance中已经保存了节点的实例。调用cacheVnode方法会将render方法中缓存的节点和key缓存在cache以及keys中。此外,mounted中还设置了include、exclude的监听方法,以便及时更新cache和keys中缓存节点和节点的key。当设置了max值且keys的列表长度超过max值时,会采取先进先出的原则调用pruneCacheEntry方法将缓存最久未使用的的节点从cache和keys移除并且执行$destroy销毁组件实例,

kotlin 复制代码
// src/core/components/keep-alive.js
mounted () {
    // 缓存节点
    this.cacheVNode()
    // 监听include数据变化
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    // 监听exclude数据变化
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
 }
 
 methods: {
     cacheVNode() {
      const { cache, keys, vnodeToCache, keyToCache } = this
      if (vnodeToCache) {
        // 缓存中,防止在同一时间进行多个缓存任务
        const { tag, componentInstance, componentOptions } = vnodeToCache
        // 存放组件实例
        cache[keyToCache] = {
          name: getComponentName(componentOptions),
          tag,
          // 组件实例渲染后会存在 $el 属性,后续直接复用 $el 属性
          componentInstance,
        }
        // 缓存组件的key
        keys.push(keyToCache)
        // 缓存更新算法(LRU),超过最大长度时,移除第一个缓存的组件以及组件的key
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
        this.vnodeToCache = null
      }
    }
}
 
function pruneCacheEntry (
  cache: CacheEntryMap,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const entry: ?CacheEntry = cache[key]
  if (entry && (!current || entry.tag !== current.tag)) {
    entry.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

组件渲染流程

示例:

初始化渲染

Vue的渲染会经过patch过程,在patch过程中会执行createElm方法将节点挂载到界面。而在createElm方法中会执行createComponent方法来挂载组件。

scss 复制代码
// src/vore/vdom/patch.js
function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      /**
       * 此虚拟节点在之前的渲染中使用过!
       * 当前它被用作新节点,当它用作插入引用节点时,覆盖其 elm 会导致潜在的修补错误。相反,我们在为节点创建关联的 DOM 元素之前按需克隆节点。
       */
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    vnode.isRootInsert = !nested // for transition enter check
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
    // ...
}

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    /**
     * 获取 vnode.data 对象
     */
    let i = vnode.data
    if (isDef(i)) {
      /**
       * 验证组件实例是否已经存在 && 被 keep-alive 包裹
       */
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      /**
       * 执行 vnode.data.hook.init 钩子函数,该函数在将 render helper 时讲过
       * 如果是被 keep-alive 包裹的组件,则再执行 prepatch 钩子,用 vnode 上的各个属性更新 oldVnode 上的相关属性
       * 如果是组件没有被 keep-alive 包裹或者首次渲染,则初始化组件,并进入挂载阶段
       */
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        i(vnode, false /* hydrating */)
      }
      
      if (isDef(vnode.componentInstance)) {
        /**
         * 如果 vnode 是一个子组件,则调用 init 钩子之后会创建一个组件实例,并挂载
         * 这时候就可以给组件执行各个模块的 create 钩子
         */
         
        initComponent(vnode, insertedVnodeQueue)
        /**
         * 将组件的 DOM 节点插入到父节点内
         */
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {
          /**
           * 组件被 keep-alive 包裹的情况,激活组件
           */
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

createComponent方法中定义了isReactivated用于标记当时节点是否处于被激活的状态。当初次渲染时,由于组件的render方法提前执行,此时keepAlive为true,而此时AC组件还没有被实例化,因此vnode.componentInstance为null,因此此时isReactivated为false。当AC组件节点被实例化后,componentInstance就保存了组件实例。当AC组件再次被激活时,isReactivated为true。

ini 复制代码
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive

接下来执行init方法,在init方法中,会判断组件是否已被缓存,如果被缓存就执行prepatch方法,否则调用createComponentInstanceForVnode方法创建组件实例并且将组件实例赋值给vnode.componentInstance。

初始化渲染时,直接执行createComponentInstanceForVnode方法初始化AC组件实例并且将组件实例赋值给vnode.componentInstance。

php 复制代码
// src/core/vdom/create-component.js
const componentVNodeHooks = {
  /**
   * 初始化
   * @param {*} vnode 
   * @param {*} hydrating 
   */
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // keep-alive components, treat as a patch
      /**
       * 被 keep-alive 包裹的组件
       */
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      /**
       * 创建组件实例,即 new vnode.componentOptions.Ctor(options) => 得到 Vue 组件实例
       */
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      /**
       * 执行组件的 $mount 方法,进入挂载阶段,接下来就是通过编译器得到 render 函数,接着走挂载、patch 这条路,直到组件渲染到界面
       */
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },
  //...
}
arduino 复制代码
// src/core/vdom/create-component.js
// 创建子组件实例
export function createComponentInstanceForVnode (
  // we know it's MountedComponentVNode but flow doesn't
  vnode: any,
  // activeInstance in lifecycle state
  parent: any
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  /**
   * 检查内联模版渲染函数
   */
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  /**
   * new vnode.componentOptions.Ctor(options) => Vue 实例
   */
  return new vnode.componentOptions.Ctor(options)
}

在createComponentInstanceForVnode方法中,最后执行的vnode.componentOptions.Ctor(options)创建组件实例时,会执行_init()方法。

当组件实例化完成后,执行mount方法,进行render编译、patch挂载。当mount方法执行完成后,vnode.$el已经保存了需要挂载到界面的dom元素。

bash 复制代码
child.$mount(hydrating ? vnode.elm : undefined, hydrating)

mount方法执行完,回到createComponent方法中接着执行后续代码。此时vnode.componentInstance已保存了组件的实例以及el。

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

在判断vnode.componentInstance有值后,就做了下面两件事:

  1. 调用initComponent方法,将vnode.componentInstance.$el赋值给vnode.elm
scss 复制代码
// src/core/vdom/patch.js
function initComponent (vnode, insertedVnodeQueue) {
    if (isDef(vnode.data.pendingInsert)) {
      insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
      vnode.data.pendingInsert = null
    }
    vnode.elm = vnode.componentInstance.$el
    if (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 hook
      insertedVnodeQueue.push(vnode)
    }
  }
  1. 调用insert方法,将vnode.elm中保存的真实dom元素插入到父元素中
sql 复制代码
function insert (parent, elm, ref) {
   if (isDef(parent)) {
     if (isDef(ref)) {
        if (nodeOps.parentNode(ref) === parent) {
          nodeOps.insertBefore(parent, elm, ref)
        }
      } else {
        nodeOps.appendChild(parent, elm)
      }
   }
}

至此,初始化渲染是在正常渲染AC组件的同时将AC组件进行缓存,等到重复渲染使用。

重复渲染

当重新激活AC组件时,组件的缓存被重新激活。

再次经历patch过程,在patch过程中会执行patchVnode方法去对比新旧vnode节点以及它们的children节点进行逻辑更新。

scss 复制代码
// src/core/vdom/patch.js
function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    if (oldVnode === vnode) {
      return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // clone reused vnode
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    const elm = vnode.elm = oldVnode.elm

    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }
    // 静态树的重用元素
    // 仅在克隆虚拟节点时才执行此操作 - 如果未克隆新节点,则意味着渲染函数已被热重载 API 重置,需要执行适当的重新渲染。
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch)
        }
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
}

在执行patchVnode方法时遇到组件节点会执行prepatch方法。在prepatch方法中调用updateChildComponent方法。

arduino 复制代码
// src/core/vdom/create-compoent.js
const componentVNodeHooks = {
    // ...
    prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    /**
     * 新的 VNode 的组件配置项
     */
    const options = vnode.componentOptions
    /**
     * 旧的 VNode 的组件实例
     */
    const child = vnode.componentInstance = oldVnode.componentInstance
    /**
     * 用 VNode 上的属性更新 child 上的各种属性
     */
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  }
}

updateChildComponent方法会更新实例属性和方法。

php 复制代码
// src/core/instance/lifecycle.js
export function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  if (process.env.NODE_ENV !== 'production') {
    isUpdatingChildComponent = true
  }

  // 确定组件是否具有插槽子项,我们需要在覆盖 $options._renderChildren 之前执行此操作。
  
  // 检查是否有动态作用域槽(手写或编译,但具有动态槽名)。从模板编译的静态范围槽具有"$stable"标记
  const newScopedSlots = parentVnode.data.scopedSlots
  const oldScopedSlots = vm.$scopedSlots
  const hasDynamicScopedSlot = !!(
    (newScopedSlots && !newScopedSlots.$stable) ||
    (oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
    (newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key) ||
    (!newScopedSlots && vm.$scopedSlots.$key)
  )

  // 父级的任何静态插槽子级可能在父级更新期间已更改。动态作用域槽也可能已更改。在这种情况下,必须强制更新以确保正确性。
  const needsForceUpdate = !!(
    // 存在新的静态插槽
    renderChildren ||
    // 存在老的静态插槽
    vm.$options._renderChildren ||
    hasDynamicScopedSlot
  )

  vm.$options._parentVnode = parentVnode
  vm.$vnode = parentVnode // update vm's placeholder node without re-render

  if (vm._vnode) {
    // 重新设置子节点树的父节点
    vm._vnode.parent = parentVnode
  }
  vm.$options._renderChildren = renderChildren

  // 在渲染期间使用他们,更新$attrs和$listeners的hash可能会触发子节点的更新
  vm.$attrs = parentVnode.data.attrs || emptyObject
  vm.$listeners = listeners || emptyObject

  // 更新props
  if (propsData && vm.$options.props) {
    toggleObserving(false)
    const props = vm._props
    const propKeys = vm.$options._propKeys || []
    for (let i = 0; i < propKeys.length; i++) {
      const key = propKeys[i]
      const propOptions: any = vm.$options.props // wtf flow?
      props[key] = validateProp(key, propOptions, propsData, vm)
    }
    toggleObserving(true)
    // keep a copy of raw propsData
    vm.$options.propsData = propsData
  }

  // 更新listeners
  listeners = listeners || emptyObject
  const oldListeners = vm.$options._parentListeners
  vm.$options._parentListeners = listeners
  updateComponentListeners(vm, listeners, oldListeners)

  // 更新插槽属性 + 强制更新(如果有子项)
  if (needsForceUpdate) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
  }

  if (process.env.NODE_ENV !== 'production') {
    isUpdatingChildComponent = false
  }
}

此外,由于组件包裹着插槽,因此在执行updateChildComponent方法的过程中,needForceUpdate为true,它会先去更新实例的插槽属性,接着执行vm.$forceUpdate方法。

ini 复制代码
if (needsForceUpdate) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
}
Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    if (vm._watcher) {
      vm._watcher.update()
    }
  }

vm.$forceUpdate方法会执行vm._watcher.update方法,从而重新执行组件的render方法。当前激活的AC组件已经在初始化阶段被缓存,因此在执行render方法的过程中,会将缓存中的componentInstance复制给vnode.componentInstance,并且更新keys列表,最后返回节点。

kotlin 复制代码
if (cache[key]) {
    vnode.componentInstance = cache[key].componentInstance
    // make current key freshest
    remove(keys, key)
    keys.push(key)
 } else {
    // delay setting the cache until update
    this.vnodeToCache = vnode
    this.keyToCache = key
 }

render方法执行完成,进行节点的patch挂载阶段。在这里又会去执行createComponent方法,接着执行init方法。此时各种条件已经满足会执行prepatch方法,进而调用updateChildComponent方法去更新实例的属性和方法。

此次执行init方法,没有像初始化渲染时进行组件实例化,执行$mount方法,因此不会执行created、mounted钩子函数。

php 复制代码
// src/core/vdom/create-compoent.js
const componentVNodeHooks = {
  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 (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    /**
     * 新的 VNode 的组件配置项
     */
    const options = vnode.componentOptions
    /**
     * 旧的 VNode 的组件实例
     */
    const child = vnode.componentInstance = oldVnode.componentInstance
    /**
     * 用 VNode 上的属性更新 child 上的各种属性
     */
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },
  // ...
}

init方法执行完成后,回到createComponent方法中,接着执行后续代码,此时isReactivated为true。

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

返回createComponent其实就做了三件事:

  1. 重新对vnode.elm赋值
  2. 将vnode.elm插入到父元素中
  3. 执行缓存节点的相关动画

最后回到patch中,执行removeVnodes方法移除旧节点。至此本次缓存渲染流程结束。

生命周期函数

组件给缓存的组件添加了activated、deactivated两个生命周期函数。

activated

在patch方法中,会执行invokeInsertHook方法。

arduino 复制代码
// src/core/vdom/patch.js
function invokeInsertHook (vnode, queue, initial) {
    // 延迟insert钩子的执行时间,等元素真正插入后在给根节点插入insert钩子函数
    if (isTrue(initial) && isDef(vnode.parent)) {
      vnode.parent.data.pendingInsert = queue
    } else {
      for (let i = 0; i < queue.length; ++i) {
        queue[i].data.hook.insert(queue[i])
      }
    }
}

invokeInsertHook方法中会调用insert方法。

scss 复制代码
// src/core/vdom/create-component.js
insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // 在更新期间,保持活动状态组件的子组件可能会更改,
        // 因此直接在此处遍历树可能会在不正确的子组件上调用激活的钩子。
        // 相反,我们将它们推送到一个队列中,该队列将在整个补丁过程结束后进行处理。
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
}

在insert方法中,会判断节点是否被渲染,如果已被渲染就执行queueActivatedComponent。如果未被渲染,则执行activateChildComponent方法。

在queueActivatedComponent方法中,当前实例的钩子函数只是被缓存起来,并没有立即添加到执行的堆栈中执行。

arduino 复制代码
// src/core/oberser/scheduler.js
export function queueActivatedComponent (vm: Component) {
  // setting _inactive to false here so that a render function can
  // rely on checking whether it's in an inactive tree (e.g. router-view)
  vm._inactive = false
  activatedChildren.push(vm)
}

等到所有的渲染完成之后,在nextTick后会执行flushSchedulerQueue方法,进而调用callActivatedHooks方法。

scss 复制代码
// src/core/oberser/scheduler.js
function flushSchedulerQueue () {
  // ...

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // ...
}

callActivatedHooks中会给activatedQueue列表中保存的实例依次执行activateChildComponent方法。而activateChildComponent方法会将每个实例以及递归实例的子实例执行回调activated生命周期函数,等待后续执行。

ini 复制代码
// src/core/oberser/scheduler.js
function callActivatedHooks (queue) {
  for (let i = 0; i < queue.length; i++) {
    queue[i]._inactive = true
    activateChildComponent(queue[i], true /* true */)
  }
}
// src/core/instance/lifecycle.js
export function activateChildComponent (vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = false
    if (isInInactiveTree(vm)) {
      return
    }
  } else if (vm._directInactive) {
    return
  }
  if (vm._inactive || vm._inactive === null) {
    vm._inactive = false
    for (let i = 0; i < vm.$children.length; i++) {
      activateChildComponent(vm.$children[i])
    }
    callHook(vm, 'activated')
  }
}
deactivated

在patch方法中,会调用removeVnodes方法移除旧节点。而在removeVnodes方法中,会执行removeAndInvokeRemoveHook。

scss 复制代码
// src/core/vdom/patch.js
function removeVnodes (vnodes, startIdx, endIdx) {
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx]
      if (isDef(ch)) {
        if (isDef(ch.tag)) {
          removeAndInvokeRemoveHook(ch)
          invokeDestroyHook(ch)
        } else { // Text node
          removeNode(ch.elm)
        }
      }
    }
}

removeAndInvokeRemoveHook会去执行invokeDestroyHook方法。invokeDestroyHook会执行destroy方法。在destroy方法中会调用deactivateChildComponent方法,deactivateChildComponent方法与activateChildComponent方法类似,回调组件的deactivated钩子函数,并且递归去回调它的所有子组件的deactivated钩子函数。

scss 复制代码
// src/core/vdom/patch.js
function invokeDestroyHook (vnode) {
    let i, j
    const data = vnode.data
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
      for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
    }
    if (isDef(i = vnode.children)) {
      for (j = 0; j < vnode.children.length; ++j) {
        invokeDestroyHook(vnode.children[j])
      }
    }
 }

// src/core/vdom/create-component.js
destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
}

总结

组件是抽象组件,不会被渲染到界面上。初始化渲染节点时,它采用LRU策略去缓存组件的vnode,等到再次渲染节点时,会读取缓存中的节点使其渲染到界面上实现状态缓存,同时添加了activated和deactivated两个钩子函数,供组件在使用。

相关推荐
如若12327 分钟前
对文件内的文件名生成目录,方便查阅
java·前端·python
滚雪球~1 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语1 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
supermapsupport1 小时前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
brrdg_sefg1 小时前
WEB 漏洞 - 文件包含漏洞深度解析
前端·网络·安全
胡西风_foxww1 小时前
【es6复习笔记】rest参数(7)
前端·笔记·es6·参数·rest
m0_748254881 小时前
vue+elementui实现下拉表格多选+搜索+分页+回显+全选2.0
前端·vue.js·elementui
星就前端叭2 小时前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc
m0_748234522 小时前
前端Vue3字体优化三部曲(webFont、font-spider、spa-font-spider-webpack-plugin)
前端·webpack·node.js
Web阿成2 小时前
3.学习webpack配置 尝试打包ts文件
前端·学习·webpack·typescript