Vue之keep-alive

使用场景:

  1. 动态组件切换,当你选择了一篇文章,切换到 b 标签,然后再切换回 a,是不会继续展示你之前选择的文章的。这是因为你每次切换新标签的时候,Vue 都创建了一个新的 currentTabComponent 实例;
  2. 当我们第一次进入列表页需要请求一下数据,当我从列表页进入详情页,详情页不缓存也需要请求下数据,然后返回列表页,这时候我们使用keep-alive来缓存组件,防止二次渲染,这样会大大的节省性能。
  3. 使用一些tab页频繁的切换,但是数据不会频繁的刷新

作用:

缓存组件内部状态,避免重新渲染=>是一个抽象组件,自身不会渲染一个DOM元素,也不会出现在父组件链中

用法:

  1. 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们
  2. 缓存所有路径匹配到的路由组件,包括路由组件里面的组件,
   <!-- 基本 -->
    <keep-alive>
      <component :is="view"></component>
    </keep-alive>
    
    <!-- 多个条件判断的子组件 -->
    <keep-alive>
      <comp-a v-if="a > 1"></comp-a>
      <comp-b v-else></comp-b>
    </keep-alive>
    <!— 路由 -->
    <keep-alive>
        <router-view></router-view>
    </keep-alive>
    // routes 配置
    export default [
      {
        path: '/',
        name: 'home',
        component: Home,
        meta: {
          keepAlive: true // 需要被缓存
        }
      }, {
        path: '/:id',
        name: 'edit',
        component: Edit,
        meta: {
          keepAlive: false // 不需要被缓存
        }
      }
    ]
    // 多层嵌套路由会出现问题,不缓存
    <keep-alive>
        <router-view v-if="$route.meta.keepAlive">
            <!-- 这里是会被缓存的视图组件,比如 Home! -->
        </router-view>
    </keep-alive>

    <router-view v-if="!$route.meta.keepAlive">
        <!-- 这里是不被缓存的视图组件,比如 Edit! -->
    </router-view>

参数:缓存想要缓存的路由

  1. include:匹配的路由/组件会被缓存
  2. exclude:匹配的路由/组件不会被缓存
  3. max:最大缓存数
  4. 使用方法:
    • 采用逗号分隔的字符串形式
    • 正则形式,必须采用v-bind形式使用
    • 数组形式,必须采用v-bind形式使用
  5. 匹配规则:
    • 首先匹配组件的name选项
    • 如果name选项不可用,则匹配它的局部注册名称(父组件components选项的键值)
    • 匿名组件,不可匹配(路由组件没有name选项,并且没有注册的组件名)
    • 只能匹配当前被包裹的组件,不能匹配更下面嵌套的子组件=>例如:只能匹配路由组件的name选项,不能匹配路由组件里面的嵌套组件name选项
    • 不会在函数式组件中正常工作,因为他们没有缓存实例
    • exclude的优先级>include
  <!-- 逗号分隔字符串 -->
    <keep-alive include="a,b">
      <component :is="view"></component>
    </keep-alive>
    
    <!-- 正则表达式 (使用 `v-bind`) -->
    <keep-alive :include="/a|b/">
      <component :is="view"></component>
    </keep-alive>
    
    <!-- 数组 (使用 `v-bind`) -->
    <keep-alive :include="['a', 'b']">
      <component :is="view"></component>
    </keep-alive>
    
    <!— 缓存路由 -->
    <keep-alive include='a'>
        <router-view></router-view>
    </keep-alive>

钩子函数:

在被keep-alive包含的组件/路由里,多出了两个生命周期的钩子:activated和deactivated

  1. activated在组件第一次渲染时会被调用,之后在每次缓存组件被激活时调用
 // 第一次进入缓存路由/组件,在mounted后面,beforeRouteEnter守卫传给next的回调函数之前
 beforeMount=> 如果你是从别的路由/组件进来(组件销毁destroyed/或离开缓存deactivated)=>mounted=> activated 进入缓存组件 => 执行 beforeRouteEnter回调
 //  因为组件被缓存了,再次进入缓存路由/组件时,不会触发这些钩子 beforeCreate create beforeMount mounted
 组件销毁destroyed/或离开缓存deactivated => activated 进入当前缓存组件  => 执行beforeRouteEnter回调 ( 组件缓存或销毁,嵌套组件的销毁和缓存也在这里触发)
  1. deactivated:组件被停用(离开路由)时调用
    • 使用了keep-alive就不会调用beforeDestory和destroyed,因为组件没被销毁,被缓存起来了=>可以看作beforeDestory的替代,如果缓存了组件,要在组件销毁的时候做一些事件,可以放在这个钩子
    组件内的离开当前路由钩子beforeRouteLeave =>  路由前置守卫 beforeEach =>
    全局后置钩子afterEach => deactivated 离开缓存组件 => activated 进入缓存组件(如果你进入的也是缓存路由)
    // 如果离开的组件没有缓存的话 beforeDestroy会替换deactivated 
    // 如果进入的路由也没有缓存的话  全局后置钩子afterEach=>销毁 destroyed=> beforeCreate等

实现原理:

// 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) // 缓存虚拟dom
    this.keys = [] // 缓存的虚拟dom的键集合
  },

  destroyed () {
    for (const key in this.cache) { // 删除所有的缓存
      pruneCacheEntry(this.cache, key, this.keys) // 遍历调用pruneCacheEntry函数删除=>删除缓存VNode并执行对应组件实例的destory钩子函数
    }
  },

  mounted () {
    // 实时监听黑白名单的变动
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name)) // 实时更新/删除this.cache对象数据
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  // src/core/components/keep-alive.js
  render () {
    const slot = this.$slots.default // 获取插槽
    const vnode: VNode = getFirstComponentChild(slot) // 获取keep-alive包裹着的第一个子组件对象及其组件名
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) { // 存在组件参数
      // 获取组件名
      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
      }
      // 根据组件ID和tag生成缓存key,并在缓存对象中查找是否已缓存过该组件实例
      const { cache, keys } = this
      const key: ?string = vnode.key == null // 定义组件的缓存key
        // 相同的钩子函数可能会被作为不同的组件,所以仅仅cid是不够的
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      // 如果存在该组件实例,直接取出缓存值并更新该key在this.keys中的位置(更新key的位置是实现LRU置换策略的关键)
      if (cache[key]) { // 已经缓存过该组件
        vnode.componentInstance = cache[key].componentInstance
        remove(keys, key)
        keys.push(key) // 调整key排序
      } else {
        // 在this.chche对象中存储该组件实例并保存key值
        cache[key] = vnode // 缓存组件对象
        keys.push(key)
        // 检查缓存的实例数量是否超过max的设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key)
        if (this.max && keys.length > parseInt(this.max)) { // 超过缓存数限制,将第一个删除
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }
      // 将该组件实例的keepAlive属性值设为true
      vnode.data.keepAlive = true // 渲染和执行被包裹组件的钩子函数需要用到
    }
    return vnode || (slot && slot[0])
  }   
}

keep-alive组件的渲染=>不会生成真正的DOM节点

// src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
  const options = vm.$options
  // 找到第一个非abstract的父组件实例
  let parent = options.parent
  // 在keep-alive中,设置了abstract:true,Vue就会跳过该组件实例=>最后构建的组件树中就不会包含keep-alive组件,那么由组件树渲染的DOM树自然也不会有keep-alive相关的节点了
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }
  vm.$parent = parent
  // ...
}

keep-alive包裹的组件使用缓存:在patch阶段中,会执行createConponent函数

 // src/core/vdom/patch.js
 function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    // 在首次加载被包裹组件时,由keep-alive中的render函数可知,vnode.componentInstance的值是undefined,keepAlive的值是true,因为keep-alive作为父组件,它的render函数会先于被包裹组件执行,那么就执行到i(vnode, false /* hydrating */),后面的逻辑就不再执行
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        i(vnode, false /* hydrating */) // 走正常的init钩子函数执行组件的mount
      }
      // 再次访问被包裹的组件时,vnode.componentInstance的值就已经缓存的组件实例,那么会往下执行
      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm) // 将缓存的DOM(vnode.elm)插入父元素中
        if (isTrue(isReactivated)) {
          // reactivateComponent函数中会执行insert(parentElm, vnode.elm, refElm) 把缓存的 DOM 对象直接插入到目标元素中
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) 
        }
        return true
      }
    }
  }

不执行组件的created、mounted等钩子函数的原因:

// src/core/vdom/create-component.js
const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    // 不再进入$mount过程,mounted之前的钩子函数(beforeCreate、created、mounted)都不再执行
    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)
    }
  }
  // ...
}

activated钩子函数=>执行时机是 包裹的组件渲染的时候

// src/core/vdom/patch.js
  // 调用组件实例(VNode)自身的insert钩子函数
  function invokeInsertHook (vnode, queue, initial) {
    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])  // 调用VNode自身的insert钩子函数
      }
    }
  }
// src/core/vdom/create-component.js
const componentVNodeHooks = {
  // init()
  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) { 
      // 判断<keep-alive>包裹的组件是否已经mounted
      if (context._isMounted) {
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  // ...
}
// 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
  }
  // 递归地执行它的所有子组件的activated钩子函数
  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')
  }
}

常用场景及方法:

场景1:
切换tab,进行缓存,但又希望可以刷新数据
解决办法:
1.给用户机会触发刷新,增加下拉加载刷新事件
2.将获取数据的操作写在activated步骤

场景2:
前进刷新,后退缓存用户浏览数据
搜索页面==>到搜索结果页时,搜索结果页面要重新获取数据,
搜索结果页面==>点击进入详情页==>从详情页返回列表页时,要保存上次已经加载的数据和自动还原上次的浏览位置。

<keep-alive> 
    <router-view v-if="$route.meta.keepAlive"/> 
</keep-alive> 
<router-view v-if="!$route.meta.keepAlive"/>
// list是我们的搜索结果页面 
// router.js
{
  path: '/list',
  name: 'List',
  component: List,
  meta: {
    isUseCache: false, // 默认不缓存
    keepAlive: true  // 是否使用 keep-alive
  }
}
// list组件的activated钩子
activated() { 
  //isUseCache为false时才重新刷新获取数据
  //因为对list使用keep-alive来缓存组件,所以默认是会使用缓存数据的 
  if(!this.$route.meta.isUseCache){ 
    this.list = []; // 清空原有数据
    this.onLoad(); // 这是我们获取数据的函数 
  } 
  this.$route.meta.isUseCache = false // 通过这个控制刷新
},
// list组件的beforeRouteLeave钩子函数
// 跳转到详情页时,设置需要缓存 => beforeRouterLeave:离开当前路由时 => 导航在离开该组件的对应路由时调用,可以访问组件实例this=>用来禁止用户离开,比如还未保存草稿,或者在用户离开前把定时器销毁
beforeRouteLeave(to, from, next){
  if(to.name=='Detail'){
    from.meta.isUseCache = true
  }
  next()
}

场景3:
事件绑定了很多次,比如上传点击input监听change事件,突然显示了多张相同图片的问题
也就是说,DOM在编译后就缓存在内容中了,如果再次进入还再进行事件绑定初始化则就会发生这个问题
解决办法: 在mounted中绑定事件,因为这个只执行一次,并且DOM已准备好。如果插件绑定后还要再执行一下事件的handler函数的话,那就提取出来,放在activated中执行。比如:根据输入内容自动增长textarea的高度,这部分需要监听textarea的input和change事件,并且页面进入后还要再次执行一次handler函数,更新textarea高度(避免上次输入的影响)。

参考链接:
https://ustbhuangyi.github.io/vue-analysis/extend/keep-alive.html#内置组件
https://juejin.im/post/5b41bdef6fb9a04fe63765f1

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值