Keep-alive of Vue

scenes to be used:

  1. Dynamic component switching, when you select an article, switch to the b tag, and then switch back to a, the article you previously selected will not continue to be displayed. This is because every time you switch a new tab, Vue creates a new currentTabComponent instance;
  2. When we enter the list page for the first time, we need to request data. When I enter the detail page from the list page, the detail page needs to request data without being cached, and then return to the list page. At this time, we use keep-alive to cache the component to prevent two This will greatly save performance.
  3. Use some tab pages to switch frequently, but the data will not be refreshed frequently

effect:

Cache the internal state of the component to avoid re-rendering => is an abstract component, it will not render a DOM element by itself, nor will it appear in the parent component chain

usage:

  1. When wrapping dynamic components, inactive component instances are cached instead of destroying them
  2. Cache all routing components matched by paths, including components in routing components,
   <!-- 基本 -->
    <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>

Parameters: cache the route you want to cache

  1. include: matching routes/components will be cached
  2. exclude: matching routes/components will not be cached
  3. max: Maximum number of caches
  4. Instructions:
    • Use comma-separated string form
    • Regular form, must be used in v-bind form
    • Array form, must be used in v-bind form
  5. Matching rules:
    • First match the name option of the component
    • If the name option is not available, match its local registered name (the key value of the parent component components option)
    • Anonymous component, unmatched (the routing component has no name option and no registered component name)
    • Can only match the currently wrapped component, not the nested subcomponents below => For example: only match the name option of the routing component, not the nested component name option in the routing component
    • Will not work properly in functional components because they do not have cached instances
    • Priority of 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>

Hook function:

In the components/routes included in keep-alive, there are two more life cycle hooks: activated and deactivated

  1. activated is called when the component is rendered for the first time, and then every time the cached component is activated
 // 第一次进入缓存路由/组件,在mounted后面,beforeRouteEnter守卫传给next的回调函数之前
 beforeMount=> 如果你是从别的路由/组件进来(组件销毁destroyed/或离开缓存deactivated)=>mounted=> activated 进入缓存组件 => 执行 beforeRouteEnter回调
 //  因为组件被缓存了,再次进入缓存路由/组件时,不会触发这些钩子 beforeCreate create beforeMount mounted
 组件销毁destroyed/或离开缓存deactivated => activated 进入当前缓存组件  => 执行beforeRouteEnter回调 ( 组件缓存或销毁,嵌套组件的销毁和缓存也在这里触发)
  1. deactivated: called when the component is deactivated (leaves the route)
    • If you use keep-alive, beforeDestory and destroyed will not be called, because the component is not destroyed and is cached => it can be regarded as a replacement for beforeDestory. If the component is cached, you need to do some events when the component is destroyed. This hook
    组件内的离开当前路由钩子beforeRouteLeave =>  路由前置守卫 beforeEach =>
    全局后置钩子afterEach => deactivated 离开缓存组件 => activated 进入缓存组件(如果你进入的也是缓存路由)
    // 如果离开的组件没有缓存的话 beforeDestroy会替换deactivated 
    // 如果进入的路由也没有缓存的话  全局后置钩子afterEach=>销毁 destroyed=> beforeCreate等

Implementation principle:

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

Rendering of keep-alive components => no real DOM nodes will be generated

// 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 packaged components use cache: in the patch phase, the createConponent function will be executed

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

Reasons for not executing hook functions such as created and mounted of components:

// 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 hook function => execution timing is when the packaged component is rendered

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

Common scenarios and methods:

Scenario 1:
Switch tabs, cache, but hope to refresh the data.
Solutions:
1. Give the user a chance to trigger refresh and add a pull-down load refresh event
2. Write the data acquisition operation in the activated step

Scenario 2:
Forward refresh, backward cache user browsing data
Search page ==> When you reach the search result page, the search result page needs to retrieve data again, the
search result page ==> Click to enter the details page ==> When returning to the list page from the details page , To save the last loaded data and automatically restore the last browsing location.

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

Scenario 3: The
event has been bound many times. For example, when uploading and clicking input to monitor the change event, multiple pictures of the same picture are suddenly displayed
. In other words, the DOM is cached in the content after compilation, and the event will be bound if it is entered again. This problem will occur if the initialization is fixed. The
solution: bind the event in mounted, because this is only executed once, and the DOM is ready. If the handler function of the event needs to be executed after the plug-in is bound, it will be extracted and executed in activated. For example: automatically increase the height of the textarea according to the input content, this part needs to monitor the input and change events of the textarea, and execute the handler function again after the page is entered to update the textarea height (to avoid the impact of the previous input).

Reference link:
https://ustbhuangyi.github.io/vue-analysis/extend/keep-alive.html#Built-in components
https://juejin.im/post/5b41bdef6fb9a04fe63765f1

Guess you like

Origin blog.csdn.net/zn740395858/article/details/90141539
Recommended