最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 深入学习keep-alive

    正文概述 掘金(zhousibao)   2020-12-08   509

    keep-alive是什么,它有什么作用

    • keep-alive是vue的内置组件,无需单独安装手动注册,keep-alive不会向DOM添加额外节点。
    • keep-alive是一个能提供缓存功能,保存子组件内部状态的组件。
    • keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
    • 当被缓存的子组件再次切换为活动状态时,不会执行其完整的生命周期,而是相应的执行activated、deactivated生命周期函数。

    keep-alive的用法

    Props:
        include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
        exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
        max - 数字。最多可以缓存多少组件实例。
    
    • 路由中使用:
    <keep-alive :include="includeList" :exclude="excludeList" :max="maxNum">
      <router-view />
    </keep-alive>
    
    • 态组件中使用:
    <keep-alive>
      <component :is="view"/>
    </keep-alive>
    

    源码分析

    // 源码 => vue/src/core/components/keep-alive.js 
    export default {
      name: 'keep-alive',
      abstract: true, //定义抽象组件 判断当前组件虚拟dom是否渲染成真实dom
    
      props: {
        include: patternTypes,
        exclude: patternTypes,
        max: [String, Number]
      },
    
      created () {
        this.cache = Object.create(null) // 缓存VNode
        this.keys = [] // 缓存VNode的key
      },
    
      destroyed () {
        // 销毁时删除所有缓存的VNode
        for (const key in this.cache) {
          pruneCacheEntry(this.cache, key, this.keys)
        }
      },
    
      mounted () {
        // 监听 include和exclude属性,及时的更新缓存
        // pruneCache 对cache做遍历,把不符合新规则的VNode从缓存中移除
        this.$watch('include', val => {
          pruneCache(this, name => matches(val, name))
        })
        this.$watch('exclude', val => {
          pruneCache(this, name => !matches(val, name))
        })
      },
    
      render () {
        const slot = this.$slots.default
        const vnode: VNode = getFirstComponentChild(slot) // 只处理第一个子元素
        const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
        // 存在组件配置选项
        if (componentOptions) {
          // 获取组件名
          const name: ?string = getComponentName(componentOptions)
          const { include, exclude } = this
          if (
            // not included || excluded
            (include && (!name || !matches(include, name))) || (exclude && name && matches(exclude, name))
          ) {
            return vnode
          }
    
          const { cache, keys } = this
          const key: ?string = vnode.key == null
            ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
            : vnode.key
          if (cache[key]) {
            vnode.componentInstance = cache[key].componentInstance
            // 将当前活跃组件的位置放入末尾
            remove(keys, key)
            keys.push(key)
          } else {
            cache[key] = vnode
            keys.push(key)
            // 超出缓存最大数量,移除最前面的VNode
            if (this.max && keys.length > parseInt(this.max)) {
              pruneCacheEntry(cache, keys[0], keys, this._vnode)
            }
          }
    
          // 标记
          vnode.data.keepAlive = true
        }
        return vnode || (slot && slot[0])
      }
    }
    

    分析

    1、created时创建cache、keys缓存容器,用于收集需要缓存的VNode。
    2、mounted时监听include、exclude,在缓存规则改变的时候过滤更新缓存集合cache、keys。其中pruneCachecache做遍历,它的核心是pruneCacheEntrypruneCacheEntry会调用需要过滤的组件实例的$destroy(),销毁组件实例。

    function pruneCache (keepAliveInstance: any, filter: Function) {
      const { cache, keys, _vnode } = keepAliveInstance
      for (const key in cache) {
        const cachedNode: ?VNode = cache[key]
        if (cachedNode) {
          const name: ?string = getComponentName(cachedNode.componentOptions)
          if (name && !filter(name)) {
            pruneCacheEntry(cache, key, keys, _vnode)
          }
        }
      }
    }
      
     function pruneCacheEntry (
      cache: VNodeCache,
      key: string,
      keys: Array<string>,
      current?: VNode
    ) {
      const cached = cache[key]
      if (cached && (!current || cached.tag !== current.tag)) {
        cached.componentInstance.$destroy()
      }
      cache[key] = null
      remove(keys, key)
    }
    

    3、destroyed 删除所有缓存的VNode,并调用对应组件实例的destory钩子函数。
    4、keep-alive不是使用常规的templete模版的方式,而是直接实现了一个render函数,所以每次触发渲染的时候都会执行render函数。render函数的作用就是获取第一个子组件,先对其进行include、exclude判断,接下来再判断如果不存在于cache中,则将其添加至cache的末尾;如果已存在于cache中,则将其调整至cache的末尾。这样做的目的是将较为活跃的缓存组件保存在cache的末尾,当缓存的组件数量大于设定的max值时,销毁cache中位置靠前(不活跃)的组件实例。
    5、keep-alive是一个抽象组件,不会生成真实的节点。由上知,keep-alive定义了abstract:true属性。那vue会忽略该组件的实例,因此不会在DOM树上生成相应的节点。

    // src/core/instance/lifecycle.js
    export function initLifecycle (vm: Component) {
        const options= vm.$options
        // 找到第一个非abstract父组件实例
        let parent = options.parent
        if (parent && !options.abstract) {
            while (parent.$options.abstract && parent.$parent) {
                  parent = parent.$parent
            }
            parent.$children.push(vm)
        }
        vm.$parent = parent
        // ...
    }
    

    keep-alive渲染过程

    Vue的渲染过程:new Vue -> init -> $mount -> compile -> render -> vnode -> patch -> DOM

    那被keep-alive包裹的组件和普通的组件有哪些不同呢?我想熟悉vue的人都应该能回答的上来:保存了组件内部的状态,被缓存后不会执行created、mounted等钩子函数,相应的会执行存activated、deactivated两个生命周期钩子函数。

    可那又是怎么实现的呢??我们带着问题接着往下看。

    举一例子:

    <template>
      <div id="app">
        <keep-alive>
          <component :is='view'></component>
          <p>这里是一个文本</p>
        </keep-alive>
        <el-button @click="changeView">切换组件</el-button>
      </div>
    </template>
    
    <script>
    import A from './views/A'
    import B from './views/B'
    export default {
      name: 'App',
      components: {
        A,
        B
      },
      data(){
        return {
          view:'A'
        }
      },
      methods: {
        changeView(){
          this.view = this.view === 'A' ? 'B' : 'A'
        }
      },
    };
    </script>
    

    首次渲染A:
    这里没有渲染p标签也验证了keep-alive只渲染第一个子组件。
    深入学习keep-alive

    Vue 的渲染最后都会到patch过程,而组件的patch过程会执行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 */)
        }
        if (isDef(vnode.componentInstance)) {
          initComponent(vnode, insertedVnodeQueue)
          insert(parentElm, vnode.elm, refElm)
          if (isTrue(isReactivated)) {
            reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
          }
          return true
        }
      }
    }
    

    注意其中定义的isReactivated变量,表示是否是已经被缓存的组件。由于是首次渲染,vnode.componentInstanceundefined,所以isReactivated为false。因此和普通组件的渲染的流程一样,接下来执行initComponent方法。

    // 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)
      }
    }
    

    接下来我们切换到组件B
    深入学习keep-alive

    由于B也是首次渲染,可以看到与普通组件几乎没有区别。多执行了activated生命周期。

    再次切换到A组件
    深入学习keep-alive

    这里在patch过程之前会执行prepatch的钩子函数。流程如下:

    // src/core/vdom/create-component
    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
    export function updateChildComponent (
      vm: Component,
      propsData: ?Object,
      listeners: ?Object,
      parentVnode: MountedComponentVNode,
      renderChildren: ?Array<VNode>
    ) {
      const hasChildren = !!(
        renderChildren ||          
        vm.$options._renderChildren ||
        parentVnode.data.scopedSlots || 
        vm.$scopedSlots !== emptyObject 
      )
    
      // ...
      if (hasChildren) {
        vm.$slots = resolveSlots(renderChildren, parentVnode.context)
        vm.$forceUpdate()
      }
    }
    

    updateChildComponent方法中,我们需要关注hasChildren部分。因为<keep-alive>组件本质上相当于存在一个default slot,所以hasChildren为真,即执行updateChildComponent时需要对slots做重新解析。并触发<keep-alive>组件实例$forceUpdate逻辑,也就是重新执行<keep-alive>render方法。然后命中cache缓存,直接返回缓存中的vnode.componentInstance,接着进入patch过程,执行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 */)
        }
        if (isDef(vnode.componentInstance)) {
          initComponent(vnode, insertedVnodeQueue)
          insert(parentElm, vnode.elm, refElm)
          if (isTrue(isReactivated)) {
            reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
          }
          return true
        }
      }
    }
    

    和首次渲染不同的是,这里isReactivatedtrue,在执行initComponent函数的时候不会再执行组件的mount过程了,这也就是被<keep-alive>包裹的组件在有缓存的时候就不会在执行组件的created、mounted等钩子函数的原因了。

    然后执行reactivateComponent

    // src/core/vdom/patch.js
    function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
      let i
      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通过执行insert(parentElm,vnode.elm, refElm)就把缓存的 DOM对象直接插入到目标元素中。

    以上完成了数据更新的情况下的渲染过程,那activated、deactivated生命周期是在什么情况下执行的呢?上面说到,在渲染的最后一步,会执行invokeInsertHook(vnode,insertedVnodeQueue,isInitialPatch)函数执行vnodeinsert钩子函数:

    // src/core/vdom/create-component.js
    const componentVNodeHooks = {
      insert (vnode: MountedComponentVNode) {
        const { context, componentInstance } = vnode
        if (!componentInstance._isMounted) {
          componentInstance._isMounted = true
          callHook(componentInstance, 'mounted')
        }
        if (vnode.data.keepAlive) {
          if (context._isMounted) {
            // vue-router#1212
            // During updates, a kept-alive component's child components may
            // change, so directly walking the tree here may call activated hooks
            // on incorrect children. Instead we push them into a queue which will
            // be processed after the whole patch process ended.
            queueActivatedComponent(componentInstance)
          } else {
            activateChildComponent(componentInstance, true /* direct */)
          }
        }
      },
      // ...
    }
    

    这里判断如果是被<keep-alive>包裹的组件已经mounted,那么则执行queueActivatedComponent(componentInstance) ,否则执行activateChildComponent(componentInstance, 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')
      }
    }
    

    可以看到这里就是执行组件的acitvated钩子函数,并且递归去执行它的所有子组件的activated 钩子函数。

    //src/core/observer/scheduler.js
    export function queueActivatedComponent (vm: Component) {
      vm._inactive = false
      activatedChildren.push(vm)
    }
    

    这个逻辑很简单,把当前vm实例添加到activatedChildren 数组中,等所有的渲染完毕,在nextTick后会执行flushSchedulerQueue

    function flushSchedulerQueue () {
      // ...
      const activatedQueue = activatedChildren.slice()
      callActivatedHooks(activatedQueue)
      // ...
    } 
    
    function callActivatedHooks (queue) {
      for (let i = 0; i < queue.length; i++) {
        queue[i]._inactive = true
        activateChildComponent(queue[i], true)  }
    }
    

    也就是遍历所有的activatedChildren,执行activateChildComponent方法。通过队列调的方式就是把整个activated时机延后了。

    activated钩子函数,也就有对应的deactivated钩子函数,它是发生在vnodedestory钩子函数:

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

    对于<keep-alive>包裹的组件而言,它会执行deactivateChildComponent(componentInstance, true)方法

    // src/core/instance/lifecycle.js
    export function deactivateChildComponent (vm: Component, direct?: boolean) {
      if (direct) {
        vm._directInactive = true
        if (isInInactiveTree(vm)) {
          return
        }
      }
      if (!vm._inactive) {
        vm._inactive = true
        for (let i = 0; i < vm.$children.length; i++) {
          deactivateChildComponent(vm.$children[i])
        }
        callHook(vm, 'deactivated')
      }
    }
    

    总结

    通过以上分析,我们已经知道keep-alive组件能够缓存子组件,也知道了它实现的原理。在需要反复创建组件的时候可以使用keep-alive提高性能。

    这个时候我又想到了v-ifv-show:

    v-if: 渲染开销小,切换即重新渲染, 切换开销大。
    v-show:一开始便渲染所有,渲染开销大。
    keep-alive: 初始渲染开销小,切换时如果不存在缓存则开始渲染,如果已存在缓存则读取缓存。
    

    可以看到keep-alive似乎就是存在于v-ifv-show之间的一个综合考虑渲染开销和切换开销的特殊存在。

    参考

    • vue技术揭秘

    下载网 » 深入学习keep-alive

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元