当一个组件在视图中消失的时候会出现什么情况呢?

  1. <template>
  2. <div class="tab">
  3. <div class="nav">
  4. <div
  5. v-for="(value, key) in navData"
  6. :key="key"
  7. :class="['nav-item', { active: key === currentTab }]"
  8. @click="setCurrentTab(key)">
  9. {{ value }}
  10. </div>
  11. </div>
  12. <div class="component">
  13. <component :is="currentComponent"></component>
  14. </div>
  15. </div>
  16. </template>
  17. <script>
  18. import Intro from "./components/Intro/index.vue";
  19. import Article from "./components/Article/index.vue"
  20. import List from "./components/List/index.vue"
  21. export default {
  22. components: {
  23. Intro,
  24. Article,
  25. List
  26. },
  27. data() {
  28. return {
  29. currentTab: "intro",
  30. navData: {
  31. intro: "Intro",
  32. article: "Article",
  33. list: "List"
  34. }
  35. };
  36. },
  37. computed: {
  38. currentComponent() {
  39. switch (this.currentTab) {
  40. case "intro":
  41. return Intro;
  42. case "article":
  43. return Article;
  44. case "list":
  45. return List;
  46. }
  47. }
  48. },
  49. methods: {
  50. // 切换 Tba 的值
  51. setCurrentTab(key) {
  52. this.currentTab = key;
  53. }
  54. }
  55. };
  56. </script>
  1. export default {
  2. name: "Intro",
  3. mounted() {
  4. console.log("Intro mounted!");
  5. },
  6. unmounted() {
  7. console.log("Intro unmounted!");
  8. }
  9. };

image.png
当我们从 Intero Tab 切换到 Article Tab 的的时候,能够看到 Intro 组件的mountedunmounted都进行了触发。
这是因为<component>组件动态渲染的时候是不会进行缓存组件的实例,每次切换 Tab 的时候组件都会进行重新初始化!

之前我们说过 Vue 模版渲染的大致流程:

  • createApp创建应用实例;
  • template生成 AST 树(AST 树主要描述了template是什么样子的,这有点像虚拟节点,AST 树上有很多 Vue 本身的东西,例如 v-if、v-for、v-show、v-on…);
  • 形成 AST 树后才能根据树的内容最后变为 JS 的逻辑,并且过滤掉浏览器不认识的结构;
  • 最后 AST 树 => vNode 虚拟节点 => vDom 虚拟元素 => rDom 真实元素;

每当视图要更新又会执行如下的流程:

  • 数据变化更新内容 => 产生 vNode => old vNode => compare 进行比较 => diff 寻找差异 => patch 打补丁 => 更新 rDom 描述 => 根据 patch => 更新真实 Dom

回到上面的案例,我们在切换 Tab 的时候,Vue 底层会进行计算,判断 vNode 有没有发生变化:

  • 没有:重新组装 vNode => 更新 DOM
  • 有:现有的 vNode => 更新 DOM
    • <keep-alive>会缓存当前组件的 vNode,不再进行重新初始化渲染

所以我们可以把<component><keep-alive>进行包裹:

  1. <!-- 其他内容 -->
  2. <tempalte>
  3. <div class="component">
  4. <keep-alive>
  5. <component :is="currentComponent"></component>
  6. </keep-alive>
  7. </div>
  8. </tempalte>

钩子函数

因为<keep-alive>会缓存组件的实例,不再进行初始化,所以当我们切换 Tab 的时候 Intro 组件的mountedunmounted生命周期函数将不会再执行,取而代之的是<keep-alive>的钩子函数activateddeactivated

  1. export default {
  2. name: "Intro",
  3. mounted() {
  4. console.log("Intro mounted!");
  5. },
  6. unmounted() {
  7. console.log("Intro unmounted!");
  8. },
  9. activated() {
  10. console.log("Intro activated!");
  11. },
  12. deactivated() {
  13. console.log("Intro deactivated!");
  14. }
  15. };

image.png
因为 Intro 组件没有进行销毁,所以不会执行unmounted函数!

属性

<keep-alive>允许我们传递 prop:
1、excludle表示排除某个组件不进行缓存;
excludle属性接收组件的名称,默认是找组件的name属性,如果组件内部没有name属性则找components注册时提供的key值!

  1. <!-- 接收字符串,多个组件名称用 , 分割 -->
  2. <keep-alive exclude="Intro,List">
  3. <component :is="currentComponent"></component>
  4. </keep-alive>
  1. <!-- 接收数组 -->
  2. <keep-alive :exclude="['Intro', 'List']">
  3. <component :is="currentComponent"></component>
  4. </keep-alive>
  1. <!-- 接收正则 -->
  2. <keep-alive :exclude="/n|c/">
  3. <component :is="currentComponent"></component>
  4. </keep-alive>

2、include表示包含某个组件进行缓存;
includeexcludle属性一致,都按照优先组件的name属性,如果组件内部没有name属性则找components注册时提供的key值!

  1. <!-- 接收字符串,多个组件名称用 , 分割 -->
  2. <keep-alive include="Intro,List">
  3. <component :is="currentComponent"></component>
  4. </keep-alive>
  1. <!-- 接收数组 -->
  2. <keep-alive :include="['Intro', 'List']">
  3. <component :is="currentComponent"></component>
  4. </keep-alive>
  1. <!-- 接收正则 -->
  2. <keep-alive :include="/n|c/">
  3. <component :is="currentComponent"></component>
  4. </keep-alive>

:::warning ⚠️ 注意
excludeinclude都推荐数组的写法! :::

3、max表示限制可被缓存的最大组件实例数量;
如果缓存达到了 n 个组件,在创建新的组件实例之前,缓存组件时间最久的且没有被访问的组件实例会被销毁!

  1. <keep-alive :max="2">
  2. <component :is="currentComponent"></component>
  3. </keep-alive>

模拟简易的 keep-alive

  1. Demo
  2. ├─ index.html
  3. ├─ package-lock.json
  4. ├─ package.json
  5. └─ src
  6. ├─ components
  7. ├─ Comp1.js
  8. ├─ Comp2.js
  9. └─ Comp3.js
  10. └─ mian.js

我们的 Demo 使用 Vite 进行编译启动,package.json 文件内容如下:

  1. {
  2. "name": "Demo",
  3. "version": "1.0.0",
  4. "description": "",
  5. "main": "index.js",
  6. "scripts": {
  7. "test": "echo \"Error: no test specified\" && exit 1",
  8. "dev": "vite"
  9. },
  10. "keywords": [],
  11. "author": "",
  12. "license": "ISC",
  13. "devDependencies": {
  14. "vite": "^4.3.5"
  15. }
  16. }

首先现在 index.html 文件中搭建一个 Tab 的布局:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. <div id="app">
  11. <!-- Tab 按钮区域 -->
  12. <div id="buttons">
  13. <span data-key="comp1">COMP 1</span>
  14. <span data-key="comp2">COMP 2</span>
  15. <span data-key="comp3">COMP 3</span>
  16. </div>
  17. <div id="wrapper"></div>
  18. </div>
  19. <script type="module" src="./src/mian.js"></script>
  20. </body>
  21. </html>

index.html 文件逻辑都放到了 main.js 文件中:

  1. const app = {
  2. }

首先,我们需要导入 components 下面的 3 个组件:

  1. import comp1 from "./components/Comp1";
  2. import comp2 from "./components/Comp2";
  3. import comp3 from "./components/Comp3";
  4. const App = {
  5. components: {
  6. comp1,
  7. comp2,
  8. comp3
  9. }
  10. };

然后我们需要获取页面中三个按钮元素,并绑定点击事件:

  1. import comp1 from "./components/Comp1";
  2. import comp2 from "./components/Comp2";
  3. import comp3 from "./components/Comp3";
  4. const oBtns = document.getElementById("buttons");
  5. const oWrapper = document.getElementById("wrapper");
  6. const App = {
  7. components: {
  8. comp1,
  9. comp2,
  10. comp3
  11. },
  12. bindEvent() {
  13. oBtns.addEventListener("click", this.handleBtnClick.bind(this), false);
  14. },
  15. handleBtnClick(e) {
  16. const tar = e.target;
  17. const tagName = tar.tagName.toLowerCase();
  18. }
  19. };

拿到 btn 按钮后,我们要把组件的内容转换为一个简易的 vNode 对象:

  1. import comp1 from "./components/Comp1";
  2. import comp2 from "./components/Comp2";
  3. import comp3 from "./components/Comp3";
  4. const oBtns = document.getElementById("buttons");
  5. const oWrapper = document.getElementById("wrapper");
  6. const App = {
  7. components: {
  8. comp1,
  9. comp2,
  10. comp3
  11. },
  12. bindEvent() {
  13. oBtns.addEventListener("click", this.handleBtnClick.bind(this), false);
  14. },
  15. handleBtnClick(e) {
  16. const tar = e.target;
  17. const tagName = tar.tagName.toLowerCase();
  18. if (tagName === "span") {
  19. const key = tar.dataset.key;
  20. const vNode = this.setVNode(this.components[key]); // 传递组件对象
  21. const rNode = this.setRNode(vNode); // 传递 vNode 对象
  22. oWrapper.innerHTML = "";
  23. oWrapper.appendChild(rNode); // 渲染 rNode
  24. }
  25. },
  26. setVNode(comp) {
  27. const { template, name } = comp;
  28. const regTag = template.match(/\<(.+?)\>/)[1]; // 获取标签的名称
  29. const regContent = template.match(/\>(.+?)\</)[1]; // 获取元素的内容
  30. // 例如 {tag:'h1', children:'This is COMP-1', mark:'Comp1'}
  31. return {
  32. tag: regTag,
  33. children: regContent,
  34. mark: name
  35. };
  36. },
  37. setRNode(vNode) {
  38. const { tag, children, mark } = vNode;
  39. // 根据 tag 创建对应的 DOM 元素
  40. const node = document.createElement(tag);
  41. node.innerText = children;
  42. // 然后创建后的 DOM
  43. return node;
  44. }
  45. };

最后我们还需要执行一下我们整个 App 对象:

  1. import comp1 from "./components/Comp1";
  2. import comp2 from "./components/Comp2";
  3. import comp3 from "./components/Comp3";
  4. const oBtns = document.getElementById("buttons");
  5. const oWrapper = document.getElementById("wrapper");
  6. const App = {
  7. components: {
  8. comp1,
  9. comp2,
  10. comp3
  11. },
  12. init() {
  13. this.bindEvent();
  14. },
  15. bindEvent() {
  16. oBtns.addEventListener("click", this.handleBtnClick.bind(this), false);
  17. },
  18. handleBtnClick(e) {
  19. const tar = e.target;
  20. const tagName = tar.tagName.toLowerCase();
  21. if (tagName === "span") {
  22. const key = tar.dataset.key;
  23. const vNode = this.setVNode(this.components[key]); // 传递组件对象
  24. const rNode = this.setRNode(vNode); // 传递 vNode 对象
  25. oWrapper.innerHTML = "";
  26. oWrapper.appendChild(rNode); // 渲染 rNode
  27. }
  28. },
  29. setVNode(comp) {
  30. const { template, name } = comp;
  31. const regTag = template.match(/\<(.+?)\>/)[1]; // 获取标签的名称
  32. const regContent = template.match(/\>(.+?)\</)[1]; // 获取元素的内容
  33. // 例如 {tag:'h1', children:'This is COMP-1', mark:'Comp1'}
  34. return {
  35. tag: regTag,
  36. children: regContent,
  37. mark: name
  38. };
  39. },
  40. setRNode(vNode) {
  41. const { tag, children, mark } = vNode;
  42. // 根据 tag 创建对应的 DOM 元素
  43. const node = document.createElement(tag);
  44. node.innerText = children;
  45. // 然后创建后的 DOM
  46. return node;
  47. }
  48. };
  49. App.init();

屏幕录制2023-05-11 15.39.37.gif
到这里我们就简单了实现了一个 Tab 的切换,下面我要给<div id="wrapper"></div>加上<keep-alive>进行包裹,进行缓存。

  1. <body>
  2. <div id="app">
  3. <div id="buttons">
  4. <span data-key="comp1">COMP 1</span>
  5. <span data-key="comp2">COMP 2</span>
  6. <span data-key="comp3">COMP 3</span>
  7. </div>
  8. <keep-alive>
  9. <div id="wrapper"></div>
  10. </keep-alive>
  11. </div>
  12. <script type="module" src="./src/mian.js"></script>
  13. </body>
  1. import comp1 from "./components/Comp1";
  2. import comp2 from "./components/Comp2";
  3. import comp3 from "./components/Comp3";
  4. const oBtns = document.getElementById("buttons");
  5. const oWrapper = document.getElementById("wrapper");
  6. const App = {
  7. components: {
  8. comp1,
  9. comp2,
  10. comp3
  11. },
  12. compCache: {}, // 新增一个缓存对象
  13. init() {
  14. this.bindEvent();
  15. },
  16. bindEvent() {
  17. oBtns.addEventListener("click", this.handleBtnClick.bind(this), false);
  18. },
  19. handleBtnClick(e) {
  20. const tar = e.target;
  21. const tagName = tar.tagName.toLowerCase();
  22. /* if (tagName === "span") {
  23. const key = tar.dataset.key;
  24. const vNode = this.setVNode(this.components[key]); // 传递组件对象
  25. const rNode = this.setRNode(vNode); // 传递 vNode 对象
  26. oWrapper.innerHTML = "";
  27. oWrapper.appendChild(rNode); // 渲染 rNode
  28. } */
  29. // 这里 vNode 将不再直接调用 setVNode 返回,而是要进行判断
  30. if (tagName === "span") {
  31. const key = tar.dataset.key;
  32. let vNode = null;
  33. // 如果缓存对象中存在 key
  34. if (this.compCache[key]) {
  35. vNode = this.compCache[key];
  36. }else {
  37. // 否则直接去调用 setVNode 生成 vNode
  38. vNode = this.setVNode(this.components[key]);
  39. // 如果 oWrapper 的父节点是 <keep-alive> 那就把当前组件的 vNode 存到 compCache 中去
  40. if (this.checkKeppAlive(oWrapper)) {
  41. this.compCache[key] = vNode;
  42. }
  43. }
  44. }
  45. // 将 vNode 转换为 rNode ,最后进行渲染
  46. const rNode = this.setRNode(vNode);
  47. oWrapper.innerHTML = "";
  48. oWrapper.appendChild(rNode);
  49. },
  50. setVNode(comp) {
  51. const { template, name } = comp;
  52. const regTag = template.match(/\<(.+?)\>/)[1]; // 获取标签的名称
  53. const regContent = template.match(/\>(.+?)\</)[1]; // 获取元素的内容
  54. // 例如 {tag:'h1', children:'This is COMP-1', mark:'Comp1'}
  55. return {
  56. tag: regTag,
  57. children: regContent,
  58. mark: name
  59. };
  60. },
  61. setRNode(vNode) {
  62. const { tag, children, mark } = vNode;
  63. // 根据 tag 创建对应的 DOM 元素
  64. const node = document.createElement(tag);
  65. node.innerText = children;
  66. // 然后创建后的 DOM
  67. return node;
  68. },
  69. checkKeppAlive(wrapper) {
  70. const outerWrapper = wrapper.parentNode.tagName.toLowerCase();
  71. return outerWrapper === "keep-alive";
  72. }
  73. };
  74. App.init();

屏幕录制2023-05-11 15.49.04.gif

我们还可以添加两个钩子函数,当组件激活的时候执行:

  1. import comp1 from "./components/Comp1";
  2. import comp2 from "./components/Comp2";
  3. import comp3 from "./components/Comp3";
  4. const oBtns = document.getElementById("buttons");
  5. const oWrapper = document.getElementById("wrapper");
  6. const App = {
  7. components: {
  8. comp1,
  9. comp2,
  10. comp3
  11. },
  12. mounted(callback) {
  13. callback && callback();
  14. },
  15. activated(callback) {
  16. callback && callback();
  17. },
  18. compCache: {}, // 新增一个缓存对象
  19. init() {
  20. this.bindEvent();
  21. },
  22. bindEvent() {
  23. oBtns.addEventListener("click", this.handleBtnClick.bind(this), false);
  24. },
  25. handleBtnClick(e) {
  26. const tar = e.target;
  27. const tagName = tar.tagName.toLowerCase();
  28. /* if (tagName === "span") {
  29. const key = tar.dataset.key;
  30. const vNode = this.setVNode(this.components[key]); // 传递组件对象
  31. const rNode = this.setRNode(vNode); // 传递 vNode 对象
  32. oWrapper.innerHTML = "";
  33. oWrapper.appendChild(rNode); // 渲染 rNode
  34. } */
  35. // 这里 vNode 将不再直接调用 setVNode 返回,而是要进行判断
  36. if (tagName === "span") {
  37. const key = tar.dataset.key;
  38. let vNode = null;
  39. // 如果缓存对象中存在 key
  40. if (this.compCache[key]) {
  41. vNode = this.compCache[key];
  42. }else {
  43. // 否则直接去调用 setVNode 生成 vNode
  44. vNode = this.setVNode(this.components[key]);
  45. // 如果 oWrapper 的父节点是 <keep-alive> 那就把当前组件的 vNode 存到 compCache 中去
  46. if (this.checkKeppAlive(oWrapper)) {
  47. this.compCache[key] = vNode;
  48. }
  49. }
  50. }
  51. // 将 vNode 转换为 rNode ,最后进行渲染
  52. const rNode = this.setRNode(vNode);
  53. oWrapper.innerHTML = "";
  54. oWrapper.appendChild(rNode);
  55. if (this.checkKeppAlive(oWrapper)) {
  56. this.activated(() => {
  57. console.log(key, "activated");
  58. });
  59. } else {
  60. this.mounted(() => {
  61. console.log(key, "mounted");
  62. });
  63. }
  64. },
  65. setVNode(comp) {
  66. const { template, name } = comp;
  67. const regTag = template.match(/\<(.+?)\>/)[1]; // 获取标签的名称
  68. const regContent = template.match(/\>(.+?)\</)[1]; // 获取元素的内容
  69. // 例如 {tag:'h1', children:'This is COMP-1', mark:'Comp1'}
  70. return {
  71. tag: regTag,
  72. children: regContent,
  73. mark: name
  74. };
  75. },
  76. setRNode(vNode) {
  77. const { tag, children, mark } = vNode;
  78. // 根据 tag 创建对应的 DOM 元素
  79. const node = document.createElement(tag);
  80. node.innerText = children;
  81. // 然后创建后的 DOM
  82. return node;
  83. },
  84. checkKeppAlive(wrapper) {
  85. const outerWrapper = wrapper.parentNode.tagName.toLowerCase();
  86. return outerWrapper === "keep-alive";
  87. }
  88. };
  89. App.init();

屏幕录制2023-05-11 15.53.03.gif

源码地址:
JSPlusPlus/腾讯课堂/Vue本尊07/01 at main · xiechen1201/JSPlusPlus