2025前端社招最新面试题汇总- vue篇

378 阅读35分钟

1. 对于MVVM的理解?

MVVM 是 Model-View-ViewModel 的缩写。
Model代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。
View 代表UI 组件,它负责将数据模型转化成UI 展现出来。
ViewModel 监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步View 和 Model的对象,连接Model和View。
在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。

2. vue中MVVM原理及其实现:数据双向绑定,响应式原理

vue2实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty() 来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调。

1.实现compile,进行模板的编译,包括编译元素(指令)、编译文本等,达到初始化视图的目的,并且还需要绑定 好更新函数;
2.实现Observer,监听所有的数据,并对变化数据发布通知;
3.实现watcher,作为一个中枢,接收到observe发来的通知,并执行compile中相应的更新方法。

  • observer:利用Object.defineProperty,实现observer,将要监控的对象的所有属性都添加上get和set方法,给每一个属性都定义dep对象=>subs->[watcher1,watcher2]。这样对象的属性发生改变的时候,就会触发相应的set,发布一个通知出来,订阅这个消息的watcher就需要收到消息、触发回调函数
class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;

    Dep.target = this; // 每次只操作一个watcher
    this.oldValue = this.vm[key]; // 这里读取会触发get,get中用一下就可以消除了
    Dep.target = null;
  }

  update() {
    const newVal = this.vm[this.key];
    this.cb(newVal);
    this.oldValue = newVal;
  }
}

class Compiler {
  constructor(vm) {
    this.vm = vm;
    this.el = vm.$el;
    this.compile(this.el);
  }
  compile(el) {
    const childNodes = el.childNodes;
    console.log("childNodes", childNodes);
    Array.from(childNodes).forEach((node) => {
      if (node.nodeType === 1) {
        this.compileText(node);
      }
      if ((node.childNodes && node.childNodes, length !== 0)) {
        this.compile(node);
      }
    });
  }
  compileText(node) {
    const reg = /{{(.+?)}}/g; // ? 表示惰性匹配
    const value = node.textContent.replace(/\s/g, ""); // 替换所有空格和换行
    const tokens = [];
    let result,
      index,
      lastIndex = 0;
    // 一个个命中,依次处理命中值
    while ((result = reg.exec(value))) {
      index = result.index;
      // 找到值了,把一段文本中命中位置之前的东西放进来
      if (index > lastIndex) {
        tokens.push(value.slice(lastIndex, index));
      }

      const key = result[1]; // reg方法中的【1】 是正则表达式括号中的内容,这里就是 {{ msg }} 中的msg
      tokens.push(this.vm[key]); // 把真正的值push进去
      lastIndex = index + result[0].length; // 下一个的起点

      const pos = tokens.length - 1; // 记录tokens中当前这个命中值的位置,后面好替换
      // 更新的时候触发
      new Watcher(this.vm, key, (newVal) => {
        tokens[pos] = newVal;
        node.textContent = tokens.join("");
      });
    }
    // 循环完成之后,后面还有尾部内容,直接加进去
    if (lastIndex < value.length) {
      tokens.push(value.slice(lastIndex));
    }
    // 初始值的解析
    if (tokens.length) {
      node.textContent = tokens.join("");
    }
    console.log("tokens", tokens);
  }
}
class Dep {
  constructor() {
    // watcher的集合
    this.subs = [];
  }
  // 添加一个watcher
  addSub(sub) {
    this.subs.push(sub);
  }
  // 触发watcher中的动作
  notify() {
    this.subs.forEach((item) => item.update());
  }
}

class Observer {
  constructor(data) {
    this.data = data;
    this.dep = new Dep();
    this.walk();
  }
  walk() {
    Object.keys(this.data).forEach((key) => {
      defineReactive(this.data, key, this.data[key], this.dep);
    });
  }
}

function defineReactive(data, key, value,dep) {
  if (typeof value === "object" && value !== null) {
    new Observer(value);
  }

  Object.defineProperty(data, key, {
    // configurable: true,
    get() {
      Dep.target && dep.addSub(Dep.target);
      return value;
    },
    set(newVal) {
      if (value === newVal) return;
      value = newVal;
      if (typeof value === "object" && value !== null) {
        new Observer(value);
      }

      dep.notify();
    },
  });
}
class Vue {
  constructor(options) {
    this.$options = options || {};
    const el = options.el;
    this.$el = typeof el === "string" ? document.querySelector(el) : el;
    this.$data = options.data || {};

    // 将属性注入到VUE实例
    proxy(this, this.$data);
    console.log("this", this);

    // 创建Observer 对data数据进行监听变化,使得我们可以在数据被读取和改变时能够做出反应
    new Observer(this.$data);

    new Compiler(this);
  }
}

function proxy(vm, data) {
  Object.keys(data).forEach((key) => {
    Object.defineProperty(vm, key, {
      // 相当于读取和赋值都是对$data中的内容进行的
      get() {
        return data[key];
      },
      set(newVal) {
        data[key] = newVal;
      },
    });
  });
}
  • 消息订阅器:Dep:在set函数中监听到数据变化,调用dep.notify()通知订阅者,再调用订阅者的update方法
  • 订阅中心:WatcherObserver和Compile之间通信的桥梁是Watcher订阅中心,其主要职责是:
    1、在自身实例化时往属性订阅器(Dep)里面添加自己,与Observer建立连接;
    2、自身必须有一个update()方法,与Compile建立连接;
    3、当属性变化时,Observer中dep.notice()通知,然后能调用自身(Watcher)的update()方法,并触发Compile中绑定的回调,实现更新。
  • compile编译模板

上面的响应式是在Vue2中采取的方式,有一个问题,Object.defineProperty的缺陷是只能监听一个属性,监听整个对象的时候,需要用到for in 循环

Vue3中用proxy写监听:
  • 可以对整个对象进行监听,省去了for…in循环提升效率
  • 省去额外的中间存储量
  • 可以监听数组,不用再单独对数组做特异性操作
  1. 在get时收集依赖: 收集 不同代理对象不同属性 所依赖的 副作用函数
  2. 在set时触发依赖: 取出当前属性所依赖的所有副作用函数, 重新执行
// weakMap  -> map => set
const bucket = new WeakMap();

// 防止对象多次重复响应式处理
const reactiveMap = new WeakMap();
let acitveEffect = null;
const isObj = (obj) => {
  return typeof obj === "object" && obj !== null;
};

function track(target, key) {
  if (!acitveEffect) return;

  let mapTarget = bucket.get(target);

  if (!mapTarget) {
    mapTarget = new Map();
    bucket.set(target, mapTarget);
  }
  let set = mapTarget.get(key);
  if (!set) {
    set = new Set();
    mapTarget.set(key, set);
  }
  set.add(acitveEffect);
}

function trigger(target, key) {
  let mapTarget = bucket.get(target);
  if (!mapTarget) return;
  // 取出对应属性的副作用函数set集合
  let set = mapTarget.get(key);
  if (!set) return;
  console.log("set", set);
  set.forEach((fn) => {
    fn();
  });
}
function reactive(obj) {
  if (!isObj(obj)) {
    return obj;
  }
  if (reactiveMap.has(obj)) {
    return reactiveMap.get(obj);
  }
  const proxyRes = new Proxy(obj, {
    // receiver 代表当前proxy对象 或者 继承proxy的对象,它保证传递正确的 this 给 getter,setter
    get(target, key, receiver) {
      // 在get操作时, 收集依赖
      track(target, key);
      const res = Reflect.get(target, key, receiver);

      // 多层嵌套
      if (isObj(res)) {
        return reactive(res);
      }
      return res;
    },
    set(target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver);
      if (oldValue === value) {
        return;
      }
      const res = Reflect.set(target, key, value, receiver);
      trigger(target, key);
      return res;
      //   return
    },
  });
  reactiveMap.set(obj, proxyRes);
  return proxyRes;
}

// 副作用函数:引用了数据的函数

function effect(fn) {
  if (typeof fn !== "function") return;

  acitveEffect = fn;
  fn();
  acitveEffect = null;
}

// ref的实现,将基本类型包装成对象
function ref(value) {
  const obj = {
    value,
  };
  return reactive(obj);
}

export { reactive, effect, ref };
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="app" class="dd"></div>

    <script type="module">
        import { effect, reactive, ref } from './reactive3.js'

        const pState1 = reactive({
            name: '小浪',
            age: 22,
            test: {
                test1: {
                    test2: 21,
                },
            },
        })
        effect(function test13() {
            app.innerHTML = `<p>${pState1.test.test1.test2}</p>`
            console.log(app, pState1.age)
            console.log('effectage')
        })

        // 可以深层监听
        setTimeout(() => {
            pState1.test.test1.test2 = 666
        }, 2000)

    </script>
    <style>
    </style>
</body>

</html>
为什么proxy 和 reflect 配合使用?
场景使用 Reflect的作用
this绑定通过 receiver参数确保方法中的 this 指向代理对象。否则无法触发依赖收集
返回值标准化提供一致的返回值(如布尔值),简化逻辑处理。
覆盖所有操作与 Proxy 陷阱一一对应,避免遗漏边界情况。
代码健壮性确保操作经过代理,避免直接操作原始对象。

Vue3 的响应式系统通过 Proxy + Reflect 的组合,实现了对对象操作的精细化拦截,同时保证了上下文和返回值的正确性,这是其高效且可靠的核心机制之一。

Vue中监听数组:

在vue2中,如果直接更改数组下标的值,如arr[1] = xx 响应式是无法生效的,这是由于性能方面的考虑放弃了这个功能的实现,另外defineProperty也不能检测到数组长度的变化,对于数组的响应式,设置了7个变异数组方法解决问题

pushpopshiftunshiftsplicesortreverse

变异方法,就是这些数组方法的原有功能保持不变,我们在这些方法的基础上进行了重写。

总结

  • Object.defineProperty 对数组和对象的表现一致,并非不能监控数组下标的变化,vue2.x中无法通过数组索引来实现响应式数据的自动更新是vue本身的设计导致的,不是 defineProperty 的锅。

  • Object.defineProperty 和 Proxy 本质差别是,defineProperty 只能对属性进行劫持,所以出现了需要递归遍历,新增属性需要手动 Observe 的问题。

3. vue渲染流程

在Vue.js中,组件的渲染流程是一个关键过程,它涉及到数据的响应式系统、虚拟DOM(Virtual DOM)以及DOM的更新。

1.初始化

当Vue实例被创建时,首先会进行初始化。这包括设置数据观察、编译模板、挂载实例到DOM上等步骤。

  1. 数据响应式

Vue使用Object.defineProperty(在Vue 3中使用了Proxy)来将数据对象的属性转换为getter/setter。这样,当属性被访问或修改时,Vue能够追踪到依赖这些属性的计算属性和组件。

  1. 编译模板

Vue会将模板字符串(或通过.vue文件中的模板)编译成渲染函数。这个渲染函数是一个纯JavaScript函数,它描述了如何根据组件的状态来构建DOM。

  1. 创建虚拟DOM

渲染函数执行时,会生成一个虚拟DOM(VNode)。虚拟DOM是一个轻量级的JavaScript对象,它精确地描述了应该如何去真实地更新DOM。

  1. 对比与打补丁

Vue使用一种高效的算法(如“最小化重新渲染”)来比较新旧虚拟DOM树。这个过程中,Vue会找出需要实际改变的部分,并只对这些部分进行DOM更新,而不是整个DOM树。

  1. 真实DOM更新

根据虚拟DOM的变化,Vue会生成一个最小的更新补丁(patch),然后应用到真实的DOM上。这个过程是高效的,因为它只更新实际变化的部分。

  1. 挂载与更新

一旦组件被挂载到DOM上,每当组件的数据发生变化时,Vue会重复上述过程:重新渲染组件、生成新的虚拟DOM、对比并打补丁到真实DOM上。

4. vue2和vue3的区别

  • 源码的变化:vue2是用js,vue3用ts, 所以Vue3更好的支持TS
  • 写法上变化:Vue2: 选项式API; Vue3: 组合式API
  • 响应式实现: Vue2是definePropety对对象属性的劫持, Vue3是proxy对整体对象的劫持
  • 生命周期
    • Vue2: beforeCreate、created、beforMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed;
    • Vue3: setup、onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount、onUnmounted
  • 实例化
    • Vue2: new Vue
    • Vue3: createApp
  • 组件层面
    • Vue3 templete支持多个根标签 Fragments
    • Vue3 新增Teleport组件,将组件内部模板挂载到想挂的DOM上

传送门(Teleport),它可以将一个组件挂载到另外的DOM结构里,并且不受目标挂载点父元素的影响,他的数据处理和逻辑仍然是他原来的。这个新特性可以应用在最常见的alert或者toast等等上:

多个telepot组件指定同一个目标时,进行追加挂载

cn.vuejs.org/guide/built…

//在Son组件中
<template>
  <div class="Son">
    <div>我是可爱的小炸弹</div>
    <teleport to="#teleport-target">
      <h1>Boom</h1>
    </teleport>
  </div>
</template>

<script>
export default {
  name: "Son"
};
</script>
<style scoped>
.Son {
  color: red;
}
</style>

index.html中
<body>
    <div id="#app"></div>
    <div id="teleport-target" style="color:blue">
    </div>
</body>
    • Vue3 新增异步组件 defineAsyncComponent 声明,可以懒加载组件,(可以使用v-if 决定什么时候加载这个组件)
    • vue2 和vue3 的异步组件的区别: blog.csdn.net/mmc123125/a…
import { defineAsyncComponent } from "vue"
// simple usage
const LoginPopup = defineAsyncComponent(() => import("./components/LoginPopup.vue"))

// 高级用法
const AsyncPopup = defineAsyncComponent({ 
  loader: () => import("./LoginPopup.vue"),
   // 加载异步组件时要使用的组件
  loadingComponent: LoadingComponent,
   // 加载失败时要使用的组件
  errorComponent: ErrorComponent, 
  // 在显示 loadingComponent 之前的延迟 | 默认值:200(单位 ms)
  delay: 1000, 
  // 如果提供了 timeout ,并且加载组件的时间超过了设定值,将显示错误组件
  // 默认值:Infinity(即永不超时,单位 ms)
  timeout: 3000 
})
    • Vue3 新增宏:defineEmits、defineModel、defineProps, defineExpose

juejin.cn/post/728289…

  • 公共逻辑抽离
    • Vue2: mixin
    • Vue3: hooks
  • v-if 和 v-for 优先级的不同
    • vue2: v-for比v-if优先
    • Vue3: v-if比v-for优先
    • 一般不建议v-if和v-for一起使用
  • diff 算法优化: vue3 有静态标记
特性Vue 2Vue 3
比较策略双端比较(头尾指针遍历)动态规划(最长递增子序列优化)
节点复用逻辑全量比较,可能产生冗余 DOM 操作精准识别可复用节点,减少 DOM 移动次数
静态优化无编译时静态分析编译时静态标记(Patch Flags)
事件处理每次更新重新绑定事件事件缓存(减少重复绑定)
块级优化Block Tree 动态块追踪
  • 打包体积优化,Vue3更好支持Tree shaking

5. Vue的生命周期

vue2:

钩子函数:立即执行的回调函数

  • beforeCreate(创建前) 数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到data、computed、watch、methods上的方法和数据。
  • created(创建后) 实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到 $el 属性。
    • 一般可以在这里请求异步数据
  • beforeMount(载入前) 在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。完成了 el 和 data 初始化
  • mounted(载入后) 在el被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html 页面中。此过程完成挂载,dom渲染
  • beforeUpdate(更新前) 在数据更新之前调用,发生在虚拟DOM重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。
  • updated(更新后) 在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
  • beforeDestroy(销毁前) 在实例销毁之前调用。实例仍然完全可用。
  • destroyed(销毁后) 在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。

转存失败,建议直接上传图片文件

vue3:

onBeforeCreate 和onCreated已经废弃,直接在setup中

setup():组件创建之前
onBeforeMount:模板编译之前,还没有挂载到页面上
onMounted:模板编译完成,挂载到页面上
// 首次访问页面会执行上面三个生命周期
onBeforeUpdate :数据更新之前
onUpdated:数据更新之后
onBeforeUnmount:实例销毁之前,移除监听事件、定时器等
onUnmounted:实例销毁之后


// 使用keep-alive组件包裹的组件的新增生命周期
onDeactivated:组件被缓存时调用
onActivated:组件被激活时调用

1.什么是vue生命周期?
答: Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。

2.vue生命周期的作用是什么?
答:它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。

3.vue生命周期总共有几个阶段?
答:它可以总共分为8个阶段:创建前/后, 载入前/后,更新前/后,销毁前/销毁后。

4.第一次页面加载会触发哪几个钩子?
答:会触发 下面这几个beforeCreate, created, beforeMount, mounted 。

5.DOM 渲染在 哪个周期中就已经完成?

答:DOM 渲染在 mounted 中就已经完成了。
6. Vue 子组件和父组件执行顺序

  • 加载渲染过程:
  1. 父组件 beforeCreate
  2. 父组件 created
  3. 父组件 beforeMount
  4. 子组件 beforeCreate
  5. 子组件 created
  6. 子组件 beforeMount
  7. 子组件 mounted
  8. 父组件 mounted
  • 更新过程:
  1. 父组件 beforeUpdate
  2. 子组件 beforeUpdate
  3. 子组件 updated
  4. 父组件 updated
  • 销毁过程:
  1. 父组件 beforeDestroy

  2. 子组件 beforeDestroy

  3. 子组件 destroyed

  4. 父组件 destoryed

  5. 父组件监听子组件的生命周期、

子组件 手动触发

// 父组件
<child @mounted="handle"></child>

// 子组件 手动触发
mounted() {
  this.$emit('mounted')
}

vue2中父组件直接监听

<child @hook:mounted="handle"></child>

vue3中父组件直接监听

<Text @vue:mounted="fn" />

6. Vue组件间的通信

juejin.cn/post/699968…

7. vuex

怎么使用?哪种功能场景使用它?

vuex是用于管理Vue内部页面的数据状态、提供统一数据操作的生态系统,相当于一个数据仓库,任何组件都可以存取仓库中的数据。

五个属性:

(1)state state为单一状态树,在state中需要定义我们所需要管理的数组、对象、字符串等等,只有在这里定义了,在vue.js的组件中才能获取你定义的这个对象的状态。

(2)getters getter有点类似vue.js的计算属性,当我们需要从store的state中派生出一些状态,那么我们就需要使用getter,getter会接收state作为第一个参数,而且getter的返回值会根据它的依赖被缓存起来,只有getter中的依赖值(state中的某个需要派生状态的值)发生改变的时候才会被重新计算。

(3)mutations 更改store中state状态的唯一方法就是提交mutation,就很类似事件。每个mutation都有一个字符串类型的事件类型和一个回调函数,我们需要改变state的值就要在回调函数中改变。我们要执行这个回调函数,那么我们需要执行一个相应的调用方法:store.commit。

(4)action action可以提交mutation,在action中可以执行store.commit,而且action中可以有任何的异步操作。在页面中如果我们要使用这个action,则需要执行store.dispatch

(5)module module其实只是解决了当state中很复杂臃肿的时候,module可以将store分割成模块,每个模块中拥有自己的state、mutation、action和getter。

vuex3和vuex4的区别 :juejin.cn/post/722367…

// 引入vuex 
import Vue from 'vue' 
import Vuex from 'vuex'
//挂载Vuex 
Vue.use(Vuex) 

// 创建仓库
const store = new Vuex.Store({ 
    namespaced: true, // 开启命名空间
    //store实例 
    // 全局 Store → 对象
    state: { 
        count: 0 
    }, 
    // 模块 → 函数
    state() {
      return {
        count: 0
      }
    }
    mutations: { 
        increment (state,data) { 
            state.count = data 
        } 
    }, 
    actions: { 
        //context上下文 
        increment (context,payload ) {
            //模拟异步操作
            setTimeout(()=>{ 
                context.commit('increment', payload) 
            },1000) 
        } 
    }, 
    getters:{
      decorationName(state) {
        return state.count++ // 可以传参
      },
    } 
})
// 使用 在组件方法中使用
 //引用变量 
methods:{ 
    add(){ 
        console.log(this.$store.state.name) 
        console.log(this.$store.getters.decorationName)
    } 
}
//改变变量,
// increment是mutations中定义的改变state的方法 
this.$store.commit('moduleA/increment',15) 
//改变变量,在action中 
this.$store.dispatch('increment') 

// 在vuex4中 使用 setup中使用
import { useStore } from 'vuex'
let vuexStore = useStore()
vuexStore.state.name
console.log(vuexStore.state.moduleA.count) //命名空间
vuexStore.commit('increment',14)
vuexStore.dispatch('increment',14)

8. pinia

  • Pinia 没有 Mutations
  • Actions 支持同步和异步
  • 没有模块的嵌套结构
  • 更好的 TypeScript 支持
  • 用 TS 类型推断
  • 不需要注入、导入函数、调用它们,享受自动补全,让我们开发更加方便
  • 无需手动添加 store,它的模块默认情况下创建就自动注册的
  • Vue2 和 Vue3 都支持
  • 支持 Vue DevTools
  • 模块热更新
  • 支持使用插件扩展 Pinia 功能
  • 支持服务端渲染

全新的状态管理库,因为在 Vue3 中使用 Vuex 的话需要使用 Vuex4,还只能作为一个过渡的选择,存在很大缺陷,所以在 Componsition API 诞生之后,也就设计了全新的状态管理 Pinia。

  • state 用来存储全局状态,它必须是箭头函数,为了在服务端渲染的时候避免交叉请求导致的数据状态污染所以只能是函数,而必须用箭头函数则为了更好的 TS 类型推导
  • getters 就是用来封装计算属性,它有缓存的功能
  • actions 就是用来封装业务逻辑,修改 state,可以支持同步和异步
//安装
npm install pinia
// 加载
import { createPinia } from 'pinia'
createApp(App).use(createPinia()).mount('#app')

// 使用
import { defineStore } from 'pinia'
export const userStore = defineStore('user', {
    state: () => {
        return { 
            count: 1,
            arr: []
        }
    },
    getters: {
       // 方法一,接收一个可选参数 state
      myCount(state){
          console.log('调用了') // 页面中使用了三次,这里只会执行一次,然后缓存起来了
          return state.count + 1
      },
      // 方法二,不传参数,使用 this
      // 但是必须指定函数返回值的类型,否则类型推导不出来
      myCount(): number{
          return this.count + 1
      }
    },
    actions: {
      changeState(num: number){ // 不能用箭头函数,否则会绑定到外部的this
        this.count += num
      },
      async loadUserList(){
          const list = await getUserList()
          this.list = list
      }
    }
})


// 引用变量
import { userStore } from '../store'
import { storeToRefs } from 'pinia' // 支持解构之后响应式

const store = userStore()
// 解构,会失去响应式
// const { count } = userStore()
// 支持解构响应式
const { count } = storeToRefs(userStore())

// 调用方法
const handleClick = () => {
    // 方法一
    store.count++
    
    // 方法二,需要修改多个数据,建议用 $patch 批量更新,传入一个对象
    store.$patch({
        count: user_store.count1++,
        // arr: user_store.arr.push(1) // 错误
        arr: [ ...user_store.arr, 1 ] // 可以,但是还得把整个数组都拿出来解构,就没必要
    })
    
    // 使用 $patch 性能更优,因为多个数据更新只会更新一次视图
    
    // 方法三,还是$patch,传入函数,第一个参数就是 state
    store.$patch( state => {
        state.count++
        state.arr.push(1)
    })

      // 方法四。使用actions
      store.changeState(1)
}

9. Vue的路由实现:

路由这个概念最初是由后端提出来的,在我们没有SPA单页面应用之前,使用的一直都是后端路由,根据不同的路由返回不同的页面。SPA极大地提升了用户体验,它允许页面在不刷新的情况下更新页面内容,使内容的切换更加流畅。

前端路由可以帮助我们在仅有一个页面的情况下,“记住”用户当前走到了哪一步——为 SPA 中的各个视图匹配一个唯一标识。这意味着用户前进、后退触发的新内容,都会映射到不同的 URL 上去。此时即便他刷新页面,因为当前的 URL 可以标识出他所处的位置,因此内容也不会丢失。

9.1. vue-router

专为Vue打造的路由管理工具

vue-router在实现单页面前端路由时,提供了两种方式:Hash模式和History模式。 默认为Hash模式

  • hash模式: 本质上是改变window.location.href属性 “#”后面的内容,于是当 URL 改变时,页面不会重新加载。 www.xxx.com/#/login 我们在切换路由的时候改变的就是 #后面的内容, 同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。

是怎么监听hash变化的——hashchange()

<body>
    <a href="#/home">Home</a>
    <br>
    <a href="#/about">About</a>
    <br>
  <div id="app">Home Page</div>
  <script>
    const app = document.getElementById('app')

    const router = {
      mode: 'hash',
      routes: [
        { path: '/home', component: 'Home Page' },
        { path: '/about', component: 'About Page' },
      ]
    }

    window.addEventListener('hashchange', (event) => {
      const path = location.hash.slice(1)
      const route = router.routes.find(r => r.path === path)
      if (route) {
        app.innerHTML = route.component
      }
    })
  </script>
</body>
  • history模式:模式充分利用了html5 history interface 中新增的 history.pushState() 和 history.replaceState() 方法。这两个方法应用于浏览器记录栈,在当前已有的 back、forward、go 基础之上,它们提供了对历史记录修改的功能。不过这种模式需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 不存在的页面就会返回 404。所以要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面。

vue-router是基于路由和组件的 路由用于设定访问路径, 将路径和组件映射起来. 在vue-router的单页面应用中, 页面的路径的改变就是组件的切换.

  • 步骤一: 安装vue-router npm install vue-router --save

  • 步骤二: 在模块化工程中使用它(因为是一个插件, 所以可以通过Vue.use()来安装路由功能)

第一步:导入路由对象,并且调用 Vue.use(VueRouter)

第二步:创建路由实例,并且传入路由映射配置

第三步:在Vue实例中挂载创建的路由实例

import Vue from 'vue' 
import VueRouter from 'vue-router' 
//1.注入插件 
Vue.use(VueRouter) 
//2.定义路由 
const routes=[ 
  { 
    path:'/home', 
    component:Home 
  }, 
  { 
    path:'/about:id',   // 动态路由
    component:About 
  },
  { 
    path: '/users/:id',
    component: () => import('./views/UserDetails.vue')  // 路由懒加载
},
] 
//3.创建router实例 
// scrollBehavior 滚动到指定位置
const router = new VueRouter({ 
  mode: 'history',
  routes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return { top: 0, behavior: 'smooth' }
    }
})

route和router的区别

答:route是路由信息对象,包括path,component,name,params,hash,query等路由信息参数。而router是“路由实例”对象,包括了路由的跳转方法,钩子函数等。

// option-api
this.$router.repalce({
    params: { // 动态路由的部分
      id: 3333
    }, 
    query: {  // 问好后面的内容
      name: 'wangmo'
    }
})

// composition-api
  
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
function pushWithQuery(query) {
  router.push({
    name: 'search',
    query: {
      ...route.query,
      ...query,
    },
  })
}


  // 动态添加路由
import { useRouter } from 'vue-router';

const router = useRouter();
const newRoute = {
  path: '/hello', 
  name: 'hello', 
  component: () => import('../components/HelloWorld.vue'), // 动态加载组件
};
router.addRoute(newRoute);
// 删除路由
router.removeRoute('xxx');

9.2. 路由懒加载

  • 动态路由

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。

// 动态导入
// ,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。
const UserDetails = () => import('./views/UserDetails.vue')
routes: [
  { path: '/users/:id', component: UserDetails }
  // 或在路由定义里直接使用它
  { path: '/users/:id', component: () => import('./views/UserDetails.vue') },
],
  • webpack打包

有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4):

const UserDetails = () =>
  import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
const UserDashboard = () =>
  import(/* webpackChunkName: "group-user" */ './UserDashboard.vue')
const UserProfileEdit = () =>
  import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue')
  • vite打包
// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'group-user': [
            './src/UserDetails',
            './src/UserDashboard',
            './src/UserProfileEdit',
          ],
        },
      },
    },
  },
})

9.3. 钩子导航守卫

导航守卫(Navigation Guards)机制,用于在路由切换前、切换后或切换取消时执行自定义逻辑。

https://router.vuejs.org/zh/guide/advanced/navigation-guards.html

(1)全局前置/钩子:beforeEach、beforeResolve、afterEach

beforeEach 可以返回的值如下:

  • false: 取消当前的导航。如果浏览器的 URL 改变了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
  • 一个路由地址: 通过一个路由地址重定向到一个不同的地址,如同调用 router.push(),且可以传入诸如 replace: truename: 'home' 之类的选项。它会中断当前的导航,同时用相同的 from 创建一个新导航。

router.beforeEach(async (to, from) => {
   if (to.name !== 'Login') {
     // 将用户重定向到登录页面
     return { name: 'Login' }
   }
 })

// 之前的版本中有 next 参数,每一个分支逻辑都要有一个next
router.beforeEach((to, from, next) => {
  if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
  else next()
})

(2)路由独享的守卫:beforeEnter

beforeEnter 守卫 只在进入路由时触发,不会在 paramsqueryhash 改变时触发。例如,从 /users/2 进入到 /users/3 或者从 /users/2#info 进入到 /users/2#projects不会触发。它们只有在 从一个不同的 路由导航时,才会被触发。(之前的版本有next)

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from) => {
      // reject the navigation
      return false
    },
  },
]
  • 组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
<script>
export default {
  beforeRouteEnter(to, from, next) {
    // 在渲染该组件的对应路由被验证前调用
    // 不能获取组件实例 `this` !
    // 因为当守卫执行时,组件实例还没被创建!
    // 注意点,beforeRouteEnter组件内还访问不到this,因为该守卫执行前组件实例还没有被创建,
    需要传一个回调给 next来访问,例如
    next(target => {        
        if (from.path == '/classProcess') {          
            target.isFromProcess = true        
        }      
    })  
  },
  beforeRouteUpdate(to, from) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
    // 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
  },
  beforeRouteLeave(to, from) {
    // 在导航离开渲染该组件的对应路由时调用
    // 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
  },
}
</script>

9.4. 完整的导航解析流程

    1. 导航被触发。

    2. 在失活的组件里调用 beforeRouteLeave 守卫。

    3. 调用全局的 beforeEach 守卫。

    4. 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。

    5. 在路由配置里调用 beforeEnter

    6. 解析异步路由组件。

    7. 在被激活的组件里调用 beforeRouteEnter

    8. 调用全局的 beforeResolve 守卫(2.5+)。

    9. 导航被确认。

    10. 调用全局的 afterEach 钩子。

    11. 触发 DOM 更新。

    12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

9.5. Vue-router跳转和location.href有什么区别

  • 使用 location.href= /url 来跳转,简单方便,但是刷新了页面;
  • 使用 history.pushState( /url ) ,无刷新页面,静态跳转;
  • 引进 router ,然后使用 router.push( /url ) 来跳转,使用了 diff 算法,实现了按需加载,减少了 dom 的消耗。其实使用 router 跳转和使用 history.pushState() 没什么差别的,因为vue-router就是用了 history.pushState() ,尤其是在history模式下。

10. Vue与Angular以及React的区别?

(1)与AngularJS的区别

相同点:

  • 都支持指令:内置指令和自定义指令;
  • 都支持过滤器:内置过滤器和自定义过滤器;
  • 都支持双向数据绑定;
  • 都不支持低端浏览器。

不同点:

AngularJS的学习成本高,比如增加了Dependency Injection特性,而Vue.js本身提供的API都比较简单、直观;在性能上,AngularJS依赖对数据做脏检查,所以Watcher越多越慢;Vue.js使用基于依赖追踪的观察并且使用异步队列更新,所有的数据都是独立触发的。

与React的区别

相同点:

  • React采用特殊的JSX语法,Vue.js在组件开发中也推崇编写.vue特殊文件格式,对文件内容都有一些约定,两者都需要编译后使用;
  • 中心思想相同:一切都是组件,组件实例之间可以嵌套;都提供合理的钩子函数,可以让开发者定制化地去处理需求;
  • 都不内置列数AJAX,Route等功能到核心包,而是以插件的方式加载;在组件开发中都支持mixins的特性。 不同点:
  • React采用的Virtual DOM会对渲染出来的结果做脏检查;Vue.js在模板中提供了指令,过滤器等,可以非常方便,快捷地操作Virtual DOM。

11. Vue指令

  • v-html:渲染文本(能解析 HTML 标签)容易受到xss攻击,
  • v-text:渲染文本(统统解析成文本)
  • v-bind:动态绑定属性,如src,href,类和样式等。语法糖 ”:“,也可用在props传递参数中
  • v-for:循环 数组:v-for ="(item,index) in arr" :key="index" 对象:v-for=”{value,key,index}in obj“ :key="index" 官方推荐我们在使用v-for时,给对应的元素或组件添加上一个:key属性。key的作用主要是为了高效的更新虚拟DOM
  • v-once:只绑定一次,不会随着数据的改变而改变。 可以避免多次渲染,提高性能。
  • v-model:绑定模型,双向数据绑定model和view中的值进行同步变化 -
  • v-on:绑定事件,事件监听,语法糖:”@“ v-on:click="btnClick” -
  • v-pre :跳过渲染: 比如在模板中显示大括号
<span v-pre>{{ this will not be compiled }}</span>
  • v-if v-show:条件渲染

v-if后面的条件为false时,对应的元素以及其子元素不会渲染。 v-if和v-show都可以决定一个元素是否渲染,那么开发中我们如何选择呢?

    • v-if当条件为false时,压根不会有对应的元素在DOM中。
    • v-show当条件为false时,仅仅是将元素的display属性设置为none而已。 开发中如何选择呢? 当需要在显示与隐藏之间切片很频繁时,使用v-show 当只有一次切换时,通过使用v-if
  • v-memo: v3.2以后可用

缓存一个模板的子树。在元素和组件上都可以使用。为了实现缓存,该指令需要传入一个固定长度的依赖值数组进行比较。如果数组里的每个值都与最后一次的渲染相同,那么整个子树的更新将被跳过。

一般与v-for配合使用,v-memo的值是一个数组。数组的值不改变的情况,该组件及子组件就会跳过更新

  • v-memo 绑定的值没改变,子组件引用的响应数据变了,也不会更新
<div v-for="item in list" :key="item.id" v-memo="[item ]">
  <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
  <p>...more child nodes</p>
</div>

11.1. Vue-cli自定义指令

vue中提供了一套为数据驱动视图更为方便的操作,这些操作被称为指令系统。我们看到的v-开头的行内属性,比如v-html,v-bind等都是指令,不同的指令可以完成或实现不同的功能,除了核心功能默认内置的指令 ( v-model v-show ), Vue 也允许注册自定义指令。

只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令。

vue2: v2.cn.vuejs.org/v2/guide/cu…

bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。

我们会在稍后讨论渲染函数时介绍更多 VNodes 的细节。

componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

unbind:只调用一次,指令与元素解绑时调用。

vue3: cn.vuejs.org/guide/reusa…

const myDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode) {
    // 下面会介绍各个参数的细节
  },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode) {}
}

vue2 和vue3中钩子函数不一样,vue3 setup中也简化了用法。

使用方法:

//指令的使用
<input v-focus>

// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})
// 局部指令
directives: {
  focus: {
    // 指令的定义
    inserted: function (el) {
      el.focus()
    }
  }
}

一些常见指令的手写实现 juejin.cn/post/690602…

v-draggable



app.directive('draggable', {
  mounted(el) {
    el.style.position = 'absolute'
    el.style.cursor = 'move'
    const dist = {
      x: 0,
      y: 0
    }
    let isDraggle = false
    el.addEventListener('mousedown', (e) => {
      isDraggle = true
      // 记录初始差值
      // pagex 鼠标相对于整个页面的距离,包含被滚动的部分
      // offsetleft: 当前元素相对于其带有定位的父元素(最近的祖先元素)的顶部偏移量(单位:像素)。

      dist.x = e.pageX - el.offsetLeft;
      dist.y = e.pageY - el.offsetTop;
    })
    el.addEventListener('mousemove', (e) => {
      if (isDraggle) {
        // 不断更新新的位置坐标
        el.style.left = e.pageX - dist.x + 'px'
        el.style.top = e.pageY - dist.y + 'px'
      }
    })
    el.addEventListener('mouseup', () => {
      if (isDraggle) {
        isDraggle = false
        dist.x = 0
        dist.y = 0
      }
    })
  }
})

v-loadImage 图片懒加载

  • IntersectionObserver 判断是否进入视口
  • 节流的传参和ts中this的使用
/* eslint-disable */
const loadImage = {
  inserted(el: any, { value }: any) {
    el.$src = value
    // IntersectionObserver存在兼容性问题
    IntersectionObserver ? obEvent(el) : scrollEvent(el)
  },
  componentUpdate(el: any, binding: any) {
    el.$src = binding.value
  },
  unbind(el: any) {
    IntersectionObserver && el.$ob.disconnect()
  }
}

const obEvent = (el: any) => {
  console.log('obEvent', obEvent)
  const ob = new IntersectionObserver((entries) => {
    if (entries[0].isIntersecting && el.$src) {
      el.src = el.$src
    }
  })
  ob.observe(el)
  el.$ob = ob
}

const scrollEvent = (el: any) => {
  load(el)
  const handler = throttle(load, 2000)
  window.addEventListener('scroll', () => {
    handler(el)
  })
}

const load = (el: any) => {
  // innerHeight是当前浏览器视口高度
  const innerHeight = window.innerHeight;
  // html元素被卷起来的高度
  const scrollTop = document.documentElement.scrollTop;
  // el.offsetTop 是img元素距离父元素顶部的距离
  if (el.offsetTop < (innerHeight + scrollTop) && el.offsetTop > scrollTop) {
      el.src = el.$src
  } else {
      el.src = ''
  }
}

function throttle(fn: any, delay: any) {
  let start = Date.now()
  return function (this: unknown, ...args: any[]) {
    if (Date.now() - start > delay) {
      fn.apply(this, args)
      start = Date.now()
    }
  }
}

export default loadImage

12. Vue的过滤器

(1)什么是过滤器? 将要展示的数据进行一定的处理,然后进行展示,注意过滤器不改变原来的数据,只是产生了新的数据

(2)过滤器的使用

// 过滤器的使用
<li>商品价格:{{item.price | filterPrice}}</li>

<div :class="item.status | statusFilter"></div>


过滤器接收表达式的值 (msg) 作为第一个参数。
全局过滤器: 
Vue.filter("过滤器名称",function(value1,value2){ 
    //逻辑代码 
}) 
局部过滤器:
new Vue({ 
  filters: { 
    filterPrice (price) {
      return price ? ('¥' + price) : '--'
    }
  } 
}) 

过滤器的优势‌:

  • 简单且易于使用,适合文本格式化。
  • 可以在模板中直接复用。(只能在模板中使用)

过滤器的局限性‌:

  • 主要用于文本格式化,对于复杂逻辑处理不够灵活。
  • 不缓存结果,每次调用都会执行函数,可能影响性能。

计算属性的优势‌:

  • 基于依赖进行缓存,性能更好。
  • 可以处理复杂的逻辑,使模板更加简洁。
  • 可以修改属性

计算属性的局限性‌:

  • 需要先在组件的computed选项中定义,不能在模板中直接定义。

13. keep-alive

keep-alive是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染DOM。

使用include 和exclude 来指定特定组件包含或排除

max可以定义组件最大的缓存个数,如果超过了这个个数的话,在下一个新实例创建之前,就会将以缓存组件中最久没有被访问到的实例销毁掉

如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。

当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件、触发 activated钩子函数。

keep-alive在各个生命周期里都做了啥吧:

  • created:初始化一个cache、keys,前者用来存缓存组件的虚拟dom集合,后者用来存缓存组件的key集合
  • mounted:实时监听include、exclude这两个的变化,并执行相应操作
  • destroyed:删除掉所有缓存相关的东西

14. 计算属性和侦听器watch和方法

对于Computed:

  • 它支持缓存,只有依赖的数据发生了变化,才会重新计算
  • 不支持异步,当Computed中有异步操作时,无法监听数据的变化
  • computed的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data声明过,或者父组件传递过来的props中的数据进行计算的。
  • 如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用computed
  • 如果computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性的属性值;在computed中,属性有一个get方法和一个set方法,当数据发生变化时,会调用set方法。
//vue2
computed:{
    fullName:{
        get(){
            return this.firstName + '-' + this.lastName
        },
        //set什么时候调用? 当fullName被修改时。
        set(value){
            const arr = value.split('-') //将数组用‘-’符号分割为数组
            this.firstName = arr[0]
            this.lastName = arr[1]
        }
    },
    innerHeight(){
        return this.name
    }
}


//vue3
const plusOne = computed({
  get: () => count.value + 1,
  set: (val) => {
    count.value = val - 1
  }
})

plusOne.value = 1

Watch:

  • 它不支持缓存,数据变化时,它就会触发相应的操作
  • 支持异步监听
  • 监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
  • 当一个属性发生变化时,就需要执行相应的操作
  • 监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
    • immediate:组件加载立即触发回调函数
    • deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep无法监听到数组和对象内部的变化。

当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch。

watch 的使用

juejin.cn/post/717549…

vue2中:

// 字符串
watch: {
  inputValue(newVal, oldVal){
    console.log(newVal, oldVal, 'inputValue')
  }
},
  // 对象
watch: {
  "obj.inputValue"(newVal, oldVal) {
    // 查看改变的值
    console.log(newVal, oldVal, "inputValue");
  },
},
// 深度监听
watch: {
    obj: { // 对象内部的值改变择改变
      handler(newVal, oldVal) {
          // 查看改变的值
          console.log(newVal, oldVal, "inputValue");
      },
      deep: true, // 深度监听
      immediate: true, // 初次执行
    },
},

// 手动停止监听
this.$watch('inputValue', null); // 传入null即可停止监听
// 组件销毁时会自动停止监听

// 在watch中使用if判断条件控制是否执行

vue3中

  • watch可以访问新值和旧值,watchEffect不能访问。
  • watch需要指明监听的对象,也需要指明监听的回调。watchEffect不用指明监视哪一个属性,监视的回调函数中用到哪个属性,就监视哪个属性。
  • watch只有监听的值发生变化的时候才会执行,但是watchEffect不同,每次代码加载watchEffect都会执行
// 基本类型
const stopWatch = watch(
  () => inputValue.value, // 监听Form表单
  async (newVal) => {
    console.log(newVal, 'inputValue'); '谁不是'
  },
  {
    immediate: true, // 初次进入页面执行
  }
);
// 监听多个值
watch(
  () => [obj.value.inputValue, obj.value.inputOneValue], // obj对象内部的值
  async (newVal) => {
    console.log(newVal, 'inputValue'); // 数组的格式['谁不是', '隔开了']
  },
  {
    immediate: true, // 初次进入页面执行
    deep: true //深度监听
  }
);
// 可以手动停止watch监听
stopWatch()


// watchEffect
// 只要watchEffect有被修改的值就会发生变化
watchEffect(() => {
  const a = obj.value.inputValue;
  const b = obj.value.inputOneValue;
  console.log(obj.value, a, b, 'obj');
});

method:

不支持缓存

15. 事件修饰符

<!-- 单击事件将停止传递 , 防止事件冒泡-->
<a @click.stop="doThis"></a>

<!-- 提交事件将不再重新加载页面 , 防止执行预设的行为-->
<form @submit.prevent="onSubmit"></form>

<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>

<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>

<!--  只会触发自己范围内的事件,不包含子元素; -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>

<!-- 只会触发一次-->
<div @click.once="onclick"></div>

16. v-if 和v-show 的区别

  • 手段:v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display样式属性控制显隐;
  • 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换;
  • 编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且DOM元素保留;
  • 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;
  • 使用场景:v-if适合运营条件不大可能改变;v-show适合频繁切换。

17. v-model

vue 2的 v-model:可以使用在原生组件和自定义组件中

父组件使用
<child v-model="value"></child>
// 是下面写法的语法糖
<child :value="value" @input="el => value = el.target.value"></child>

// 子组件
<input v-bind:value="value" v-on:input="onmessage"></nput>
props: {
  value: {
    type: String,
    required: true
  }
},
methods:{
    onmessage(e){
        $emit('input',e.target.value)
    }
}
// 也可以使用models自定义
models: {
    event: 'change',
    prop: 'value2'
},

vue 3的 v-model:

https://cn.vuejs.org/guide/components/v-model.html

  • 3.4之前
<!-- Parent.vue -->
<Child v-model="foo"/>
相当于:
<Child
  :modelValue="foo"
  @update:modelValue="$event => (foo = $event)"
/>

<!-- Child.vue -->

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

v-model带参数和多个参数形式

<!-- Parent.vue -->
<Child v-modelname="foo" v-modelage="222"/>

<!-- Child.vue -->

<template>
  <input
    :value="props.name"
    @input="emit('update:name', $event.target.value)"
  />
<input
    :value="props.age"
    @input="emit('update:age', $event.target.value)"
  />
</template>
<script setup>
const props = defineProps(['name','age'])
const emit = defineEmits(['update:age','update:name'])
</script>
  • 3.4之后
<!-- Parent.vue -->
<Child v-model="countModel" />

<!-- Child.vue -->
defineModel() 返回的值是一个 ref。它可以像其他 ref 一样被访问以及修改,
不过它能起到在父组件和当前变量之间的双向绑定的作用:

·它的 .value 和父组件的 v-model 的值同步;
·当它被子组件变更了,会触发父组件绑定的值一起更新。
<script setup>
const model = defineModel()

function update() {
  model.value++
}
</script>

<template>
  <div>Parent bound v-model is: {{ model }}</div>
  <button @click="update">Increment</button>
</template>
  • .sync

.sync修饰符可以实现子组件与父组件的双向绑定,并且可以实现子组件同步修改父组件的值。

// 父组件
// 正常父传子: 
<son :a="num" :b="num2"></son>

// 加上sync之后父传子: 
<son :a.sync="num" :b.sync="num2"></son> 

// 它等价于
<son
  :a="num" @update:a="val=>num=val"
  :b="num2" @update:b="val=>num2=val">
</son> 


// 子组件
<template>
  <input
    :value="props.a"
    @input="emit('update:a', $event.target.value)"
  />
</template>
<script setup>
const props = defineProps(['a'])
const emit = defineEmits(['update:a'])
</script>

18. data中的数据为什么不是对象而是函数

JavaScript中的对象是引用类型的数据,当多个实例引用同一个对象时,只要一个实例对这个对象进行操作,其他实例中的数据也会发生变化。

而在Vue中,更多的是想要复用组件,那就需要每个组件都有自己的数据,这样组件之间才不会相互干扰。

所以组件的数据不能写成对象的形式,而是要写成函数的形式。数据以函数返回值的形式定义,这样当每次复用组件的时候,就会返回一个新的data,也就是说每个组件都有自己的私有数据空间,它们各自维护自己的数据,不会干扰其他组件的正常运行。

19. 虚拟dom

vue 渲染和diff 过程:

  1. 用JS对象模拟DOM(虚拟DOM)
  2. 把此虚拟DOM转成真实DOM并插入页面中(render)
  3. 如果有事件发生修改了虚拟DOM,比较两棵虚拟DOM树的差异,得到差异对象(diff)
  4. 把差异对象应用到真正的DOM树上(patch)

虚拟 DOM 简单说就是 用JS对象来模拟 DOM 结构, (Virtual Node),简称 vnode

它的表达方式就是把每一个真实dom元素都转为一个对象

虚拟DOM是对DOM的抽象,这个对象是更加轻量级的对 DOM的描述。它可以更好的跨平台。

在代码渲染到页面之前,vue会把代码转换成一个对象(虚拟 DOM)。以对象的形式来描述真实DOM结构,最终渲染到页面。在每次数据发生变化前,虚拟DOM都会缓存一份,变化之时,现在的虚拟DOM会与缓存的虚拟DOM进行比较。在vue内部封装了diff算法,通过这个算法来进行比较,渲染时修改对应变化的地方,原先没有发生改变的通过原先的数据进行渲染。

// 真实dom
<template>
    <div id="app" class="container">
        <h1>沐华</h1>
    </div>
</template>


// 虚拟dom
{
  tag:'div',
  props:{ id:'app', class:'container' },
  children: [
    { tag: 'h1', children:'沐华' }
  ]
}

Virtual DOM的更新DOM的准备工作耗费更多的时间,也就是JS层面,相比于更多的DOM操作它的消费是极其便宜的。

  • 真实DOM∶ 生成HTML字符串+重建所有的DOM元素
  • 虚拟DOM∶ 生成vNode+ DOMDiff+必要的dom更新

DIff算法

Diff 算法,在 Vue 里面就是叫做 patch ,通过新旧虚拟 DOM 对比(即 patch 过程),找出最小变化的地方转为进行 DOM 操作。Diff 算法的时候都遵循深度优先,同层比较的策略做了一些优化,来计算出最小变化

Diff 算法的优化

    1. 只比较同一层级,不跨级比较
    2. 比较标签名:如果同一层级的比较标签名不同,就直接移除老的虚拟 DOM 对应的节点, 不继续按这个树状结构做深度比较
    3. 比较 key:如果标签名相同,key 也相同,就会认为是相同节点, 也不继续按这个树状结构做深度比较
    4. 当前两个node的key和基本属性相同,调用updateChildren对子节点进行diff(新旧node都有子节点)

vue2 采用了双端diff算法

juejin.cn/post/684490…

vue3 :最长递增子序列

key 的作用

vue 中 key 值的作用可以分为两种情况来考虑:

  • 标识一个独立的元素: 第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。
  • 高效的更新渲染虚拟 DOM: 第二种情况是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。在不使用 key 的时候,每个元素对应的位置关系都是 index,导致我们插入的元素到后面的全部元素,对应的位置关系都发生了变更,所以全部都会执行更新操作。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM

key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速


Vue3 的优化

Vue3 的 diff 算法通过‌静态标记、双端预处理、LIS 优化、Fragment 支持‌等策略,实现了更高效的 DOM 更新。这些优化使 Vue3 在复杂场景下(如列表渲染、动态组件)性能显著优于 Vue2‌

静态标记与非全量 Diff‌

  • 静态标记(PatchFlag) ‌:在创建虚拟 DOM 时,根据内容是否会变化添加静态标记,后续仅对比带标记的节点‌。
  • 静态提升‌:将静态节点提升为常量,避免重复创建,直接复用已存在的 DOM 元素‌。

预处理相同前后缀节点‌

  • 双端快速比对‌:
    • 头部比对‌:自前向后跳过相同前缀节点(如 A-BA-B)‌。
    • 尾部比对‌:自后向前跳过相同后缀节点(如 C-D-ED-E)‌。
  • 减少遍历范围‌:仅处理中间差异部分(如 B-CC-F-B),跳过无需操作的节点‌。

最长递增子序列(LIS)优化‌

  • 减少 DOM 移动次数‌:通过 LIS 算法确定旧节点在新序列中的最长递增顺序,仅移动无法被序列覆盖的节点(如 C-B 移动一次而非多次)‌。
  • 时间复杂度优化‌:LIS 算法复杂度为 O(n log n),相比全量比对更高效‌。

‌Fragment 与动态属性优化‌

  • Fragment 支持‌:合并多根节点的层级(如 <template>),减少虚拟 DOM 的嵌套,提升比对效率‌6。
  • 动态属性快速路径‌:对动态属性(如 :classv-if)单独处理,减少比较开销‌6。

事件缓存与复用策略‌

  • 事件缓存‌:缓存事件处理函数,避免重复绑定(如 @click 事件)‌。
  • 复用判断优化‌:通过 isSameVNodeType 严格判断节点类型和 key,避免无效更新‌。

20. vue中封装的数组方法

在Vue2中,对响应式处理利用的是Object.defineProperty对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行hack,让Vue能监听到其中的变化。

push/pop/shift/unshift/splice/sort/reverse

那 Vue 是如何实现让这些数组方法实现元素的实时更新的呢,下面是Vue 中对这些方法的封装:

// src/core/observer/array.js

// 引入 def 函数,用于在对象上定义属性
import { def } from '../util/index';
// 获取数组的原型对象
const arrayProto = Array.prototype;
// 创建一个新的对象,该对象的原型是数组的原型对象,用于重写数组的方法
export const arrayMethods = Object.create(arrayProto);

// 通过遍历数组的一些方法,重写数组的方法
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
  // 获取原始的数组方法
  const original = arrayProto[method];

  // 在重写的数组方法上进行封装,使用 def 函数定义属性
  def(arrayMethods, method, function mutator (...args) {
    // 调用原始的数组方法,获取原始方法的返回值
    const result = original.apply(this, args);
    // 获取数组对象的 Observer 对象
    const ob = this.__ob__;

    // 对于一些方法(push、unshift、splice)可能会新增元素,需要进行响应式处理
    let inserted;
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args;
        break;
      case 'splice':
        // splice 方法的参数从第三个参数开始是新增的元素
        inserted = args.slice(2);
        break;
    }
    // 如果有新增的元素,调用 Observer 对象的 observeArray 方法进行观察
    if (inserted) ob.observeArray(inserted);

    // 通知变化
    ob.dep.notify();
    // 返回原始方法的返回值
    return result;
  });
});

上述代码首先创建了一个名为arrayMethods的对象,该对象继承自数组的原型对象Array.prototype。然后,通过循环遍历数组的一些方法(例如pushpopshift等),对这些方法进行了重新定义。在重新定义的方法中,先调用原始的数组方法,然后获取数组对象的__ob__属性,即其Observer对象。接着,根据不同的数组操作,可能会获取到新增的元素,然后调用observeArray方法对新增的元素进行观察,以实现嵌套对象的响应式处理。最后,通过ob.dep.notify()通知依赖该数组的Watcher进行更新。

简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的的 Observer 对象,如果有新增加的值,就调用observeArray 继续对新的值观察变化,然后手动调用 notify,通知渲染 watcher,执行 update。

21. Vue 单页应用与多页应用的区别

  • SPA单页面应用(SinglePage Web Application),指只有一个主页面的应用,一开始只需要加载一次js、css等相关资源。所有内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。
  • MPA多页面应用 (MultiPage Application),指有多个独立页面的应用,每个页面必须重复加载js、css等相关资源。多页应用跳转,需要整页资源刷新。

22. Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?

  • Vue 的视图更新是异步的,不会立即同步执行。
  • 通过异步更新队列,Vue 实现了高效的批量 DOM 更新。
  • 如果需要确保在 DOM 更新后执行操作,可以使用 this.$nextTick

在 Vue 中,当 data 中的某个属性发生变化时,视图不会立即同步重新渲染。Vue 使用异步更新队列来优化性能,具体流程如下:

  1. 数据变化:当你修改 data 中的某个属性时,Vue 会检测到这个变化。
  2. 依赖触发:Vue 会通知所有依赖该属性的 Watcher,告知它们数据已更新。
  3. 异步更新队列:Vue 不会立即更新 DOM,而是将这些 Watcher 推入一个异步更新队列。
  4. 批量更新:在下一个事件循环中,Vue 会清空这个队列,执行所有 Watcher 的更新操作,最终统一更新 DOM。

22.1.1. 为什么是异步更新?

  • 性能优化:同步更新 DOM 会导致频繁的重绘和回流,影响性能。通过异步更新,Vue 可以将多个数据变化合并为一次 DOM 更新。
  • 避免不必要的渲染:如果在同一个事件循环中多次修改数据,Vue 只会执行一次最终的渲染。

22.1.2. 如何确保在 DOM 更新后执行操作?

如果你需要在 DOM 更新后执行某些操作,可以使用 Vue.nextTick 方法:

this.someData = 'new value';
this.$nextTick(() => {
  // DOM 已经更新
  console.log('DOM updated');
});

23. nexttick

  • nextTick 是 Vue 提供的一个工具,用于在 DOM 更新完成后执行回调函数。Vue 的更新机制是异步的,当你修改 data 中的某个属性时,Vue 不会立即更新 DOM,而是将这些更新操作推入一个队列,并在下一个事件循环中批量执行。这样做是为了优化性能,避免频繁的 DOM 操作。如果你在修改数据后立即尝试操作 DOM,可能会发现 DOM 还没有更新,因为 Vue 的更新是异步的。使用 nextTick,确保你的操作在 DOM 更新完成后执行。
  • 使用场景包括操作更新后的 DOM、在组件更新后执行逻辑等,或在created中操作dom(这时dom还没有渲染)
  • 它的实现依赖于浏览器的异步机制(如 PromiseMutationObserversetTimeout)。

实现原理:

将传入的回调函数包装成异步任务,异步任务又分微任务和宏任务,为了尽快执行所以优先选择微任务;

nextTick 提供了四种异步方法 Promise.then、MutationObserver、setImmediate、setTimeout(fn,0)

// 改造后的 $myNextTick 方法
Vue.prototype.$myNextTick = function (fn) {
  // 返回 Promise 的同时支持回调参数
  const promise = new Promise((resolve) => {
    if (typeof MutationObserver !== "undefined") {
      let observer = new MutationObserver(() => {
        resolve()      // 触发 Promise 解决
        observer.disconnect() // 清理观察器
      })
      const textNode = document.createTextNode(' ')
      observer.observe(textNode, { characterData: true })
      textNode.data = '~' // 触发变更
    } else {
      setTimeout(resolve, 0)
    }
  })

  // 处理回调函数
  if (typeof fn === 'function') {
    return promise.then(fn) // 同时支持 .then() 和回调
  }
  
  return promise
}


// 调用 
// 回调模式
this.$myNextTick(() => {
  console.log('DOM updated!')
})

// Promise 模式
this.$myNextTick().then(() => {
  console.log('DOM updated!')
})

24. 什么是 mixin ?

  • mixin 是一个包含组件选项的对象。它可以被混入到 Vue 组件中,从而将这些选项合并到组件的选项中。
  • 作用:将通用的逻辑提取出来,避免重复代码。
  • 特点
    • mixin 中的选项会和组件的选项进行合并。
    • 如果组件和 mixin 中有相同的选项(如 datamethods 等),Vue 会按照一定的规则进行合并。
      • data 和method,命名相同使用组件里的
      • 生命周期的钩子函数:先执行mixin中的
  • 缺点
    • 命名冲突:如果 mixin 和组件中有相同的选项,可能会导致意外的覆盖行为。
    • 隐式依赖mixin 中的逻辑可能会依赖于组件的上下文,导致代码难以理解和维护。
    • 难以追踪:当多个 mixin 混入一个组件时,逻辑的来源可能不清晰,增加调试难度。

25. SSR的理解

SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端

SSR的优势:

  • 更好的SEO(SEO 是 Search Engine Optimization 的缩写,即搜索引擎优化。它是一种通过调整网站的内容、结构、外部链接等方面的优化手段,来提高网站在搜索引擎自然/免费搜索结果中的排名和可见度的过程)
  • 首屏加载速度更快

SSR的缺点:

  • 开发条件会受到限制,服务器端渲染只支持beforeCreate和created两个钩子;
  • 当需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境;
  • 更多的服务端负载。

26. Vue的性能优化手段

编码阶段:

  • SPA 页面采用keep-alive缓存组件
  • key保证唯一
  • 使用路由懒加载、异步组件
  • 防抖、节流
  • 第三方模块按需导入
  • 虚拟滚动
  • 图片懒加载

SEO优化

  • 预渲染
  • 服务端渲染SSR

打包优化

  • 压缩代码
  • Tree Shaking/Scope Hoisting
  • 使用cdn加载第三方模块
  • splitChunks抽离公共文件
  • sourceMap优化

用户体验

  • 骨架屏
  • 还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。

27. vue scope 是怎么做的样式隔离的

Vue 中的样式隔离是通过 Vue 单文件组件(Single File Components,简称 SFC)的 <style> 标签中的 scoped 属性实现的。当你在一个 Vue 组件的 <style> 标签上添加 scoped 属性时,Vue 会自动将该样式限定在当前组件的范围内,从而防止样式冲突和不必要的样式泄漏。

原理

Vue 在编译带有 scoped 属性的 <style> 标签时,会按照以下步骤处理样式隔离:

  1. 生成唯一的作用域 ID:Vue 为每个带有 scoped 属性的组件生成一个唯一的作用域 ID(如 data-v-f3f3eg9)。这个 ID 是随机的,确保每个组件的作用域 ID 是独一无二的。
  2. 添加作用域 ID 到模板元素:Vue 会在编译组件模板的过程中,将这个作用域 ID 作为自定义属性添加到组件模板的所有元素上。例如,如果作用域 ID 是 data-v-f3f3eg9,那么在该组件模板的所有元素上都会添加一个属性 data-v-f3f3eg9
  3. 修改 CSS 选择器:对于组件内部的每个 CSS 规则,Vue 会自动转换其选择器,使其仅匹配带有对应作用域 ID 的元素。这是通过在 CSS 选择器的末尾添加相应的作用域 ID 属性选择器来实现的。例如,如果 CSS 规则是 .button { color: red; },并且作用域 ID 是 data-v-f3f3eg9,那么该规则会被转换成 .button[data-v-f3f3eg9] { color: red; }

问题:

  • 由于样式隔离是通过属性选择器和自定义属性实现的,因此这种方法的性能可能会略低于全局样式规则。
  • scoped 样式不能影响子组件,仅限于当前的组件。如果需要影响子组件,则需要使用深度选择器(>>>/deep/)。

28. 获取组件的实例

vue2

选项式api
this获取当前实例

vue3

import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
const appContext = instance.appContext

29. vue3中 setup的用法

setup()

<https://cn.vuejs.org/api/composition-api-setup.html>

“<script setup> 的用法”

<https://cn.vuejs.org/api/sfc-script-setup.html>
    

30. vue3中ref、reactive

cn.vuejs.org/api/reactiv…

  • ref: 通过.value 访问及修改, 可以声明基本类型数据和引用类型数据
  • reactive: 直接访问、只能声明引用数据类型
const state = reactive({
  name: 'wang',
  age:22
})
- 深度监听
- 删除、更改、新增都会触发响应式
- 使用解构或 ...state 的时候会失去响应式
  • computed: 也是通过.value,声明需要 传 get、set
  • toRef: 类似ref的用法,可以把响应式数据的属性变成ref
针对一个响应式对象(reactive 封装)的 prop(属性)创建一个ref,且保持响应式
两者 保持引用关系
// 响应式对象
const state = reactive({
  name: '太凉',
  age: 18
})

// 通过toRef创建一个Ref响应式
const nameRef = toRef(state, 'name')
nameRef.value 和 state.name 保持响应式关系
  • toRefs: 将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。
setup(){
    const state = reactive({
      foo: 1,
      bar: 2
    })
    
    const stateAsRefs = toRefs(state)
    
    // 这个 ref 和源属性已经“链接上了”
    state.foo++
    console.log(stateAsRefs.foo.value) // 2
    
    stateAsRefs.foo.value++
    console.log(state.foo) // 3

  return {
    ...toRefs(state)  // 最佳用法
  }
}
  • shallRef: 浅层的ref,第二层就不会触发响应式
  • shallReactive: 浅层的reactive,第二层就不会触发响应式
  • customRef: 自定义ref

31. hook

32. 对 Vue Hook 的理解和简单案例

Vue Hook 是在 Vue 3 中引入的一种新的代码组织和状态管理方式,它受到了 React Hook 的启发。Vue Hook 主要通过组合式 API (Composition API) 来实现,让开发者能够更灵活、更高效地管理组件逻辑和状态。

是一个利用 Vue 的组合式 API 来封装和复用具有状态逻辑的函数。

更好的逻辑复用

在选项式 API (Options API) 中,逻辑复用通常依赖于 mixins 和高阶组件 (HOC)。但是,mixins 存在命名冲突和代码难以追踪的问题,而高阶组件则可能导致组件层级过深。Vue Hook 通过组合函数 (Composable) 提供了一种更简洁和直观的方式来复用逻辑。

更好的代码组织

组合式 API 允许开发者在同一地方声明和使用状态、计算属性、方法等,避免了选项式 API 中将相关逻辑分散在多个选项中的情况。这使得代码更具可读性和可维护性。

更灵活的响应式系统

Vue 3 引入了新的响应式系统,它基于 Proxy 实现,相较于 Vue 2 的基于 Object.defineProperty 的实现,性能更好,功能更强大。组合式 API 可以更方便地使用这些新的响应式功能。

Hooks 有严格的调用顺序,并不可以写在条件分支中。

Vue Hook 的核心概念

refreactive
  • ref 用于定义基本类型的响应式数据,它会返回一个带有 .value 属性的对象。
  • reactive 用于定义对象类型的响应式数据,它返回一个深度响应的对象。
组合函数 (Composable)

组合函数是复用逻辑的核心。它是一个普通的 JavaScript 函数,可以接收参数,并返回响应式数据或其他组合函数的结果。

生命周期钩子

组合式 API 提供了与选项式 API 中生命周期钩子类似的函数,如 onMountedonUpdatedonUnmounted,用于在组件的不同生命周期阶段执行特定逻辑。

 
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'
 
// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
  // 被组合式函数封装和管理的状态
  const x = ref(0)
  const y = ref(0)
 
  // 组合式函数可以随时更改其状态。
  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }
 
  // 一个组合式函数也可以挂靠在所属组件的生命周期上
  // 来启动和卸载副作用
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))
 
  // 通过返回值暴露所管理的状态
  return { x, y }
}

// 使用
<script setup>
import { useMouse } from './mouse.js'
 
const { x, y } = useMouse()
</script>
 
<template>Mouse position is at: {{ x }}, {{ y }}</template>

33. 插槽

插槽的实现实际上就是一种延时渲染,把父组件中编写的插槽内容保存到一个对象上,并且把具体渲染 DOM 的代码用函数的方式封装,然后在子组件渲染的时候,根据插槽名在对象中找到对应的函数,然后执行这些函数做真正的渲染。

  • v-slot 只能用在 template 或组件上使用,否则就会报错。
// 子组件
<div>
  <slot :text="greetingMessage" :count="1" name="name"></slot>
  <slot :text="greetingMessage" :count2="1" name="age"></slot>
</div>

// 父组件
<MyComponent>
  <template  #name="slotProps">
    {{ slotProps.text }} {{ slotProps.count }}
  </template>
  <template  #age="slotProps">
    {{ slotProps.text }} {{ slotProps.count2 }}
  </template>
</MyComponent>

juejin.cn/post/743898…