前端面试题之Vue篇

该面试题只是为了记录我自己的面试笔记,大多数摘自行内有关大佬总结,本人只是搬运工,有关链接已放置相关笔记的下面

compute 实现原理

computed 本质是一个惰性求值的观察者。

computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。

其内部通过 this.dirty 属性标记计算属性是否需要重新求值。

当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,

computed watcher 通过 this.dep.subs.length 判断有没有订阅者,

有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)

没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后 其他地方需要读取属性 的时候,它才会真正计算,即具备 lazy(懒计算)特性。比如这个属性并没有被其他地方用到,就不会重新去计算它)

浅谈 Vue 中 computed 实现原理

watch 的deep 机制

watch 本身懒监听,加上deep 属性会深度监听,一种是userDep,一种是getter/setter

watch实现原理

setup 有关

  • 在执行setup函数时,还没执行created生命周期方法,因此在setup函数中,无法使用data和methods的变量和方法。
  • 不能在setup函数中使用data和methods,在setup中使用this是无法操作的,因为setup 在生命周期beforeCreate和created之间执行,调用发生在 data property、computed property 或 methods 被解析之前,此时组件实例未实例化成功,所以data,methods等东西无法在 setup 中使用this被获取。

V-if 和 v-show 区别

  • V-if 是对dom增删操作,对性能消耗较高
  • V-show 是对css 操作,控制display 属性

vue路由模式hash和history,hash解析

Vue-Router有两种模式:hash模式和history模式。默认的路由模式是hash模式。

  1. hash模式

简介: hash模式是开发中默认的模式,它的URL带着一个#,例如:www.abc.com/#/vue,它的hash值就是#/vue。

特点:hash值会出现在URL里面,但是不会出现在HTTP请求中,对后端完全没有影响。所以改变hash值,不会重新加载页面。这种模式的浏览器支持度很好,低版本的IE浏览器也支持这种模式。hash路由被称为是前端路由,已经成为SPA(单页面应用)的标配。

原理: hash模式的主要原理就是onhashchange()事件:

ini 复制代码
window.onhashchange = function(event){
    console.log(event.oldURL, event.newURL);
    let hash = location.hash.slice(1);
}

使用onhashchange()事件的好处就是,在页面的hash值发生变化时,无需向后端发起请求,window就可以监听事件的改变,并按规则加载相应的代码。除此之外,hash值变化对应的URL都会被浏览器记录下来,这样浏览器就能实现页面的前进和后退。虽然是没有请求后端服务器,但是页面的hash值和对应的URL关联起来了。

  1. history模式

简介: history模式的URL中没有#,它使用的是传统的路由分发模式,即用户在输入一个URL时,服务器会接收这个请求,并解析这个URL,然后做出相应的逻辑处理。

特点: 当使用history模式时,URL就像这样:abc.com/user/id。相比hash模式更加好看。但是,history模式需要后台配置支持。如果后台没有正确配置,访问时会返回404。

API: history api可以分为两大部分,切换历史状态和修改历史状态:

● 修改历史状态:包括了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法,这两个方法应用于浏览器的历史记录栈,提供了对历史记录进行修改的功能。只是当他们进行修改时,虽然修改了url,但浏览器不会立即向后端发送请求。如果要做到改变url但又不刷新页面的效果,就需要前端用上这两个API。

● 切换历史状态: 包括forward()、back()、go()三个方法,对应浏览器的前进,后退,跳转操作。

虽然history模式丢弃了丑陋的#。但是,它也有自己的缺点,就是在刷新页面的时候,如果没有相应的路由或资源,就会刷出404来。

如果想要切换到history模式,就要进行以下配置(后端也要进行配置):

arduino 复制代码
const router = new VueRouter({
  mode: 'history',
  routes: [...]
})
  1. 两种模式对比

调用 history.pushState() 相比于直接修改 hash,存在以下优势:

● pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL;

● pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中;

● pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串;

● pushState() 可额外设置 title 属性供后续使用。

● hash模式下,仅hash符号之前的url会被包含在请求中,后端如果没有做到对路由的全覆盖,也不会返回404错误;history模式下,前端的url必须和实际向后端发起请求的url一致,如果没有对用的路由处理,将返回404错误。

hash模式和history模式都有各自的优势和缺陷,还是要根据实际情况选择性的使用。

vue3 静态提升

PatchFlags

ini 复制代码
export const enum PatchFlags {
  TEXT = 1,// 动态的文本节点
  CLASS = 1 << 1,  // 2 动态的 class
  STYLE = 1 << 2,  // 4 动态的 style
  PROPS = 1 << 3,  // 8 动态属性,不包括类名和样式
  FULL_PROPS = 1 << 4,  // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
  HYDRATE_EVENTS = 1 << 5,  // 32 表示带有事件监听器的节点
  STABLE_FRAGMENT = 1 << 6,   // 64 一个不会改变子节点顺序的 Fragment
  KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
  UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
  NEED_PATCH = 1 << 9,   // 512
  DYNAMIC_SLOTS = 1 << 10,  // 动态 solt
  HOISTED = -1,  // 特殊标志是负整数表示永远不会用作 diff
  BAIL = -2 // 一个特殊的标志,指代差异算法
}

Vue3中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用

这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用

css 复制代码
<span>你好</span>

<div>{{ message }}</div>

没有做静态提升之前

php 复制代码
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _createVNode("span", null, "你好"),
    _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
  ], 64 /* STABLE_FRAGMENT */))
}

做了静态提升之后

php 复制代码
const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "你好", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _hoisted_1,
    _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
  ], 64 /* STABLE_FRAGMENT */))
}

// Check the console for the AST

静态内容_hoisted_1被放置在render 函数外,每次渲染的时候只要取 _hoisted_1 即可

同时 _hoisted_1 被打上了 PatchFlag ,静态标记值为 -1 ,特殊标志是负整数表示永远不会用于 Diff

Vue自定义指令

都是分全局指令和局部指令

全局定义:Vue.directive("focus",{})

局部定义:directives:{focus:{}}

vue2

钩子函数:指令定义对象提供钩子函数

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inSerted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前调用。指令的值可能发生了改变,也可能没有。但是可以通过比较更新前后的值来忽略不必要的模板更新。
  • ComponentUpdate:指令所在组件的 VNode及其子VNode全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

钩子函数参数

  • el:指令所绑定的元素,可以用来直接操作 DOM

  • binding:一个对象,包含以下 property

    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
    • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
  • vnodeVue 编译生成的虚拟节点

  • oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用

除了 el 之外,其它参数都应该是只读的 ,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。

ini 复制代码
Vue.directive('throttle', {
  bind: (el, binding) => {
    let throttleTime = binding.value; // 节流时间
    if (!throttleTime) { // 用户若不设置节流时间,则默认2s
      throttleTime = 2000;
    }
    let cbFun;
    el.addEventListener('click', event => {
      if (!cbFun) { // 第一次执行
        cbFun = setTimeout(() => {
          cbFun = null;
        }, throttleTime);
      } else {
        event && event.stopImmediatePropagation();
      }
    }, true);
  },
});
vue3

七个钩子函数(在 Vue3 中,自定义指令的钩子函数主要有如下七种(这块跟 Vue2 差异较大))

  • created:在绑定元素的 attribute 或事件监听器被应用之前调用。在指令需要附加在普通的 v-on 事件监听器调用前的事件监听器中时,这很有用。
  • beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用。
  • mounted:在绑定元素的父组件被挂载后调用,大部分自定义指令都写在这里。
  • beforeUpdate:在更新包含组件的 VNode 之前调用。
  • updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用。
  • beforeUnmount:在卸载绑定元素的父组件之前调用
  • unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次。

虽然钩子函数比较多,看着有点唬人,不过我们日常开发中用的最多的其实是 mounted 函数。

四个参数

这里七个钩子函数,钩子函数中有回调参数,回调参数有四个,含义基本上和 Vue2 一致:

  • el:指令所绑定的元素,可以用来直接操作 DOM,我们松哥说想实现一个可以自动判断组件显示还是隐藏的指令,那么就可以通过 el 对象来操作 DOM 节点,进而实现组件的隐藏。

  • binding:我们通过自定义指令传递的各种参数,主要存在于这个对象中,该对象属性较多,如下属性是我们日常开发使用较多的几个:

    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-hasPermission="['user:delete']" 中,绑定值为 'user:delete',不过需要小伙伴们注意的是,这个绑定值可以是数组也可以是普通对象,关键是看你具体绑定的是什么,在 2.1 小节的案例中,我们的 value 就是一个数字。
  • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。

  • arg:传给指令的参数,可选。例如 v-hasPermission:[name]="'zhangsan'" 中,参数为 "name"。

  • vnode:Vue 编译生成的虚拟节点。

  • oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

除了 el 之外,其它参数都应该是只读的 ,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。

ini 复制代码
const app = createApp(App);

app.directive('onceClick',{
    mounted(el, binding, vnode) {
        el.addEventListener('click', () => {
            if (!el.disabled) {
                el.disabled = true;
                let time = binding.value;
                if (binding.arg == "s") {
                    time = time * 1000;
                }
                setTimeout(() => {
                    el.disabled = false;
                }, time);
            }
        });
    }
})
使用场景

普通DOM元素进行底层操作的时候,可以使用自定义指令

自定义指令是用来操作DOM的。尽管Vue推崇数据驱动视图的理念,但并非所有情况都适合数据驱动。自定义指令就是一种有效的补充和扩展,不仅可用于定义任何的DOM操作,并且是可复用的。

使用案例

初级应用:

鼠标聚焦

下拉菜单

相对时间转换

滚动动画

高级应用:

自定义指令实现图片懒加载

自定义指令集成第三方插件

参考链接:手把手教你在 Vue3 中自定义指令_vue.js_肥肥技术宅-华为云开发者联盟

Pinia 相较于 vuex 的优点

Pinia 和 Vuex

VuexStateGettesMutations(同步)、Actions(异步)

PiniaStateGettesActions(同步异步都支持)

Vuex 当前最新版是 4.x

  • Vuex4 用于 Vue3
  • Vuex3 用于 Vue2

Pinia 当前最新版是 2.x

  • 即支持 Vue2 也支持 Vue3

就目前而言 Pinia 比 Vuex 好太多了,解决了 Vuex 的很多问题,所以笔者也非常建议直接使用 Pinia,尤其是 TypeScript 的项目

Pinia 核心特性

  • Pinia 没有 Mutations

  • Actions 支持同步和异步

  • 没有模块的嵌套结构

    • Pinia 通过设计提供扁平结构,就是说每个 store 都是互相独立的,谁也不属于谁,也就是扁平化了,更好的代码分割且没有命名空间。当然你也可以通过在一个模块中导入另一个模块来隐式嵌套 store,甚至可以拥有 store 的循环依赖关系
  • 更好的 TypeScript 支持

    • 不需要再创建自定义的复杂包装器来支持 TypeScript 所有内容都类型化,并且 API 的设计方式也尽可能的使用 TS 类型推断
  • 不需要注入、导入函数、调用它们,享受自动补全,让我们开发更加方便

  • 无需手动添加 store,它的模块默认情况下创建就自动注册的

  • Vue2 和 Vue3 都支持

    • 除了初始化安装和SSR配置之外,两者使用上的API都是相同的
  • 支持 Vue DevTools

    • 跟踪 actions, mutations 的时间线
    • 在使用了模块的组件中就可以观察到模块本身
    • 支持 time-travel 更容易调试
    • 在 Vue2 中 Pinia 会使用 Vuex 的所有接口,所以它俩不能一起使用
    • 但是针对 Vue3 的调试工具支持还不够完美,比如还没有 time-travel 功能
  • 模块热更新

    • 无需重新加载页面就可以修改模块
    • 热更新的时候会保持任何现有状态
  • 支持使用插件扩展 Pinia 功能

  • 支持服务端渲染

链接:上手 Vue 新的状态管理 Pinia,一篇文章就够了 - 掘金

你想要的 ------ Vuex源码分析 - 掘金

vue3优化

  • 监测机制的改变

    • 3.0 将带来基于代理 Proxy的 observer 实现,提供全语言覆盖的反应性跟踪。
    • 消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:
  • 只能监测属性,不能监测对象

    • 检测属性的添加和删除;
    • 检测数组索引和长度的变更;
    • 支持 Map、Set、WeakMap 和 WeakSet。
  • 模板

    • 作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。
    • 同时,对于 render 函数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom 。
  • 对象式的组件声明方式

    • vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。
    • 3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易
  • 其它方面的更改

    • 支持自定义渲染器,从而使得 weex 可以通过自定义渲染器的方式来扩展,而不是直接 fork 源码来改的方式。
    • 支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。
    • 基于 tree shaking 优化,提供了更多的内置功能。

参考链接:Vue3.0 新特性以及使用经验总结 - 掘金

vue2可以使用vite吗

根目录创建vite.config.js,使用到require-context需处理兼容。

@vitejs/plugin-vue2

perl 复制代码
pnpm i rollup-plugin-require-context generate-source-map
javascript 复制代码
import { defineConfig, loadEnv } from "vite";
import { createVuePlugin } from "vite-plugin-vue2"; // 使用Vue2版本
import requireContext from "rollup-plugin-require-context"; // 处理兼容webpack工具require-context;
const path = require("path");

export default defineConfig(({ command, mode }) => {
  let config = {};
  const env = loadEnv(mode, process.cwd()); // 根据服务环境获取环境变量
  // 情景模式配置
  if (command === "serve") {
    // dev 独有配置
    config = {
      base: "/",
    };
  } else {
    // build 独有配置
    config = {
      base: env.VITE_APP_BASE, // 生产环境基础路径必须前后都带斜杠否则打包会出现警告提示
      build: {
        terserOptions: {
          // 打包编译清除控制台输出及debugger
          compress: {
            drop_console: true,
            drop_debugger: true,
          },
        },
        outDir: env.VITE_APP_DIR,
        assetsDir: env.VITE_APP_ASSETS,
        rollupOptions: {
          output: {
            entryFileNames: `${env.VITE_APP_ASSETS}/js/entry[hash].js`, // [name]-[hash] 入口文件
            chunkFileNames: `${env.VITE_APP_ASSETS}/js/chunk[hash].js`, // [name]-[hash] 共享文件
            assetFileNames: `${env.VITE_APP_ASSETS}/assets/[hash][extname]`, // [name]-[hash] 静态资源
          },
        },
        brotliSize: false, // 计算打包时间关闭,不进行该环节;提升bundle效率
      },
    };
  }
  return {
    ...config, // 合并开发生产环境配置
    css: {
      preprocessorOptions: {
        scss: {
          // 处理全局公共样式兼容问题
          additionalData: `@import "./src/assets/css/_config.scss"; 
        @import "./src/assets/css/bem.scss"; 
        @import "./src/assets/css/mixins.scss";
        @import "./src/assets/css/vars.scss";`,
        },
      },
    },
    resolve: {
      extensions: [".js", ".vue", ".json", ".scss"], // 处理文件扩展名
      alias: {
        "@": path.resolve(__dirname, "src"), // 处理全局别名
      },
    },
    plugins: [createVuePlugin(), requireContext()],
    // 代理配置
    server: {
      port: 8888,
      strictPort: true,
      proxy: {
        "/api": {
          target: "http://127.0.0.1:8888",
          changeOrigin: true,
          secure: false,
          ws: true,
        },
      },
    },
  };
});

参考链接:基于pnpm搭建Vite+Vue2+element-ui项目 - 掘金

Vite2搞Vue2?这题我会 - 掘金

保存当页面的状态

既然是要保持页面的状态(其实也就是组件的状态),那么会出现以下两种情况:

  • 前组件会被卸载
  • 前组件不会被卸载

那么可以按照这两种情况分别得到以下方法:

组件会被卸载:
  1. 将状态存储在LocalStorage / SessionStorage

    1. 只需要在组件即将被销毁的生命周期中在 LocalStorage / SessionStorage 中把当前组件的 state 通过 JSON.stringify() 储存下来就可以了。在这里面需要注意的是组件更新状态的时机。比如从 B 组件跳转到 A 组件的时候,A 组件需要更新自身的状态。但是如果从别的组件跳转到 B 组件的时候,实际上是希望 B 组件重新渲染的,也就是不要从 Storage 中读取信息。所以需要在 Storage 中的状态加入一个 flag 属性,用来控制 A 组件是否读取 Storage 中的状态。

    2. 优点

    3. 兼容性好,不需要额外库或工具。

    4. 简单快捷,基本可以满足大部分需求。

    5. 缺点

    6. 状态通过 JSON 方法储存(相当于深拷贝),如果状态中有特殊情况(比如 Date 对象、Regexp 对象等)的时候会得到字符串而不是原来的值。(具体参考用 JSON 深拷贝的缺点)

    7. 如果 B 组件后退或者下一页跳转并不是前组件,那么 flag 判断会失效,导致从其他页面进入 A 组件页面时 A 组件会重新读取 Storage,会造成很奇怪的现象

  2. 路由传值

    1. 通过 react-router 的 Link 组件的 prop ------ to 可以实现路由间传递参数的效果。

    2. 在这里需要用到 state 参数,在 B 组件中通过 history.location.state 就可以拿到 state 值,保存它。返回 A 组件时再次携带 state 达到路由状态保持的效果。

    3. 优点

    4. 简单快捷,不会污染 LocalStorage / SessionStorage。

    5. 可以传递 Date、RegExp 等特殊对象(不用担心 JSON.stringify / parse 的不足)

    6. 缺点

    7. 如果 A 组件可以跳转至多个组件,那么在每一个跳转组件内都要写相同的逻辑。

组件不会被卸载
  1. 单页面渲染

要切换的组件作为子组件全屏渲染,父组件中正常储存页面状态。

优点

  • 代码量少
  • 不需要考虑状态传递过程中的错误

缺点

  • 增加 A 组件维护成本
  • 需要传入额外的 prop 到 B 组件
  • 无法利用路由定位页面
keep-alive

除此之外,在Vue中,还可以是用keep-alive来缓存页面,当组件在keep-alive内被切换时组件的activated、deactivated这两个生命周期钩子函数会被执行

被包裹在keep-alive中的组件的状态将会被保留:

javascript 复制代码
// main.js
<keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
</kepp-alive>
// router.js
{
  path: '/',
  name: 'xxx',
  component: ()=>import('../src/views/xxx.vue'),
  meta:{
    keepAlive: true // 需要被缓存
  }
}

keep-alive LRU 算法

作用:实现组件缓存,保持这些组件的状态,以避免反复渲染导致的性能问题。 需要缓存组件 频繁切换,不需要重复渲染

场景:tabs标签页 后台导航,vue性能优化

原理:Vue.js内部将DOM节点抽象成了一个个的VNode节点,keep-alive组件的缓存也是基于VNode节点的而不是直接存储DOM结构。它将满足条件(pruneCache与pruneCache)的组件在cache对象中缓存起来,在需要重新渲染的时候再将vnode节点从cache对象中取出并渲染

原理

  • 获取 keep-alive 包裹着的第一个子组件对象及其组件名
  • 根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例
  • 根据组件 ID 和 tag 生成缓存 Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)
  • 在 this.cache 对象中存储该组件实例并保存 key 值,之后检查缓存的实例数量是否超过 max 的设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(即是下标为 0 的那个 key)
  • 最后组件实例的 keepAlive 属性设置为 true,这个在渲染和执行被包裹组件的钩子函数会用到,这里不细说

LRU 缓存淘汰算法

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

keep-alive 的实现正是用到了 LRU 策略,将最近访问的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被访问的组件,当缓存实例超过 max 设置值,删除 this.keys[0]

kotlin 复制代码
export default {
  name: "keep-alive",
  abstract: true, // 抽象组件属性 ,它在组件实例建立父子关系的时候会被忽略,发生在 initLifecycle 的过程中
  props: {
    include: patternTypes, // 被缓存组件
    exclude: patternTypes, // 不被缓存组件
    max: [String, Number] // 指定缓存大小
  },
 
  created() {
    this.cache = Object.create(null); // 缓存
    this.keys = []; // 缓存的VNode的键
  },
 
  destroyed() {
    for (const key in this.cache) {
      // 删除所有缓存
      pruneCacheEntry(this.cache, key, this.keys);
    }
  },
 
  mounted() {
    // 监听缓存/不缓存组件
    this.$watch("include", val => {
      pruneCache(this, name => matches(val, name));
    });
    this.$watch("exclude", val => {
      pruneCache(this, name => !matches(val, name));
    });
  },

  render() {
    // 获取第一个子元素的 vnode
    const slot = this.$slots.default;
    const vnode: VNode = getFirstComponentChild(slot);
    const componentOptions: ?VNodeComponentOptions =
      vnode && vnode.componentOptions;
    if (componentOptions) {
      // name不在inlcude中或者在exlude中 直接返回vnode
      // 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;
      // 获取键,优先获取组件的name字段,否则是组件的tag
      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;

      // 命中缓存,直接从缓存拿vnode 的组件实例,并且重新调整了 key 的顺序放在了最后一个
      if (cache[key]) {
       vnode.componentInstance = cache[key].componentInstance;
        // make current key freshest
        remove(keys, key);
        keys.push(key);
      }  
      // 不命中缓存,把 vnode 设置进缓存
      else {
       cache[key] = vnode;
       keys.push(key);
        // prune oldest entry
        // 如果配置了 max 并且缓存的长度超过了 this.max,还要从缓存中删除第一个
       if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode);
        }
      }
      
      // keepAlive标记位
      vnode.data.keepAlive = true;
    }
    return vnode || (slot && slot[0]);
  }
};

传送门 ☞ # 全面讲解LRU算法

shallowRef

diff算法

React、Vue2、Vue3的三种Diff算法 - 掘金

相关推荐
宇宙李5 分钟前
2024java面试-软实力篇
面试·职场和发展
孙小二写代码2 小时前
[leetcode刷题]面试经典150题之1合并两个有序数组(简单)
算法·leetcode·面试
珊珊而川2 小时前
【浏览器面试真题】sessionStorage和localStorage
前端·javascript·面试
markzzw2 小时前
我在 Thoughtworks 被裁前后的经历
前端·javascript·面试
无名之逆3 小时前
计算机专业的就业方向
java·开发语言·c++·人工智能·git·考研·面试
爱棋笑谦3 小时前
二叉树计算
java·开发语言·数据结构·算法·华为od·面试
码拉松3 小时前
千万不要错过,优惠券设计与思考初探
后端·面试·架构
魔术师卡颂4 小时前
如何让“学源码”变得轻松、有意义
前端·面试·源码
Estrella164 小时前
经典 web 页面排版:三栏布局
前端·css·面试
AntDreamer5 小时前
在实际开发中,如何根据项目需求调整 RecyclerView 的缓存策略?
android·java·缓存·面试·性能优化·kotlin