Vanson's Eternal Blog

Vue夯实基础

Vue basic.png
Published on
/170 mins read/---

双向数据绑定原理

v-model 本质上是一个语法糖,它可以看作是 value 属性和 input 事件的组合。它允许开发者以更简洁的方式实现数据的双向绑定。

语法糖(Syntactic sugar)指的是一种在计算机语言中添加的语法,这种语法对语言的功能并没有影响,但是可以使程序员写出更简洁、更易读的代码。它就像糖衣一样,让代码看起来更甜美。

基本原理

v-model 实际上是结合了 v-bind 和 v-on 这两个指令来实现的:

  • v-bind:将 Vue 实例中的数据绑定到表单元素的 value 属性上。
  • v-on:监听表单元素的 input 事件,当用户输入发生变化时,将新的值更新回 Vue 实例的数据中。

具体实现过程

  • 编译阶段:
    • Vue 在编译模板时,会将 v-model 指令解析成 v-bind 和 v-on 指令。
    • 对于不同的表单元素(如 input、textarea、select),会根据其类型选择合适的 v-bind 属性和 v-on 事件。
  • 运行时:
    • 数据到视图:当 Vue 实例的数据发生变化时,v-bind 会将新的值同步到表单元素的 value 属性上,从而更新视图。
    • 视图到数据:当用户在表单元素中输入内容时,v-on 监听到的 input 事件会触发一个更新函数,将新的值同步回 Vue 实例的数据中。

不同标签的默认行为

input 和 textarea

  • 属性:value
  • 事件:input

select:

  • 属性:value
  • 事件:change

checkbox 和 radio:

  • 属性:checked
  • 事件:change

自定义 v-model

Vue 允许开发者通过 model 属性来自定义 v-model 的行为。可以通过 prop 和 event 属性来自定义绑定的属性和触发的事件。

<template>
  <div>
    <custom-input v-model:title="message"></custom-input>
    <p>{{ message }}</p>
  </div>
</template>
 
<script>
import CustomInput from './CustomInput.vue'
 
export default {
  components: {
    CustomInput,
  },
  data() {
    return {
      message: '',
    }
  },
}
</script>
<!-- CustomInput.vue -->
<template>
  <input :value="title" @input="$emit('update:title', $event.target.value)" />
</template>
 
<script>
export default {
  props: ['title'],
}
</script>
  • v-model:title 指定了 prop 为 title,event 为 update:title。
  • CustomInput 组件通过 props 接收 title 属性。
  • 当用户在 input 中输入内容时,触发 input 事件,并通过 $emit 更新父组件的 message 数据。

Vue的单向数据流

指的是数据从父组件流向子组件,而子组件不能直接修改父组件的数据。这种机制确保了数据流向的清晰和可预测性,避免了复杂的状态管理问题。

特性

  • 父到子的单向绑定:父组件的 prop 更新会传递到子组件,但子组件不能直接修改父组件的 prop。
  • 防止意外状态修改:单向数据流防止子组件意外改变父组件的状态,从而避免应用中数据流向混乱。
  • 数据刷新机制:每次父组件更新时,子组件中的所有 prop 都会刷新为最新值。

子组件修改 prop 的正确方式

如果子组件需要修改从父组件传递的 prop,应该通过 $emit 派发一个自定义事件,由父组件来修改数据。

<!-- 子组件 -->
<template>
  <div>
    <p>当前值:{{ value }}</p>
    <button @click="updateValue">更新值</button>
  </div>
</template>
 
<script>
export default {
  props: ['value'],
  methods: {
    updateValue() {
      // 通过 $emit 派发一个自定义事件,通知父组件更新数据
      this.$emit('update:value', newValue);
    }
  }
}
</script>
 
<!-- 父组件 -->
<template>
  <div>
    <child-component :value="parentValue" @update:value="parentValue = $event" />
  </div>
</template>
 
<script>
import ChildComponent from './ChildComponent.vue';
 
export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      parentValue: '初始值'
    };
  }
}
</script>
 

常见场景和解决方案

  1. 将 prop 用作初始值

如果 prop 用于传递初始值,而子组件希望将其作为本地数据使用,可以在子组件中定义一个本地的 data 属性,并将 prop 用作其初始值。

<template>
  <div>
    <p>计数:{{ counter }}</p>
    <button @click="increment">增加</button>
  </div>
</template>
 
<script>
export default {
  props: ['initialCounter'],
  data() {
    return {
      // 使用 prop 作为本地数据的初始值
      counter: this.initialCounter
    };
  },
  methods: {
    increment() {
      this.counter++;
    }
  }
}
</script>
  1. 转换原始值

如果 prop 以原始值传入且需要进行转换,可以使用 prop 的值来定义一个计算属性。

<template>
  <div>
    <p>标准化大小:{{ normalizedSize }}</p>
  </div>
</template>
 
<script>
export default {
  props: ['size'],
  computed: {
    // 使用 prop 的值定义计算属性
    normalizedSize() {
      return this.size.trim().toLowerCase();
    }
  }
}
</script>

Vue 的异步更新队列

Vue 在更新 DOM 时是异步执行的。当数据发生变化时,Vue 不会立即更新 DOM,而是将这些更新操作放入一个队列中。

在下一个事件循环中,Vue 会批量处理这些更新操作,从而提高性能。

为什么需要异步更新队列?

  • 性能优化:避免频繁的 DOM 更新,减少不必要的计算和操作。
  • 一致性:确保视图与数据的一致性,避免中间状态的视图更新。
  • 避免闪烁:减少页面闪烁,提高用户体验。

工作原理

  • 数据变化:当响应式数据发生变化时,Vue 的 Watcher 监听器会检测到这些变化。
  • 加入队列:Vue 将更新操作放入一个异步队列中,并在队列中去重,避免重复更新。
  • 异步执行:在下一个事件循环的微任务阶段,Vue 会批量处理队列中的更新操作,触发 DOM 渲染。
<template>
  <div>
    <h1 @click="updateMessage">{{ message }}</h1>
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      message: '原始值',
    };
  },
  methods: {
    updateMessage() {
      this.message = '修改后的值1';
      this.message = '修改后的值2';
      this.message = '修改后的值3';
      console.log('数据已更新,但 DOM 还未更新');
      this.$nextTick(() => {
        console.log('DOM 已更新');
      });
    },
  },
};
</script>
 

使用 nextTick,Vue 提供了 this.$nextTick 方法,允许在 DOM 更新完成后执行回调。这确保了在数据更新后,可以立即获取到更新后的 DOM

Vue中的合并对象

使用 Object.assign()

Object.assign() 是 JavaScript 的内置方法,用于将多个对象的属性复制到一个目标对象上。它不会修改原始对象,而是返回一个新的对象。

data() {
  return {
    originalObject: {
      name: 'John',
      age: 30,
    },
    newObject: {
      city: 'New York',
    },
  };
},
methods: {
  mergeObjects() {
    // 使用 Object.assign() 合并对象
    this.originalObject = Object.assign({}, this.originalObject, this.newObject);
    console.log(this.originalObject);
    // 输出: { name: 'John', age: 30, city: 'New York' }
  },
},
 

使用 Vue.set()

Vue.set() 是 Vue 提供的一个方法,用于添加或更新对象的属性,并确保这些属性是响应式的。它可以直接修改对象的属性。

data() {
  return {
    originalObject: {
      name: 'John',
      age: 30,
    },
    newObject: {
      city: 'New York',
    },
  };
},
methods: {
  mergeObjects() {
    // 使用 Vue.set() 合并对象
    Object.keys(this.newObject).forEach(key => {
      Vue.set(this.originalObject, key, this.newObject[key]);
    });
    console.log(this.originalObject);
    // 输出: { name: 'John', age: 30, city: 'New York' }
  },
},
 

使用扩展运算符

在现代 JavaScript 中,还可以使用扩展运算符 ... 来合并对象。这种方法简单且直观。

data() {
  return {
    originalObject: {
      name: 'John',
      age: 30,
    },
    newObject: {
      city: 'New York',
    },
  };
},
methods: {
  mergeObjects() {
    // 使用扩展运算符合并对象
    this.originalObject = { ...this.originalObject, ...this.newObject };
    console.log(this.originalObject);
    // 输出: { name: 'John', age: 30, city: 'New York' }
  },
},

注意事项

  • 响应式:Vue.set() 确保添加的属性是响应式的,而 Object.assign() 和扩展运算符需要确保整个对象被替换,以确保 Vue 能够检测到变化。
  • 性能:扩展运算符和 Object.assign() 创建新的对象,而 Vue.set() 直接修改现有对象。
  • 适用场景:根据具体需求选择合适的方法。如果需要确保响应式,建议使用 Vue.set()。

setup 语法糖

介绍 <script setup> 是 Vue 3 提供的一种语法糖,用于更简洁地书写组件,特别是在使用 Composition API 时。 它将 setup 函数的内容直接放置在 <script> 标签内,省略了显式定义 setup 函数的步骤,从而减少了模板代码的冗余,使代码更加简洁明了

<script setup>
import { ref, onMounted } from 'vue'
 
const count = ref(0)
 
const increment = () => {
  count.value++
}
 
onMounted(() => {
  console.log('Component mounted')
})
</script>
 
<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

原理

1、编译阶段的转换

  • 所有在 <script setup> 中声明的变量和函数,都会被自动包含在 setup 函数的作用域中。
  • 编译器会自动生成 setup 函数,并将 <script setup> 中的代码包裹在其中
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

会被编译器转换为:

setup() {
  import { ref } from 'vue';
  const count = ref(0);
  return { count };
}

2、自动暴露变量到模板

<script setup> 中导入的组件,无需显式注册到 components 选项中。这是通过以下机制实现的:

  • 编译器会解析 <script setup> 中的 import 语句,识别出所有导入的组件。
  • 这些组件会被自动注册为本地组件,可以直接在模板中使用

3、自动注册组件 在 <script setup> 中导入的组件,无需显式注册到 components 选项中。这是通过以下机制实现的: 编译器会解析 <script setup> 中的 import 语句,识别出所有导入的组件。 这些组件会被自动注册为本地组件,可以直接在模板中使用

4、defineProps 和 defineEmits 的实现 defineProps 和 defineEmits 是<script setup> 中的两个特殊宏,用于声明组件的 props 和 emits:

  • defineProps 和 defineEmits 在编译阶段会被替换为对应的 props 和 emits 选项。
  • 编译器会解析这些宏的调用,并生成对应的 props 和 emits 选项
<script setup>
const props = defineProps({
  title: String
});
const emit = defineEmits(['increment']);
</script>

会被编译器转换为:

export default {
  props: {
    title: String,
  },
  emits: ['increment'],
  setup(props, { emit }) {
    return { props, emit }
  },
}

5、顶层作用域的处理 在 <script setup> 中,所有变量和函数都处于顶层作用域,无需手动管理作用域。这是通过编译器的处理实现的: 编译器会自动处理变量和函数的作用域,确保它们在模板中可用

主要特性

1、无需显式 return: 在 <script setup> 中定义的变量和函数自动在模板中可用,无需手动 return。 所有定义的变量和方法都可以直接在模板中使用。

2、自动导入组件: 在 <script setup> 中导入的组件不需要显式注册到 components 选项中,Vue 会自动将其注册为本地组件。

<script setup>
import MyComponent from './MyComponent.vue';
</script>
 
<template>
  <MyComponent />
</template>
 

3、声明 props 和 emits: 可以直接使用 defineProps 和 defineEmits 函数来声明组件的 props 和 emits,无需通过 props 和 emits 选项显式定义。

<script setup>
const props = defineProps({
  title: String,
  count: Number,
})
 
const emit = defineEmits(['increment'])
 
const handleClick = () => {
  emit('increment')
}
</script>
 
<template>
  <div>
    <h1>{{ props.title }}</h1>
    <p>{{ props.count }}</p>
    <button @click="handleClick">Increment</button>
  </div>
</template>

4、顶层作用域: 在 <script setup> 中定义的所有变量和函数都在顶层作用域中声明,无需手动管理作用域,Vue 会自动处理依赖关系。

5、组合式 API 和模板语法更简洁: 由于所有内容都直接声明在 <script setup> 中,减少了样板代码,并且可以直接使用 Vue 的响应式系统和生命周期钩子,无需过多的配置。

nextTick

nextTick 是 Vue.js 提供的一个方法,用于在 DOM 更新完成后执行回调函数。

使用场景

确保 DOM 更新完成,在数据更新后,确保 DOM 已经更新完成,再执行某些操作。

this.message = 'New Message'
this.$nextTick(() => {
  console.log(this.$el.textContent) // 输出: New Message
})

优化性能,将多个回调函数合并到一个事件循环中,减少不必要的 DOM 操作。

this.message = 'New Message'
this.$nextTick(() => {
  // 执行一些操作
})
this.$nextTick(() => {
  // 执行另一些操作
})

实现原理

nextTick 的实现基于 JavaScript 的异步机制,主要使用了以下几种方法:

  • Promise(微任务)
  • MutationObserver(微任务)
  • setImmediate(宏任务)
  • setTimeout(宏任务,作为兜底方案)

Vue 的 nextTick 实现会根据环境自动选择最优的方法。以下是 nextTick 的核心实现逻辑:

let callbacks = [];
let pending = false;
 
function flushCallbacks() {
  callbacks.forEach(callback => callback());
  callbacks = [];
  pending = false;
}
 
let timerFunc;
 
// 尝试使用 Promise
if (typeof Promise !== 'undefined' && Promise.resolve) {
  const p = Promise.resolve();
  timerFunc = () => {
    p.then(flushCallbacks);
  };
} else if (typeof MutationObserver !== 'undefined') {
  // 尝试使用 MutationObserver
  let counter = 1;
  const textNode = document.createTextNode('');
  const observer = new MutationObserver(flushCallbacks);
  observer.observe(textNode, { characterData: true });
  timerFunc = () => {
    textNode.data = counter = (counter + 1) % 2;
  };
} else if (typeof setImmediate !== 'undefined') {
  // 尝试使用 setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks);
  };
} else {
  // 兜底方案:使用 setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}
 
export function nextTick(cb) {
  callbacks.push(cb);
  if (!pending) {
    pending = true;
    timerFunc();
  }
}
 

工作原理

  • 回调收集:nextTick 将所有回调收集到一个数组 callbacks 中。
  • 异步调度:根据环境选择最优的异步方法(Promise、MutationObserver、setImmediate 或 setTimeout)。
  • 回调执行:在下一次事件循环中,执行所有收集的回调。

优先级

  • 微任务优先:Promise 和 MutationObserver 属于微任务,优先级高于宏任务。
  • 宏任务兜底:如果微任务不可用,则使用 setImmediate 或 setTimeout。

Vue单页面和多页面

对比项单页面应用(SPA)传统多页面应用(MPA)
定义一个主页面,通过路由动态加载页面片段,局部刷新。多个独立页面,页面跳转时整页刷新。
用户体验页面切换流畅,交互效果炫酷。页面跳转速度慢,用户体验较差。
性能减少服务器压力,仅加载必要资源。每次跳转重新加载资源,增加服务器压力。
SEO不友好,需要额外优化(如 SSR)。友好,搜索引擎爬虫易于索引。
导航功能需手动实现前进、后退功能。浏览器自带前进、后退功能。
初次加载首屏加载时间较长,但可通过懒加载优化。每次跳转加载整个页面,速度较慢。
复杂度前端逻辑复杂,对开发者要求高。逻辑简单,适合小型项目。
适用场景复杂交互型应用(如管理后台、在线商城)。简单静态网站或内容型网站。
前后端分离职责分明,便于团队协作。前后端耦合度高,协作困难。
开发效率组件化开发,代码复用性高。代码复用性低。
浏览器兼容性依赖现代浏览器,兼容性较差。不依赖复杂 JavaScript,兼容性更好。

Vue路由实现

Hash 模式

location.hash:

  • location.hash 的值是 URL 中 # 后面的部分。
  • 当 location.hash 发生变化时,浏览器不会重新加载页面,也不会发送请求到服务器。
  • Vue Router 通过监听 hashchange 事件来检测路由的变化,并更新视图。

工作流程:

  • 用户点击链接或调用 router.push 方法,URL 的 hash 部分发生变化。
  • 浏览器触发 hashchange 事件。
  • Vue Router 监听到 hashchange 事件,解析新的 hash 值。
  • 根据新的 hash 值匹配路由规则,更新视图。

history 模式

HTML5 History API:

  • 主要使用 history.pushState() 和 history.replaceState() 方法。
  • history.pushState():在浏览器历史记录中添加一个新记录,不会重新加载页面。
  • history.replaceState():替换当前历史记录,不会重新加载页面。
  • 浏览器会触发 popstate 事件,当用户点击浏览器的前进或后退按钮时。

HTML5 History API:

  • 主要使用 history.pushState() 和 history.replaceState() 方法。
  • history.pushState():在浏览器历史记录中添加一个新记录,不会重新加载页面。
  • history.replaceState():替换当前历史记录,不会重新加载页面。
  • 浏览器会触发 popstate 事件,当用户点击浏览器的前进或后退按钮时。

route和router的区别?

$router

$router 是 Vue Router 的实例对象,用于管理整个应用的路由。它是一个全局对象,包含了许多关键的方法和属性,用于导航和路由管理。

push:向 history 栈添加一个新的记录。

this.$router.push('home');
this.$router.push({ path: 'home' });
this.$router.push({ name: 'user', params: { userId: 123 }});
this.$router.push({ path: 'register', query: { plan: '123' }});
 

go:页面路由跳转,前进或后退。

this.$router.go(-1); // 后退
this.$router.go(1);  // 前进
 

replace:替换当前的页面,不会向 history 栈添加新的记录。

this.$router.replace('home');
this.$router.replace({ path: 'home' });
 

**route** route 是一个对象,表示当前的路由信息,包含了当前 URL 解析得到的信息。它是一个响应式对象,用于访问当前路由的状态。

  • path:当前路由的路径,总是解析为绝对路径。
  • params:包含动态片段和全匹配片段的 key/value 对象。
  • query:包含 URL 查询参数的 key/value 对象。
  • hash:当前路由的 hash 值(不带 #),如果没有 hash 值,则为空字符串。
  • fullPath:完整解析后的 URL,包含查询参数和 hash 的完整路径。
  • matched:包含当前匹配的路径中所有片段所对应的配置参数对象的数组。
  • name:当前路径的名字。
  • meta:路由元信息。

路由跳转和location.href

location.href 跳转

通过修改 window.location.href 来实现页面跳转。 简单方便,但会刷新页面,适用于跨域或完全跳转。

  • 页面会刷新,重新加载整个页面。
  • 适用于跨域跳转或完全跳转到其他网站。
  • 会更新浏览器的历史记录。

window.location.href = '/new-page';

路由跳转 通过 Vue Router 提供的导航方法(如 push、replace)来实现页面跳转。 无刷新页面,适用于单页面应用,提供更好的用户体验。

  • 页面不会刷新,只更新页面的局部内容。
  • 适用于单页面应用(SPA)中的页面跳转。
  • 保持浏览器的历史记录,支持前进和后退。
this.$router.push('/new-page');
this.$router.replace('/new-page');
 

Vue Router钩子函数

全局钩子

beforeEach:在全局范围内拦截导航,在导航确认之前调用。

  • 参数:to(目标路由对象)、from(当前路由对象)、next(导航守卫)。
  • 作用:通常用于权限验证、页面跳转等。
router.beforeEach((to, from, next) => {
  const role = localStorage.getItem('ms_username');
  if (!role && to.path !== '/login') {
    next('/login');
  } else if (to.meta.permission) {
    role === 'admin' ? next() : next('/403');
  } else {
    if (navigator.userAgent.indexOf('MSIE') > -1 && to.path === '/editor') {
      Vue.prototype.$alert('vue-quill-editor组件不兼容IE10及以下浏览器,请使用更高版本的浏览器查看', '浏览器不兼容通知', {
        confirmButtonText: '确定'
      });
    } else {
      next();
    }
  }
});
 

afterEach:在全局范围内拦截导航,在导航完成后调用。

  • 参数:to(目标路由对象)、from(当前路由对象)。
  • 作用:通常用于页面加载后的操作,如页面标题更新、分析工具追踪等。
router.afterEach((to, from) => {
  document.title = to.meta.title || '默认标题';
});
 

单个路由钩子

在路由配置中为特定路由定义钩子,用于在进入或离开该路由时执行逻辑。

const routes = [
  {
    path: '/dashboard',
    component: Dashboard,
    beforeEnter: (to, from, next) => {
      // 自定义逻辑
      next();
    }
  }
];
 

组件路由钩子

beforeRouteEnter:在路由进入组件时调用,此时组件实例尚未创建。参数:to、from、next。

export default {
  beforeRouteEnter(to, from, next) {
    next(vm => {
      if (vm.$route.meta.hasOwnProperty('auth_key') && vm.$route.meta.auth_key !== '') {
        if (!vm.hasPermission(vm.$route.meta.auth_key)) {
          vm.$router.replace('/admin/noPermission');
        }
      }
    });
  }
};
 
 

beforeRouteUpdate:在路由更新(参数变化)时调用。参数:to、from、next。

export default {
  beforeRouteUpdate(to, from, next) {
    // 自定义逻辑
    next();
  }
};
 

beforeRouteLeave:在路由离开组件时调用。参数:to、from、next。

export default {
  beforeRouteLeave(to, from, next) {
    // 自定义逻辑
    next();
  }
};
 

Vue Router导航守卫

可以通过使用 Vue Router 提供的导航守卫来保护路由。

导航守卫是一种在路由跳转之前或之后执行的函数,用于验证用户的授权状态或其他条件。

创建路由配置

首先,创建一个路由配置文件,定义需要保护的路由。

// router.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import Login from './views/Login.vue';
import Home from './views/Home.vue';
import ProtectedPage from './views/ProtectedPage.vue';
 
Vue.use(VueRouter);
 
const router = new VueRouter({
  routes: [
    {
      path: '/login',
      name: 'login',
      component: Login,
    },
    {
      path: '/',
      name: 'home',
      component: Home,
    },
    {
      path: '/protected',
      name: 'protected',
      component: ProtectedPage,
      beforeEnter: (to, from, next) => {
        // 路由级守卫
        if (isLoggedIn()) {
          next();
        } else {
          next('/login');
        }
      },
    },
  ],
});
 
// 全局守卫
router.beforeEach((to, from, next) => {
  if (to.name === 'login') {
    if (isLoggedIn()) {
      next('/');
    } else {
      next();
    }
  } else if (!isLoggedIn()) {
    next('/login');
  } else {
    next();
  }
});
 
function isLoggedIn() {
  // 检查用户是否登录
  return localStorage.getItem('isLoggedIn') === 'true';
}
 
export default router;
 

创建登录组件

创建一个登录组件,用于模拟用户登录。

<template>
  <div>
    <h1>登录页面</h1>
    <button @click="login">登录</button>
  </div>
</template>
 
<script>
export default {
  methods: {
    login() {
      // 模拟登录
      localStorage.setItem('isLoggedIn', 'true');
      this.$router.push('/');
    },
  },
};
</script>
 

创建受保护的页面

创建一个受保护的页面,只有登录用户才能访问。

<template>
  <div>
    <h1>受保护的页面</h1>
    <p>只有登录用户才能看到这个页面。</p>
    <button @click="logout">退出登录</button>
  </div>
</template>
 
<script>
export default {
  methods: {
    logout() {
      localStorage.setItem('isLoggedIn', 'false');
      this.$router.push('/login');
    },
  },
  beforeRouteEnter(to, from, next) {
    // 组件内守卫
    if (localStorage.getItem('isLoggedIn') === 'true') {
      next();
    } else {
      next('/login');
    }
  },
};
</script>
 

工作原理

  • 全局守卫:
    • 使用 router.beforeEach 定义一个全局守卫,对所有路由进行统一的权限验证。
    • 如果用户未登录且访问的不是登录页面,则重定向到登录页面。
    • 如果用户已登录且访问的是登录页面,则重定向到主页。
  • 路由级守卫:
    • 在路由配置中,使用 beforeEnter 守卫对特定路由进行额外的验证。
    • 如果用户未登录,则重定向到登录页面。
  • 组件内守卫:
    • 在组件中,使用 beforeRouteEnter 守卫在组件加载前进行验证。
    • 如果用户未登录,则重定向到登录页面。

注意事项

  • 登录状态管理:在实际应用中,登录状态通常存储在 Vuex 或 Pinia 中,而不是直接存储在 localStorage 中。
  • 安全性:在生产环境中,确保后端也对用户进行验证,避免仅依赖前端验证。
  • 性能优化:确保导航守卫的逻辑简洁高效,避免不必要的计算。

Vue组件data是函数的原因

  • 避免数据共享:对象是引用类型,若 data 是对象,所有实例共享同一数据,一处修改影响所有实例。
  • 确保数据独立:每个组件实例应有独立数据,互不干扰。将 data 定义为函数,每次实例化时调用该函数返回新对象,确保数据独立。

Vuex 使用

Vuex 是 Vue.js 的状态管理模式,用于集中管理应用的状态。

state

存放应用的状态数据。

const store = new Vuex.Store({
  state: {
    count: 0
  }
});

getter

类似于 Vue 的计算属性,用于派生状态。

const store = new Vuex.Store({
  getters: {
    doubleCount: state => state.count * 2
  }
});
 

action

用于处理异步操作,通过 store.dispatch 触发。

const store = new Vuex.Store({
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment');
      }, 1000);
    }
  }
});
 

mutation

定义修改状态的方法,必须是同步的。

const store = new Vuex.Store({
  mutations: {
    increment(state) {
      state.count++;
    }
  }
});
 

modules

用于将 store 分模块管理,每个模块拥有自己的 state、mutations、actions 和 getters。

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
};
 
const store = new Vuex.Store({
  modules: {
    a: moduleA
  }
});
 
// 访问模块状态:store.state.a.count
 

Vuex的mapState

mapState 是 Vuex 提供的一个辅助函数,用于将 Vuex store 中的 state 映射到组件的计算属性上。

这使得组件可以方便地访问 Vuex store 中的状态,而无需手动编写重复的代码。

工作原理

  • 映射状态:mapState 将 store 中的 state 属性映射为组件的计算属性。
  • 响应式更新:当 store 中的状态发生变化时,映射的计算属性会自动更新。
  • 简化代码:避免手动编写重复的计算属性代码。

基本用法

mapState 可以将 store 中的 state 属性映射为组件的计算属性。

import { mapState } from 'vuex';
 
export default {
  computed: {
    // 使用 mapState 映射 store 中的 state
    ...mapState({
      count: state => state.count,
      message: state => state.message,
    }),
  },
};
 

对象展开运算符

mapState 返回一个对象,可以使用对象展开运算符 ... 将其混入到组件的 computed 属性中。

import { mapState } from 'vuex';
 
export default {
  computed: {
    ...mapState({
      count: state => state.count,
      message: state => state.message,
    }),
    // 其他计算属性
    doubleCount() {
      return this.count * 2;
    },
  },
};
 

映射多个状态

可以同时映射多个状态属性。

import { mapState } from 'vuex';
 
export default {
  computed: {
    ...mapState(['count', 'message', 'user']),
  },
};
 

命名空间

如果 Vuex store 使用了命名空间,可以在 mapState 中指定命名空间。

import { mapState } from 'vuex';
 
export default {
  computed: {
    ...mapState('moduleA', {
      count: state => state.count,
      message: state => state.message,
    }),
  },
};
 

Vue 的filter

一种用于格式化数据的工具,可以用于模板中的文本插值和表达式。

全局过滤器

全局过滤器需要在 Vue 实例创建之前注册,以便在整个应用中使用。

Vue.filter('testfilter', function (value, text) {
  return value + text;
});
 

使用:

 
<template>
  <div>{{ test | testfilter('Hello') }}</div>
</template>
 
<script>
export default {
  data() {
    return {
      test: 'Vue'
    };
  }
};
</script>
 

工作原理

  • 定义过滤器:使用 Vue.filter 方法定义全局过滤器。过滤器可以接受一个或多个参数。
  • 使用过滤器:在模板中,通过 | 符号使用过滤器。过滤器可以链式调用,依次处理数据。
  • 性能优化:过滤器的计算结果会被缓存,除非数据发生变化,否则不会重新计算。

注意事项

  • 性能:过滤器的计算结果会被缓存,但如果数据频繁变化,可能会影响性能。
  • 替代方案:在某些场景下,可以使用计算属性或方法代替过滤器,以获得更好的性能和可维护性。
  • 链式调用:过滤器可以链式调用,但需要确保每个过滤器的输出都符合下一个过滤器的输入要求。

局部过滤器

局部过滤器在组件实例对象中定义,仅在该组件中有效。

export default {
  data() {
    return {
      test: 'Vue'
    };
  },
  filters: {
    changemsg(val, text) {
      return val + text;
    }
  }
};
 

使用:

<template>
  <div>{{ test | changemsg('Hello') }}</div>
</template>
 

使用方式

过滤器只能在 {{}} 和 v-bind 中使用。定义时第一个参数是预处理的值,后面的参数是调用时传入的参数。

<template>
  <div>
    <h3 :title="test | changemsg(1234)">{{ test | changemsg(4567) }}</h3>
    <h2>{{ name | filter1 | filter2 | filter3 }}</h2>
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      test: 'Vue',
      name: 'John'
    };
  },
  filters: {
    changemsg(val, text) {
      return val + text;
    }
  }
};
</script>
 

Vue CLI 项目中注册多个全局过滤器

在 Vue CLI 项目中,可以通过创建一个单独的文件来定义和暴露过滤器,然后在 main.js 中导入并注册。

// 过滤器文件(filters.js)
const filter1 = function (val) {
  return val + '--1';
};
 
const filter2 = function (val) {
  return val + '--2';
};
 
const filter3 = function (val) {
  return val + '--3';
};
 
export default {
  filter1,
  filter2,
  filter3
};
 
// 在 main.js 中导入并注册
 
import Vue from 'vue';
import App from './App.vue';
import filters from './filters/filters.js';
 
// 注册全局过滤器
Object.keys(filters).forEach(key => {
  Vue.filter(key, filters[key]);
});
 
Vue.config.productionTip = false;
 
new Vue({
  render: h => h(App)
}).$mount('#app');
 

Vue的computed实现原理

初始化阶段

  • 生命周期函数:在组件实例的 beforeCreate 阶段,Vue 开始处理 computed 属性。
  • 遍历 computed 配置:Vue 遍历 computed 配置中的所有属性。
  • 创建 Watcher:为每个 computed 属性创建一个 Watcher 对象,并传入 computed 配置中的 getter 函数。
  • 依赖收集:getter 函数运行时会收集依赖,即 computed 属性所依赖的响应式数据。

延迟执行与缓存机制

  • lazy 配置:与渲染函数不同,computed 属性的 Watcher 不会立即执行。它使用了 lazy 配置,允许 Watcher 在需要时才执行。
  • 缓存属性:
    • value:保存 Watcher 运行的结果,初始值为 undefined。
    • dirty:标记当前的 value 是否已经过时(脏值),初始值为 true。

代理模式与访问逻辑

  • 挂载到组件实例:Vue 使用代理模式,将 computed 属性挂载到组件实例中。
  • 读取逻辑:
    • 如果 dirty 为 true,运行 getter 函数,计算依赖,将结果保存在 Watcher 的 value 中,设置 dirty 为 false,然后返回结果。
    • 如果 dirty 为 false,直接返回 Watcher 的 value。

依赖变化处理

  • 依赖收集:computed 属性的依赖同时收集到组件的 Watcher。
  • 触发更新:
    • 当 computed 属性的依赖变化时,会触发 computed 属性的 Watcher,此时只需设置 dirty 为 true,不做其他处理。
    • 由于依赖同时收集到组件的 Watcher,组件会重新渲染。重新渲染时会读取 computed 属性,由于 dirty 已为 true,会重新运行 getter 进行运算。

setter 处理

  • 直接运行 setter:当设置 computed 属性时,直接运行 setter 函数。

v-if 和 v-show

v-if

  • 渲染机制:条件不成立时,不渲染 DOM 元素。
  • 切换性能:条件变化时,会导致元素的重新渲染,性能开销较大。
  • 适用场景:适合条件很少改变的情况,例如根据用户权限显示或隐藏某些功能。

v-show

  • 渲染机制:条件不成立时,渲染 DOM 元素,但通过 CSS 的 display: none 隐藏。
  • 切换性能:条件变化时,仅切换样式,不重新渲染,性能较好。
  • 适用场景:适合需要频繁切换显示状态的情况,例如切换菜单的显示和隐藏。
特性v-ifv-show
渲染条件为真时才渲染。无论条件真假都渲染,通过 CSS 控制显示。
销毁重建切换时销毁重建。切换时保留。
性能初始渲染开销小,切换开销大。初始渲染开销大,切换开销小。
适用场景切换频率低,条件判断开销大。切换频率高,条件判断开销小。

当v-if与v-for一起使用时,v-for具有比v-if更高的优先级,这意味着v-if将分别重复运行于每个v-for循环中。 所以,不推荐v-if和v-for同时使用。如果v-if和v-for一起用的话,vue中的的会自动提示v-if应该放到外层去

v-if、v-show、v-html原理

v-if

基于条件渲染元素及其子节点。条件为假时,元素不会被渲染到 DOM 中。

原理:

  • 编译阶段:v-if 指令被编译成 JavaScript 代码,生成一个虚拟 DOM 节点树,其中包含条件判断逻辑。
  • 渲染阶段:
  • 条件为 true 时,生成相应的虚拟 DOM 并转换为真实 DOM 元素插入页面。
  • 条件为 false 时,移除相关的虚拟 DOM 节点以及对应的真实 DOM 元素。
  • 切换时:条件变化时,Vue 会执行完整的 DOM 操作(创建或销毁),即插入节点时进行挂载、销毁时调用卸载钩子并从 DOM 中移除。

v-show

始终渲染元素,只是通过 CSS 的 display 属性来控制显示与隐藏。

原理:

  • 编译阶段:v-show 指令被转换成一个更新节点 display 样式的函数。
  • 渲染阶段:在初次渲染时,v-show 所作用的元素会被创建并插入到 DOM 中。
  • 切换时:v-show 仅仅修改元素的 display 样式(display: none 或 display: ''),并不会触发元素的重新挂载或销毁。

v-html

动态渲染 HTML 片段的指令,将表达式的内容作为 innerHTML 插入到元素中。

原理:

  • 编译阶段:v-html 指令被编译成更新节点内容的逻辑,将给定的 HTML 字符串解析为真正的 HTML 结构,并插入到目标元素中。
  • 渲染阶段:当表达式的值发生变化时,Vue 会更新目标元素的 innerHTML 内容,并将之前的内容完全替换为新的内容。

缺点:

  • 存在潜在的 XSS(跨站脚本攻击)风险,如果插入的 HTML 内容是用户输入的或未经过安全过滤的内容,可能会导致安全隐患。
  • Vue 无法直接追踪 v-html 插入的 HTML 中的 DOM 变化(例如事件绑定),因此复杂交互时不建议使用。

对比:

指令原理优点缺点适用场景
v-if基于条件渲染控制节点的创建与销毁,逻辑复杂,切换时有性能损耗。减少初始渲染开销,不会渲染不需要的元素。频繁切换时,销毁和重新创建 DOM 节点会有较大的性能消耗。需要完全移除或条件性地渲染某些 DOM 节点。
v-show仅操作 CSS 样式来控制显示与隐藏,切换时开销小。不会触发 DOM 的创建或销毁,频繁切换时性能更好。即使不可见,元素仍然存在于 DOM 中,占用内存。只是简单的显示与隐藏切换(例如折叠面板)。
v-html通过 innerHTML 属性动态渲染 HTML 片段,使用不当时有安全风险。可以渲染复杂的 HTML 内容。存在 XSS 风险,Vue 无法追踪插入的 HTML 中的 DOM 变化。需要动态插入 HTML 片段,并且内容经过了安全过滤。

Vue中的v-pre

跳过这个元素及其子元素的编译过程,直接将原始内容渲染到页面上。这意味着 Vue 不会对这些内容进行模板解析或动态绑定处理。

具体作用

  • 跳过编译:Vue 通常会对模板中的内容进行解析和编译,但 v-pre 会让 Vue 完全忽略这个元素及其子元素的内容。
  • 显示原始内容:如果元素中包含 Vue 的模板语法(如 {{ message }} 或指令),这些内容会直接显示为原始文本,而不会被解析为动态内容。

使用场景

  • 当你需要显示包含 Vue 模板语法的原始内容时(例如文档或代码片段)。
  • 当你需要调试 Vue 应用时,避免 Vue 对某些内容进行不必要的编译。
<div v-pre>
  {{ message }} <!-- 这里的 {{ message }} 不会被 Vue 解析 -->
  <span v-if="isVisible">Hello</span> <!-- 这里的 v-if 也不会被 Vue 处理 -->
</div>
 
 

输出:

 
{{ message }}
<span v-if="isVisible">Hello</span>

Vue中的v-cloak

v-cloak 是 Vue.js 提供的一个内置指令,用于解决首屏加载时的闪屏问题。

它通过在 HTML 元素上设置样式,隐藏未编译的模板,直到 Vue 实例完成编译后才显示内容。

使用方法

在 HTML 元素中使用 v-cloak 指令:

<div v-cloak>
  {{ message }}
</div>
 

在 CSS 中定义 [v-cloak] 样式:

[v-cloak] {
  display: none;
}
 

注意事项

  • [v-cloak] 样式需要在 Vue 实例编译之前定义,通常放在 <link> 引入的 CSS 中,或者直接写在内联样式中。
  • 如果样式写在 import 引入的 CSS 中,可能会因为加载顺序问题导致样式不生效。

工作原理

  • 在 Vue 实例初始化之前,v-cloak 会隐藏绑定的 HTML 元素。
  • 当 Vue 实例完成编译后,v-cloak 指令会被移除,绑定的 HTML 元素会显示出来。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue v-cloak 示例</title>
  <style>
    [v-cloak] {
      display: none;
    }
  </style>
</head>
<body>
  <div id="app" v-cloak>
    <h1>{{ message }}</h1>
  </div>
 
  <script src="https://cdn.jsdelivr.net/npm/vue@3.0.0/dist/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          message: 'Hello Vue!'
        };
      }
    });
 
    app.mount('#app');
  </script>
</body>
</html>
 
 

v-for中的key

为了高效地更新虚拟 DOM,Vue.js 要求 v-for 中的每个节点都有一个唯一的 key 属性。

key 是一个特殊的属性,用于给每个节点提供一个唯一的标识。它的主要作用是帮助 Vue.js 的虚拟 DOM Diff 算法正确地识别和更新节点。

为什么需要 key?

  • 高效的更新:key 帮助 Vue.js 快速识别哪些节点发生了变化(新增、删除、更新),从而减少不必要的 DOM 操作。
  • 正确的渲染:key 确保 Vue.js 能够正确地复用和排序节点,避免因为节点顺序变化导致的渲染错误。

key 的最佳实践

  • 唯一性:key 应该是唯一的,最好是使用数据中的唯一标识符(如数据库中的 ID)。
  • 避免使用索引:虽然可以使用数组索引作为 key,但不推荐,因为索引会随着数组的变化而变化,可能导致性能问题和渲染错误。
  • 动态列表:在动态列表中(如添加、删除、排序),key 的唯一性尤为重要。
<template>
  <div>
    <ul>
      <!-- 使用 v-for 渲染动态列表 -->
      <li v-for="item in filteredItems" :key="item.id">
        {{ item.name }}
      </li>
    </ul>
    <button @click="addItem">Add Item</button>
    <button @click="removeItem">Remove Item</button>
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      items: [
        { id: 1, name: 'Item 1' },
        { id: 2, name: 'Item 2' },
        { id: 3, name: 'Item 3' },
      ],
    };
  },
  computed: {
    filteredItems() {
      // 返回过滤后的列表
      return this.items.filter(item => item.id % 2 === 0);
    },
  },
  methods: {
    addItem() {
      const newItem = { id: this.items.length + 1, name: `Item ${this.items.length + 1}` };
      this.items.push(newItem);
    },
    removeItem() {
      if (this.items.length > 0) {
        this.items.pop();
      }
    },
  },
};
</script>
 

Vue的transition

用于管理元素或组件过渡动画的内置组件。它允许你定义进入、离开和列表变化时的过渡效果。

基本用法

transition 组件通过 name 属性定义过渡的名称,并包裹需要动画的内容:

<template>
  <transition name="fade">
    <div v-if="show">需要动画的内容</div>
  </transition>
</template>
 
<script>
export default {
  data() {
    return {
      show: true
    };
  }
};
</script>

过渡的类

transition 提供了六个过渡类,用于定义进入和离开时的动画效果:

  • v-enter:进入过渡的初始状态,在元素插入前生效,插入后一帧删除。
  • v-enter-active:在元素插入前生效,在动画完成后删除。
  • v-enter-to:在元素插入后一帧生效,在动画完成后删除。
  • v-leave:离开过渡的初始状态,在元素离开时生效,下一帧删除。
  • v-leave-active:在离开过渡时生效,在动画完成后删除。
  • v-leave-to:离开过渡结束状态,在离开过渡下一帧生效,在动画完成后删除。 这些类会根据 name 属性动态生成,例如 name="fade" 时,类名会变为 fade-enter、fade-enter-active 等。

自定义类

可以通过 transition 的属性直接指定自定义的过渡类:

<template>
  <transition
    enter-class="enter-class"
    enter-active-class="enter-active-class"
    enter-to-class="enter-to-class"
    leave-class="leave-class"
    leave-active-class="leave-active-class"
    leave-to-class="leave-to-class"
  >
    <div v-if="show">需要动画的内容</div>
  </transition>
</template>

过渡类型

可以使用 type 属性指定 Vue 监听 transition 或 animation 事件:

<template>
  <transition name="fade" type="animation">
    <div v-if="show">需要动画的内容</div>
  </transition>
</template>

过渡时间

可以通过 duration 属性设置过渡时间,可以是数字或对象:

<template>
  <transition name="fade" :duration="1000">
    <div v-if="show">需要动画的内容</div>
  </transition>
</template>
 
<template>
  <transition name="fade" :duration="{ enter: 1000, leave: 300 }">
    <div v-if="show">需要动画的内容</div>
  </transition>
</template>
 

过渡钩子函数

transition 提供了多个钩子函数,用于在过渡的不同阶段执行自定义逻辑:

  • before-enter:进入过渡前调用。
  • enter:进入过渡时调用,通常用于手动触发动画。
  • after-enter:进入过渡完成后调用。
  • enter-cancelled:进入过渡取消时调用。
  • before-leave:离开过渡前调用。
  • leave:离开过渡时调用,通常用于手动触发动画。
  • after-leave:离开过渡完成后调用。
  • leave-cancelled:离开过渡取消时调用。
<template>
  <transition
    name="fade"
    @before-enter="beforeEnter"
    @enter="enter"
    @after-enter="afterEnter"
    @enter-cancelled="enterCancelled"
    @before-leave="beforeLeave"
    @leave="leave"
    @after-leave="afterLeave"
    @leave-cancelled="leaveCancelled"
  >
    <div v-if="show">需要动画的内容</div>
  </transition>
</template>
 
<script>
export default {
  data() {
    return {
      show: true
    };
  },
  methods: {
    beforeEnter(el) {
      console.log('beforeEnter', el);
    },
    enter(el, done) {
      console.log('enter', el);
      done(); // 手动触发动画完成
    },
    afterEnter(el) {
      console.log('afterEnter', el);
    },
    enterCancelled(el) {
      console.log('enterCancelled', el);
    },
    beforeLeave(el) {
      console.log('beforeLeave', el);
    },
    leave(el, done) {
      console.log('leave', el);
      done(); // 手动触发动画完成
    },
    afterLeave(el) {
      console.log('afterLeave', el);
    },
    leaveCancelled(el) {
      console.log('leaveCancelled', el);
    }
  }
};
</script>

keep-alive说明

用于缓存组件实例,避免组件切换时的重复渲染。

场景:适用于需要频繁切换且保留状态的组件,如路由切换、标签页切换等。

常用属性

  • include:指定哪些组件需要被缓存(支持字符串、正则表达式或数组)。
  • exclude:指定哪些组件不需要被缓存(支持字符串、正则表达式或数组)。
  • max:指定缓存的最大组件数,超出时按 LRU 策略淘汰。

生命周期钩子

  • activated:组件被激活时调用。
  • deactivated:组件被停用时调用。

keep-alive 的主要特点

  • 缓存组件:当组件在 keep-alive 中被切换时,Vue 并不会销毁组件实例,而是将其保存在内存中,当组件再次激活时,状态和 DOM 都能恢复原状。
  • 保留状态:组件的本地状态、输入框中的内容、滚动位置等在重新切换回来时不会丢失。
  • 生命周期钩子:当组件被 keep-alive 缓存时,会触发 deactivated 钩子;再次激活时,会触发 activated 钩子。这两个钩子可以用于处理组件被缓存和重新激活时的操作。

keep-alive 的实现原理

  • 缓存机制:
    • keep-alive 会在内部维护一个缓存池(一个对象),用来存储已渲染过的组件实例。它通过 VNode(虚拟 DOM 节点)进行组件的缓存。
    • 当组件第一次被渲染时,keep-alive 会将组件实例存入缓存池中,而不是销毁该实例。
    • 当组件被切换时,keep-alive 会检查缓存池中是否存在该组件实例。如果存在,直接从缓存池中取出并重新渲染。如果不存在,才会重新创建组件实例。
  • 组件的 VNode 缓存:
    • Vue 的 VNode 是 keep-alive 缓存的核心,它在组件切换时,会将 VNode 存入缓存,并在组件重新激活时,使用已缓存的 VNode 来恢复组件。
    • keep-alive 通过 include 和 exclude prop 来决定哪些组件应该被缓存或不缓存。
  • 生命周期钩子:
    • 在 keep-alive 组件中,通常组件不会在卸载时触发 destroyed 钩子,而是触发 deactivated 钩子,这表明该组件已经被缓存,但不再处于活动状态。
    • 当组件被再次激活时,activated 钩子会被调用,这意味着组件恢复为活动状态,但不需要重新创建。

keep-alive 缓存了什么?

  • 组件的本地状态:如 data 中定义的属性和数据。
  • DOM 状态:组件的 DOM 树,包括输入框中的内容、滚动位置等会被保留。
  • 组件实例的所有属性:比如响应式属性、计算属性、方法等都不会丢失。

注意事项

  • 缓存策略:keep-alive 采用 LRU(Least Recently Used,最近最少使用)缓存策略,即当缓存满了时,最久未使用的组件会被移除。
  • 数据变化:如果组件的数据发生变化,缓存中的组件也会随之更新。
  • 生命周期钩子:缓存的组件不会触发 created 和 mounted 钩子,只会触发 activated 钩

使用场景

  • 路由切换:在单页面应用(SPA)中,使用 keep-alive 缓存路由组件,避免每次切换时重新加载和初始化组件。
  • 标签页切换:在多标签页的界面中,使用 keep-alive 缓存每个标签页的内容,避免用户在切换标签页时重新加载数据。
  • 性能优化:在性能敏感的场景中,例如列表滚动、动画等,通过缓存组件来避免不必要的渲染,提高性能。
<template>
  <div>
    <!-- 使用 keep-alive 缓存路由组件 -->
    <keep-alive>
      <router-view v-if="$route.meta.keepAlive" />
    </keep-alive>
    <!-- 不需要缓存的路由组件 -->
    <router-view v-else />
  </div>
</template>
 

LRU策略

LRU 缓存的基本操作

  • get(key):如果缓存中存在该 key,则返回对应的值,并将该 key 变为最近使用的(即更新它的顺序)。
  • put(key, value):将 key-value 插入缓存。如果缓存已满,则移除最久未使用的 key-value,然后插入新的数据。

常见的数据结构

  • 哈希表(Map):用于快速定位缓存中的数据,时间复杂度是 O(1)。
  • 双向链表(Doubly Linked List):用于维护元素的使用顺序,链表头表示最近使用的元素,链表尾表示最久未使用的元素,时间复杂度是 O(1)。

LRU 缓存策略的核心思路

  • 每次访问缓存时,将访问的节点移到链表的头部,表示它是最近使用的。
  • 每次插入数据时,如果缓存已满,则移除链表尾部的节点,表示淘汰最近最少使用的元素。

使用 JavaScript 实现 LRU 缓存

1、简单实现(使用 Map 和 Set)

class LRUCache {
    constructor(capacity) {
        this.capacity = capacity; // 缓存的最大容量
        this.map = new Map(); // 哈希表,用于存储键值对,支持 O(1) 时间复杂度的查找
        this.keys = new Set(); // 存储 key 的访问顺序,利用 Set 的特性保持插入顺序
    }
 
    // 获取缓存中的值
    get(key) {
        if (!this.map.has(key)) {
            return -1; // 如果 key 不存在,则返回 -1
        }
        // 更新访问顺序,将当前 key 变为最近使用的
        this.keys.delete(key); // 从 Set 中删除当前 key
        this.keys.add(key); // 将当前 key 添加到 Set 的末尾(表示最近使用)
        return this.map.get(key); // 返回对应的值
    }
 
    // 向缓存中插入值
    put(key, value) {
        // 如果 key 已存在,更新值并调整访问顺序
        if (this.map.has(key)) {
            this.map.set(key, value); // 更新值
            this.keys.delete(key); // 从 Set 中删除当前 key
            this.keys.add(key); // 将当前 key 添加到 Set 的末尾(表示最近使用)
            return;
        }
 
        // 如果缓存已满,移除最近最少使用的元素(Set 中的第一个元素)
        if (this.map.size >= this.capacity) {
            const leastUsedKey = this.keys.values().next().value; // 获取 Set 中的第一个元素
            this.keys.delete(leastUsedKey); // 从 Set 中删除
            this.map.delete(leastUsedKey); // 从 Map 中删除
        }
 
        // 插入新元素并更新访问顺序
        this.map.set(key, value); // 将新元素添加到 Map 中
        this.keys.add(key); // 将新元素添加到 Set 的末尾(表示最近使用)
    }
}
 
// 测试
const lru = new LRUCache(2);
lru.put(1, 1); // 缓存: {1=1}
lru.put(2, 2); // 缓存: {1=1, 2=2}
console.log(lru.get(1)); // 返回 1, 缓存顺序更新: {2=2, 1=1}
lru.put(3, 3); // 缓存容量已满,移除最近最少使用的 2, 缓存: {1=1, 3=3}
console.log(lru.get(2)); // 返回 -1(缓存中不存在)
lru.put(4, 4); // 缓存容量已满,移除最近最少使用的 1, 缓存: {3=3, 4=4}
console.log(lru.get(1)); // 返回 -1(缓存中不存在)
console.log(lru.get(3)); // 返回 3
console.log(lru.get(4)); // 返回 4
 

2、优化实现(使用双向链表)

class Node {
    constructor(key, value) {
        this.key = key; // 节点的键
        this.value = value; // 节点的值
        this.prev = null; // 前驱节点
        this.next = null; // 后继节点
    }
}
 
class LRUCache {
    constructor(capacity) {
        this.capacity = capacity; // 缓存的最大容量
        this.map = new Map(); // 哈希表,用于快速查找节点
        this.head = new Node(0, 0); // 头节点,表示最近使用的节点
        this.tail = new Node(0, 0); // 尾节点,表示最久未使用的节点
        this.head.next = this.tail; // 初始化双向链表
        this.tail.prev = this.head;
    }
 
    // 获取缓存中的值
    get(key) {
        if (!this.map.has(key)) {
            return -1; // 如果 key 不存在,则返回 -1
        }
        const node = this.map.get(key); // 从哈希表中获取节点
        this.remove(node); // 将节点从链表中移除
        this.add(node); // 将节点添加到链表头部(表示最近使用)
        return node.value; // 返回对应的值
    }
 
    // 向缓存中插入值
    put(key, value) {
        if (this.map.has(key)) {
            const node = this.map.get(key); // 获取现有节点
            node.value = value; // 更新值
            this.remove(node); // 将节点从链表中移除
            this.add(node); // 将节点添加到链表头部(表示最近使用)
        } else {
            // 如果缓存已满,移除最近最少使用的节点(尾部节点的前一个节点)
            if (this.map.size === this.capacity) {
                this.remove(this.tail.prev); // 移除尾部节点的前一个节点
            }
            const node = new Node(key, value); // 创建新节点
            this.add(node); // 将新节点添加到链表头部
        }
    }
 
    // 将节点添加到链表头部
    add(node) {
        node.prev = this.head; // 设置新节点的前驱为头节点
        node.next = this.head.next; // 设置新节点的后继为头节点的后继
        this.head.next.prev = node; // 更新头节点后继的前驱为新节点
        this.head.next = node; // 更新头节点的后继为新节点
        this.map.set(node.key, node); // 将新节点添加到哈希表中
    }
 
    // 从链表中移除节点
    remove(node) {
        node.prev.next = node.next; // 更新节点前驱的后继为节点的后继
        node.next.prev = node.prev; // 更新节点后继的前驱为节点的前驱
        this.map.delete(node.key); // 从哈希表中删除节点
    }
}
 
// 测试
const lru = new LRUCache(2);
lru.put(1, 1); // 缓存: {1=1}
lru.put(2, 2); // 缓存: {1=1, 2=2}
console.log(lru.get(1)); // 返回 1, 缓存顺序更新: {2=2, 1=1}
lru.put(3, 3); // 缓存容量已满,移除最近最少使用的 2, 缓存: {1=1, 3=3}
console.log(lru.get(2)); // 返回 -1(缓存中不存在)
lru.put(4, 4); // 缓存容量已满,移除最近最少使用的 1, 缓存: {3=3, 4=4}
console.log(lru.get(1)); // 返回 -1(缓存中不存在)
console.log(lru.get(3)); // 返回 3
console.log(lru.get(4)); // 返回 4
 

组件通信

父子组件通信

父组件向子组件传递数据:

  • 方式:通过 props。
  • 原理:父组件通过 props 将数据传递给子组件,子组件通过 props 选项声明接收的属性。
<!-- ParentComponent.vue -->
<template>
  <ChildComponent :message="parentMessage" />
</template>
 
<script>
import ChildComponent from './ChildComponent.vue'
 
export default {
  components: { ChildComponent },
  data() {
    return {
      parentMessage: 'Hello from Parent',
    }
  },
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>{{ message }}</div>
</template>
 
<script>
export default {
  props: ['message'],
}
</script>

子组件向父组件传递数据:

  • 方式:通过 $emit
  • 原理:子组件通过 $emit 触发自定义事件,父组件监听这些事件并处理。
<!-- ChildComponent.vue -->
<template>
  <button @click="sendMessage">Send Message</button>
</template>
 
<script>
export default {
  methods: {
    sendMessage() {
      this.$emit('message-sent', 'Hello from Child')
    },
  },
}
</script>
<!-- ParentComponent.vue -->
<template>
  <ChildComponent @message-sent="handleMessage" />
</template>
 
<script>
import ChildComponent from './ChildComponent.vue'
 
export default {
  components: { ChildComponent },
  methods: {
    handleMessage(message) {
      console.log(message) // 输出: Hello from Child
    },
  },
}
</script>

获取父子组件实例:

  • 方式:通过 $parent$children
  • 原理:Vue 实例提供了 parent 和 children 属性,可以用来访问父组件或子组件的实例。

使用 ref 获取实例:

  • 方式:通过 ref。
  • 原理:通过 ref 属性为子组件指定一个引用名称,父组件可以通过这个引用名称访问子组件的实例。

Provide / Inject:

  • 方式:通过 provide 和 inject。
  • 原理:父组件通过 provide 提供数据,子组件通过 inject 注入数据。适用于跨层级的依赖注入。

兄弟组件通信

Event Bus:

  • 方式:通过全局事件总线。

  • 原理:

    • 创建 Event Bus:通过创建一个 Vue 实例作为事件总线,所有组件都可以通过这个实例进行事件的派发和监听。
    • 派发事件:使用 EventBus.$emit(eventName, payload) 派发一个事件,其中 eventName 是事件名称,payload 是传递的数据。
    • 监听事件:使用 EventBus.$on(eventName, callback) 监听事件,其中 callback 是事件触发时执行的函数。
    • 移除事件监听器:在组件销毁时,使用 EventBus.$off(eventName) 移除事件监听器,以避免内存泄漏。

注意事项:

  • 性能问题:Event Bus 会导致组件之间的紧耦合,过多的全局事件可能会导致性能问题。
  • 内存泄漏:确保在组件销毁时移除事件监听器,避免内存泄漏。
  • 替代方案:对于复杂的全局状态管理,可以考虑使用 Vuex(Vue 2)或 Pinia(Vue 3)。
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
<!-- ComponentA.vue -->
<template>
  <button @click="sendMessage">Send Message</button>
</template>
 
<script>
import { EventBus } from './event-bus.js'
 
export default {
  methods: {
    sendMessage() {
      EventBus.$emit('message-sent', 'Hello from Component A')
    },
  },
}
</script>
<!-- ComponentB.vue -->
<template>
  <div>{{ message }}</div>
</template>
 
<script>
import { EventBus } from './event-bus.js'
 
export default {
  data() {
    return {
      message: '',
    }
  },
  created() {
    EventBus.$on('message-sent', (message) => {
      this.message = message
    })
  },
}
</script>

Vuex:

  • 方式:通过 Vuex 状态管理。
  • 原理:通过 Vuex 的全局状态管理来实现跨组件通信。组件可以通过 this.$store 访问和修改状态。

跨级组件通信

Vuex:

  • 方式:通过 Vuex 状态管理。
  • 原理:与兄弟组件通信类似,通过 Vuex 的全局状态管理来实现跨级组件通信。

$attrs$listeners

  • 方式:通过 $attrs$listeners
  • 原理:$attrs 包含了父组件传递给子组件的非 props 属性,$listeners 包含了父组件传递给子组件的事件监听器。子组件可以通过 $attrs$listeners 将这些属性和事件传递给更深层次的子组件。
<!-- ParentComponent.vue -->
<template>
  <ChildComponent :custom-attr="parentMessage" @custom-event="handleEvent" />
</template>
 
<script>
import ChildComponent from './ChildComponent.vue'
 
export default {
  components: { ChildComponent },
  data() {
    return {
      parentMessage: 'Hello from Parent',
    }
  },
  methods: {
    handleEvent(message) {
      console.log(message) // 输出: Hello from Child
    },
  },
}
</script>
<!-- ChildComponent.vue -->
<template>
  <GrandChildComponent v-bind="$attrs" v-on="$listeners" />
</template>
 
<script>
import GrandChildComponent from './GrandChildComponent.vue'
 
export default {
  components: { GrandChildComponent },
}
</script>
<!-- GrandChildComponent.vue -->
<template>
  <div>{{ customAttr }}</div>
  <button @click="sendEvent">Send Event</button>
</template>
 
<script>
export default {
  props: ['customAttr'],
  methods: {
    sendEvent() {
      this.$emit('custom-event', 'Hello from GrandChild')
    },
  },
}
</script>

provide/inject 和 Props区别

特性provide/injectprops
适用场景用于跨层级的组件通信,特别是在多层级嵌套的组件中。用于父子组件之间的直接通信。
数据流向数据从祖先组件流向后代组件,但不需要直接的父子关系。数据从父组件流向子组件。
性能影响会增加 Vue 的响应式系统负担,因为它们会创建响应式引用。性能更高,因为它们是直接的父子通信,没有额外的开销。
灵活性提供了更大的灵活性,可以跨层级传递数据。适合简单的父子通信,数据流向清晰。
响应式提供的数据是响应式的,但需要确保提供的数据是响应式对象(如 refreactive)。props 本身是响应式的,子组件可以监听 props 的变化。
使用场景适合提供全局或全局范围内共享的数据,如主题配置、用户信息等。适合在父子组件之间传递数据,确保数据流向清晰。

provide/inject

// 祖先组件
export default {
  provide() {
    return {
      theme: this.theme,
      user: this.user,
    };
  },
  data() {
    return {
      theme: 'dark',
      user: { name: 'John Doe', age: 30 },
    };
  },
};
 
// 后代组件
export default {
  inject: ['theme', 'user'],
  created() {
    console.log(this.theme); // 'dark'
    console.log(this.user); // { name: 'John Doe', age: 30 }
  },
};
 

Props

// 父组件
<template>
  <child-component :theme="theme" :user="user"></child-component>
</template>
 
<script>
import ChildComponent from './ChildComponent.vue';
 
export default {
  components: {
    ChildComponent,
  },
  data() {
    return {
      theme: 'dark',
      user: { name: 'John Doe', age: 30 },
    };
  },
};
</script>
 
// 子组件
export default {
  props: ['theme', 'user'],
  created() {
    console.log(this.theme); // 'dark'
    console.log(this.user); // { name: 'John Doe', age: 30 }
  },
};

如何自定义组件?

  1. 搭建模板:定义组件的 HTML 结构和样式。
  2. 定义逻辑:实现组件的内部逻辑,包括数据、方法和生命周期钩子。
  3. 定义输入:通过 props 接收外部数据。
  4. 定义输出:通过 $emit 暴露事件。
  5. 复用和扩展:通过插槽和事件增强组件的灵活性和复用性。
<template>
  <!-- 1. 搭建组件的模板 -->
  <div class="my-component">
    <h2>{{ title }}</h2>
    <p>{{ description }}</p>
    <button @click="handleClick" class="btn">点击我</button>
    <slot name="content"></slot>
  </div>
</template>
 
<script>
export default {
  name: 'MyComponent',
  // 2. 定义组件的逻辑
  // 2.1 定义组件的数据输入(Props)
  props: {
    title: {
      type: String,
      required: true
    },
    description: {
      type: String,
      default: '这是一个默认描述'
    },
    isActive: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      internalState: false
    };
  },
  // 2.2 定义组件的数据输出(事件)
  methods: {
    handleClick() {
      this.$emit('click', '按钮被点击了');
    },
    handleUpdate(value) {
      this.$emit('update', value);
    }
  },
  // 可选:定义生命周期钩子
  mounted() {
    console.log('MyComponent mounted');
  }
};
</script>
 
<style scoped>
/* 3. 封装组件的样式 */
.my-component {
  padding: 20px;
  border: 1px solid #ccc;
  border-radius: 5px;
}
 
.btn {
  padding: 8px 16px;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
 
.btn:hover {
  background-color: #45a049;
}
</style>

在父组件中使用封装好的组件

 
<template>
  <div>
    <!-- 使用插槽增强组件的灵活性 -->
    <my-component
      :title="'组件标题'"
      :description="'组件描述'"
      :is-active="true"
      @click="handleComponentClick"
    >
      <template #content>
        <p>这是父组件提供的内容</p>
      </template>
    </my-component>
  </div>
</template>
 
<script>
import MyComponent from './MyComponent.vue';
 
export default {
  components: {
    MyComponent
  },
  methods: {
    handleComponentClick(message) {
      console.log('组件点击事件:', message);
    }
  }
};
</script>
 

组件的单例模式

在 Vue 中,可以通过 Vuex 作为状态管理工具来实现组件的单例模式。单例模式确保一个类只有一个实例,并提供一个全局访问点。

工作原理

  • Vuex Store:Vuex store 用于存储组件实例的状态,确保所有组件都可以访问到同一个实例。
  • 组件创建:在组件的 created 钩子中,检查 Vuex 中是否已经存在该组件的实例。如果存在,则使用已存在的实例;如果不存在,则将当前实例存储到 Vuex 中。
  • 组件销毁:在组件的 beforeDestroy 钩子中,从 Vuex 中移除组件实例,避免内存泄漏。
  • 状态更新:通过 Vuex 的 mutations 更新组件实例的状态,确保所有实例共享同一个状态。

注意事项

  • 性能:单例模式可以减少组件的创建和销毁开销,但需要确保在不需要时及时移除实例。
  • 状态管理:使用 Vuex 管理状态可以确保组件之间的状态一致性,但需要合理设计状态结构。
  • 适用场景:单例模式适用于需要全局共享状态或行为的组件,如通知中心、用户信息组件等。
// store.js
// 首先,创建一个 Vuex store,用于管理组件实例的状态。
import Vue from 'vue';
import Vuex from 'vuex';
 
Vue.use(Vuex);
 
export default new Vuex.Store({
  state: {
    componentInstances: {}, // 存储组件实例
  },
  mutations: {
    setComponentInstance(state, { name, instance }) {
      state.componentInstance[name] = instance;
    },
    removeComponentInstance(state, name) {
      delete state.componentInstance[name];
    },
  },
  getters: {
    getComponentInstance: (state) => (name) => {
      return state.componentInstance[name];
    },
  },
});
 
{/* 创建一个需要作为单例的组件,并在组件的 created 钩子中检查是否已经存在实例。 */}
 
<template>
  <div>
    <h1>单例组件</h1>
    <p>{{ message }}</p>
    <button @click="updateMessage">更新消息</button>
  </div>
</template>
 
<script>
import { mapState, mapMutations } from 'vuex';
 
export default {
  name: 'SingletonComponent',
  data() {
    return {
      message: '这是单例组件的初始消息',
    };
  },
  computed: {
    ...mapState({
      componentInstances: (state) => state.componentInstance,
    }),
  },
  created() {
    // 检查是否已经存在实例
    if (this.componentInstance[this.name]) {
      // 如果存在,使用已存在的实例
      Object.assign(this, this.componentInstance[this.name]);
    } else {
      // 如果不存在,将当前实例存储到 Vuex 中
      this.setComponentInstance({
        name: this.name,
        instance: this,
      });
    }
  },
  beforeDestroy() {
    // 在组件销毁时,从 Vuex 中移除实例
    this.removeComponentInstance(this.name);
  },
  methods: {
    ...mapMutations(['setComponentInstance', 'removeComponentInstance']),
    updateMessage() {
      this.message = '消息已更新';
    },
  },
};
</script>
 
{/* 在父组件中,多次使用单例组件,确保它们共享同一个实例。 */}
 
<template>
  <div>
    <h1>父组件</h1>
    <singleton-component></singleton-component>
    <singleton-component></singleton-component>
    <singleton-component></singleton-component>
  </div>
</template>
 
<script>
import SingletonComponent from './SingletonComponent.vue';
 
export default {
  components: {
    SingletonComponent,
  },
};
</script>
 

Vue中组件复用的方式

组件组合

通过父子组件组合来实现复用,将逻辑划分为更小的组件,以便在不同场景下重复使用。

<template>
  <div>
    <h1>父组件</h1>
    <child-component :message="parentMessage" @child-event="handleChildEvent"></child-component>
  </div>
</template>
 
<script>
// 子组件
const ChildComponent = {
  props: ['message'],
  template: `
    <div>
      <h2>子组件</h2>
      <p>{{ message }}</p>
      <button @click="$emit('child-event', '事件从子组件传递到父组件')">触发事件</button>
    </div>
  `,
};
 
export default {
  components: {
    ChildComponent,
  },
  data() {
    return {
      parentMessage: '这是父组件传递给子组件的消息',
    };
  },
  methods: {
    handleChildEvent(data) {
      console.log('子组件传递的数据:', data);
    },
  },
};
</script>
 

插槽(Slots)

插槽允许父组件向子组件传递结构化内容,v-slot 是 Vue 3 中的统一插槽语法。插槽有默认插槽和具名插槽,具名插槽允许传递多个不同位置的内容。

<template>
  <div>
    <h1>使用插槽的父组件</h1>
    <slot-component>
      <template v-slot:header>
        <h2>这是头部内容</h2>
      </template>
      <p>这是默认插槽的内容</p>
      <template v-slot:footer>
        <p>这是底部内容</p>
      </template>
    </slot-component>
  </div>
</template>
 
<script>
// 子组件
const SlotComponent = {
  template: `
    <div>
      <div class="header">
        <slot name="header"></slot>
      </div>
      <div class="content">
        <slot></slot>
      </div>
      <div class="footer">
        <slot name="footer"></slot>
      </div>
    </div>
  `,
};
 
export default {
  components: {
    SlotComponent,
  },
};
</script>

Mixin

Mixin 是一种复用组件逻辑的方式,允许在多个组件之间共享通用的功能。通过定义 Mixin 并在组件中引入,可以复用特定的行为逻辑。不过,Mixin 可能带来命名冲突,使用时要注意。

<template>
  <div>
    <h1>使用 Mixin 的组件</h1>
    <p>{{ mixinData }}</p>
    <button @click="mixinMethod">调用 Mixin 中的方法</button>
  </div>
</template>
 
<script>
// 定义 Mixin
const MyMixin = {
  data() {
    return {
      mixinData: '这是 Mixin 中的数据',
    };
  },
  methods: {
    mixinMethod() {
      console.log('这是 Mixin 中的方法');
    },
  },
};
 
export default {
  mixins: [MyMixin],
  mounted() {
    console.log('组件挂载,Mixin 数据:', this.mixinData);
  },
};
</script>

自定义指令

通过自定义指令,可以为多个组件或元素复用某些行为逻辑,例如点击外部关闭等功能。

<template>
  <div>
    <h1>自定义指令示例</h1>
    
    <!-- 使用自定义指令 v-focus -->
    <input v-focus placeholder="自动聚焦的输入框" />
    
    <!-- 使用带参数的自定义指令 -->
    <div v-highlight:blue="'highlight'">这个文本会被高亮显示</div>
    
    <!-- 使用修饰符 -->
    <div v-highlight.once:blue="'highlight'">这个文本只会被高亮一次</div>
  </div>
</template>
 
<script>
export default {
  directives: {
    // 定义局部自定义指令
    highlight: {
      // 指令的定义
      created(el, binding, vnode) {
        console.log('highlight 指令已创建', el);
      },
      mounted(el, binding) {
        // 获取参数和修饰符
        const { value, arg, modifiers } = binding;
        if (modifiers.once) {
          console.log('highlight 指令只会执行一次');
        }
        el.style.backgroundColor = value;
      },
      updated(el, binding) {
        console.log('highlight 指令已更新', el);
      },
    },
  },
  mounted() {
    console.log('组件已挂载');
  },
};
</script>

带参数和修饰符的自定义指令:

<template>
  <div>
    <h1>带参数和修饰符的自定义指令</h1>
    
    <!-- 带参数和修饰符 -->
    <div v-highlight:blue.once="'highlight'">这个文本会被高亮显示一次</div>
    
    <!-- 动态参数 -->
    <div v-highlight:[color]="'highlight'">这个文本的颜色由 data 中的 color 决定</div>
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      color: 'red',
    };
  },
  directives: {
    highlight: {
      mounted(el, binding) {
        const { value, arg, modifiers } = binding;
        if (modifiers.once) {
          console.log('highlight 指令只会执行一次');
        }
        el.style.backgroundColor = value;
      },
    },
  },
};
</script>
 

生命周期钩子:

  • created:指令绑定到元素时调用。
  • beforeMount:在绑定元素的父组件挂载之前调用。
  • mounted:在绑定元素的父组件挂载之后调用。
  • beforeUpdate:在绑定元素的父组件更新之前调用。
  • updated:在绑定元素的父组件更新之后调用。
  • beforeUnmount:在绑定元素的父组件卸载之前调用。
  • unmounted:在绑定元素的父组件卸载之后调用。

高阶组件(HOC)

高阶组件是一种工厂函数,接收一个组件作为参数并返回一个增强后的组件,通常用于逻辑复用。

<template>
  <div>
    <h1>使用高阶组件</h1>
    <enhanced-component></enhanced-component>
  </div>
</template>
 
<script>
// 原始组件
const OriginalComponent = {
  template: `
    <div>
      <h2>原始组件</h2>
      <p>{{ message }}</p>
    </div>
  `,
  data() {
    return {
      message: '这是原始组件的内容',
    };
  },
};
 
// 高阶组件
const withLogging = (WrappedComponent) => {
  return {
    components: {
      WrappedComponent,
    },
    template: `
      <div>
        <WrappedComponent></WrappedComponent>
      </div>
    `,
    mounted() {
      console.log('高阶组件已挂载');
    },
  };
};
 
// 增强后的组件
const EnhancedComponent = withLogging(OriginalComponent);
 
export default {
  components: {
    EnhancedComponent,
  },
};
</script>

Vue组件中的this

this 是一个非常重要的关键字,用于在组件的上下文中引用组件实例本身。

this 可以在组件的实例方法、生命周期钩子、计算属性等上下文中使用,以访问组件的 data、methods、props 等属性和方法。

使用场景

  • 实例方法:在组件的方法中使用 this 访问组件的属性和方法。
  • 生命周期钩子:在生命周期钩子中使用 this 初始化或清理资源。
  • 计算属性:在计算属性中使用 this 访问 data 或 props 中的值。
<template>
  <div>
    <h1>{{ message }}</h1>
    <p>计数: {{ count }}</p>
    <button @click="incrementCount">增加计数</button>
    <p>双倍计数: {{ doubleCount }}</p>
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      message: 'Hello Vue!',
      count: 0,
    };
  },
  methods: {
    // 实例方法
    incrementCount() {
      this.count++; // 使用 this 访问 count
      console.log('计数已增加到:', this.count);
    },
  },
  created() {
    // 生命周期钩子
    console.log('组件已创建,message:', this.message);
  },
  computed: {
    // 计算属性
    doubleCount() {
      return this.count * 2; // 使用 this 访问 count
    },
  },
  beforeDestroy() {
    // 生命周期钩子
    console.log('组件即将销毁,count:', this.count);
  },
};
</script>

箭头函数中的 this

在箭头函数中,this 不会绑定到组件实例,而是继承外层的 this 值。因此,在需要访问组件实例的上下文中,避免使用箭头函数。

methods: {
  // 避免使用箭头函数
  incrementCount: () => {
    console.log(this.count); // this 不会指向组件实例
  },
},
 

在 Vue2 中检测数组的变化?

由于 Object.defineProperty 的限制,Vue 无法直接监听数组的索引或 length 属性的变化,因此 Vue 通过劫持数组的方法来实现对数组变化的响应式处理。

函数劫持

Vue 2.x 通过劫持数组的常用方法(如 push、pop、shift、unshift、splice、sort、reverse 等),重写了这些方法,使得它们能够触发响应式更新。

反过来说:这些方法会直接修改数组,并触发视图更新。

原型链重写

Vue 将 data 中的数组的原型链指向了自己定义的数组原型方法。这样,当调用数组的 API 时,Vue 可以拦截这些操作并通知依赖更新。

递归遍历

如果数组中包含引用类型(如对象或数组),Vue 会对这些引用类型再次递归遍历并进行监控,从而实现深度响应式。

简易实现:

// 定义一个响应式数组的原型方法
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
 
// 重写数组方法
const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
 
methodsToPatch.forEach((method) => {
  const original = arrayProto[method]
  arrayMethods[method] = function (...args) {
    // 调用原生方法
    const result = original.apply(this, args)
    // 触发响应式更新
    console.log(`Array method called: ${method}`, args)
    // 返回原生方法的结果
    return result
  }
})
 
// 创建一个响应式数组
function createReactiveArray(arr) {
  arr.__proto__ = arrayMethods
  arr.forEach((item, index) => {
    // 递归遍历数组中的引用类型
    if (typeof item === 'object') {
      arr[index] = makeReactive(item)
    }
  })
  return arr
}
 
// 递归遍历对象
function makeReactive(obj) {
  if (Array.isArray(obj)) {
    return createReactiveArray(obj)
  } else {
    // 对象响应式处理(简化版)
    Object.keys(obj).forEach((key) => {
      defineReactive(obj, key, obj[key])
    })
    return obj
  }
}
 
// 定义响应式属性
function defineReactive(obj, key, value) {
  if (typeof value === 'object') {
    value = makeReactive(value)
  }
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      console.log(`Getting ${key}`)
      return value
    },
    set(newValue) {
      console.log(`Setting ${key} to ${newValue}`)
      value = newValue
    },
  })
}
 
// 示例
const data = {
  name: 'Vue 2',
  list: [1, 2, { nested: 'object' }],
}
 
makeReactive(data)
 
data.list.push(3) // 输出: Array method called: push [3]
data.list[2].nested = 'updated' // 输出: Setting nested to updated

defineProperty和proxy区别?

性能问题:初始化遍历对象所有 key

  • Vue 2 (Object.defineProperty):
    • 必须遍历每一个属性进行 getter/setter 劫持。
    • 初始化时需要递归遍历所有嵌套对象的属性,性能开销大。
  • Vue 3 (Proxy):
    • 直接代理整个对象,而非每个属性。
    • 初始化时无需递归遍历所有嵌套对象的属性,只需代理一次对象。

优化策略: 按需响应式:Vue 3 在访问具体属性时才递归生成响应式对象,而非初始化时立即遍历整个对象树。这大大减少了性能开销,尤其在深层嵌套结构时,性能提升更为明显。

Dep 和 Watcher 占用内存较多

  • Vue 2 (Object.defineProperty):需要为每个属性维护 dep 实例和 watcher 实例,额外占用较多内存。
  • Vue 3 (Proxy):依赖追踪和更新通知机制优化,使用更轻量的 Effect 机制。

优化策略: 使用 Effect 和 Reactive 管理依赖:Vue 3 使用 Effect 机制管理响应式数据依赖,取代了 Vue 2 的 watcher,并通过一个全局的 ReactiveEffect 实例追踪所有响应式的依赖关系,减少了内存占用。

无法监听数组元素变化

  • Vue 2 (Object.defineProperty):无法直接监听数组元素的变化,需通过重写数组操作方法(如 push、pop 等)进行拦截。
  • Vue 3 (Proxy):可直接监听数组的操作,无需手动劫持数组方法。

优化策略: 全方位监听:Proxy 能直接监听数组长度、索引变动及元素操作(如 push、pop 等),无需额外操作重写,减少代码复杂度和潜在性能损耗。

动态新增/删除属性的监听

  • Vue 2 (Object.defineProperty):动态新增或删除属性无法被劫持,需通过 Vue.set 和 Vue.delete 方法显式操作对象。
  • Vue 3 (Proxy):可直接捕获动态属性的添加和删除,无需额外的 API 调用。

优化策略: 动态属性响应式:Proxy 天然支持对动态属性(包括数组项)新增和删除的响应式处理,无需使用特定 API,简化了开发体验。

不支持复杂数据结构(如 Map、Set)

  • Vue 2 (Object.defineProperty):-无法处理复杂对象如 Map 和 Set,这类数据结构无法被响应式处理。
  • Vue 3 (Proxy):支持所有原生 JavaScript 数据结构,包括 Map、Set 等。

优化策略:支持复杂数据结构:Vue 3 的响应式系统能够很好地处理 Map、Set 等复杂数据结构,确保其变化可以被追踪和响应,适用于复杂应用场景(如管理缓存或列表等)。

理解MVVM

MVVM(Model-View-ViewModel) 是一种设计模式,用于将用户界面(View)与业务逻辑(Model)分离,通过一个中间层(ViewModel)来管理数据绑定和交互。

它是 MVC(Model-View-Controller)模式的演变,将 Controller 替换为 ViewModel,以更好地支持数据绑定和响应式编程。

Model(模型层)

代表数据模型,是应用程序的核心数据和业务逻辑。

作用:

  • 存储和管理应用程序的数据。
  • 提供数据的增删改查等操作。
  • 不直接与用户界面交互,而是通过 ViewModel 进行数据同步。

View(视图层)

定义:代表用户界面,是用户与应用程序交互的部分。

作用:

  • 显示数据,即 Model 层的数据。
  • 捕获用户输入(如点击按钮、输入文本等)。
  • 通过数据绑定机制与 ViewModel 层进行交互。
  • 视图的变化会通知 ViewModel 层更新数据。

ViewModel(视图模型层)

定义:是 View 和 Model 之间的桥梁,负责管理数据绑定和交互逻辑。

作用:

  • 将 Model 层的数据绑定到 View 层。
  • 处理 View 层的用户输入,更新 Model 层的数据。
  • 监听 Model 层的数据变化,并将变化同步到 View 层。
  • 提供数据转换和格式化功能,以满足视图的显示需求。
 
graph TD
    A[用户操作 View] -->|触发事件| B[ViewModel 捕获事件]
    B -->|更新 Model 数据| C[Model 数据变化]
    C -->|通知 ViewModel| D[ViewModel 检测到数据变化]
    D -->|更新 View 数据| E[View 更新显示]
    E -->|用户看到更新| A
    C -->|数据变化| D
    D -->|同步数据到 View| E
 

Vue生命周期

vue的生命周期有哪些及每个生命周期做了什么?

初始化阶段

beforeCreate

  • 触发时机:实例初始化后,data 和 methods 还未初始化。
  • 特点:无法访问 data 和 methods,适合初始化全局变量。

created

  • 触发时机:实例创建完成后,data 和 methods 已初始化。
  • 特点:可以访问 data 和 methods,但无法访问 DOM。适合获取初始数据,更改数据不会触发 updated。适合进行初始数据的获取,例如从后端 API 获取数据。

挂载阶段

beforeMount

  • 触发时机:挂载开始之前,模板已编译。
  • 特点:虚拟 DOM 已创建,即将渲染。可以更改数据,不会触发 updated。

mounted

  • 触发时机:挂载完成后,真实 DOM 已挂载。
  • 特点:可以访问真实 DOM,适合操作 DOM 和绑定事件监听器。

更新阶段

beforeUpdate

  • 触发时机:响应式数据更新,虚拟 DOM 重新渲染之前。
  • 特点:可以更改数据,不会触发额外的重渲染。例如缓存旧数据。

updated

  • 触发时机:DOM 更新完成后。
  • 特点:DOM 已更新,适合进行 DOM 操作。避免在此更改数据,防止无限循环。

销毁阶段

beforeDestroy

  • 触发时机:实例销毁之前。
  • 特点:实例仍可使用,适合进行善后工作,如清除计时器。

destroyed

  • 触发时机:实例销毁之后。
  • 特点:组件已完全销毁,无法再进行操作。
setup

├─ onBeforeMount
│     ↓
├─ onMounted
│     ↓
├─ onBeforeUpdate
│     ↓
├─ onUpdated
│     ↓
├─ onBeforeUnmount
│     ↓
├─ onUnmounted

使用场景

  • onMounted:用于在组件挂载后执行 DOM 操作。
  • onUnmounted:用于在组件销毁前清理资源,如定时器、事件监听器等。
  • onBeforeUnmount:用于在组件销毁前执行一些清理逻辑。
  • onUpdated:用于在组件更新后执行一些逻辑,如重新计算布局等。

Vue生命周期

生命周期钩子执行顺序

Vue 的父组件和子组件生命周期钩子函数执行顺序。

加载渲染过程

  1. 父组件的 beforeCreate、created、beforeMount 钩子依次执行。
  2. 子组件的 beforeCreate、created、beforeMount、mounted 钩子依次执行。
  3. 最后父组件的 mounted 钩子执行。
graph TD
    A[父 beforeCreate] --> B[父 created]
    B --> C[父 beforeMount]
    C --> D[子 beforeCreate]
    D --> E[子 created]
    E --> F[子 beforeMount]
    F --> G[子 mounted]
    G --> H[父 mounted]
 

子组件更新过程

  1. 父组件的 beforeUpdate 钩子执行。
  2. 子组件的 beforeUpdate 和 updated 钩子依次执行。
  3. 最后父组件的 updated 钩子执行。
graph TD
    A[父 beforeUpdate] --> B[子 beforeUpdate]
    B --> C[子 updated]
    C --> D[父 updated]

销毁过程

  1. 父组件的 beforeDestroy 钩子执行。
  2. 子组件的 beforeDestroy 和 destroyed 钩子依次执行。
  3. 最后父组件的 destroyed 钩子执行。
graph TD
    A[父 beforeDestroy] --> B[子 beforeDestroy]
    B --> C[子 destroyed]
    C --> D[父 destroyed]

父组件监听子组件生命周期钩子

@hook 语法:适用于需要父组件监听子组件生命周期钩子的场景,简化了代码,减少了子组件中的 $emit 调用。

Parent.vue

 
<template>
  <div>
    <Child @hook:mounted="doSomething" />
  </div>
</template>
 
<script>
import Child from './Child.vue';
 
export default {
  components: {
    Child
  },
  methods: {
    doSomething() {
      console.log('父组件监听到 mounted 钩子函数...');
    }
  }
};
</script>
 

Child.vue

 
<template>
  <div>
    <p>子组件内容</p>
  </div>
</template>
 
<script>
export default {
  mounted() {
    console.log('子组件触发 mounted 钩子函数...');
  }
};
</script>

Ajax请求放在哪个生命周期?

在 Vue 中,接口请求通常可以放在 created、beforeMount 和 mounted 生命周期钩子中。然而,推荐在 created 钩子中进行接口请求。

原因

  • 更快获取数据,减少页面加载时间,提升用户体验。
  • 确保代码一致性,在服务端渲染(SSR)场景中,created 钩子可以正常工作。
  • 避免页面闪屏问题,在模板渲染之前就准备好数据,避免用户看到空白或加载状态。

Vue的响应式原理

Vue 2 的响应式原理

Object.defineProperty 来实现响应式数据。以下是其核心原理:

数据劫持:

  • 在初始化数据时,Vue 使用 Object.defineProperty 重新定义 data 中的所有属性。
  • 通过 get 和 set 方法拦截对属性的访问和修改。

依赖收集:

  • 当页面使用某个响应式属性时,get 方法会被触发,进行依赖收集(将当前组件的 watcher 收集起来)。
  • 如果属性发生变化,set 方法会被触发,通知相关依赖进行更新操作(发布订阅模式)。

Vue 3 的响应式原理

Vue 3 使用 Proxy 代替 Object.defineProperty 来实现响应式数据。以下是其核心原理:

Proxy 代理:

  • Proxy 可以直接监听对象和数组的变化,并且提供了多达 13 种拦截方法(如 get、set、deleteProperty 等)。
  • Proxy 是 ES6 的新标准,浏览器厂商会对其进行持续的性能优化。

深度代理:

  • Proxy 只会代理对象的第一层。为了实现深度代理,Vue 3 在 get 操作中判断返回值是否为对象,如果是,则通过 reactive 方法对返回值进行代理。这样就实现了深度响应式。

性能优化:

  • 在处理数组时,可能会触发多次 get 和 set 操作。为了避免不必要的触发,Vue 3 在 set 操作中进行了优化:
    • 判断 key 是否为当前被代理对象 target 的自身属性。
    • 判断旧值与新值是否相等。
    • 只有满足以上两个条件之一时,才执行 trigger 操作。

Vue 的 $destroy

用于手动销毁 Vue 实例。这个方法会清理相关的事件监听器和所有子组件,确保实例被正确地从内存中移除,避免内存泄漏。

$destroy 的作用

  • 销毁实例:将 Vue 实例从挂载状态变为未挂载状态。
  • 清理事件监听:移除实例上的所有事件监听器。
  • 销毁子组件:递归地销毁所有子组件。
  • 移除响应式系统:停止数据的响应式系统,不再追踪数据变化。
  • 生命周期影响:触发 beforeDestroy 和 destroyed 生命周期钩子。
<template>
  <div>
    <h1>Vue $destroy 示例</h1>
    <button @click="destroyInstance">销毁实例</button>
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      message: '这是一个 Vue 实例',
    };
  },
  methods: {
    destroyInstance() {
      this.$destroy();
      console.log('实例已销毁');
    },
  },
  beforeDestroy() {
    console.log('beforeDestroy 钩子已触发');
  },
  destroyed() {
    console.log('destroyed 钩子已触发');
  },
};
</script>
 

工作原理

  • 销毁实例:调用 $destroy 方法后,Vue 实例从挂载状态变为未挂载状态。
  • 清理事件监听:实例上的所有事件监听器都会被移除。
  • 销毁子组件:递归地销毁所有子组件。
  • 移除响应式系统:停止数据的响应式系统,不再追踪数据变化。
  • 生命周期钩子:beforeDestroy 和 destroyed 钩子会被依次调用。

注意事项

  • 手动销毁:通常情况下,Vue 会自动管理实例的生命周期,但在某些情况下(如动态组件或复杂的组件树),可能需要手动销毁实例。
  • 性能优化:通过手动销毁不再需要的实例,可以避免不必要的内存占用和性能开销。
  • 避免重复销毁:确保不要多次调用 $destroy 方法,避免不必要的操作。

响应式引用

响应式引用是一种机制,它允许 Vue 跟踪数据的变化,并在数据变化时自动更新视图。

Vue 通过 Proxy(在 Vue 3 中)或 Object.defineProperty(在 Vue 2 中)来实现这种响应式机制。

工作原理

  • 数据包装:使用 ref 或 reactive 等函数将普通数据包装成响应式数据。
  • 依赖收集:当组件首次渲染时,Vue 会收集所有依赖于该数据的视图或计算属性。
  • 数据变化检测:当数据发生变化时,Vue 会检测到这些变化。
  • 视图更新:Vue 会自动更新所有依赖于该数据的视图或计算属性。

响应式引用的创建

1、ref:用于创建单个值的响应式引用。

import { ref } from 'vue';
 
const count = ref(0);
console.log(count.value); // 0
 
count.value = 1; // 响应式更新
console.log(count.value); // 1
 

2、reactive:用于创建深度响应式的对象。

import { reactive } from 'vue';
 
const state = reactive({
  name: 'Alice',
  age: 30
});
 
state.age = 31; // 响应式更新
 

3、shallowRef:用于创建浅层响应式引用,仅引用本身响应式。

import { shallowRef } from 'vue';
 
const user = shallowRef({ name: 'Alice', age: 30 });
 
user.value = { name: 'Bob', age: 25 }; // 响应式更新
user.value.age = 31; // 不会响应式更新
 

4、shallowReactive:用于创建浅层响应式对象,仅顶层属性响应式。

import { shallowReactive } from 'vue';
 
const state = shallowReactive({
  user: { name: 'Alice', age: 30 },
  items: ['apple', 'banana']
});
 
state.items.push('orange'); // 响应式更新
state.user.age = 31; // 不会响应式更新
 

Vue 2 和Vue 3的区别

源码组织方式变化

  • Vue 2:使用 JavaScript 编写,源码较为复杂。
  • Vue 3:使用 TypeScript 重写,源码更加清晰,类型安全,便于维护和扩展。

支持 Composition API

  • Vue 2:使用 Options API,组件逻辑通过 data、methods、computed 等选项组织。
  • Vue 3:引入 Composition API,基于函数的 API,更加灵活地组织组件逻辑。通过 setup 函数,可以将逻辑拆分为可复用的函数。

响应式系统提升

  • Vue 2:使用 Object.defineProperty 实现响应式,只能监听对象的属性,不能监听动态新增或删除的属性,数组变化需要特殊处理。
    • 初始化时性能开销较大,尤其是对象嵌套较深时。
    • 无法监听动态新增的属性或删除的属性。
    • 数组的响应式依赖于对数组方法(如 push、pop 等)的重写,无法直接监听索引或 length 属性的变化。
  • Vue 3:使用 Proxy 实现响应式,可以监听动态新增或删除的属性,以及数组的变化。性能优化更好,支持深度响应式。
    • 性能优化:Proxy 不需要在初始化时递归遍历所有属性,而是按需拦截属性的访问和修改,减少了初始化时的性能开销。
    • 动态监听:Proxy 可以直接监听动态新增的属性、删除的属性,以及数组的索引和 length 属性。
    • 更灵活的响应式:通过 reactive 和 ref 两种方式实现响应式,开发者可以根据需求选择适合的 API。

编译优化

  • Vue 2:通过标记静态根节点优化 diff,减少不必要的比较。
  • Vue 3:标记和提升所有静态节点,diff 时只对比动态节点内容,优化了渲染性能。
    • 静态提升:在编译时标记静态节点,跳过对静态节点的比较,只对比动态节点。
    • patch flag:为动态节点添加 patch flag,标记节点的更新类型(如 text、class 等),减少不必要的 diff 操作。
    • 事件缓存:缓存事件处理函数,避免重复生成。
    • Fragments:允许模板中直接使用多个同级节点,而不需要包裹在一个根节点中,减少了 DOM 操作的开销。

打包体积优化

  • Vue 2:包含一些不常用的 API(如 inline-template、filter)。
  • Vue 3:移除了一些不常用的 API,减小了打包体积。
    • Tree-shaking:支持模块化,未使用的功能不会被打包到最终代码中。
    • 移除冗余 API:例如移除了 inline-template 和 filter,推荐使用更现代的替代方案(如计算属性)。

生命周期的变化

  • Vue 2:生命周期钩子包括 beforeCreate、created、beforeMount、mounted 等。
  • Vue 3:引入 setup 函数,替代了 beforeCreate 和 created。setup 是 Composition API 的入口,用于初始化响应式数据和方法。

Template 模板支持

  • Vue 2:模板必须有一个根标签。
  • Vue 3:模板支持多个根标签,更加灵活。

Vuex 状态管理

  • Vue 2:通过 new Vuex.Store 创建 Vuex 实例。
  • Vue 3:通过 createStore 创建 Vuex 实例,更加灵活。

路由获取页面实例与路由信息

  • Vue 2:通过 this.$routerthis.$route 获取路由实例和路由信息。
  • Vue 3:通过 getCurrentInstance 获取当前组件实例,通过 useRoute 和 useRouter 获取路由信息和路由实例。

Props 的使用变化

  • Vue 2:通过 this.props 获取 props 的内容。
  • Vue 3:直接通过 props 参数获取 props 的内容,更加直观。

父子组件传值

  • Vue 2:通过 this.$emit 向父组件传递数据。
  • Vue 3:在向父组件传递数据时,如果使用了自定义事件名称(如 backData),需要在 emits 中定义。

Vue 2.x 的 Diff 算法

Vue 2.x 的 Diff 算法主要基于 Snabbdom,它是一种高效的虚拟 DOM 比对库。Vue 2 的 Diff 算法的核心步骤如下:

同级比较:

仅在同一层级比较节点,不跨层级。

双端比较:

  • 通过四个指针(旧前、旧后、新前、新后)进行对比:

    • 旧前 vs 新前
    • 旧后 vs 新后
    • 旧前 vs 新后
    • 旧后 vs 新前

Key 的作用:

通过 key 识别节点,优化复用

缺点

  • 全量比较:即使只有部分变化,也会重新遍历整个 VNode 树。
  • 无法充分利用静态节点优化:静态节点也会被重新创建和比较

Vue 3.x 的 Diff 算法

静态标记与 PatchFlag

  • 在编译阶段给 VNode 添加 PatchFlag,用于标记节点的更新类型。
  • 静态节点在编译时被标记,运行时直接复用,无需重新创建与对比。

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

  • 通过 getSequence 函数(源码中使用二分法优化)生成 LIS 索引数组。
  • 保持顺序的节点(LIS 中的元素)无需移动。
  • 非 LIS 元素按序插入对应的正确位置,最小化 DOM 操作次数。

Block Tree

  • 将动态节点组织为树结构,减少比较范围。

性能优化

  • 时间复杂度从 O(n^2) 降低到接近 O(n)。
  • 更智能的节点移动处理,显著减少 DOM 操作。

优点

  • 更高效,减少不必要的比较。
  • 更好的静态节点优化。
  • 更智能的节点移动处理
flowchart TD
    A[新旧子节点数组] --> B{头头对比}
    B --相同--> C[指针右移]
    B --不同--> D{尾尾对比}
    D --相同--> E[指针左移]
    D --不同--> F{新头旧尾}
    F --相同--> G[移动节点]
    F --不同--> H{旧头新尾}
    H --相同--> I[移动节点]
    H --不同--> J[遍历剩余节点]
    J --> K[生成最长递增子序列]
    K --> L[最小移动操作]
 
 
// 核心 Diff 函数(伪代码)
function diff(oldChildren, newChildren, parentEl) {
  let oldStartIdx = 0, oldEndIdx = oldChildren.length - 1
  let newStartIdx = 0, newEndIdx = newChildren.length - 1
  let oldStartVNode = oldChildren, oldEndVNode = oldChildren[oldEndIdx]
  let newStartVNode = newChildren, newEndVNode = newChildren[newEndIdx]
 
  // 1. 双端对比
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isSameVNode(oldStartVNode, newStartVNode)) {
      // 头头相同:复用节点
      patchVNode(oldStartVNode, newStartVNode)
      oldStartVNode = oldChildren[++oldStartIdx]
      newStartVNode = newChildren[++newStartIdx]
    } else if (isSameVNode(oldEndVNode, newEndVNode)) {
      // 尾尾相同:复用节点
      patchVNode(oldEndVNode, newEndVNode)
      oldEndVNode = oldChildren[--oldEndIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else if (isSameVNode(oldStartVNode, newEndVNode)) {
      // 旧头与新尾相同:移动节点到末尾
      parentEl.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling)
      patchVNode(oldStartVNode, newEndVNode)
      oldStartVNode = oldChildren[++oldStartIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else if (isSameVNode(oldEndVNode, newStartVNode)) {
      // 旧尾与新头相同:移动节点到头部
      parentEl.insertBefore(oldEndVNode.el, oldStartVNode.el)
      patchVNode(oldEndVNode, newStartVNode)
      oldEndVNode = oldChildren[--oldEndIdx]
      newStartVNode = newChildren[++newStartIdx]
    } else {
      // 进入复杂 Diff 逻辑(最长递增子序列优化)
      break
    }
  }
 
  // 2. 处理剩余节点
  if (oldStartIdx > oldEndIdx) {
    // 新增节点
    addNewNodes(newChildren, newStartIdx, newEndIdx, parentEl)
  } else if (newStartIdx > newEndIdx) {
    // 删除旧节点
    removeOldNodes(oldChildren, oldStartIdx, oldEndIdx, parentEl)
  } else {
    // 3. 最长递增子序列优化(核心优化点)
    const newIndexMap = createKeyToNewIndexMap(newChildren, newStartIdx, newEndIdx)
    const lis = getLongestIncreasingSubsequence(newIndexMap)
    let lisIndex = 0
    
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
      const oldVNode = oldChildren[i]
      if (oldVNode === null) continue
      
      const newIndex = newIndexMap.get(oldVNode.key)
      if (newIndex === undefined) {
        // 删除不存在节点
        parentEl.removeChild(oldVNode.el)
      } else {
        if (lis[lisIndex] === newIndex) {
          // 位置无需移动
          lisIndex++
        } else {
          // 移动节点到正确位置
          const anchor = newChildren[newIndex - 1].el.nextSibling
          parentEl.insertBefore(oldVNode.el, anchor)
        }
        // 递归更新子节点
        patchVNode(oldVNode, newChildren[newIndex])
      }
    }
  }
}
 
// 判断是否为相同 VNode
function isSameVNode(a, b) {
  return a.key === b.key && a.tag === b.tag
}
 
// 生成最长递增子序列(贪心 + 二分)
function getLongestIncreasingSubsequence(arr) {
  // ... 具体实现省略(Vue 3 使用 O(n log n) 算法)
}
 
 
优化项Vue 2.xVue 3.x
时间复杂度O(n^2)接近 O(n)
DOM 移动次数较多显著减少
静态处理静态提升跳过 Diff
动态列表性能较差优异

Vue3虚拟Dom

虚拟 DOM 是真实 DOM 的轻量级 JavaScript 对象抽象,通过 { tag, props, children } 结构描述 DOM 节点‌。

其核心价值在于减少直接操作真实 DOM 的性能损耗,通过 Diff 算法计算最小更新范围,实现高效渲染‌

VNode 结构

  • tag:标签名或组件类型
  • props:属性和事件(如 class、id、onClick)
  • children:子节点(文本或嵌套 VNode 数组)
const vnode = {
  tag: 'div',
  props: { class: 'container' },
  children: [{ tag: 'h1', children: '标题' }]
}
 

虚拟 DOM 的工作流程

‌- 初始化阶段‌

  • ‌模板编译‌:将模板转换为 render() 函数,生成初始 VNode 树‌18。

  • ‌挂载‌:通过 mount() 将 VNode 转换为真实 DOM 并渲染到页面‌8。

  • ‌响应式更新阶段‌

    • 数据变化触发 render() 重新执行,生成新 VNode 树‌16。
    • ‌Diff 算法‌:对比新旧 VNode 树,识别差异节点(Vue 3 采用‌双端 Diff 优化‌,减少比对次数)‌14。
  • ‌Patch 阶段‌

    • ‌靶向更新‌:根据 Diff 结果,仅更新变化的真实 DOM 节点(通过 PatchFlags 标记动态属性,如 TEXT、CLASS)‌46。
    • ‌异步队列‌:批量处理 DOM 更新,避免频繁重排重绘‌

Vue 3 的优化策略

  • ‌静态提升(Static Hoisting)‌

    • 将静态节点(无动态绑定的内容)提升到渲染函数外,避免重复生成‌18。
    • 示例:模板中的固定标题 <h1>Hello</h1> 仅生成一次 VNode‌8。
  • ‌靶向更新(Patch Flags)‌

    • 动态标记(如 TEXT=1、CLASS=2)精准定位需更新的属性,跳过无变化部分‌36。
    • 对比 Vue 2 的全量 Diff,性能提升 30%-50%‌4。
  • ‌惰性挂载与 Tree Shaking‌

    • 按需生成组件 VNode,减少内存占用‌1。
    • 配合编译时优化,剔除未使用的代码‌

简化实现

// 1. 定义 VNode 结构
class VNode {
  constructor(tag, props, children) {
    this.tag = tag          // 标签名(如 'div')
    this.props = props || {}// 属性对象(如 { class: 'box' })
    this.children = children||[] // 子节点数组
  }
}
 
// 2. 创建虚拟DOM
function createElement(tag, props, children) {
  return new VNode(tag, props, children)
}
 
// 3. 将虚拟DOM渲染为真实DOM
function render(vnode) {
  if (typeof vnode === 'string') {
    return document.createTextNode(vnode) // 文本节点
  }
 
  const el = document.createElement(vnode.tag)
  
  // 添加属性
  for (const key in vnode.props) {
    el.setAttribute(key, vnode.props[key])
  }
 
  // 递归渲染子节点
  vnode.children.forEach(child => {
    el.appendChild(render(child))
  })
 
  return el
}
 
// 4. Diff算法核心(简化版)
function diff(oldVNode, newVNode) {
  // 类型不同直接替换
  if (oldVNode.tag !== newVNode.tag) {
    return { type: 'REPLACE', node: newVNode }
  }
 
  // 属性变化
  const propsPatches = {}
  const oldProps = oldVNode.props
  const newProps = newVNode.props
  
  // 检测新增/修改属性
  for (const key in newProps) {
    if (oldProps[key] !== newProps[key]) {
      propsPatches[key] = newProps[key]
    }
  }
 
  // 检测删除属性
  for (const key in oldProps) {
    if (!(key in newProps)) {
      propsPatches[key] = undefined
    }
  }
 
  // 子节点Diff(仅示例层级比较)
  const childPatches = []
  const len = Math.max(oldVNode.children.length, newVNode.children.length)
  
  for (let i = 0; i < len; i++) {
    childPatches.push(diff(oldVNode.children[i], newVNode.children[i]))
  }
 
  return {
    type: 'UPDATE',
    props: propsPatches,
    children: childPatches
  }
}
 
// 5. 应用补丁更新DOM
function patch(parent, patches, index = 0) {
  if (!patches) return
 
  const el = parent.childNodes[index]
  
  switch (patches.type) {
    case 'REPLACE':
      const newNode = render(patches.node)
      parent.replaceChild(newNode, el)
      break
    case 'UPDATE':
      // 更新属性
      for (const key in patches.props) {
        if (patches.props[key] === undefined) {
          el.removeAttribute(key)
        } else {
          el.setAttribute(key, patches.props[key])
        }
      }
      // 更新子节点
      patches.children.forEach((childPatch, i) => {
        patch(el, childPatch, i)
      })
      break
  }
}
 
// ------------------- 使用示例 -------------------
// 初始化虚拟DOM
const oldVTree = createElement('div', { class: 'box' }, [
  createElement('h1', {}, ['Title']),
  createElement('p', { id: 'content' }, ['Hello World'])
])
 
// 创建新虚拟DOM
const newVTree = createElement('div', { class: 'box active' }, [
  createElement('h1', {}, ['New Title']),
  createElement('p', { id: 'content' }, ['Updated Content'])
])
 
// 首次渲染
const app = document.getElementById('app')
const realDOM = render(oldVTree)
app.appendChild(realDOM)
 
// 计算差异并更新
setTimeout(() => {
  const patches = diff(oldVTree, newVTree)
  patch(app, patches) // 执行DOM更新
}, 2000)
 
 

Vue3响应式原理

reactive reactive 是 Vue 3 中创建响应式对象的核心函数。它的作用是将普通对象转换为响应式对象。

function reactive(target) {
  // 如果不是对象,直接返回
  if (!isObj(target)) {
    return target;
  }
 
  // 创建拦截器 handler
  const baseHandlers = {
    get,
    set,
    deleteProperty
  };
 
  // 使用 Proxy 包装对象
  return new Proxy(target, baseHandlers);
}
 
function isObj(target) {
  return target !== null && typeof target === 'object';
}

拦截器 handler 的实现:

  • get:拦截属性的访问操作。
    • 收集依赖(track)。
    • 如果当前属性的值是对象,则递归创建 Proxy。
  • set:拦截属性的赋值操作。
    • 如果新值与旧值不同,则更新值并触发更新(trigger)。
  • deleteProperty:拦截属性的删除操作。
    • 如果对象中存在该属性,则删除并触发更新(trigger)。
import { reactive } from 'vue';
 
const state = reactive({
  user: {
    name: 'Alice',
    age: 30
  },
  items: ['apple', 'banana']
});
 
state.user.age = 31; // 响应式更新
state.items.push('orange'); // 响应式更新

特性:

  • 深度响应式:对象中的嵌套对象、数组、引用类型都会被递归转换为响应式。
  • 自动依赖追踪:当响应式对象中的属性被读取时,Vue 会自动追踪依赖;当这些属性发生变化时,依赖的视图或计算属性会自动更新。

reactive 在实际开发中非常常用,适合需要深度响应的场景。

effect

effect 用于定义一个响应式副作用函数。当访问响应式对象的属性时,会自动收集依赖。

 
let activeEffect = null;
 
function effect(fn) {
  const _effect = function() {
    activeEffect = _effect;
    fn();
    activeEffect = null;
  };
  _effect();
}
 
  • activeEffect 是一个全局变量,用于标识当前正在执行的副作用函数。
  • 当 effect 被调用时,会将传入的函数包装为一个副作用函数,并立即执行。

track

track 用于收集依赖,将当前的 activeEffect 与响应式对象的某个属性关联起来。

const targetMap = new WeakMap();
 
function track(target, key) {
  if (!activeEffect) return;
 
  // 获取 target 对应的依赖映射
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
 
  // 获取 key 对应的依赖集合
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }
 
  // 将当前的 effect 添加到依赖集合中
  dep.add(activeEffect);
}
 
  • 使用 WeakMap 存储响应式对象与依赖映射的关系。
  • 每个响应式对象的属性对应一个 Set,存储依赖的 effect。

trigger

trigger 用于触发依赖的更新,当响应式对象的属性发生变化时,会通知所有相关的 effect 重新执行。

function trigger(target, key) {
  // 获取 target 对应的依赖映射
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
 
  // 获取 key 对应的依赖集合
  const dep = depsMap.get(key);
 
  // 触发所有依赖的 effect
  dep && dep.forEach(effect => {
    effect();
  });
}

当属性值发生变化时,trigger 会找到所有与该属性相关的 effect,并重新执行它们。

Vue Compiler实现原理

Parse(解析)

  • 输入:模板字符串(template)。
  • 输出:抽象语法树(AST)。
  • 作用:将模板字符串解析为 AST,表示模板的语法结构。
  • 实现:使用正则表达式或其他解析技术,将模板分解为节点(如元素、文本、指令等)。

Optimize(优化)

  • 输入:AST。
  • 输出:优化后的 AST。
  • 作用:遍历 AST,标记静态节点,减少渲染时的 diff 开销。
  • 实现:标记不会因数据变化而变化的节点为静态节点,优化性能。

Generate(生成)

  • 输入:优化后的 AST。
  • 输出:渲染函数(render)。
  • 作用:将优化后的 AST 转换为渲染函数的字符串表示,通过 new Function 转换为可执行函数。
  • 实现:生成的渲染函数返回一个 VNode,用于生成虚拟 DOM。

watch 与 computed 的区别

computed 函数

创建计算属性,基于其他响应式数据计算派生值。计算属性具有缓存特性,只在依赖的数据发生变化时重新计算。

import { ref, computed } from 'vue';
 
const count = ref(1);
const doubleCount = computed(() => count.value * 2);
 
console.log(doubleCount.value); // 2
 
count.value = 2;
console.log(doubleCount.value); // 4
 

特性:

  • 依赖追踪:自动追踪依赖的响应式数据,只在依赖数据变化时重新计算。
  • 缓存机制:具有缓存特性,避免重复计算,提高性能。
  • 只读:默认情况下,计算属性是只读的。
  • 可读写:可以通过传入 getter 和 setter 创建可读写的计算属性。

可写的 computed:

const count = ref(1);
 
const doubleCount = computed({
  get: () => count.value * 2,
  set: (val) => {
    count.value = val / 2;
  }
});
 
doubleCount.value = 10; // 通过 setter 修改原始数据
console.log(count.value); // 5
 

watch 函数

用于监听一个或多个响应式数据的变化,并在数据变化时执行特定的副作用。

import { ref, watch } from 'vue';
 
const count = ref(0);
 
watch(count, (newValue, oldValue) => {
  console.log(`Count changed from ${oldValue} to ${newValue}`);
});
 
count.value = 1; // 打印:Count changed from 0 to 1
 

特性:

  • 灵活性:允许执行副作用,如异步操作、API 调用等。
  • 监听多个数据:可以同时监听多个响应式数据。
  • 深度监听:通过 { deep: true } 选项实现深度监听,监听嵌套对象的属性变化。
  • 立即执行:通过 { immediate: true } 选项在监听器创建时立即执行回调函数。

深度监听和立即执行:

import { reactive, watch } from 'vue';
 
const state = reactive({
  user: { name: 'Alice', age: 30 }
});
 
watch(
  () => state.user,
  (newValue, oldValue) => {
    console.log('User data changed:', newValue);
  },
  { deep: true, immediate: true }
);
 

监听多个值:

const firstName = ref('John');
const lastName = ref('Doe');
 
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`Name changed from ${oldFirst} ${oldLast} to ${newFirst} ${newLast}`);
});
 
firstName.value = 'Jane'; // 打印:Name changed from John Doe to Jane Doe
 
特性computedwatch
用途定义派生数据,基于响应式数据计算结果。监听响应式数据的变化并执行副作用。
缓存具有缓存特性,只在依赖数据变化时重新计算。不缓存结果,每次数据变化都会执行回调函数。
返回值返回计算结果,通常用于模板或计算逻辑。不返回值,主要用于执行副作用(如 API 调用、状态更新等)。
只读默认只读,但可以通过 getter 和 setter 创建可读写的计算属性。不涉及只读性,专注于监听和响应数据变化。
灵活性适用于简单的计算逻辑。适用于复杂的副作用逻辑,如异步操作、手动 DOM 操作等。
监听多个数据不直接支持,但可以通过组合计算属性实现。支持监听多个响应式数据。
深度监听不支持,计算属性的依赖是自动追踪的。支持通过 { deep: true } 实现深度监听。
立即执行不支持,计算属性在首次访问时计算。支持通过 { immediate: true } 在监听器创建时立即执行回调。

使用场景

  • computed:
    • 用于计算派生数据,如表单验证、复杂数据的动态计算。
    • 当需要基于其他响应式数据计算一个新的值,并希望这个值具有缓存特性时。
  • watch:
    • 用于处理数据变化的副作用,如保存数据到本地存储、发起 API 请求。
    • 当需要在数据变化时执行异步操作或复杂的逻辑时。

watch 和 watchEffect

watch

参考上文

watchEffect

一种自动依赖追踪的监听器,Vue 会在副作用函数执行时自动追踪所有依赖的数据,当这些依赖发生变化时,副作用会重新执行。

特性:

  • 自动依赖追踪:无须显式声明依赖,Vue 会自动追踪副作用函数中使用到的响应式数据。
  • 立即执行:在创建时立即执行副作用,并在依赖的任何响应式数据发生变化时重新执行。
  • 无法获取旧值:不提供新旧值的比较,因为副作用会自动重新运行。
  • 适用于简单的副作用:通常用于处理简单的副作用逻辑,如 DOM 操作或响应式数据的快速同步。
import { ref, watchEffect } from 'vue';
 
const count = ref(0);
 
watchEffect(() => {
  console.log(`Count is now: ${count.value}`);
});
 
count.value = 1; // 打印:Count is now: 1
count.value = 2; // 打印:Count is now: 2
 
特性watchwatchEffect
依赖声明方式显式声明要监听的响应式数据。自动追踪副作用函数中使用到的响应式数据,无需显式声明。
副作用执行时机回调函数只在指定的响应式数据发生变化时执行,可以通过配置控制是否立即执行。在副作用创建时立即执行,并在依赖的任何响应式数据发生变化时重新执行。
回调函数参数提供新值和旧值,方便进行比较或逻辑处理。不提供新值和旧值,主要侧重于自动重新执行副作用。
适用场景适合用于监听具体响应式数据的变化,并执行复杂的逻辑或副作用。适合用于自动追踪响应式依赖、无需手动声明依赖的场景。

使用场景

  • watch:适合精确控制依赖监听的场景,尤其是需要进行异步操作或新旧值对比时。
  • watchEffect:适合简化处理副作用的场景,尤其是在需要自动追踪多个响应式数据依赖时,无需明确声明。

vue 修饰符都有哪些

事件修饰符 事件修饰符用于修改事件的行为,例如阻止默认行为或停止事件冒泡。

  • .stop:调用 event.stopPropagation(),阻止事件冒泡。<button @click.stop="doThis">Click me</button>
  • .prevent:调用 event.preventDefault(),阻止默认行为。<form @submit.prevent="onSubmit">Submit</form>
  • .capture:在事件冒泡的捕获阶段触发事件处理器。<div @click.capture="doThis">Click me</div>
  • .self:只有当事件是从该元素本身触发时才触发事件处理器。<div @click.self="doThis">Click me</div>
  • .once:事件只触发一次。<button @click.once="doThis">Click me</button>
  • .passive:以被动模式添加事件监听器,提高滚动性能。<div @scroll.passive="onScroll">Scroll me</div>

按键修饰符 按键修饰符用于监听特定的按键事件。

  • .enter:监听 Enter 键。<input @keyup.enter="onEnter" />
  • .tab:监听 Tab 键。<input @keyup.tab="onTab" />
  • .ctrl:监听 Ctrl 键。<input @keyup.ctrl.enter="onCtrlEnter" />
  • .shift:监听 Shift 键。<input @keyup.shift.enter="onShiftEnter" />
  • .alt:监听 Alt 键。<input @keyup.alt.enter="onAltEnter" />
  • .meta:监听 Meta 键(Mac 上的 Command 键)。<input @keyup.meta.enter="onMetaEnter" />

表单修饰符

表单修饰符用于修改表单输入的行为,例如自动去除输入值的空白字符。

  • .lazy:将 v-model 的更新时机改为 change 事件。<input v-model.lazy="message" />
  • .number:将输入值自动转换为数字。<input v-model.number="age" />
  • .trim:自动去除输入值的空白字符。<input v-model.trim="message" />

emit、on、once、off理解

$emit

用途:触发当前实例上的自定义事件,并将附加参数传递给监听器回调。

实现原理:在 Vue 实例的事件系统中,emit 会查找与事件名对应的回调函数列表,并依次调用这些回调,将传入的参数传递给它们。 `this.emit('my-event', data);`

$on 用途:监听实例上的自定义事件,并在事件触发时调用回调函数。

实现原理:$on 会将回调函数添加到与事件名对应的回调列表中。当事件被触发时,这些回调会被依次执行。

this.$on('my-event', function(data) {
  console.log(data);
});
 

$once

用途:监听一个自定义事件,但只触发一次。在第一次触发后,监听器会被自动移除。

实现原理:$once 会将回调函数包装在一个只执行一次的函数中,然后将其添加到事件的回调列表。触发后,包装函数会移除自身。

this.$once('my-event', function(data) {
  console.log(data);
});

$off

用途:移除自定义事件的监听器。

  • 不带参数:移除所有事件监听器。
  • 带事件名:移除该事件的所有监听器。
  • 带事件名和回调:只移除指定的回调监听器。

实现原理:$off 会从事件的回调列表中移除对应的回调函数。如果未指定事件或回调,则清空所有回调。

// 移除所有监听器
this.$off();
 
// 移除指定事件的所有监听器
this.$off('my-event');
 
// 移除指定事件的特定回调
this.$off('my-event', callback);
 

Vue的Slot

在子组件中使用 <slot> 标签定义插槽,父组件中插入的内容会替换子组件中的 <slot> 标签。

子组件:

<template>
  <div class="child-component">
    <h2>子组件标题</h2>
    <slot></slot>
  </div>
</template>

父组件:

<template>
  <div>
    <child-component>
      <p>这是父组件插入的内容,会替换子组件的 <slot> 标签。</p>
    </child-component>
  </div>
</template>
 
<script>
import ChildComponent from './ChildComponent.vue';
 
export default {
  components: {
    ChildComponent
  }
};
</script>

具名插槽

当子组件中有多个插槽时,可以通过 name 属性为插槽命名。父组件中使用 slot 属性指定内容插入到哪个插槽。

子组件:

<template>
  <div class="child-component">
    <h2>子组件标题</h2>
    <slot name="header"></slot>
    <slot></slot>
    <slot name="footer"></slot>
  </div>
</template>
 

父组件:

<template>
  <div>
    <child-component>
      <template v-slot:header>
        <h3>这是插入到 header 插槽的内容</h3>
      </template>
      <p>这是插入到默认插槽的内容。</p>
      <template v-slot:footer>
        <p>这是插入到 footer 插槽的内容</p>
      </template>
    </child-component>
  </div>
</template>
 

v-slot作用域插槽

作用域插槽允许父组件访问子组件的局部数据,通过 slot-scope 或 v-slot 语法实现。

原理

  • 编译阶段:在编译阶段,v-slot 会将插槽内容转换为一个渲染函数,存储在父组件的 scopedSlots 对象中。
  • 运行时:在子组件渲染时,v-slot 的渲染函数会被调用,生成虚拟 DOM 节点(VNode),并绑定相应的插槽作用域数据。
  • 响应式更新:当插槽作用域中的数据发生变化时,Vue 的响应式系统会触发 VNode 的更新,重新渲染插槽内容

子组件:

<template>
  <div class="child-component">
    <h2>子组件标题</h2>
    <slot name="items" :items="items"></slot>
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      items: ['Item 1', 'Item 2', 'Item 3']
    };
  }
};
</script>
 

父组件:

<template>
  <div>
    <child-component>
      <template v-slot:items="slotProps">
        <ul>
          <li v-for="item in slotProps.items" :key="item">{{ item }}</li>
        </ul>
      </template>
    </child-component>
  </div>
</template>
 

自定义指令

全局指令 全局指令通过 Vue.directive 方法定义,作用于所有组件。

Vue.directive('my-directive', {
  bind(el, binding, vnode) {
    // 初始化时调用
  },
  inserted(el, binding, vnode) {
    // 元素插入父级时调用
  },
  update(el, binding, vnode, oldVnode) {
    // 元素更新时调用
  },
  componentUpdated(el, binding, vnode, oldVnode) {
    // 元素更新完成后调用
  },
  unbind(el, binding, vnode) {
    // 元素所在的模板删除时调用
  }
});

简写方式: 如果只需要一个钩子函数,可以直接简写为一个函数:

Vue.directive('focus', function(el, binding) {
  // 自定义逻辑
});

使用:

<template>
  <input v-focus>
</template>

组件指令

组件指令在组件的 directives 选项中定义,仅在该组件中有效。

export default {
  directives: {
    'my-directive': {
      bind(el, binding, vnode) {
        // 初始化时调用
      },
      inserted(el, binding, vnode) {
        // 元素插入父级时调用
      },
      update(el, binding, vnode, oldVnode) {
        // 元素更新时调用
      },
      componentUpdated(el, binding, vnode, oldVnode) {
        // 元素更新完成后调用
      },
      unbind(el, binding, vnode) {
        // 元素所在的模板删除时调用
      }
    }
  }
};
 

钩子函数

自定义指令的钩子函数及其调用时机:

  • bind:指令第一次绑定到元素时调用,只调用一次。
  • inserted:元素插入父级时调用。
  • update:元素更新时调用。
  • componentUpdated:元素更新完成后调用。
  • unbind:元素所在的模板删除时调用。

钩子函数参数

钩子函数接收以下参数:

  • el:指令绑定的 DOM 元素。
  • binding:一个对象,包含以下属性:
    • name:指令名称(不包括 v-)。
    • value:指令的绑定值。
    • oldValue:指令的旧值(仅在 update 和 componentUpdated 中有效)。
    • expression:指令绑定的原始表达式。
    • arg:指令的参数。
    • modifiers:指令的修饰符(如 v-focus.show.async 的修饰符为 { show: true, async: true })。
  • vnode:Vue 编译生成的虚拟 DOM。
  • oldVnode:上一个虚拟 DOM(仅在 update 和 componentUpdated 中有效)。

Vue 性能优化

编码阶段

  • 减少 data 中的数据:data 中的数据会增加 getter 和 setter,并收集对应的 watcher,尽量减少不必要的数据。
  • 避免 v-if 和 v-for 连用:v-if 和 v-for 连用会导致性能问题,尽量避免。
  • 使用事件代理:如果需要使用 v-for 给每项元素绑定事件,使用事件代理可以减少事件监听器的数量。
  • 使用 keep-alive 缓存组件:对于 SPA 页面,使用 keep-alive 缓存组件可以减少组件的重复渲染。
  • 使用 v-if 替代 v-show:在更多的情况下,使用 v-if 替代 v-show,因为 v-if 不会渲染不需要的 DOM 元素。
  • 保证 key 的唯一性:在使用 v-for 时,确保 key 的唯一性,避免 Vue 误判 DOM 的变化。
  • 使用路由懒加载和异步组件:通过路由懒加载和异步组件,按需加载资源,减少初始加载时间。
  • 防抖和节流:对频繁触发的事件(如输入、滚动等)使用防抖和节流技术,减少不必要的计算。
  • 按需导入第三方模块:使用按需导入的方式加载第三方模块,减少打包体积。
  • 长列表滚动到可视区域动态加载:对于长列表,只加载可视区域的数据,减少内存占用。
  • 图片懒加载:对图片资源使用懒加载技术,减少初始加载时间。
  • SEO 优化:使用预渲染或服务端渲染(SSR)提升 SEO 效果。

打包优化

  • 压缩代码:通过 Webpack 等工具压缩代码,减少打包体积。
  • Tree Shaking/Scope Hoisting:利用 Tree Shaking 和 Scope Hoisting 技术去除未使用的代码。
  • 使用 CDN 加载第三方模块:通过 CDN 加载第三方模块,减少服务器负载。
  • 多线程打包:使用 HappyPack 等工具实现多线程打包,加快打包速度。
  • splitChunks 抽离公共文件:通过 splitChunks 抽离公共文件,减少重复代码。
  • sourceMap 优化:合理配置 sourceMap,在开发和生产环境中选择合适的配置。

用户体验优化

  • 骨架屏:在页面加载时显示骨架屏,提升用户体验。
  • PWA:使用渐进式 Web 应用(PWA)技术,提升应用的离线体验。
  • 缓存优化:使用客户端缓存和服务端缓存,减少重复请求。
  • 服务端开启 Gzip 压缩:通过服务端开启 Gzip 压缩,减少传输数据量。

Vue组件懒加载

组件懒加载是指在 Vue 应用中,通过动态导入的方式按需加载组件。这种方式不仅限于路由组件,可以应用于任何需要延迟加载的组件。

Vue 异步组件

Vue 提供了异步组件的功能,可以实现按需加载组件。每个组件会生成一个单独的 JS 文件。

{
  path: '/home',
  name: 'home',
  component: resolve => require(['@/components/home'], resolve)
},
{
  path: '/index',
  name: 'index',
  component: resolve => require(['@/components/index'], resolve)
},
{
  path: '/about',
  name: 'about',
  component: resolve => require(['@/components/about'], resolve)
}
 

ES6 的 import()

使用 ES6 的 import() 语法实现懒加载。可以指定 webpackChunkName 来控制打包文件的名称,多个组件可以合并打包成一个文件。

// 每个组件生成一个单独的 JS 文件
const Home = () => import('@/components/home');
const Index = () => import('@/components/index');
const About = () => import('@/components/about');
 
// 指定相同的 `webpackChunkName`,合并打包成一个 JS 文件
const Home = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/home');
const Index = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/index');
const About = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/about');
 
{
  path: '/home',
  component: Home
},
{
  path: '/index',
  component: Index
},
{
  path: '/about',
  component: About
}
 

Webpack 的 require.ensure()

使用 Webpack 的 require.ensure() 方法实现懒加载。通过指定 chunkName,可以将多个路由打包成一个文件。

{
  path: '/home',
  name: 'home',
  component: r => require.ensure([], () => r(require('@/components/home')), 'demo')
},
{
  path: '/index',
  name: 'index',
  component: r => require.ensure([], () => r(require('@/components/index')), 'demo')
},
{
  path: '/about',
  name: 'about',
  component: r => require.ensure([], () => r(require('@/components/about')), 'demo-01')
}
 

Vue路由懒加载

路由懒加载是指在 Vue Router 中,通过动态导入(import())的方式按需加载路由组件。

这样可以减少初始加载时的资源消耗,只在用户访问某个路由时才加载对应的组件。

  • 按需加载:只有当用户访问某个路由时,对应的组件才会被加载。
  • 优化性能:减少初始加载时间,提高应用性能。
  • 代码分割:Webpack 会将每个动态导入的组件单独打包。

创建路由配置

使用 Webpack 的动态导入特性来定义路由组件。

// router.js
import Vue from 'vue';
import VueRouter from 'vue-router';
 
Vue.use(VueRouter);
 
const router = new VueRouter({
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import('./components/Home.vue'), // 懒加载 Home 组件
    },
    {
      path: '/about',
      name: 'about',
      component: () => import('./components/About.vue'), // 懒加载 About 组件
    },
    {
      path: '/contact',
      name: 'contact',
      component: () => import('./components/Contact.vue'), // 懒加载 Contact 组件
    },
  ],
});
 
export default router;
 

在 Vue 应用中使用路由

在 Vue 应用的入口文件中,引入并使用路由配置。

// main.js
import Vue from 'vue';
import App from './App.vue';
import router from './router';
 
Vue.config.productionTip = false;
 
new Vue({
  router,
  render: (h) => h(App),
}).$mount('#app');
``
 
**创建组件**
 
创建需要懒加载的组件。
 
Home.vue
 
```vue 
<template>
  <div>
    <h1>首页</h1>
    <p>这是首页内容。</p>
  </div>
</template>
 
<script>
export default {
  name: 'Home',
  created() {
    console.log('Home 组件已加载');
  },
};
</script>

About.vue

<template>
  <div>
    <h1>关于我们</h1>
    <p>这是关于我们页面的内容。</p>
  </div>
</template>
 
<script>
export default {
  name: 'About',
  created() {
    console.log('About 组件已加载');
  },
};
</script>

Contact.vue

<template>
  <div>
    <h1>联系我们</h1>
    <p>这是联系我们页面的内容。</p>
  </div>
</template>
 
<script>
export default {
  name: 'Contact',
  created() {
    console.log('Contact 组件已加载');
  },
};
</script>
 

创建导航链接

在主应用组件中,创建导航链接以便用户可以访问不同的页面。

<template>
  <div>
    <nav>
      <router-link to="/">首页</router-link> |
      <router-link to="/about">关于我们</router-link> |
      <router-link to="/contact">联系我们</router-link>
    </nav>
    <router-view></router-view>
  </div>
</template>
 
<script>
export default {
  name: 'App',
};
</script>
 

工作原理

  • 动态导入:使用 () => import('path/to/component') 语法动态导入组件。Webpack 会将这些动态导入的组件单独打包,按需加载。
  • 路由配置:在路由配置中,将动态导入的组件分配给对应的路径。
  • 按需加载:当用户访问某个路由时,对应的组件才会被加载,从而减少初始加载时间。

注意事项

  • 性能优化:懒加载可以显著提高应用的初始加载性能,特别是在路由较多时。
  • 代码分割:Webpack 会自动将动态导入的组件进行代码分割,确保每个组件单独打包。
  • 加载状态:可以添加加载状态或错误处理逻辑,以提高用户体验。

Vue首页白屏解决

路由懒加载

使用 Vue Router 的懒加载功能,按需加载组件,减少首屏加载的资源量。

const routes = [
  {
    path: '/home',
    component: () => import(/* webpackChunkName: "home" */ './views/Home.vue')
  },
  {
    path: '/about',
    component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
  }
];

优化打包体积

通过 Webpack 的 splitChunks 和 TerserPlugin 来优化打包体积,减少首屏加载的文件大小。

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    },
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,
          },
        },
      }),
    ],
  },
};
 

使用骨架屏

在页面加载时显示骨架屏,提升用户体验,减少白屏时间。

<template>
  <div>
    <div v-if="loading" class="skeleton">
      <!-- 骨架屏内容 -->
    </div>
    <router-view v-else></router-view>
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      loading: true
    };
  },
  mounted() {
    setTimeout(() => {
      this.loading = false;
    }, 1000);
  }
};
</script>
 
<style>
.skeleton {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: #f5f5f5;
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>
 

使用 CDN 加速

将静态资源(如 Vue、Vuex 等)托管到 CDN 上,减少主包体积,提高加载速度。

服务端渲染(SSR)

使用 Vue 的服务端渲染技术,提前在服务器端生成 HTML,减少首屏渲染时间。

移除 console.log

在生产环境中删除掉不必要的 console.log。console.log 会占用一定的系统资源,虽然单次调用的影响很小,但如果频繁调用,会对性能产生一定的负面影响。

特别是在生产环境中,console.log 可能会导致页面加载变慢,影响用户体验。

// vue.config.js
const TerserPlugin = require('terser-webpack-plugin');
 
module.exports = {
  configureWebpack: {
    optimization: {
      minimize: true,
      minimizer: [
        new TerserPlugin({
          terserOptions: {
            compress: {
              drop_console: true, // 移除 console.log
              drop_debugger: true // 移除 debugger
            }
          }
        })
      ]
    }
  }
};
 

启用 GZIP 压缩

在 Nginx 配置文件中启用 GZIP 压缩,减少传输数据量。

http {
  gzip on;
  gzip_disable "msie6";
  gzip_vary on;
  gzip_proxied any;
  gzip_comp_level 8;
  gzip_buffers 16 8k;
  gzip_min_length 100;
  gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
}
 

Vue SPA 首屏速度优化

请求优化

  • CDN:将第三方库和静态资源部署到 CDN,减少项目体积,加速资源加载。
  • CDN 根据用户位置智能选择最近节点,降低延迟。

缓存策略

  • 强缓存:对静态资源设置强缓存,max-age 设为较长值,路径加哈希确保更新后获取新资源。
  • 减少重复请求,提升加载速度。

压缩与协议

  • Gzip 压缩:开启 Gzip 压缩,减小传输资源体积。
  • HTTP/2:利用 HTTP/2 多路复用,突破浏览器 TCP 连接限制,提升加载效率。

按需加载

  • 路由懒加载:按需加载路由组件,减少首屏代码量。const Home = () => import('./views/Home.vue');
  • 代码分割:利用 Webpack 的代码分割功能,将非首屏代码异步加载。

预渲染

  • 骨架屏:首屏加载时显示骨架屏,减少白屏时间。
  • 预渲染:使用服务端渲染(SSR)或静态生成,提前生成 HTML 内容。

合理使用第三方库

  • 按需加载:使用按需加载工具(如 babel-plugin-component),仅加载所需组件。
  • 体积分析:使用 webpack-bundle-analyzer 分析打包体积,优化大模块。

封装与架构

  • 公共封装:封装公共组件、插件、过滤器等,减少重复代码,便于维护。
  • 项目架构:合理组织项目结构,提升开发效率和可维护性。

资源优化

  • 图片懒加载:延迟加载非首屏图片,减少初始加载时间。
  • SVG 图标:使用 SVG 替代图片图标,减少 HTTP 请求,提升质量。
  • 图片压缩:使用 image-webpack-loader 压缩图片,减小体积。

Vue 组件中name的好处

  • 递归组件:通过名字找到对应的组件,实现递归组件。
  • 缓存功能:通过 name 实现 <keep-alive> 的缓存功能。
  • 组件识别:通过 name 识别组件,尤其在跨级组件通信时非常重要。
  • 调试工具:Vue Devtools 中显示的组件名称由 name 决定,便于调试。
<template>
  <div>
    <!-- 父组件 -->
    <h1>父组件</h1>
    
    <!-- 使用 keep-alive 缓存子组件 -->
    <keep-alive include="MyComponent">
      <my-component v-if="showComponent"></my-component>
    </keep-alive>
    
    <button @click="toggleComponent">切换组件显示</button>
  </div>
</template>
 
<script>
// 子组件
const MyComponent = {
  name: 'MyComponent', // 定义组件名称
  data() {
    return {
      count: 0,
    };
  },
  template: `
    <div>
      <h2>子组件 ({{ count }})</h2>
      <button @click="count++">计数 +1</button>
    </div>
  `,
};
 
export default {
  components: {
    MyComponent, // 注册子组件
  },
  data() {
    return {
      showComponent: true, // 控制子组件的显示
    };
  },
  methods: {
    toggleComponent() {
      this.showComponent = !this.showComponent;
    },
    findComponentByName() {
      // 通过 name 查找子组件
      const myComponent = this.$children.find(child => child.$options.name === 'MyComponent');
      if (myComponent) {
        console.log('找到子组件:', myComponent);
      } else {
        console.log('未找到子组件');
      }
    },
  },
  mounted() {
    // 在组件挂载后查找子组件
    this.findComponentByName();
  },
};
</script>

理解$root$refs$parent

$root

用途:获取 Vue 的根实例。 使用场景:在简单的项目中,可以将公共数据存储在根实例上,作为全局状态管理的替代方案(类似 Vuex)。

// 在根实例上设置公共数据
this.$root.globalData = 'Hello from root';
 
// 在子组件中访问根实例的数据
console.log(this.$root.globalData); // 输出: Hello from root

$refs

用途:直接访问子组件或 DOM 元素。 使用场景:可以代替事件 emit 和 $on,用于直接操作子组件。

使用方式:

  • 在子组件上使用 ref 属性。
  • 通过 this.$refs 访问子组件实例。

注意事项:

  • $refs 只在组件渲染完成后生效。
  • $refs 不是响应式的,应避免在模板或计算属性中使用。
<template>
  <div>
    <child-component ref="childComponent"></child-component>
    <button @click="callChildMethod">调用子组件方法</button>
  </div>
</template>
 
<script>
import ChildComponent from './ChildComponent.vue';
 
export default {
  components: {
    ChildComponent
  },
  methods: {
    callChildMethod() {
      this.$refs.childComponent.childMethod();
    }
  }
};
</script>
 

$parent

用途:从子组件访问父组件的实例。 使用场景:可以替代通过 props 传递数据的方式,但容易增加调试和理解的难度。

<!-- 父组件 -->
<template>
  <div>
    <child-component></child-component>
  </div>
</template>
 
<script>
import ChildComponent from './ChildComponent.vue';
 
export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      parentData: 'Hello from parent'
    };
  }
};
</script>
 
<!-- 子组件 -->
<template>
  <div>
    <p>{{ parentData }}</p>
  </div>
</template>
 
<script>
export default {
  mounted() {
    this.parentData = this.$parent.parentData;
  }
};
</script>

ref 的作用

ref 是 Vue 中的一个属性,用于给元素或子组件注册引用信息。其主要作用是为父组件提供一种方式,能够直接访问子组件或 DOM 元素。

ref 的特点

  • 在普通 DOM 元素上使用时:引用指向的是 DOM 元素本身。
  • 在子组件上使用时:引用指向的是子组件的实例。

基本用法:获取 DOM 元素

在页面中直接获取 DOM 元素,以便进行操作(如获取元素的尺寸、位置、内容等)。

<template>
  <div ref="myDiv">Hello</div>
</template>
<script>
export default {
  mounted() {
    console.log(this.$refs.myDiv) // 输出 DOM 元素
  },
}
</script>

获取子组件中的 data,通过 ref 引用子组件实例,访问子组件的 data 属性。

<template>
  <ChildComponent ref="child" />
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
  components: { ChildComponent },
  mounted() {
    console.log(this.$refs.child.someData) // 访问子组件的 data
  },
}
</script>

调用子组件中的方法,通过 ref 引用子组件实例,调用子组件的方法。

<template>
  <ChildComponent ref="child" />
  <button @click="callChildMethod">Call Child Method</button>
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
  components: { ChildComponent },
  methods: {
    callChildMethod() {
      this.$refs.child.someMethod() // 调用子组件的方法
    },
  },
}
</script>

composition和options API?

结构与语法

Options API: 使用多个选项对象来组织组件的逻辑,例如 data、methods、computed、watch、mounted 等。每个选项都有特定的功能。

export default {
  data() {
    return {
      count: 0,
    }
  },
  methods: {
    increment() {
      this.count++
    },
  },
  mounted() {
    console.log('Component is mounted')
  },
}

Composition API: 使用 setup 函数,将逻辑组织在一起,不再依赖于各个选项。可以更灵活地将逻辑划分为可复用的函数或组合函数(composables)。

import { ref, onMounted } from 'vue'
 
export default {
  setup() {
    const count = ref(0)
 
    const increment = () => {
      count.value++
    }
 
    onMounted(() => {
      console.log('Component is mounted')
    })
 
    return {
      count,
      increment,
    }
  },
}

逻辑复用与组织

  • Options API:逻辑根据功能分散到不同的选项中,复用逻辑较为复杂,通常需要通过 mixins 或继承来实现,但可能导致逻辑分散、难以维护。
  • Composition API:可以将逻辑封装为独立的函数或 composables,这些函数可以在不同组件中复用。它提供了更灵活的逻辑组织方式,尤其适合复杂的应用程序。

类型支持

  • Options API:对于 TypeScript 的支持相对较弱,类型推断有限,类型安全性较低。
  • Composition API:设计时考虑了 TypeScript,类型推断与类型安全性更好,代码编写过程中更友好。

响应式系统

  • Options API:响应式系统是通过 data 选项来定义响应式数据。
  • Composition API:响应式系统通过 ref、reactive 等更显式的 API 来处理,能够更清晰地控制响应式数据的创建和使用。

代码组织与复用

  • Options API:代码根据功能分散在不同的生命周期钩子和选项中,例如数据初始化放在 data,事件监听放在 mounted,逻辑分离较为明确,但难以组织复杂逻辑。
  • Composition API:代码组织更灵活,所有逻辑可以集中在 setup 函数内。逻辑复用更简单,通过 composables 和自定义函数可以更加模块化和清晰。

Composition API 与 React Hook 的对比

Vue 的 Composition API 在设计思想上借鉴了 React Hook,都旨在提供一种更灵活的方式来组织和复用组件逻辑。

调用限制

  • React Hook:
    • 不能在循环、条件或嵌套函数中调用 Hook,否则会导致 Hook 的调用顺序不一致,从而引发错误。
    • 必须确保总是在 React 函数组件的顶层调用 Hook。
  • Vue Composition API:
    • 可以在循环、条件或嵌套函数中调用 Composition API 提供的函数(如 ref、reactive 等)。
    • 调用顺序不受限制,灵活性更高。

调用时机

  • React Hook:
    • 每次组件重渲染时,Hook 都会被重新调用。
    • 这意味着每次渲染都会执行 useState、useEffect 等 Hook 的逻辑。
  • Vue Composition API:
  • setup 函数在组件实例化时只调用一次。
  • 响应式数据的更新由 Vue 的响应式系统自动处理,无需在每次渲染时重新调用。

性能影响

  • React Hook:
    • 每次重渲染都需要调用 Hook,对垃圾回收(GC)压力较大。
    • 性能相对较低,尤其是在复杂组件中。
  • Vue Composition API:
    • 基于 Vue 的响应式系统,性能优化由 Vue 内部完成。
    • 对 GC 的压力较小,性能相对更高。

依赖管理

  • React Hook:
    • 需要手动确定依赖关系,如 useEffect、useMemo 等必须手动传入依赖。
    • 必须保证依赖顺序正确,否则会导致组件性能下降或逻辑错误。
  • Vue Composition API:
    • 响应式系统自动实现依赖收集,无需手动传入依赖。
    • Vue 内部自动优化依赖关系,减少性能问题。

响应式机制

  • React Hook:
    • 基于 JavaScript 的闭包和调用顺序来管理状态和副作用。
    • 使用 useState、useEffect 等 Hook 来实现响应式逻辑。
  • Vue Composition API:
    • 基于 Vue 的响应式系统,通过 ref、reactive 等 API 实现响应式数据管理。
    • 响应式数据的更新由 Vue 的响应式系统自动处理。

shallowReactive

在 Vue 3 的 Composition API 中,shallowReactive 是一个用于创建浅层响应式对象的函数。

它的主要作用是将对象的 顶层属性 转换为响应式,但不会递归地将嵌套对象的属性转换为响应式。

这种方式可以优化性能,避免不必要的代理开销。

浅层响应式

  • 只有对象的 顶层属性 是响应式的。
  • 嵌套对象的属性不会自动响应式更新。

性能优化

  • 相比 reactive,shallowReactive 的性能开销更小。
  • 适用于嵌套对象无需响应式更新的场景。

适用场景

  • 当你明确知道嵌套对象不需要响应式更新时。
  • 需要优化性能,避免递归代理嵌套对象。
import { shallowReactive } from 'vue';
 
const state = shallowReactive({
  user: {
    name: 'Alice',
    age: 30
  },
  items: ['apple', 'banana']
});
 
// 顶层属性是响应式的
state.items.push('orange'); // 响应式更新,因为 `items` 是顶层属性
 
// 嵌套对象的属性不是响应式的
state.user.age = 31; // 不会响应式更新,因为 `user` 是嵌套对象

使用建议

  • 如果你需要对嵌套对象的属性也进行响应式更新,可以结合 reactive 和 shallowReactive。
  • 如果你明确知道嵌套对象不需要响应式更新,直接使用 shallowReactive 可以避免性能浪费。
const state = shallowReactive({
  user: reactive({ name: 'Alice', age: 30 }), // 嵌套对象使用 reactive
  items: ['apple', 'banana']
});
 
state.user.age = 31; // 响应式更新,因为 `user` 是 reactive 对象
 
特性reactiveshallowReactive
响应式深度深度响应式(嵌套对象也响应式)浅层响应式(仅顶层属性响应式)
性能开销较高(递归代理嵌套对象)较低(仅代理顶层属性)
适用场景需要全局深度跟踪数据变化嵌套对象无需响应式更新,或性能敏感场景

ref shallowRef isRef toRefs函数

ref 函数

创建一个响应式引用,用于处理原始数据类型或单个对象/数组引用。

import { ref } from 'vue';
 
const count = ref(0);
console.log(count.value); // 0
 
count.value++; // 响应式更新
console.log(count.value); // 1
 

特性:

  • 将任何类型的值包装成响应式数据。
  • 访问和修改值时需要通过 .value 属性。
  • 适合处理单个原始值或简单对象的响应式场景。

shallowRef 函数

创建一个浅层响应式引用,仅对象或数组的引用本身是响应式的,内部属性不会自动响应式。

import { shallowRef } from 'vue';
 
const user = shallowRef({
  name: 'Alice',
  age: 30
});
 
user.value.age = 31; // 不会触发响应式更新
user.value = { name: 'Bob', age: 25 }; // 更换整个对象,响应式更新
 

特性:

  • 仅对象或数组的引用是响应式的。
  • 适合需要频繁替换对象引用而不关心内部属性变化的场景。

isRef 函数

检查一个对象是否是由 ref 或 shallowRef 创建的响应式引用。

import { ref, isRef } from 'vue';
 
const count = ref(0);
console.log(isRef(count)); // true
 
const notRef = 42;
console.log(isRef(notRef)); // false
 

toRefs 函数

将响应式对象的每个属性转换为 ref,通常与 reactive 一起使用。

import { reactive, toRefs } from 'vue';
 
const state = reactive({
  name: 'Alice',
  age: 30
});
 
const { name, age } = toRefs(state);
 
name.value = 'Bob'; // 响应式更新
age.value++; // 响应式更新
 
  • 将 reactive 对象的属性转换为 ref,保留响应式特性。
  • 适用于解构对象时保持响应式。

readonly isReadonly shallowReadonly

readonly

将一个对象转换为深度只读的响应式对象,所有嵌套属性都不可修改。

import { reactive, readonly } from 'vue';
 
const state = reactive({ name: 'Alice', age: 30 });
const readonlyState = readonly(state);
 
readonlyState.age = 31; // 无效操作,Vue 会发出警告
console.log(readonlyState.age); // 仍为 30
 
  • 深度只读:递归地将对象及其所有嵌套属性转换为只读状态。
  • 保护数据:防止无意中修改数据。
  • 开发环境警告:在开发环境中修改只读对象会发出警告。

shallowReadonly

将对象的顶层属性设置为只读,嵌套对象的属性仍可修改。

import { shallowReadonly } from 'vue';
 
const state = {
  user: { name: 'Alice', age: 30 },
  items: ['apple', 'banana']
};
 
const shallowState = shallowReadonly(state);
 
shallowState.user.age = 31; // 有效操作
shallowState.items.push('orange'); // 有效操作
 
shallowState.user = { name: 'Bob', age: 25 }; // 无效操作
 

特性:

  • 浅层只读:仅顶层属性只读,嵌套对象的属性可修改。
  • 适用场景:保护对象结构,但允许内部数据修改。

isReadonly

检查对象是否由 readonly 或 shallowReadonly 创建的只读对象。

import { readonly, isReadonly } from 'vue';
 
const state = { name: 'Alice' };
const readonlyState = readonly(state);
 
console.log(isReadonly(state)); // false
console.log(isReadonly(readonlyState)); // true

Class与Style动态绑定?

动态绑定 class

1、对象语法

通过对象语法,可以动态地绑定一个对象,其中的键是类名,值是布尔值,表示是否应用该类。

<template>
  <div v-bind:class="{ active: isActive, 'text-danger': hasError }">
    动态绑定类
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      isActive: true,
      hasError: false
    };
  }
}
</script>
 
  • active 类会在 isActive 为 true 时应用。
  • text-danger 类会在 hasError 为 true 时应用。

2、数组语法

通过数组语法,可以动态地绑定一个数组,数组中的每个元素可以是字符串、对象或函数返回值。

<template>
  <div v-bind:class="[isActive ? activeClass : '', errorClass]">
    动态绑定类
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      isActive: true,
      activeClass: 'active',
      errorClass: 'text-danger'
    };
  }
}
</script>
 
  • isActive ? activeClass : '':根据 isActive 的值动态选择类名。
  • errorClass:静态类名,始终应用。

动态绑定 style

1、对象语法

通过对象语法,可以动态地绑定一个对象,其中的键是样式属性,值是样式值。

<template>
  <div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }">
    动态绑定样式
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      activeColor: 'red',
      fontSize: 30
    };
  }
}
</script>
  • color 属性会根据 activeColor 的值动态设置。
  • fontSize 属性会根据 fontSize 的值动态设置,并拼接单位 px。

2、数组语法

通过数组语法,可以动态地绑定一个数组,数组中的每个元素可以是样式对象。

<template>
  <div v-bind:style="[styleColor, styleSize]">
    动态绑定样式
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      styleColor: {
        color: 'red'
      },
      styleSize: {
        fontSize: '23px'
      }
    };
  }
}
</script>
 

styleColor 和 styleSize 是两个样式对象,它们会被合并应用到元素上。

注意事项

  • 对象语法中的布尔值:在对象语法中,值为 false 的类不会被应用。
  • 数组语法中的优先级:在数组语法中,后面的样式对象会覆盖前面的同名样式。
  • 样式单位:在动态绑定样式时,需要确保样式值包含正确的单位(如 px、% 等)。
  • 驼峰命名法:在对象语法中,样式属性可以使用驼峰命名法(如 fontSize)或引号包裹的 kebab-case(如 'font-size')。

Vue2给数组项赋值数据监听

在 Vue 中,直接给数组项赋值或修改数组长度时,Vue 无法检测到这些变化。这是因为 JavaScript 的限制,Vue 无法追踪这些操作。以下是具体的解释和解决方法:

直接给数组项赋值

当你直接通过索引设置数组项的值时,Vue 无法检测到变化。例如:

vm.items[indexOfItem] = newValue;

这种情况下,Vue 不会触发视图更新,因为 Vue 的响应式系统无法追踪到这种变化。

解决:

1、Vue.set:使用 Vue.set 方法来设置数组项的值。

Vue.set(vm.items, indexOfItem, newValue);

2、vm.set:vm.set 是 Vue.set 的一个别名,也可以用来设置数组项的值。

vm.$set(vm.items, indexOfItem, newValue);

3、vm.set:vm.set 是 Vue.set 的一个别名,也可以用来设置数组项的值。

vm.items.splice(indexOfItem, 1, newValue);

修改数组的长度

当你直接修改数组的长度时,Vue 也无法检测到这些变化。例如:

vm.items.length = newLength;

这种情况下,Vue 不会触发视图更新。

解决:

splice:使用 Array.prototype.splice 方法来修改数组的长度。

vm.items.splice(newLength);

Vue 3给数组项赋值数据监听

在 Vue 3 中,直接给数组项赋值或修改数组长度时,Vue 能够检测到这些变化。这是因为 Proxy 可以动态拦截所有属性的访问和修改,包括数组的索引访问和长度修改

// Vue 3 响应式系统示例
const data = new Proxy(
  { message: 'Hello, Vue 3!', items: [1, 2, 3] },
  {
    get(target, key) {
      console.log(`读取 ${key}`);
      return target[key];
    },
    set(target, key, value) {
      console.log(`更新 ${key}`);
      target[key] = value;
      return true;
    }
  }
);
 
// 直接给数组项赋值
data.items[0] = 10;
console.log(data.items); // 输出:[10, 2, 3]
 
// 修改数组长度
data.items.length = 2;
console.log(data.items); // 输出:[10, 2]
 

delete 和 Vue.delete区别

delete 操作符 delete 是 JavaScript 中的一个操作符,用于删除对象的属性。

当用于数组时,它只会将指定索引处的元素设置为 undefined,而不会改变数组的长度或重新索引数组。

let arr = [1, 2, 3, 4];
delete arr[2];
console.log(arr); // [1, 2, undefined, 4]
console.log(arr.length); // 4
 

Vue.delete

行为:Vue.delete 是 Vue 提供的一个全局方法,用于安全地删除对象的属性或数组的元素。

它会直接从数组中移除元素,并更新数组的长度,同时通知 Vue 的响应式系统,确保视图能够正确更新。

let arr = [1, 2, 3, 4];
Vue.delete(arr, 2);
console.log(arr); // [1, 2, 4]
console.log(arr.length); // 3
 

Vue常见问题及解决方案

样式污染

组件间的样式相互影响,导致样式污染。

解决方案:

  • 使用 scoped:在组件的 <style scoped> 中定义样式,确保样式只作用于当前组件。
  • 唯一标识符:在组件根元素上添加唯一的 class 或 id,并在样式中使用这些标识符。
  • CSS Modules:使用 CSS Modules 来模块化样式,避免全局样式冲突。

router-link 在安卓上不起作用

在安卓设备上,router-link 的点击事件可能无法正常触发。

  1. 使用 .native 修饰符:通过 .native 修饰符直接监听原生的 click 事件。

<router-link to="/path" @click.native="handleClick">Link</router-link>

  1. 检查 Babel 配置:确保项目中安装了 @babel/polyfill 并正确配置,以解决转码问题。

初始化页面出现闪屏乱码

在 Vue 应用初始化时,可能会看到未编译的模板内容(如 {{ data }})。

  • 使用 v-cloak 指令:在模板中使用 v-cloak,并在 CSS 中设置 [v-cloak] { display: none; }
  • 隐藏根元素:在 index.html 中隐藏根元素,直到 Vue 实例挂载完成后再显示。

router-link 上事件无效

router-link 上的事件(如 @click)可能无法正常触发。 解决方案:

  1. 使用 .native 修饰符:直接监听原生的 click 事件。<router-link to="/path" @click.native="handleClick">Link</router-link>
  2. 自定义事件处理:通过 @click 监听事件,并在事件处理函数中使用 this.$router.push 进行导航。

<router-link to="/path" @click="handleClick">Link</router-link>

methods: {
  handleClick() {
    this.$router.push('/path');
  }
}
 

Vue 项目国际化

问题描述:需要支持多语言的国际化应用。

解决方案:

  • 使用 Vue I18n:通过 Vue I18n 插件实现多语言支持。
  • 动态切换语言:在应用中动态切换语言,支持用户选择。

参考文档: