Vanson's Eternal Blog

Vue夯实基础

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

双向数据绑定原理

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

基本原理

  • 绑定属性:v-model 默认会绑定 value 属性到数据模型。
  • 监听事件:v-model 默认会监听 input 事件来更新数据模型。

不同标签的默认行为

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 数据。

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路由实现

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 事件,当用户点击浏览器的前进或后退按钮时。

Vue组件data是函数的原因

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

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 隐藏。
  • 切换性能:条件变化时,仅切换样式,不重新渲染,性能较好。
  • 适用场景:适合需要频繁切换显示状态的情况,例如切换菜单的显示和隐藏。

keep-alive说明

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

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

常用属性

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

生命周期钩子

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

实现原理

缓存机制:

  • keep-alive 组件内部维护了一个缓存对象,用于存储它的子组件实例。
  • 当一个组件被首次渲染时,keep-alive 会将其实例存储到缓存中。
  • 当组件被停用时,实例会被保留在缓存中,而不是被销毁。
  • 当组件再次被激活时,keep-alive 会从缓存中取出实例并复用

LRU 算法 keep-alive 使用了 LRU(Least Recently Used,最近最少使用)算法来管理缓存。

  • 缓存存储:使用 Map 存储键值对,用于快速访问缓存内容。
  • 访问顺序跟踪:通过双向链表(LinkedList)跟踪缓存的访问顺序,访问过的元素会被移动到链表的前端,最久未访问的元素位于链表的尾部。
  • 淘汰机制:当缓存满时,删除链表尾部的元素,即最少使用的元素

条件缓存:

  • 通过 include 和 exclude 属性,可以指定哪些组件需要被缓存,哪些组件不需要被缓存。

使用场景

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

组件通信

父子组件通信

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

  • 方式:通过 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 实例作为事件总线。
// 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>
 

在 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
 

理解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

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

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 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 实现响应式,只能监听对象的属性,不能监听动态新增或删除的属性,数组变化需要特殊处理。
  • Vue 3:使用 Proxy 实现响应式,可以监听动态新增或删除的属性,以及数组的变化。性能优化更好,支持深度响应式。

编译优化

  • Vue 2:通过标记静态根节点优化 diff,减少不必要的比较。
  • Vue 3:标记和提升所有静态节点,diff 时只对比动态节点内容,优化了渲染性能。

打包体积优化

  • Vue 2:包含一些不常用的 API(如 inline-template、filter)。
  • Vue 3:移除了一些不常用的 API,减小了打包体积。

生命周期的变化

  • 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 操作。

优点

  • 更高效,减少不必要的比较。
  • 更好的静态节点优化。
  • 更智能的节点移动处理
优化项Vue 2.xVue 3.x
时间复杂度O(n^2)接近 O(n)
DOM 移动次数较多显著减少
静态处理静态提升跳过 Diff
动态列表性能较差优异

Vue Compiler实现原理

Parse(解析)

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

Optimize(优化)

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

Generate(生成)

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

watch 与 computed 的区别

数据混入与监听:

  • computed:计算属性会混入到 Vue 实例中,直接作为实例的属性访问。
  • watch:监听 data 或 props 中数据的变化,需要通过 watch 选项定义。

缓存机制:

  • computed:有缓存,只有当依赖的值变化时才会重新计算。
  • watch:没有缓存,每次数据变化都会触发回调。

异步支持:

  • computed:不支持异步操作。
  • watch:支持异步操作,可以执行异步任务。

一对多与多对一:

  • watch:一对多,监听某个值的变化,执行对应的操作。
  • computed:多对一,计算属性依赖于多个其他属性。

回调参数:

  • watch:回调函数接收两个参数,第一个是最新值,第二个是旧值。
  • computed:计算属性作为函数时,有 get 和 set 方法,默认走 get 方法,get 必须有返回值。

使用场景:

  • computed:适用于基于现有数据派生新数据的场景,如格式化显示、计算总和等。
  • watch:适用于执行复杂逻辑、异步操作或需要监听多个属性变化的场景。

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" />

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 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 决定,便于调试

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>

接口请求放在哪个生命周期?

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

原因

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

参考文档:

https://mp.weixin.qq.com/s/NjXjYMO_X7yxAb6-yh1iww

https://mp.weixin.qq.com/s/qvau2pLp8gg5eoPYqi1aLA

https://mp.weixin.qq.com/s/yUl78yJ-3E9KFx1bWKZzzA

https://mp.weixin.qq.com/s/TZ0TrthBChTtbB81jzDXgA

https://mp.weixin.qq.com/s/3vC5_Nkzgz80vWBEkhCjiw

https://mp.weixin.qq.com/s/qMutfBr6bA_fWYjAKF7s9A