2025 vue3,vue2对比面试题精华?

1,302 阅读20分钟

内容主题中有vue2 和 vue3进行比较的内容,也有部分和ract对比的内容,比较重要的内容我都会写出代码,You can keep watching if you need to ,OR NO 出门右转,内容持续更新中,后续会收集或遇到好的问题...

  1. 12月29日 更新了 $atter$listeners

1. vue3为什么要升级proxy,废弃defineProperty

defineProperty 无法监听数组索引和长度的变化,无法监听对象新增属性,必须在初始化时就遍历对象的所有层级

proxy 正好完美的避免了这些问题:不需要递归变量对象,在使用的时候才会收集依赖,懒收集

2. 路由守卫的使用

有全局路由守卫和组件内守卫,局部路由守卫

全局路由守卫前置 router.beforeEach,常用于检测用户是否登录,跳转大登录页面

全局路由守卫后置router.afterEach,常用于每次路由跳转完成记录用户访问日志

路由守卫用在路由中

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from) => {
      // reject the navigation
      return false
    },
  },
]

3. Composition API与React Hook很像,区别是什么

  1. 执行时机
    1. react 每次渲染都会执行
    2. vue setup 只会执行一次,响应式系统处理更新
  2. 响应式系统
    1. react 需要手动触发更新
    2. vue 自动追踪依赖更新
  3. 性能优化
    1. react 需要手动进行性能优化(useMemo,useCallback)
    2. vue 响应式系统自动优化
  4. Compositon API的调用不需要顾虑调用顺序,也可以在循环、条件、嵌套函数中使用,而react 只能在顶层作用域中调用hooks

4. 为什么Vue不使用React 的分片更新?

其实这个问题也可以叫做:为什么 Vue 不需要时间分片?对于这个问题其实尤雨溪也在英文社区里回答过,也有前端大牛翻译发布在公众号上,那么下面我也进行一下总结。

  1. 第一,首先时间分片是为了解决 CPU 进行大量计算的问题,因为 React 本身架构的问题,在默认的情况下更新会进行很多的计算,就算使用 React 提供的性能优化 API,进行设置,也会因为开发者本身的问题,依然可能存在过多计算的问题。
  2. 第二,而 Vue 通过响应式依赖跟踪,在默认的情况下可以做到只进行组件树级别的更新计算,而默认下 React 是做不到的(据说 React 已经在进行这方面的优化工作了),再者 Vue 是通过 template 进行编译的,可以在编译的时候进行非常好的性能优化,比如对静态节点进行静态节点提升的优化处理,而通过 JSX 进行编译的 React 是做不到的。
  3. 第三,React 为了解决更新的时候进行过多计算的问题引入了时间分片,但同时又带来了额外的计算开销,就是任务协调的计算,虽然 React 也使用最小堆等的算法进行优化,但相对 Vue 还是多了额外的性能开销,因为 Vue 没有时间分片,所以没有这方面的性能担忧。(时间分片本身就是性能开销)
  4. 第四,根据研究表明,人类的肉眼对 100 毫秒以内的时间并不敏感,所以时间分片只对于处理超过 100 毫秒以上的计算才有很好的收益,而 Vue 的更新计算是很少出现 100 毫秒以上的计算的,所以 Vue 引入时间分片的收益并不划算。

5. vue中的data为什么是一个函数?

Vue 中的 data 必须是个函数,因为当 data 是函数时,
组件实例化的时候这个函数将会被调用,返回一个对象,
计算机会给这个对象分配一个内存地址,实例化几次就分配几个内存地址,
他们的地址都不一样,所以每个组件中的数据不会相互干扰,改变其中一个组件的状态,其它组件不变。

简单来说,就是为了保证组件的独立性和可复用性,如果 data 是个函数的话,
每复用一次组件就会返回新的 data,类似于给每个组件实例创	建一个私有的数据空间,
保护各自的数据互不影响

解释的不错:https://blog.csdn.net/qq_42072086/article/details/108060494

6. MVC 和 MVVM的区别

MVC:M(model数据)、V(view视图),C(controlle控制器)
缺点是前后端无法独立开发,必须等后端接口做好了才可以往下走;
前端没有自己的数据中心,太过依赖后台

MVVM:M(model数据)、V(view视图)、VM(viewModel控制数据的改变和控制视图)
html部分相当于View层,可以看到这里的View通过通过模板语法来声明式的将数据渲染进DOM元素,
当ViewModelModel进行更新时,通过数据绑定更新到ViewVue实例中的data相当于Model层,而ViewModel层的核心是Vue中的双向数据绑定,
即Model变化时VIew可以实时更新,View变化也能让Model发生变化

MVVMMVC最大的区别就是:它实现了ViewModel的自动同步,
也就是当Model的属性改变时,我们不用再自己手动操作Dom元素,来改变View的显示,
而是改变属性后该属性对应View层显示会自动改变

7. v-model 原理

是采用数据劫持结合发布者-订阅者模式的方式,
通过Object.defineProperty()来劫持各个属性的setter,getter,
在数据变动时发布消息给订阅者,触发相应的监听回调从而达到数据和视图同步。

----------------------------------------
v-model只不过是一个语法糖而已,真正的实现靠的还是
v-bind:绑定响应式数据
触发oninput 事件并传递数据
-----------------------
https://blog.csdn.net/qq_43742385/article/details/114572773
<input
  v-bind:value="searchText"
  v-on:input="searchText = $event.target.value"
>
<!--
自html5开始,input每次输入都会触发oninput事件,
所以输入时input的内容会绑定到searchText中,于是searchText的值就被改变;
$event 指代当前触发的事件对象;
$event.target 指代当前触发的事件对象的dom;
$event.target.value 就是当前dom的value值;
在@input方法中,value => searchText;
在:value中,searchText => value;
-->
---------------
https://juejin.cn/post/7090841582173192199
通过 v-bind:value 绑定 username 变量,每次输入内容的时候触发input事件
通过事件对象参数 event.target.value 获得输入的内容,并且把这个内容赋值给username
此时更改username时input输入框会变化,更改input输入框时username变量会变,从而实现了v-model的双向绑定功能

vue2 和 vue3 的区别

  1. 默认触发更新方法不同 this.$emit('input',8888),vue3emit('update:modelValue',8888)
  2. vue3 支持一个组件多个v-model
<!-- Vue3 的实现 -->
<template>
  <!-- 父组件 -->
  <custom-input v-model="message"/>
  
  <!-- 等价于 -->
  <custom-input
    :modelValue="message"
    @update:modelValue="message = $event"
  />
</template>

<!-- 子组件 -->
<script setup>
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const updateValue = (val) => {
  emit('update:modelValue', val)  // Vue3 使用 update:modelValue 事件
}
</script>

<!-- Vue2 的实现 -->
<template>
  <!-- 父组件 -->
  <custom-input v-model="message"/>
  
  <!-- 等价于 -->
  <custom-input
    :value="message"
    @input="message = $event"
  />
</template>

<!-- 子组件 -->
<script>
export default {
  props: {
    value: String
  },
  methods: {
    updateValue(val) {
      this.$emit('input', val)  // Vue2 使用 input 事件
    }
  }
}
</script>

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

v-if是通过添加和删除元素来进行显示或者隐藏
v-show是通过操作DOM修改display样式来修改元素的显示和隐藏
如果需要频繁的进行元素的显示和隐藏使用v-show性能更好

9. v-for中为什么要有key

key 可以提高虚拟DOM的更新效率。

在vue中,默认“就地复用”的策略,在DOM操作的时候,如果没有key 就会造成选项错乱

key 只能是字符串或者number,其他类型不可以

1. 虚拟DOM中key的作用:

key是虚拟DOM对象的标识,当数据发生变化时,
Vue会根据【新数据】生成【新的虚拟DOM】,  f
随后Vue进行【新的虚拟DOM】与【旧的虚拟DOM】差异比较比较规则如

2. 比较规则:

1)旧虚拟DOM找到了与新虚拟DOM相同的key:

若虚拟DOM中内容没变,直接使用之前的真实DOM
若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM

3. 用index作为key可能会引发的问题:

1)若对数据进行:逆序添加、逆序删除等破坏顺序的操作,会产生没有必要的真实DOM更新==>界面效果没问题,但效率低
2)如果结构中还包含输入类的DOM,会产生错误的DOM更新 ==> 界面有问题

10. 打包后 dist 目录过大,解决办法?

  1. dist打包生成的文件中有 .map 文件,可以删除。在 vue.config.js文件中配置:productionSourceMap: false
  2. 组价和路由使用懒加载、按需引入等
  3. 对于文件和图片进行压缩。 安装压缩组件: compression-webpack-plugin\ 安装后进行导入配置:\ 最小化代码 minisize: true\ 分割代码: splitChunksl\ 超过限定值的文件进行压缩,threshold: 文件大小(字节为单位)

11. computed 和 watch 的区别

  1. computed是计算属性,watch是监听器,用来监听某一个值的变化进而触发相应的回调
  2. computed中的函数必须要有return返回、watch没有必须的要求返回return
  3. computed是第一次加载就触发一次,watch首次加载不会触发,如果需要首次加载需要设置immediate属性
  4. computed中的函数所依赖的属性没有发生变化,那么调用当前的函数的时候会从缓存中获取;而watch在每次监听值发生变化的时候都会执行回调。
// 6. 控制执行时机
watch(source, callback, {
  flush: 'post'    // 组件更新后执行
  // flush: 'pre'  // 组件更新前执行(默认)
  // flush: 'sync' // 同步执行
})

// 7. 停止监听
const stopWatch = watch(count, () => {
  console.log('watching...')
})

// 某个时刻停止监听
stopWatch()

使用场景:watch 比如购物车商品结算功能,computed 国际化中使用单位的换算时有使用

12. vue组件之间的数据传递

父 传 子

通过在父组件中的子组件身上绑定自定义属性,然后再子组件里使用props属性来接收即可\

爷传孙

vue2 可以直接 通过 this.atter调用爷组件中的data变量,中间的父亲组件需要绑定atter 调用爷组件中的data变量,中间的 父亲组件需要绑定atter

vue3 组合式api 通过useAtter() ,选项api 使用$atter

<!-- 爷爷组件 Grandfather.vue -->
<template>
  <Father 
    :message="message" 
    :data="someData"
    @customEvent="handleEvent"
  />
</template>

<script setup lang="ts">
const message = ref('来自爷爷的消息')
const someData = ref({ value: 'some data' })

const handleEvent = () => {
  console.log('处理事件')
}
</script>

<!-- 父组件 Father.vue -->
<template>
  <Grandson v-bind="$attrs" />
</template>

<script setup lang="ts">
// 重要:禁用 attributes 继承
defineOptions({
  inheritAttrs: false
})
</script>

<!-- 孙子组件 Grandson.vue -->
<template>
  <div>
    <p>爷爷传来的消息: {{ attrs.message }}</p>
    <p>爷爷传来的数据: {{ attrs.data.value }}</p>
    <button @click="attrs.customEvent">触发爷爷的事件</button>
  </div>
</template>

<script setup lang="ts">
import { useAttrs } from 'vue'

const attrs = useAttrs()
</script>

孙传爷

vue2 可以直接 通过 this.listeners调用孙组件中的data变量listeners 调用孙组件中的data变量atter

vue3 组合式api 通过useAtter() ,选项api 统一使用$atter

子 传 父

1)第一种方式:通过父组件给子组件传递函数类型的props实现:子组件给父组件传递数据\ 父组件:\ \

子组件:\

2)第二种方式:通过父组件给子组件绑定一个自定义事件实现:子组件给父组件传递数据 $emit 事件传递

// 子组件
export default {
  methods: {
    handleClick() {
      // 发送事件和数据
      this.$emit('update', {
        name: 'John',
        age: 20
      });
    }
  }
}

// 父组件
<child-component
  @update="handleUpdate"
/>

export default {
  methods: {
    handleUpdate(data) {
      console.log(data); // { name: 'John', age: 20 }
    }
  }
}

3)第三种方式:通过父组件给子组件绑定一个自定义事件实现:使用ref实现 ref 引用

// 父组件
<child-component ref="childRef" />

export default {
  mounted() {
    // 访问子组件的数据或方法
    console.log(this.$refs.childRef.data);
    this.$refs.childRef.someMethod();
  }
}

// 子组件
export default {
  data() {
    return {
      data: 'child data'
    }
  },
  methods: {
    someMethod() {
      // ...
    }
  }
}

4)第四种方法使用v-model 方法传值

-------------------vue2------------------
// 1. 基础用法
// 父组件
<child-component v-model="value" />

// 子组件
export default {
  props: {
    value: String  // 必须用 value 这个名字
  },
  methods: {
    updateValue(newValue) {
      this.$emit('input', newValue)  // 必须用 input 事件
    }
  }
}

// 2. 自定义 prop 和事件名
// 子组件
export default {
  model: {
    prop: 'title',    // 自定义 prop 名
    event: 'change'   // 自定义事件名
  },
  props: {
    title: String
  },
  methods: {
    updateTitle(val) {
      this.$emit('change', val)
    }
  }
}

-------------------vue3------------------
// 父组件
<template>
  <child-component v-model.capitalize="message" />
  <p>父组件的值: {{ message }}</p>
</template>

<script setup>
import { ref } from 'vue'

const message = ref('')
</script>

// 子组件
<template>
  <input 
    :value="modelValue"
    @input="handleInput"
  />
</template>

<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})

const emit = defineEmits(['update:modelValue'])

const handleInput = (e) => {
  let value = e.target.value
  if (props.modelModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:modelValue', value)
}
</script>

全局事件总线:可以实现任意组件间的数据传递

main.js:将全局事件bus,挂载到Vue的原型上,这样所有的组件都可以使用\ \ 兄弟组件A:\ \ 兄弟组件B:\ \ 4. 消息订阅与发布\ 一种组件间的通信方式,适用于任意组件间通信。

使用步骤:

  1. 安装pubsub: npm i pubsub-js
  2. 引入: import pubsub from ‘pubsub-js’
  3. 接收数据: A组件想要接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身
mounted() {
  this.pid = punsub.subscribe('xxx', (data)=>{
    ......
  })
}
  1. 提供数据: pubsub.publish(‘xxx’, 数据)
  2. 最好在beforeDestory钩子中,用pubsub.unsubscribe(pid)取消订阅

vue原型 和 原型链

blog.csdn.net/u012468376/…

不管是普通函数还是构造函数,都有一个属性prototype,用来指向这个函数的原型对象。
同时原型对象也有一个constructor属性,用来指向这个原型对象的构造函数。
原型对象的作用:用来存储实例化对象的公有属性和方法

例如

function Person (name, age) {
  this.name = name
  this.age = age
  this.say = function () {
    console.log('我会说话')
  }
}

let person1 = new Person('小明', 20)

console.log(person1.name) // 小明
console.log(person1.age) // 20
console.log(person1.say()) // 我会说话

如上:实例化出来的person1,就可以使用构造函数Person里面的属性和方法

总结

  1. 访问对象的一个属性,先在自身查找,如果没有,会访问对象的__proto__,沿着原型链查找,一直找到Object.prototype.proto
  2. 每个函数都有prototype属性,会指向函数的原型对象。
  3. 所有函数的原型对象的__proto__,会指向Object.prototype。
  4. 原型链的尽头是Object.prototype.proto,为null。

具体可参考下方文章链接地址(写的非常详细易懂):blog.csdn.net/weixin_5650…

13. $nextTick的作用

首先我们要先明白一个道理:Vue的响应式并不是数据发生变化后,DOM立即跟着发生变化的,而是按一定的策略进行DOM更新的。

作用:nextTick是在下次DOM更新循环结束之后执⾏延迟回调,在修改数据之后使⽤nextTick 是在下次 DOM 更新循环结束之后执⾏延迟回调,在修改数据之后使⽤ nextTick,则可以在回调中获取更新后的 DOM,在下次 DOM 更新循环结束之后执行延迟回调

说白了就是:当数据更新了,在DOM更新后,自动执行该函数

什么时候用?

  1. Vue⽣命周期的created()钩⼦函数进⾏的DOM操作⼀定要放在Vue.nextTick()的回调函数中,原因是在created()钩⼦函数执⾏的时候,DOM 其实并未进⾏任何渲染,⽽此时进⾏DOM操作⽆异于徒劳,所以此处⼀定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。
  2. 当项⽬中改变data函数的数据,想基于新的dom做点什么,对新DOM⼀系列的js操作都需要放进Vue.nextTick()的回调函数中

实现原理

在下一次dom更新循环结束之后执行延迟回调,nextTick使用宏任务和微任务,他会更具环境选择最适合的异步方法实现nexTick,依次

  1. promise (微任务)
  2. mutationObserver(微任务)来监视 DOM 变化
  3. setImmediate(宏任务)
  4. setTimeout(宏认为),如果以上都不行则采用setTimeout定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。

为什么需要这个api

因为vue是异步更新的,多次修改数据不会立即实现dom的更新,而是批量进行处理更新,所有就需要这个api做更新后的后续处理

14. Vue生命周期

命名变化

// Vue2          // Vue3
beforeCreate    // setup()
created         // setup()
beforeMount     // onBeforeMount
mounted         // onMounted
beforeUpdate    // onBeforeUpdate
updated         // onUpdated
beforeDestroy   // onBeforeUnmount
destroyed       // onUnmounted

组件生命周期

//渲染阶段
父 beforeCreate=>父create=>父beforeMounted=>子beforeCreate=>子create=>子beforeMounted=>
  子mounted=>父Mounted=>
//更新阶段
父 beforeUpdate=>子beforeUpdate=> 子update=>父update
//销毁阶段 
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

15. react 为什么不能学习vue3 进行静态节点标记优化性能

静态节点标记优化

Vue3 的静态节点标记是在编译时标记出不会变化的节点,以减少运行时的比较:

// Vue3 模板
<template>
  <div>
    <h1>静态标题</h1>     <!-- 静态节点 -->
    <p>{{ message }}</p>   <!-- 动态节点 -->
  </div>

</template>

// 编译后(简化版)
const render = {
  _hoisted_1: createVNode("h1", null, "静态标题"), // 静态提升
  render() {
    return createVNode("div", null, [
      _hoisted_1,  // 静态节点直接复用
      createVNode("p", null, message) // 动态节点每次重新创建
    ])
  }
}

React 不能使用静态标记的原因

  1. JSX 的动态性
// React 的 JSX
function App() {
  return (
    <div>
      <h1>标题</h1>

      <p>{message}</p>

    </div>

  )
  // 每次渲染都会执行整个函数
  // 无法在编译时确定哪些是静态的
}
  1. 函数组件的特性
function Component() {
  // 每次渲染都会重新执行
  const staticNode = <h1>标题</h1>;
  
  return (
    <div>
      {staticNode}
      <p>{message}</p>

    </div>

  );
}

主要原因:

  1. React 的函数组件每次更新都会重新执行
  2. JSX 是动态生成的,难以在编译时分析
  3. Vue 的模板是静态的,更容易在编译时分析

这就是为什么 React 不能像 Vue3 那样进行静态节点优化。

16. Vue底层实现原理

响应式系统

从初始化阶段开始new Observer实例化将数据改为响应式,将每个属性都赋予一个Dep(负责依赖收集的类(粉丝群)),

到使用数据时:会创建一个Watcher负者刷新页面的操作,Dep 会收集当前的Watcher(加入粉丝群),用于数据变化时通知Watcher进行更新

数据变化时:数据改变需要调用数据的setter修改,Dep通知当前改变数据的所有Watcher(群发通知),Watcher 更新页面(粉丝行动)

用一个例子说明一下:

  1. Observer 就像经纪人
    负责监控明星的动态
    知道什么时候有新消息
  2. Dep 就像粉丝群
    记录谁关注了这个明星
    明星有动态时通知粉丝
  3. Watcher 就像粉丝
    关注明星的动态
    收到通知后更新自己的状态
// 1. 创建响应式数据
const data = {
  message: 'hello'
}

// 2. Observer 将数据变成响应式
new Observer(data)
// 现在 data.message 变成了:
Object.defineProperty(data, 'message', {
  get() {
    // 收集使用这个数据的 Watcher
    dep.depend()
    return value
  },
  set(newVal) {
    value = newVal
    // 通知使用这个数据的 Watcher 更新
    dep.notify()
  }
})

// 3. 创建模板 Watcher
new Watcher(vm, () => {
  // 这里使用了 message
  document.getElementById('app').innerHTML = data.message
})

// 4. 当数据变化时
data.message = 'hi'  // 触发更新
1. Observer 数据劫持 ----------- 负责将数据变为响应式
    1. 判断是不是数组`isArray()`
        1. 交给`observerArray()`
    2. 是不是对象
        1. 交给`this.walk()`
    3. 判断完成后使用defineReactive ,将数据变为响应式
        1. vue2 使用Object.defineProperty get set来判断是收集依赖还是更新,vue3 使用new proxy() get中的`track(target, key)  // 收集依赖`,set中的` trigger(target, key)  // 触发更新`
// Vue2 - Object.defineProperty
function defineReactive(obj, key, val) {
  const dep = new Dep()
  
  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        dep.depend()
      }
      return val
    },
    set(newVal) {
      val = newVal
      dep.notify()
    }
  })
}

// Vue3 - Proxy
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      track(target, key)  // 收集依赖
      return res
    },
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver)
      trigger(target, key)  // 触发更新
      return res
    }
  })
}
// A. Observer(数据劫持)
// 负责把数据变成响应式
class Observer {
  constructor(data) {
    // 1. 处理数组
    if (Array.isArray(data)) {
      // 数组需要特殊处理
      this.observeArray(data)
    } 
    // 2. 处理对象
    else if (typeof data === 'object') {
      this.walk(data)
    }
  }
}
2. Dep 依赖收集 ----------- 负责存储和通知依赖
    1. 每个属性都有自己的`Dep`,dep.depend() 表示有正在在执行的Watcher收集为依赖,dep.notify() 通知所有收集的Wathcer ,更新
    2. Dep 中存储了素有依赖这个属性的Watcher
// 2. Dep 存储了所有依赖这个属性的 Watcher
class Dep {
  constructor() {
    this.subs = []  // 存储 Watcher 的数组
  }
}
// B. Dep(依赖收集器)
// 负责存储和通知依赖
class Dep {
  constructor() {
    this.subs = []  // 存储 Watcher
  }
  
  // 收集依赖
  depend() {
    if (Dep.target) {
      this.subs.push(Dep.target)
    }
  }
  
  // 通知更新
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}
3. Watcher 订阅者----------- 负责页面刷新

Watcher 订阅者作为Observer和compile之间通讯桥梁

    1. 实例化时执行get() 方法,get中将Dep.target 标记为当前的Watcher,然后执行数据的getter `message的getter`,之后将`Dep.target标记为null`将得到的结果返回,当数据变化时再次执行`update()`
// C. Watcher(订阅者)
// 负责更新页面
class Watcher {
  constructor(vm, fn) {
    this.vm = vm
    this.getter = fn
    this.get()  // 立即执行一次
  }
  
  get() {
    Dep.target = this  // 标记当前 Watcher
    this.getter()      // 触发数据的 getter
    Dep.target = null  // 清除标记
  }
}

模板编译 compile 使用的函数是compileToFunctions

function compileToFunctions(template) {
  // 1. 解析模板
  const ast = parse(template)
  
  // 2. 优化静态节点
  optimize(ast)
  
  // 3. 生成代码
  const code = generate(ast)
  
  // 4. 创建渲染函数
  return new Function(`with(this){return ${code}}`)
}

// 解析模板
function parse(template) {
  // 解析 HTML 模板为 AST
  // 处理指令、事件等
  return ast
}

// 优化
function optimize(ast) {
  // 标记静态节点
  markStatic(ast)
  // 标记静态根节点
  markStaticRoots(ast)
}

// 生成代码
function generate(ast) {
  // 将 AST 转换为渲染函数代码
  return code
}
4. 解析模版
5. 优化静态节点
6. 生成代码
7. 创建渲染函数 `updateComponent `

虚拟dom的计算

// Vue2 - 基于类的 VNode
class VNode {
  constructor(tag, data, children, text, elm, context) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.context = context
  }
}
// Vue3 - 扁平化设计
const vnode = {
  type: 'div',
  props: { class: 'container' },
  children: []
}
// 创建虚拟节点
function createElement(tag, data, children) {
  return new VNode(tag, data, children)
}

function patch(oldVnode, vnode) {
  // 比较新旧节点
  // 更新 DOM
}
  1. VNode新旧节点的对比
  2. diff算法
  3. 批量更新

组件系统

  1. 组件创建
  2. 生命周期
  3. 组件通讯

17. mixin的使用有什么弊端

mixin 项目变的复杂的时候,多个组件间有重复的逻辑就会用到mixin多个组件有相同的逻辑,抽离出来

  1. 多个mixin可能会造成命名冲突,
  2. mixin和组件可能出现多对多的关系,使项目复杂度变高
  3. 使用场景:pc端新闻列表和详情页一样的右侧栏目,可以使用mixin进行混合
    1. 劣势:变量来源不明确,不利于阅读

18. vue3中使用代理的方式实现响应式性能在特殊情况下性能反而更差

1.频繁创建和销毁的小对象
// 不好的例子
function createItems() {
  return new Array(1000).fill(0).map((_, index) => {
    // 每个对象都会被 Proxy 包装
    return reactive({
      id: index,
      value: Math.random()
    })
  })
}

// 更好的方式
function createItems() {
  // 整体作为一个响应式对象
  return reactive({
    items: new Array(1000).fill(0).map((_, index) => ({
      id: index,
      value: Math.random()
    }))
  })
}

2. 大量数据的浅层响应
// Proxy 方式(每个属性访问都要经过代理)
const state = reactive({
  list: new Array(10000).fill(0).map((_, i) => ({ id: i }))
})

// Object.defineProperty 方式(只在最外层做响应式)
const state = {
  list: new Array(10000).fill(0).map((_, i) => ({ id: i }))
}
Object.defineProperty(state, 'list', {
  get() { return list },
  set(newVal) { list = newVal; trigger() }
})

3. 频繁读取的场景
// 不好的例子
function calculate() {
  const proxy = reactive({ count: 0 })
  for(let i = 0; i < 100000; i++) {
    // 每次读取都要经过 Proxy
    temp += proxy.count
  }
}

// 更好的方式
function calculate() {
  const proxy = reactive({ count: 0 })
  // 解构出来,避免重复经过代理
  const { count } = proxy
  for(let i = 0; i < 100000; i++) {
    temp += count
  }
}

优化意见:

  1. 当只需要浅层响应式时 使用ShallowReactive({})
  2. 对不需要响应式数据时使用markRaw({})

19. vue的事件绑定是怎么实现的

原生事件绑定 最终是使用addEventListener 实现

直接绑定到DOM元素上,使用addEvetListener 进行监听事件,执行事件修饰符

// 1. 模板中的写法
<template>
  <!-- 直接绑定 -->
  <button @click="handleClick">点击</button>

  
  <!-- 带参数 -->
  <button @click="handleClick(123, $event)">点击</button>

  
  <!-- 使用修饰符 -->
  <button @click.stop.prevent="handleClick">点击</button>

</template>

// 2. 编译后的结果
{
  on: {
    click: function($event) {
      // 处理修饰符
      $event.stopPropagation()
      $event.preventDefault()
      // 调用方法
      return handleClick.call(vm, 123, $event)
    }
  }
}

// 3. 最终会转换为原生 addEventListener
element.addEventListener('click', function(e) {
  // 处理函数
})

vue 对事件绑定做了什么优化

  1. 事件代理(事件委托)

当循环添加点击事件,并不是所有的元素直接添加点击事件,而是通过判断是当前哪个元素,添加点击事件

// 1. 模板
<template>
  <ul>
    <li v-for="item in 1000" @click="handleClick">
      {{ item }}
    </li>
  </ul>
</template>

// 2. Vue 会优化成
document.querySelector('ul').addEventListener('click', function(e) {
  // 判断是否点击的是 li
  if (e.target.tagName === 'LI') {
    // 执行对应的处理函数
    handleClick(e)
  }
})

// 而不是给每个 li 都添加事件监听
// 避免了创建 1000 个事件监听器
  1. 缓存事件处理函数

定义的事件绑定时并不是每次都需要创建,而是通过缓存该事件函数

// 1. 事件处理函数的缓存
export default {
  methods: {
    handleClick(e) {
      console.log('clicked')
    }
  },
  render() {
    // Vue 会缓存事件处理函数
    const handler = this.$options.methods.handleClick.bind(this)
    // 避免每次渲染都创建新的函数
    return h('button', {
      onClick: handler
    })
  }
}

// 2. 带参数的处理
<button @click="handleClick(123, $event)">

// Vue 会生成一个包装函数并缓存
const cachedHandler = e => {
  return handleClick.call(vm, 123, e)
}
  1. 智能更新

智能更新也就是,当dom元素修改绑定元素时,并不需要重新绑定/解绑事件监听器,而是通过点击事件监听内部判断更新事件是调用哪个函数,这样就可以不同重新绑定元素的监听器了

// 1. 只在必要时更新事件监听
<button 
  v-if="visible" 
  @click="handleClick"
>

// Vue 会:
// - 只在 button 创建时添加事件监听
// - 只在 button 销毁时移除事件监听
// - visible 变化时自动处理事件绑定

// 2. 属性更新优化
<button @click="handler">  // 旧
<button @click="newHandler">  // 新

// 只更新事件处理函数的引用
// 不会重新添加/移除事件监听器
  1. 修饰符优化
// 1. 修饰符的编译优化
<button @click.stop.prevent="handleClick">

// 编译后的代码
function (e) {
  e.stopPropagation()
  e.preventDefault()
  handleClick(e)
}

// 2. 常用修饰符的快速路径
<button @click.once="handleClick">

// Vue 会使用 addEventListener 的 once 选项
element.addEventListener('click', handler, {
  once: true
})
  1. 批量更新
// 1. 事件处理中的状态更新
methods: {
  handleClick() {
    this.count++
    this.message = 'clicked'
    this.flag = true
  }
}

// Vue 会将这些更新批量处理
// 只触发一次重渲染

自定义事件

通过vue的事件系统,通过$emit触发

事件系统:主要基于发布订阅模式(观察者模式)

// 1. 父组件
<template>
  <!-- 监听子组件事件 -->
  <child-component
    @custom-event="handleCustomEvent"
    @update:title="handleTitleUpdate"
  />
</template>

// 2. 子组件
<template>
  <button @click="handleClick">触发事件</button>
</template>

<script>
export default {
  methods: {
    handleClick() {
      // 触发自定义事件
      this.$emit('custom-event', { data: 123 })
      
      // 触发 update 事件
      this.$emit('update:title', 'new title')
    }
  }
}
</script>
// 1. Vue 实例上的事件中心
class Vue {
  constructor() {
    // 事件中心
    this._events = {}
  }
  
  // 注册事件
  $on(event, fn) {
    if (!this._events[event]) {
      this._events[event] = []
    }
    this._events[event].push(fn)
    return this
  }
  
  // 触发事件
  $emit(event, ...args) {
    const cbs = this._events[event]
    if (cbs) {
      cbs.forEach(cb => cb.apply(this, args))
    }
    return this
  }
  
  // 解绑事件
  $off(event, fn) {
    // 解绑逻辑
  }
}

20. provide 和 inject,那他们的缺点是什么呢

  1. 数据来源不明确,降低代码可读性
  2. 默认非响应式,需要额外处理
  3. 容易造成命名冲突
  4. 维护和重构困难
  5. TypeScript 类型支持不友好
  6. 爷孙组件可以直接传值

21. Vue项目中常用的性能优化

让我系统地介绍 Vue 项目的性能优化方案:

1. 代码层面优化

// 1. v-if 和 v-show 的合理使用
<template>
  <!-- 频繁切换用 v-show -->
  <div v-show="isShow">频繁切换的内容</div>

  
  <!-- 不频繁切换用 v-if -->
  <div v-if="isVisible">不频繁切换的内容</div>

</template>

// 2. computed 和 watch 的合理使用
export default {
  computed: {
    // 数据计算优先用 computed
    fullName() {
      return this.firstName + ' ' + this.lastName
    }
  },
  watch: {
    // 异步或开销大的操作用 watch
    searchQuery: {
      handler: 'fetchData',
      debounce: 300 // 防抖
    }
  }
}

// 3. v-for 优化
<template>
  <!-- 添加 key -->
  <div v-for="item in list" :key="item.id">
    {{ item.name }}
  </div>

  
  <!-- 避免同时使用 v-if -->
  <template v-for="item in list" :key="item.id">
    <div v-if="item.show">{{ item.name }}</div>

  </template>

</template>

2. 路由优化

// 1. 路由懒加载
const routes = [
  {
    path: '/user',
    component: () => import('./views/User.vue')
  }
]

// 2. 路由预加载
const routes = [
  {
    path: '/user',
    component: () => import(
      /* webpackPreload: true */
      './views/User.vue'
    )
  }
]

// 3. keep-alive 缓存
<template>
  <keep-alive :include="['User', 'About']">
    <router-view/>
  </keep-alive>

</template>

3. 组件优化

// 1. 组件懒加载
components: {
  'heavy-component': () => import('./HeavyComponent.vue')
}

// 2. 大数据列表优化
<template>
  <virtual-scroller
    :items="items"
    :item-height="50"
  >
    <template v-slot="{ item }">
      <div>{{ item.name }}</div>

    </template>

  </virtual-scroller>

</template>

// 3. 避免不必要的组件更新
export default {
  data() {
    return {
      heavyData: {}
    }
  },
  // 使用 Object.freeze 冻结不需要响应式的数据
  created() {
    this.heavyData = Object.freeze(heavyData)
  }
}

4. Webpack 优化

// 1. 代码分割
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
}

// 2. 压缩资源
module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin(),
      new CssMinimizerPlugin()
    ]
  }
}

// 3. Tree Shaking
module.exports = {
  mode: 'production'
}

5. 图片优化

// 1. 懒加载
<template>
  <img v-lazy="imgUrl"/>
</template>

// 2. 使用合适的图片格式
<picture>
  <source type="image/webp" srcset="image.webp">
  <img src="image.jpg" alt="图片">
</picture>

// 3. 小图片使用 base64
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024 // 4kb
          }
        }
      }
    ]
  }
}

6. 缓存优化

// 1. 浏览器缓存
module.exports = {
  output: {
    filename: '[name].[contenthash].js'
  }
}

// 2. 组件缓存
<keep-alive>
  <component :is="currentComponent"/>
</keep-alive>

// 3. API 缓存
const cache = new Map()

async function fetchData(key) {
  if (cache.has(key)) {
    return cache.get(key)
  }
  const data = await api.getData(key)
  cache.set(key, data)
  return data
}

总结优化方向:

  1. 代码层面
  • 合理使用 v-if/v-show
  • 优化 computed/watch
  • 避免不必要的渲染
  1. 路由优化
  • 懒加载
  • 预加载
  • 缓存
  1. 组件优化
  • 懒加载
  • 虚拟滚动
  • 冻结数据
  1. 构建优化
  • 代码分割
  • 压缩资源
  • Tree Shaking
  1. 资源优化
  • 图片懒加载
  • 合适的图片格式
  • 资源压缩
  1. 缓存优化
  • 浏览器缓存
  • 组件缓存
  • 数据缓存

这就像:

  • 代码优化是调整发动机
  • 路由优化是规划路线
  • 资源优化是减轻负重
  • 缓存优化是设置加油站

22. vue中.sync修饰符 & v-model的作用,用法以及实现原理

.sync修饰符是在vue2中使用的,在vue3中已经废除了,它用于实现双向数据绑定的语法糖,用于一般数据的数据双向绑定
  1. 使用
  2. 原理
    1. 通过v-bind 和 v-on:update 实现的双向绑定,并通过$emit 进行更新,源码中在template rander 阶段会编译成props绑定和 事件监听 监听update:title
// 父组件
<template>
  // 1. 基本使用
  <child-component :title.sync="pageTitle"/>
  
  // 2. 等同于以下写法
  <child-component 
    :title="pageTitle"
    @update:title="pageTitle = $event"
  />
  
  // 3. 多个属性使用
  <child-component 
    :title.sync="pageTitle"
    :content.sync="pageContent"
  />
</template>

// 子组件
export default {
  props: ['title'],
  methods: {
    changeTitle() {
      // 触发更新
      this.$emit('update:title', '新标题')
    }
  }
}

--------------------------------------// 1. 编译阶段
// template 会被编译成 render 函数

// 2. .sync 会被展开为两部分
// a. props 绑定
{
  props: {
    title: {
      type: String,
      required: true
    }
  }
}

// b. 事件监听
{
  on: {
    'update:title': function(value) {
      this.pageTitle = value
    }
  }
}
v-model 在vue2中一个组件只能有一个,vue3中能有多个,它用于实现数据双向绑定,一般用于表单元素
  1. 默认事件为this.$emit('update:modelValue',val)vue3, this.$emit('input',val)是vue2
  2. 原理和.sync 类似
vue2 和 vue3 中的v-model的区别

让我详细对比 Vue2 和 Vue3 中 v-model 的变化:

1. 默认值的变化
// Vue2
<component v-model="value">
// 等同于
<component
  :value="value"
  @input="value = $event"
>

// Vue3
<component v-model="value">
// 等同于
<component
  :modelValue="value"
  @update:modelValue="value = $event"
>
2. 自定义 v-model 的变化
// Vue2
export default {
  model: {
    prop: 'title',
    event: 'change'
  },
  props: {
    title: String
  }
}

// Vue3
// 不再需要 model 选项
export default {
  props: {
    modelValue: String
  },
  emits: ['update:modelValue']
}
3. 多个 v-model 支持
// Vue2
// ❌ 不支持多个 v-model
<component
  v-model="value1"
  :value2="value2"
  @input2="value2 = $event"
>

// Vue3
// ✅ 支持多个 v-model
<component
  v-model="value1"
  v-model:title="value2"
  v-model:content="value3"
>
4. 修饰符处理
// Vue2
export default {
  props: {
    value: String,
    modelModifiers: {
      default: () => ({})
    }
  }
}

// Vue3
export default {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    },
    // 支持具名修饰符
    titleModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue', 'update:title']
}
5. 组件实现示例
// Vue2
<template>
  <input
    :value="value"
    @input="$emit('input', $event.target.value)"
  >
</template>

export default {
  props: ['value']
}

// Vue3
<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  >
</template>

export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
主要变化总结:
  1. 命名规范的变化
  • Vue2: value/input
  • Vue3: modelValue/update:modelValue
  1. 配置方式的变化
  • Vue2: 使用 model 选项
  • Vue3: 直接使用 props 和 emits
  1. 功能的扩展
  • 支持多个 v-model
  • 更灵活的修饰符处理
  • 更清晰的命名约定
  1. 使用方式的变化
  • 更统一的命名规范
  • 更直观的实现方式
  • 更好的类型支持

这就像:

  • Vue2 是固定的双向绑定方案
  • Vue3 是更灵活的配置系统
  • 提供了更多的自定义空间

vue-router

linkActiveClass 和 linkExactActiveClass 的区别和作用

让我用一个简单的例子来说明 linkActiveClasslinkExactActiveClass 的区别:

1. 基本区别

// 假设我们有这样的路由结构:
/user                // 用户中心
/user/profile       // 个人资料
/user/settings      // 设置

// 当我们访问 /user/profile 时:
<router-link to="/user">用户中心</router-link>

// - 会添加 linkActiveClass(因为 /user/profile 包含 /user)
// - 不会添加 linkExactActiveClass(因为不完全匹配)

<router-link to="/user/profile">个人资料</router-link>

// - 会添加 linkActiveClass
// - 也会添加 linkExactActiveClass(因为完全匹配)

const router = createRouter({
    history: createWebHistory(),
    // 配置激活类名
    linkActiveClass: 'active',           // 部分匹配时的类名
    linkExactActiveClass: 'exact-active', // 精确匹配时的类名
    routes: [...]
})

简单来说:

  • linkActiveClass:模糊匹配,只要当前路径包含这个链接的路径就会激活
  • linkExactActiveClass:精确匹配,只有当前路径完全相同时才会激活

就像:

  • linkActiveClass 是判断是否在同一个部门
  • linkExactActiveClass 是判断是否在同一个具体位置

vue-router 几种钩子函数

  1. 全局守卫
    1. router.beforeEach((to,from,next)=>{ next()//登录鉴权 }) 全局前卫守卫
    2. router.beforeResolve((to,from,next)=>{ next() }) 全局解析守卫,每个路由都会触发
    3. router.afterEach((to,from,next)=>{修改标题,统计分析})全局后置守卫
  2. 路由独享
const routes = [
  {
    path: '/users',
    component: UserList,
    beforeEnter: (to, from, next) => {
      // 只在进入这个路由时触发
      if (hasPermission) {
        next()
      } else {
        next('/403')
      }
    }
  }
]
  1. 组件内守卫
    1. 进入组件前 beforeRouterEnter 可以通过next 的回调函数调用组件实例
    2. 路由更新时 beforeRouterUpdate 也就是 user/1 变为 /user/2
    3. 离开组件前 beforeRouterLeave 可以通过访问this组件实例
export default {
  // 1. 进入组件前
  beforeRouteEnter(to, from, next) {
    // 在渲染该组件的对应路由被验证前调用
    // 不能获取组件实例 this
    fetchData().then(data => {
      next(vm => {
          // 通过 vm 访问组件实例
        })
       })
  },
  
  // 2. 路由更新时
  beforeRouteUpdate(to, from, next) {
    // 在当前路由改变,但是组件被复用时调用
    // 可以访问组件实例 this
    next()
  },
  
  // 3. 离开组件前
  beforeRouteLeave(to, from, next) {
    // 导航离开该组件时调用
    // 可以访问组件实例 this
    if (this.hasUnsavedChanges) {
      if (confirm('确定要离开吗?')) {
        next()
      } else {
        next(false)
      }
    } else {
      next()
    }
  }
}

vuex

vuex实现的是一个单向数据流,在全局拥有一个State存放数据,当组件要更改State中的数据时,必须通过mutation进行,actions无法直接修改数据可以通过mutation 间接修改数据

  1. state: 存放数据源的地方
  2. getters: 计算数据(相当于state的计算属性),对state 进行进一步的加工,导出
  3. mutation:同步,唯一能修改state数据源的地方,通过commit('mutation函数名称')来触发
  4. actions:操作行为处理模块,由组件中的**<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">$store.dispatch('action 名称', data1)</font>**来触发。然后由commit()来触发mutation的调用 , 间接更新 state。可以定义异步函数,并在回调中提交mutation,就相当于异步更新了state中的字段
  5. modules:模块化

持久化:

通过vuex-persist,将需要的数据单独进行持久化,再将组件返回的内容通过插件的形式存入Store中

import VuexPersistence from 'vuex-persist';

const vuexLocal = new VuexPersistence({
   key: 'key_value_local', // 新的键名
  storage: window.localStorage,
  expiration: 15 * 24 * 60 * 60 * 1000, // 过期时间,默认30天
  reducer: state => {
    return {
      clickLike:state.clickLike,
      themes:state.themes,
      locale:state.locale,
      // loadingHome:state.loadingHome,
    }
  }
})

export default new Vuex.Store({
  state:{
    num:1
  },
  mutations:{
    DAA_NUM:function(state,data){
      state.num+=data
    },
    SUB_NUM:function(state,data){
      state.num--
    }
  },
  active:{
    add({commit,state,dispatch},data){
      commit('DAA_NUM',data)
      dispatch('subtract',data)
    }
    subtract({commit,state},data){
      const num = state.num
      commit('SUB_NUM',data)
    }
  }
})

Pinia

  • state:数据存放地
  • actions:异步 调用state getters都可以通过this直接调用和修改
  • getters: 计算数据(相当于state的计算属性)

1. 创建 Store

// stores/counter.js
import { defineStore } from 'pinia'

// 选项式写法
export const useCounterStore = defineStore('counter', {
    // 状态
    state: () => ({
        count: 0,
        name: 'Eduardo',
        todos: []
    }),

    // 计算属性
    getters: {
        doubleCount: (state) => state.count * 2,
        // 使用 this 访问其他 getter
        doubleCountPlusOne() {
            return this.doubleCount + 1
        }
    },

    // 方法
    actions: {
        increment() {
            this.count++
        },
        async fetchTodos() {
            const todos = await api.getTodos()
            this.todos = todos
        }
    }
})

// 组合式写法
export const useCounterStore = defineStore('counter', () => {
    const count = ref(0)
    const doubleCount = computed(() => count.value * 2)
    function increment() {
        count.value++
    }

    return { count, doubleCount, increment }
})

2. 在组件中使用

<template>
    <div>
        <p>Count: {{ counter.count }}</p>

        <p>Double: {{ counter.doubleCount }}</p>

        <button @click="counter.increment">+</button>

        <button @click="increment">+</button>

    </div>

</template>

<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

// 获取 store 实例
const counter = useCounterStore()

// 解构(使用 storeToRefs 保持响应性)
const { count, doubleCount } = storeToRefs(counter)
const { increment } = counter

// 修改状态
function updateCount() {
    // 1. 直接修改
    counter.count++
    
    // 2. $patch 方法
    counter.$patch({
        count: counter.count + 1,
        name: 'John'
    })
    
    // 3. $patch 函数
    counter.$patch((state) => {
        state.count++
        state.todos.push({ text: 'new todo' })
    })
}
</script>

defineProperty 无法监听数组索引和长度的变化,无法监听对象新增属性,需要递归遍历所有对象,初始化时会全部收集完成

proxy 正好完美的避免了这些问题:不需要递归变量对象,在使用的时候才会收集依赖,懒收集