vue keep-alive实现原理

359 阅读3分钟

1.vue的渲染原理

  1. 通过.vue模版文件转换为AST抽象语法树
  2. AST转换成render函数
  3. render函数生成vnode 虚拟dom
  4. 通过patch比较当前真实与虚拟dom差异,做dom操作更新成vnode需要的内容。

2.keep-alive定义

  1. 是一个vue的全局组件,也是包含生命周期方法,
  2. keep-alive被定义为一个抽象的vnode 不做渲染,不出现在父组件上。
  3. 他主要是用来包裹标签内的元素,使内部的节点缓存起来,组件离开时候不销毁。

3.keep-alive使用方法

<keep-alive>
    <router-view></router-view> 
</keep-alive>
  1. include:可传字符串、正则表达式、数组,名称匹配成功的组件会被缓存
  2. exclude:可传字符串、正则表达式、数组,名称匹配成功的组件不会被缓存
  3. max:可传数字,限制缓存组件的最大数量

官方使用说明:
cn.vuejs.org/v2/guide/co…

在线演示:
codesandbox.io/s/reverent-…

4.实例

<!DOCTYPE html>
<html>
  <head>
    <title>Vue Component Blog Post Example</title>
    <!-- <script src="../../dist/vue.js"></script>本地源码调试  -->

    <script src="https://unpkg.com/vue@2"></script>
    <style>
      .tab-button {
  padding: 6px 10px;
  border-top-left-radius: 3px;
  border-top-right-radius: 3px;
  border: 1px solid #ccc;
  cursor: pointer;
  background: #f0f0f0;
  margin-bottom: -1px;
  margin-right: -1px;
}
.tab-button:hover {
  background: #e0e0e0;
}
.tab-button.active {
  background: #e56d6d;
}
.tab {
  border: 1px solid #ccc;
  padding: 10px;
}
.posts-tab {
  display: flex;
}
.posts-sidebar {
  max-width: 40vw;
  margin: 0;
  padding: 0 10px 0 0;
  list-style-type: none;
  border-right: 1px solid #ccc;
}
.posts-sidebar li {
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
  cursor: pointer;
}
.posts-sidebar li:hover {
  background: #eee;
}
.posts-sidebar li.selected {
  background: rgb(69, 132, 153);
}
.selected-post-container {
  padding-left: 10px;
} 

    </style>
  </head>
  <body>
    <div id="dynamic-component-demo">
      <button
        v-for="tab in tabs"
        v-bind:key="tab"
        v-bind:class="['tab-button', { active: currentTab === tab }]"
        v-on:click="currentTab = tab"
      >
        {{ tab }}
      </button>

      <keep-alive  >
        <component v-bind:is="currentTabComponent" class="tab"></component>
      </keep-alive>
    </div>

    <script>
      Vue.component("aaa", {
        data: function () {
          return {
            list: [
              {
                id: 1,
                title: "111",
                content:
                  "<p>111内容</p>"
              },
              {
                id: 2,
                title: "222",
                content:
                  "<p>222内容</p>"
              },
              {
                id: 3,
                title: "333",
                content:
                  "<p>333内容</p>"
              }
            ],
            selectedPost: null
          };
        },
        template: `
  	<div class="posts-tab">
      <ul class="posts-sidebar">
        <li
          v-for="post in list"
          v-bind:key="post.id"
          v-bind:class="{ selected: post === selectedPost }"
					v-on:click="selectedPost = post"
        >
          {{ post.title }}
        </li>
      </ul>
      <div class="selected-post-container">
      	<div
        	v-if="selectedPost"
          class="selected-post"
        >
          <h3>{{ selectedPost.title }}</h3>
          <div v-html="selectedPost.content"></div>
        </div>
        <strong v-else>
          Click on a blog title to the left to view it.
        </strong>
      </div>
    </div>
  `
      });

      Vue.component("bbb", {
        template: "<div>bbb内容</div>"
      });

      Vue.component("ccc", {
        template: "<div>ccc内容</div>"
      });
      new Vue({
        el: "#dynamic-component-demo",
        data: {
          currentTab: "aaa",
          tabs: ["aaa", "bbb", "ccc"]
        },
        computed: {
          currentTabComponent: function () {
            return  this.currentTab.toLowerCase();
          }
        }
      });
    </script>
  </body>
</html>

  1. 先点击aaa 和 333 image.png

  2. 再点击bbb

image.png

  1. 最后再点击aaa,之前的选择333 被保留下来 没有重新实例 image.png

5.keep-alive执行逻辑

  1. .vue模版文件 -> AST -> render函数 -> vnode (注意这里个vnode 是keepalive)
  2. 通过path (createPatchFunction -> createComponent -> init) 把 vnode转为真实dom
  3. keepalive 调用 $mounted 触发 vm._update(vm._render(), hydrating);
  4. 最后回调回keep-alive自己定义的 render()方法
  5. 找keepalive 插槽下的第一个子节点vnode,aaa。
  6. 同时查看是否满足缓存策略,不满足直接返回子节点vnode,待vue框架去重新生成componentInstance实例
  7. 如果满足才去读取和更新cache缓存(LRU),把componentInstance赋给vnode, 同时给子vnode打上 vnode.data.keepAlive = ture(为下次vue触发刷新渲染时,识别不重新调用render,直接patch和mounted)
  8. 第一个子组件aaa 开始实例化,render -> vnode -> $mounted 显示
  9. 点击第二个子组件bbb 由于全局的vue和切换数组是同一个大组件内,会触发vm._update (keepalive再次触发render方法)
  10. 重复步骤4, 第二个子组件bbb 开始实例化,render -> vnode -> $mounted 显示
  11. 这时候 点击切回第二个子组件aaa,再次调用keep-alive自己定义的 render()方法
  12. 但由于缓存存在aaa的vnode的实例componentInstance,直接取出赋值给aaa,不再调用render方法生成。
  13. 并且在componentVNodeHooks init方法通过 vnode.data.keepAlive 判断,跳过render,直接patch。

关键区分缓存vnode 读取的判断地方
src/core/vdom/create-component.js

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive //根据keepAlive == true标识
    ) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode) //直接转dom
    } else {
       //没有缓存 走正常render方法渲染生成vnode 再挂载。
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },
...

6.源码学习

vue.2.6
src/core/components/keep-alive.js

/* @flow */

import { isRegExp, remove } from 'shared/util'
import { getFirstComponentChild } from 'core/vdom/helpers/index'

type VNodeCache = { [key: string]: ?VNode };

function getComponentName (opts: ?VNodeComponentOptions): ?string {
  return opts && (opts.Ctor.options.name || opts.tag)
}
//支持 字符串 1,2,3 匹配
//也支持 数组[1,2,3] 匹配
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
}
//所有:根据当前是keepalive实例,遍历所有缓存实例,拿到对应name 通过matches方法判断是否匹配,不匹配则移除缓存。
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)
}

const patternTypes: Array<Function> = [String, RegExp, Array]

export default {
  name: 'keep-alive',
  abstract: true,

    //初始化传入参数
  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]  //最大可缓存数量
  },

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

  destroyed () {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys) //当整个keepalive 销毁 同步移除所有缓存
    }
  },

  mounted () {
    this.$watch('include', val => { //前端项目动态设置include 或 exclude后,会实时通知keep-alive刷新自己的
      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) {
      // 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
      
      //这里的key = tag + name 组合,主要为了实现LRU算法
      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
        remove(keys, key)
        keys.push(key)
      } else {
        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])
  }
}

相关算法

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。