前端面试题刷(1-10)

315 阅读19分钟

1.Vue 有了数据响应式,为何还要 diff ?

一句话答:数据响应式机制负责 数据与视图绑定并追踪依赖,diff算法比较前后变化,高效更新dom。

在 Vue 中,数据响应式机制负责和 diff 算法是两个不同的概念,它们各自解决了不同的问题。理解这两者的关系有助于更好地理解 Vue 的工作原理。

数据响应式机制

数据响应式机制(Reactivity System)是 Vue 的核心之一。它的主要作用是让数据变化自动更新视图。具体来说:

  1. 数据绑定:当我们在 Vue 组件中声明一个数据属性时,Vue 会将其包装成一个响应式的属性。例如,data 对象中的属性会被监听。
  2. 依赖收集:当某个属性被访问时,Vue 会记录下这个属性的依赖关系。例如,在模板中使用了 {{ message }},那么 Vue 会记录下 message 这个属性的依赖。
  3. 依赖更新:当数据发生变化时,Vue 会自动触发依赖更新。例如,如果 message 发生变化,Vue 会重新渲染依赖 message 的部分。

Diff 算法

Diff 算法主要用于比较虚拟 DOM(Virtual DOM)的变化,并找出最小的变更集,从而高效地更新实际 DOM。具体来说:

  1. 虚拟 DOM:Vue 使用虚拟 DOM 来表示视图的状态。虚拟 DOM 是一个 JavaScript 对象树,它表示了 DOM 结构。
  2. 渲染过程:每次数据发生变化时,Vue 会重新生成虚拟 DOM 树。
  3. diff 算法:Vue 使用 diff 算法来比较新旧虚拟 DOM 树的差异,并找出最小的变更集。然后根据这些变更集更新实际 DOM。

为什么需要 diff?

尽管 Vue 有数据响应式机制,但它仍然需要 diff 算法来确保高效的 DOM 更新。原因如下:

  1. 局部更新:虽然数据响应式机制可以触发视图更新,但它无法保证每次更新都是最优的。例如,如果多个数据属性同时发生变化,直接更新整个视图可能会导致不必要的重绘和重排。
  2. 最小化变更:diff 算法可以找到最小的变更集,从而只更新必要的部分,减少不必要的 DOM 操作。这对于性能优化至关重要。
  3. 复杂场景:在复杂的 UI 场景中,数据变化可能导致多个组件的状态发生变化。diff 算法可以帮助 Vue 确定哪些部分需要更新,从而避免不必要的重渲染。

示例

假设我们有一个 Vue 组件,其中包含一个列表:

html
<div id="app">
  <ul>
    <li v-for="item in items">{{ item }}</li>
  </ul>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    items: ['A', 'B', 'C']
  }
});
</script>

当 items 数组发生变化时,Vue 会重新生成虚拟 DOM 树,并使用 diff 算法来确定哪些部分需要更新。例如:

javascript
// 更新数据
this.items = ['D', 'E', 'F'];

// diff 算法会找出最小的变更集,并更新 DOM

在这个过程中,数据响应式机制负责检测数据变化并触发重新渲染,而 diff 算法则负责找到最小的变更集并更新 DOM。

总结来说,数据响应式机制和 diff 算法在 Vue 中是相辅相成的,前者负责数据变化的检测和依赖更新,后者负责高效地更新 DOM。两者结合使得 Vue 能够在数据变化时高效地更新视图。

2.vue3 为什么不需要时间分片?

答: Vue 3 中不再需要时间分片(time slicing)的原因在于其渲染机制的设计目标和现代浏览器的性能优化需求。

时间分片的概念

时间分片是一种技术,它允许长时间运行的任务被分割成一系列短小的任务,每个任务执行一小段时间,然后暂停,允许浏览器处理其他事件(如用户输入或绘制帧)。这种做法可以防止长时间的任务导致浏览器冻结或变得无响应。

Vue 3 的情况

Vue 3 采用了新的调度算法来处理更新队列,这个算法考虑到了现代浏览器的能力和用户交互的需求。具体来说:

  1. 异步更新队列异步排队):Vue 3 使用了一个异步更新队列来管理组件的更新。这意味着当状态改变时,Vue 不会立即更新 DOM,而是将更新放入队列中,并在一个适当的时机进行批量更新。这种方式减少了不必要的重绘和重排,提高了性能。
  2. Promise 微任务批量更新):Vue 3 利用了 JavaScript 的微任务(microtask)机制,通过 Promise.then 或 queueMicrotask 来安排更新。这种方式确保了所有的更新都在当前宏任务完成之后、下一个宏任务开始之前执行。这样可以确保在一次事件循环中,所有的更新都是一次性完成的,而不是分散在多次更新中。
  3. 浏览器的调度机制打断js):现代浏览器已经非常擅长处理动画和其他高优先级任务,因此它们能够在合适的时机打断 JavaScript 执行,以确保用户界面的流畅性。Vue 3 的设计充分利用了这一点,通过异步更新队列和微任务机制来保证更新不会阻塞浏览器。
  4. 性能优化Vue3 我的神):Vue 3 的设计目标之一是提高性能,特别是在复杂的大型应用中。通过异步更新队列和微任务机制,Vue 3 能够更好地利用浏览器的调度机制,从而不需要额外的时间分片技术来防止长时间任务导致的卡顿。

结论

综上所述,Vue 3 之所以不需要时间分片,是因为它采用了异步更新队列和微任务机制,这些机制能够更好地与现代浏览器的调度机制协同工作,从而确保应用的性能和响应性。这种方式不仅简化了框架的内部逻辑,还提高了整体的用户体验

3. vue3 为什么要引入 Composition API ?

Vue 3 引入 Composition API 的主要原因是为了改进代码的可读性、可维护性和可复用性,尤其是在处理复杂的组件逻辑时。以下是一些具体的理由:(干掉mixin,更小,更复用,更好测,逻辑组合,更符合函数式编程的模式,更好的 TypeScript 支持

  1. 解决 Mixin 的局限性mixin,瞧瞧这名字‘迷信’,一直用不好):Vue 2 中的 Mixin 机制虽然提供了一种方式来共享组件间的逻辑,但在实践中却带来了一些问题,比如难以追踪逻辑来源、调试困难等。Composition API 提供了一种更好的方式来封装和重用逻辑,解决了 Mixin 的局限性。
  2. 提高代码的可维护性:Composition API 允许开发者将逻辑拆分成更小的、可复用的函数(称为组合函数),这些函数可以专注于单一职责。这使得代码更加模块化,易于理解和维护。
  3. 增强代码的可测试性:由于 Composition API 可以将逻辑分离出来,因此更容易针对特定的逻辑编写单元测试。测试单个组合函数比测试整个组件更容易,因为你可以直接调用这些函数并验证它们的行为。
  4. 改善代码的可读性:Composition API 使得组件的结构更加清晰。通过将相关逻辑集中在一起,开发者可以更容易地理解组件的工作原理,而无需在不同的生命周期钩子之间跳转查找。
  5. 支持逻辑的组合:Composition API 支持逻辑的组合,这意味着可以在不同的组件中重用相同的逻辑,而不仅仅是像 Mixin 那样复制逻辑。这有助于减少代码重复,并且使得逻辑更加一致。
  6. 适应函数式编程模式:Composition API 更符合函数式编程的模式,使得 Vue 更加灵活,能够更好地与其他函数式库或框架集成。
  7. 更好的 TypeScript 支持:对于使用 TypeScript 的开发者来说,Composition API 提供了更好的类型推断和静态检查支持,使得代码更加健壮。

示例

下面是一个简单的 Composition API 的示例,展示了如何使用 setup() 函数来组织组件逻辑:

javascript
import { ref, onMounted } from 'vue';

export default {
  setup() {
    const count = ref(0);

    function increment() {
      count.value++;
    }

    onMounted(() => {
      console.log('Component is mounted!');
    });

    return {
      count,
      increment
    };
  }
`}`

在这个例子中,setup 函数被用来初始化组件的状态 (count) 并定义方法 (increment)。onMounted 是一个组合函数,用于在组件挂载完成后执行一些操作。

总结

Vue 3 引入 Composition API 的主要目的是为了改进代码的结构,使其更加模块化、可维护和可测试。通过 Composition API,开发者可以更容易地管理和重用组件逻辑,从而提高开发效率和代码质量。此外,Composition API 还增强了 Vue 的灵活性,使其更适合现代前端开发的需求。

4. 谈谈 Vue 事件机制,并手写$on、$off、$emit、$once

Vue 事件机制

Vue 的事件机制主要用于在组件之间传递消息。Vue 提供了内置的方法 $on(订阅)、$emit(触发) 和 $off(解绑) 来处理事件的订阅、触发和取消订阅。此外,Vue 还提供了 $once 方法来处理一次性事件。

事件机制的核心概念

  1. 事件中心:Vue 内部维护了一个事件中心,用于存储所有注册的事件及其对应的回调函数。
  2. 事件订阅:通过 $on 方法订阅事件。
  3. 事件触发:通过 $emit 方法触发事件。
  4. 事件取消订阅:通过 $off 方法取消订阅事件。
  5. 一次性事件:通过 $once 方法订阅一次性事件。

手写实现

下面我们将手写实现 $on$off$emit 和 $once 方法。

1. 事件中心

首先,我们需要创建一个事件中心来存储事件及其回调函数。

javascript
class EventHub {
  constructor() {
    this.events = {};
  }

  // 订阅事件
  $on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }

  // 触发事件
  $emit(eventName, ...args) {
    const callbacks = this.events[eventName];
    if (callbacks) {
      callbacks.forEach(callback => {
        callback(...args);
      });
    }
  }

  // 取消订阅事件
  $off(eventName, callback) {
    const callbacks = this.events[eventName];
    if (callbacks) {
      this.events[eventName] = callbacks.filter(cb => cb !== callback);
    }
  }

  // 订阅一次性事件
  $once(eventName, callback) {
    const onceCallback = (...args) => {
      callback(...args);
      this.$off(eventName, onceCallback);
    };
    this.$on(eventName, onceCallback);
  }
}

// 创建一个全局事件中心实例
const eventHub = new EventHub();
2. 使用示例

接下来,我们来看一个使用示例:

javascript
// 订阅事件
eventHub.$on('hello', (msg) => {
  console.log(`Received message: ${msg}`);
});

// 触发事件
eventHub.$emit('hello', 'Hello, World!');

// 取消订阅事件
eventHub.$off('hello', (msg) => {
  console.log(`Received message: ${msg}`);
});

// 订阅一次性事件
eventHub.$once('goodbye', (msg) => {
  console.log(`Goodbye message: ${msg}`);
});

// 触发一次性事件
eventHub.$emit('goodbye', 'Goodbye, World!');
eventHub.$emit('goodbye', 'Goodbye again, World!'); // 第二次不会触发

解释

  1. 事件中心

    • EventHub 类维护了一个 events 对象,用于存储事件名称及其对应的回调函数数组。
  2. 事件订阅

    • $on 方法接受事件名称和回调函数,将回调函数添加到对应事件的回调数组中。
  3. 事件触发

    • $emit 方法接受事件名称和任意数量的参数,遍历并调用对应事件的所有回调函数,并传入参数。
  4. 事件取消订阅

    • $off 方法接受事件名称和回调函数,从对应事件的回调数组中移除指定的回调函数。
  5. 一次性事件

    • $once 方法接受事件名称和回调函数,创建一个新的回调函数 onceCallback,并在触发事件后自动取消订阅。

完整代码

下面是完整的代码实现:

javascript
class EventHub {
  constructor() {
    this.events = {};
  }

  // 订阅事件
  $on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }

  // 触发事件
  $emit(eventName, ...args) {
    const callbacks = this.events[eventName];
    if (callbacks) {
      callbacks.forEach(callback => {
        callback(...args);
      });
    }
  }

  // 取消订阅事件
  $off(eventName, callback) {
    const callbacks = this.events[eventName];
    if (callbacks) {
      this.events[eventName] = callbacks.filter(cb => cb !== callback);
    }
  }

  // 订阅一次性事件
  $once(eventName, callback) {
    const onceCallback = (...args) => {
      callback(...args);
      this.$off(eventName, onceCallback);
    };
    this.$on(eventName, onceCallback);
  }
}

// 创建一个全局事件中心实例
const eventHub = new EventHub();

// 使用示例
eventHub.$on('hello', (msg) => {
  console.log(`Received message: ${msg}`);
});

eventHub.$emit('hello', 'Hello, World!');

eventHub.$off('hello', (msg) => {
  console.log(`Received message: ${msg}`);
});

eventHub.$once('goodbye', (msg) => {
  console.log(`Goodbye message: ${msg}`);
});

eventHub.$emit('goodbye', 'Goodbye, World!');
eventHub.$emit('goodbye', 'Goodbye again, World!'); // 第二次不会触发

下面是手写这些方法的简单实现:

lass EventEmitter {
 constructor() {
   this.events = {};
 }

 // 监听事件
 $on(event, callback) {
   if (!this.events[event]) {
     this.events[event] = [];
    }
    this.events[event].push(callback);
  }

  // 停止监听事件
  $off(event, callback) {
    if (!this.events[event]) return;

    if (!callback) {
      // 如果没有传递 callback,移除所有事件监听
      this.events[event] = [];
    } else {
      // 移除特定的事件监听
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
  }

  // 触发事件
  $emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback.apply(this, args));
    }
  }

  // 只监听一次事件
  $once(event, callback) {
    const wrapper = (...args) => {
      callback.apply(this, args);
      this.$off(event, wrapper);
    };
    this.$on(event, wrapper);
  }
}

// 示例
const eventBus = new EventEmitter();

// 监听事件
eventBus.$on('test', (msg) => console.log('test event:', msg));

// 触发事件
eventBus.$emit('test', 'Hello, World!');

// 监听一次事件
eventBus.$once('once', (msg) => console.log('once event:', msg));

// 触发一次性事件
eventBus.$emit('once', 'This should appear once');
eventBus.$emit('once', 'This should not appear');

// 停止监听事件
eventBus.$off('test');

// 触发事件(已经移除监听)
eventBus.$emit('test', 'This should not appear');

通过上述实现,我们可以看到 Vue 的事件机制是如何工作的,并且可以手动实现基本的事件订阅、触发和取消订阅功能。

5. computed 计算值为什么还可以依赖另外一个 computed 计算值?

在 Vue 中,computed 属性是非常强大的,因为它可以基于其他响应式属性或计算属性来计算出新的值。computed 属性的一个重要特点是它具有缓存机制,只有在其依赖的数据发生变化时才会重新计算。那么,为什么 computed 属性可以依赖另一个 computed 属性呢?以下是详细的解释和示例。

为什么 computed 可以依赖另一个 computed 属性

缓存机制和依赖结果,上一个依赖结果被缓存,当结果变化时,响应。

  1. 缓存机制

    • computed 属性具有缓存机制,这意味着只有当它的依赖项发生变化时,它才会重新计算。
    • 这种机制可以显著提高性能,尤其是在处理大量数据或复杂计算时。
  2. 依赖链

    • computed 属性可以依赖其他响应式属性(如 data 属性)或其他 computed 属性。
    • 当一个 computed 属性依赖另一个 computed 属性时,它实际上是在依赖另一个 computed 属性的最终结果。
    • 这种依赖关系形成了一条依赖链,使得最终的 computed 属性能够根据多个层次的数据变化进行计算。

示例

假设我们有一个 Vue 组件,其中包含多个 computed 属性,每个属性依赖其他属性或响应式属性。

示例代码
html
<template>
  <div>
    <p>Value A: {{ valueA }}</p>
    <p>Value B: {{ valueB }}</p>
    <p>Computed C: {{ computedC }}</p>
    <p>Computed D: {{ computedD }}</p>
    <button @click="updateValueA">Update Value A</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      valueA: 10,
      valueB: 20
    };
  },
  computed: {
    computedC() {
      return this.valueA + this.valueB;
    },
    computedD() {
      return this.computedC * 2;
    }
  },
  methods: {
    updateValueA() {
      this.valueA += 1;
    }
  }
};
</script>

详细解释

  1. 初始状态

    • valueA 的初始值为 10。
    • valueB 的初始值为 20。
  2. 第一个 computed 属性

    • computedC 依赖 valueA 和 valueB,计算公式为 valueA + valueB
    • 初始值为 10 + 20 = 30
  3. 第二个 computed 属性

    • computedD 依赖 computedC,计算公式为 computedC * 2
    • 初始值为 30 * 2 = 60
  4. 更新操作

    • 当点击按钮时,updateValueA 方法被调用,valueA 增加 1。
    • valueA 变为 11,valueB 仍为 20。
    • computedC 重新计算为 11 + 20 = 31
    • computedD 重新计算为 31 * 2 = 62

缓存机制

  1. 缓存机制

    • computedC 依赖 valueA 和 valueB,只有当这两个值发生变化时,computedC 才会重新计算。
    • computedD 依赖 computedC,只有当 computedC 的值发生变化时,computedD 才会重新计算。
  2. 性能优化

    • 通过这种缓存机制,Vue 可以避免不必要的重新计算,从而提高性能。
    • 在复杂的组件中,这种机制尤其重要,因为它可以显著减少不必要的计算和渲染。

总结

computed 属性可以依赖另一个 computed 属性,这是因为它们形成了一个依赖链。这种依赖链使得 Vue 能够根据多个层次的数据变化进行计算,并且通过缓存机制避免不必要的重新计算,从而提高性能。这种机制使得 Vue 在处理复杂逻辑时更加高效和灵活。

如果计算属性D依赖于计算属性C,C依赖于A和B,如果A和B变化了,但是C的结果没有变,那么C和D有没有重新缓存

在 Vue 中,computed 属性具有缓存机制,这意味着只有当其依赖的数据发生变化时,才会重新计算。如果 computed 属性的依赖数据变化了,但计算结果没有变化,那么该 computed 属性仍然会被重新计算。然而,如果计算结果没有变化,那么最终的渲染结果也不会改变。

让我们详细分析一下这种情况,并通过示例来说明。

详细分析

假设我们有以下依赖关系:

  • computedC 依赖于 valueA 和 valueB
  • computedD 依赖于 computedC

情况分析

  1. 初始状态

    • valueA 的初始值为 10。
    • valueB 的初始值为 20。
    • computedC 的初始值为 valueA + valueB,即 10 + 20 = 30
    • computedD 的初始值为 computedC * 2,即 30 * 2 = 60
  2. 更新 valueA 和 valueB

    • 假设 valueA 更新为 15,valueB 更新为 15。
    • computedC 的新值为 15 + 15 = 30
    • computedD 的新值为 30 * 2 = 60

详细步骤

  1. 初始状态

    javascript
    valueA: 10
    valueB: 20
    computedC: 30
    computedD: 60
    
  2. 更新 valueA 和 valueB

    javascript
    valueA: 15
    valueB: 15
    computedC: 30 (尽管依赖项变了,但计算结果没变)
    computedD: 60 (不变)
    

实现示例

html
<template>
  <div>
    <p>Value A: {{ valueA }}</p>
    <p>Value B: {{ valueB }}</p>
    <p>Computed C: {{ computedC }}</p>
    <p>Computed D: {{ computedD }}</p>
    <button @click="updateValueA">Update Value A</button>
    <button @click="updateValueB">Update Value B</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      valueA: 10,
      valueB: 20
    };
  },
  computed: {
    computedC() {
      return this.valueA + this.valueB;
    },
    computedD() {
      return this.computedC * 2;
    }
  },
  methods: {
    updateValueA() {
      this.valueA += 5;
    },
    updateValueB() {
      this.valueB -= 5;
    }
  }
};
</script>

分析

  1. 初始状态

    • valueA 的初始值为 10。
    • valueB 的初始值为 20。
    • computedC 的初始值为 10 + 20 = 30
    • computedD 的初始值为 30 * 2 = 60
  2. 更新 valueA 和 valueB

    • 点击 Update Value A 按钮,valueA 变为 15。
    • 点击 Update Value B 按钮,valueB 变为 15。
    • computedC 的新值为 15 + 15 = 30
    • computedD 的新值为 30 * 2 = 60

缓存机制

  1. computedC 的重新计算

    • 尽管 valueA 和 valueB 发生了变化,但由于 computedC 的计算结果仍然是 30,所以 computedC 会重新计算。
    • 但是,由于计算结果没有变化,最终的渲染结果不会改变。
  2. computedD 的重新计算

    • computedD 依赖于 computedC
    • 由于 computedC 的值没有变化,computedD 也会重新计算,但计算结果仍然是 60。

总结

  • computedC 的缓存

    • 如果 valueA 和 valueB 发生变化,但 computedC 的计算结果没有变化,computedC 仍然会被重新计算。
    • 但是,由于计算结果没有变化,最终的渲染结果不会改变。
  • computedD 的缓存

    • computedD 依赖于 computedC
    • 如果 computedC 的值没有变化,computedD 也会重新计算,但计算结果仍然是原来的值。

通过这种方式,Vue 的 computed 属性能够有效地利用缓存机制,避免不必要的重新计算,从而提高性能。

那么D会重新计算并缓存一遍?

是的,即使 computedC 的计算结果没有变化,computedD 依然会被重新计算并缓存一遍。这是因为在 Vue 中,computed 属性的重新计算机制是基于其依赖项的变化,而不是最终计算结果的变化。

让我们通过详细的分析和示例来进一步说明这一过程。

详细分析

假设我们有以下依赖关系:

  • computedC 依赖于 valueA 和 valueB
  • computedD 依赖于 computedC

情况分析

  1. 初始状态

    • valueA 的初始值为 10。
    • valueB 的初始值为 20。
    • computedC 的初始值为 valueA + valueB,即 10 + 20 = 30
    • computedD 的初始值为 computedC * 2,即 30 * 2 = 60
  2. 更新 valueA 和 valueB

    • 假设 valueA 更新为 15,valueB 更新为 15。
    • computedC 的新值为 15 + 15 = 30
    • computedD 的新值为 30 * 2 = 60

详细步骤

  1. 初始状态

    javascript
    valueA: 10
    valueB: 20
    computedC: 30
    computedD: 60
    
  2. 更新 valueA 和 valueB

    javascript
    valueA: 15
    valueB: 15
    computedC: 30 (尽管依赖项变了,但计算结果没变)
    computedD: 60 (不变)
    

实现示例

html
<template>
  <div>
    <p>Value A: {{ valueA }}</p>
    <p>Value B: {{ valueB }}</p>
    <p>Computed C: {{ computedC }}</p>
    <p>Computed D: {{ computedD }}</p>
    <button @click="updateValueA">Update Value A</button>
    <button @click="updateValueB">Update Value B</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      valueA: 10,
      valueB: 20
    };
  },
  computed: {
    computedC() {
      console.log('Recomputing C');
      return this.valueA + this.valueB;
    },
    computedD() {
      console.log('Recomputing D');
      return this.computedC * 2;
    }
  },
  methods: {
    updateValueA() {
      this.valueA += 5;
    },
    updateValueB() {
      this.valueB -= 5;
    }
  }
};
</script>

分析

  1. 初始状态

    • valueA 的初始值为 10。
    • valueB 的初始值为 20。
    • computedC 的初始值为 10 + 20 = 30
    • computedD 的初始值为 30 * 2 = 60
  2. 更新 valueA 和 valueB

    • 点击 Update Value A 按钮,valueA 变为 15。
    • 点击 Update Value B 按钮,valueB 变为 15。
    • computedC 的新值为 15 + 15 = 30
    • computedD 的新值为 30 * 2 = 60

控制台输出

  1. 初始状态

    • 控制台输出:

      Recomputing C
      Recomputing D
      
  2. 更新 valueA 和 valueB

    • 控制台输出:

      Recomputing C
      Recomputing D
      

缓存机制

  1. computedC 的重新计算

    • 尽管 valueA 和 valueB 发生了变化,但由于 computedC 的计算结果仍然是 30,所以 computedC 会重新计算。
    • 但是,由于计算结果没有变化,最终的渲染结果不会改变。
  2. computedD 的重新计算

    • computedD 依赖于 computedC
    • 即使 computedC 的值没有变化,computedD 也会重新计算,但计算结果仍然是 60。

为什么 computedD 会重新计算

  • 依赖关系

    • computedD 依赖于 computedC
    • 当 computedC 重新计算时,computedD 也会重新计算,即使 computedC 的值没有变化。
  • 缓存机制

    • computed 属性的缓存机制是基于其依赖项的变化,而不是最终计算结果的变化。
    • 因此,只要依赖项发生变化,computed 属性就会重新计算。

总结

  • computedC 的缓存

    • 如果 valueA 和 valueB 发生变化,即使 computedC 的计算结果没有变化,computedC 仍然会被重新计算。
    • 但是,由于计算结果没有变化,最终的渲染结果不会改变。
  • computedD 的缓存

    • computedD 依赖于 computedC
    • 即使 computedC 的值没有变化,computedD 也会重新计算,但计算结果仍然是原来的值。

通过这种方式,Vue 的 computed 属性能够有效地利用缓存机制,避免不必要的重新计算,从而提高性能。

6. 说一下 vm.$set 原理

答:使用 Object.defineProperty 方法将新添加的属性变为响应式的,通知观察者(Watcher)进行依赖收集和更新。

vm.$set 是 Vue.js 中的一个实用方法,用于向响应式对象添加或更新属性。它主要用于解决 Vue 无法检测到某些属性变更的问题。下面详细介绍 vm.$set 的原理及其使用场景。

vm.$set 的原理

  1. 基本用法

    • vm.$set 的基本语法如下:

      javascript
      Vue.set(object, propertyName, value);
      // 或者
      vm.$set(object, propertyName, value);
      
  2. 内部实现

    • vm.$set 主要有两个作用:

      1. 确保对象的响应式:将新增的属性变为响应式的。
      2. 触发视图更新:确保视图能够正确更新。

内部实现细节

  1. Vue 的响应式系统

    • Vue 使用数据劫持的方式实现响应式。具体来说,Vue 通过 Object.defineProperty 方法对对象的属性进行拦截,从而实现数据的响应式。
    • 对于已经存在的属性,Vue 可以直接监听这些属性的变化。
    • 但是,对于动态添加的新属性,Vue 无法自动检测到这些变化。
  2. vm.$set 的实现

    • vm.$set 的实现主要包括以下几个步骤:

      1. 定义响应式属性:使用 Object.defineProperty 方法将新添加的属性变为响应式的。
      2. 触发依赖收集和更新:通知观察者(Watcher)进行依赖收集和更新。

具体实现

1. 定义响应式属性
javascript
function set(target, key, val) {
  if ((key in target) && target[key] === val) {
    return;
  }

  let ob = target.__ob__;
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn('Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.');
    return;
  }

  if (!Array.isArray(target) && isReserved(key)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid using reserved property name: ' + key
    );
    return;
  }

  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return;
  } else {
    if (key in target && !(key in Object.prototype)) {
      target[key] = val;
      return;
    }

    const observer = new Observer(val);
    const onNotify = dep => {
      dep.notify();
    };

    if (!ob) {
      defineReactive(target, key, val, onNotify);
    } else {
      defineReactive(target, key, val, onNotify);
      ob.dep.notify();
    }
  }
}
2. 触发依赖收集和更新
  • defineReactive 函数用于定义响应式属性,并设置依赖收集和更新逻辑。
javascript
function defineReactive(obj, key, val, customSetter, shallow) {
  const dep = new Dep();

  const property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return;
  }

  const getter = property && property.get;
  const setter = property && property.set;

  const childOb = !shallow ? observe(val) : null;

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value;
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val;
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return;
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter();
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow ? observe(newVal) : null;
      dep.notify();
    }
  });
}

使用场景

  1. 动态添加属性

    • 当你需要动态地向一个响应式对象添加属性时,应该使用 vm.$set
    javascript
    vm.$set(vm.someObject, 'newProperty', 'newValue');
    
  2. 数组索引赋值

    • 当你需要修改数组中的某个元素时,直接使用索引赋值可能会导致 Vue 无法检测到变化。这时也应该使用 vm.$set
    javascript
    vm.$set(vm.someArray, index, newValue);
    

示例

javascript
// Vue 实例
const vm = new Vue({
  el: '#app',
  data: {
    someObject: {
      prop1: 'value1'
    },
    someArray: [1, 2, 3]
  }
});

// 动态添加属性
vm.$set(vm.someObject, 'prop2', 'value2');

// 修改数组中的元素
vm.$set(vm.someArray, 1, 4);

// 直接使用索引赋值
vm.someArray[1] = 5;  // 这样可能不会触发视图更新

总结

  • vm.$set 的主要作用

    • 确保对象的响应式:将新增的属性变为响应式的。
    • 触发视图更新:确保视图能够正确更新。
  • 使用场景

    • 动态添加属性。
    • 修改数组中的元素。

7. 怎么在 Vue 中定义全局方法?

使用 Vue.prototype Vue.prototype.$myMethod = function(){},使用全局混入 Vue.mixin,使用插件,使用Vuex 在 Vue 中定义全局方法,可以让你在整个应用中的任何组件内访问这些方法,而无需在每个组件中重复定义。以下是几种定义全局方法的方式:

1. 使用 Vue.prototype

你可以直接在 Vue.prototype 上定义方法,这样所有 Vue 组件都可以通过 this 访问到这些方法。

javascript
// 定义全局方法
Vue.prototype.$myMethod = function () {
  console.log('这是一个全局方法!');
};

// 在组件中使用
export default {
  methods: {
    localMethod() {
      this.$myMethod(); // 调用全局方法
    }
  }
};

2. 使用全局混入 Vue.mixin

全局混入允许你在所有的 Vue 组件中注入一些共同的行为,包括方法。

javascript
Vue.mixin({
  methods: {
    myGlobalMethod() {
      console.log('这是通过混入定义的全局方法');
    }
  }
});

3. 使用插件

如果你有一组相关的全局方法或组件,你可以将它们打包成一个插件。

javascript
// 插件定义
const MyPlugin = {
  install(Vue, options) {
    Vue.prototype.$pluginMethod = function () {
      console.log('这是插件定义的全局方法');
    };
  }
};

// 应用插件
Vue.use(MyPlugin);

4. 使用 Vuex

如果你的应用足够复杂,可能需要使用 Vuex 来管理状态。虽然不是直接的方法定义,但在 Vuex 中定义的 actions 和 getters 也可以在整个应用中被组件访问。

javascript
// Vuex store
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {},
  mutations: {},
  actions: {
    globalAction({ commit }) {
      console.log('这是一个 Vuex action');
    }
  },
  getters: {}
});

// 在组件中使用
export default {
  methods: {
    callGlobalAction() {
      this.$store.dispatch('globalAction'); // 调用 Vuex action
    }
  }
};

实际操作示例

假设你想定义一个全局方法 getDicts,并且想在 Vue 文件中使用这个方法。

javascript
// 导入 API 方法
import { getDicts } from '@api/someModule'; // 假设这是你的 API 模块路径

// 将方法挂载到 Vue 全局方法中
Vue.prototype.getDicts = getDicts;

// 在 Vue 文件中使用
export default {
  mounted() {
    this.getDicts().then(res => {
      console.log(res);
    });
  }
};

注意事项

  • 当你使用 Vue.prototype 添加全局方法时,要小心命名冲突,因为所有组件都能访问到这些方法。
  • 如果你使用的是 Vue 3,需要注意 Vue.prototype 已经被弃用,建议使用 Composition API 或者插件的方式来定义全局功能。

通过上述方法之一,你可以在 Vue 中定义全局方法,并在各个组件中轻松地调用它们。选择哪种方式取决于你的具体需求和项目的规模。

8 . Vue 中父组件怎么监听到子组件的生命周期?

自定义事件: 子组件 this.$emit('父组件监听事件名') 父组件 <Child @事件名="onMounted" @updated="onUpdated"/>

使用 @hook: 前缀监听子组件的生命周期钩子: <ThirdPartyComponent @hook:mounted="onMounted" @hook:updated="onUpdated"/>

使用 Refs: this.refs.childRef.refs.childRef.forceUpdate();

使用 Vuex this.$store.dispatch('updateAppData', { key: 'value' });

使用 Provide/Inject provide() { return { onLifecycleEvent: this.onLifecycleEvent }; },

provide() { 
    return { 
        onLifecycleEvent: this.onLifecycleEvent 
        }; 
    }

在 Vue 中,父组件可以通过多种方式监听到子组件的生命周期。这里有几个常见的做法:

1. 使用自定义事件

在子组件的生命周期钩子中触发自定义事件,然后父组件监听这些事件。

子组件示例
vue
<template>
  <div>
    <!-- 子组件内容 -->
  </div>
</template>

<script>
export default {
  mounted() {
    this.$emit('mounted');
  },
  updated() {
    this.$emit('updated');
  }
};
</script>
父组件示例
vue
<template>
  <div>
    <Child @mounted="onMounted" @updated="onUpdated"/>
  </div>
</template>

<script>
import Child from './Child.vue';

export default {
  components: { Child },
  methods: {
    onMounted() {
      console.log('子组件已挂载');
    },
    onUpdated() {
      console.log('子组件已更新');
    }
  }
};
</script>

2. 使用 @hook: 前缀监听子组件的生命周期钩子

从 Vue 2.3.0 版本开始,可以使用 @hook: 前缀来监听子组件的生命周期钩子。这种方法特别适用于需要监听第三方组件的情况。

父组件示例
vue
<template>
  <div>
    <ThirdPartyComponent @hook:mounted="onMounted" @hook:updated="onUpdated"/>
  </div>
</template>

<script>
import ThirdPartyComponent from 'path/to/third-party-component';

export default {
  components: { ThirdPartyComponent },
  methods: {
    onMounted() {
      console.log('第三方组件已挂载');
    },
    onUpdated() {
      console.log('第三方组件已更新');
    }
  }
};
</script>

注意事项

  • 使用自定义事件的方法比较灵活,适用于大多数情况。
  • 使用 @hook: 前缀的方法更加简洁,但只适用于特定版本的 Vue。
  • 如果子组件的生命周期钩子中需要传递额外的数据给父组件,可以通过 $emit 事件传递参数。
  • 监听子组件的生命周期钩子通常是为了在特定时刻执行一些操作,如初始化、更新数据或执行某些逻辑。

3. 使用 Refs

Vue 提供了 ref 属性,允许父组件引用子组件的实例。通过这个引用,父组件可以直接访问子组件的方法和属性,甚至可以调用子组件的生命周期钩子。

示例
vue
<!-- Parent.vue -->
<template>
  <div>
    <Child ref="childRef" />
    <button @click="onButtonClick">点击触发子组件生命周期</button>
  </div>
</template>

<script>
import Child from './Child.vue';

export default {
  components: { Child },
  methods: {
    onButtonClick() {
      // 强制触发子组件的生命周期钩子
      this.$nextTick(() => {
        this.$refs.childRef.$forceUpdate();
        console.log('强制更新后');
      });
    }
  }
};
</script>
子组件
vue
<!-- Child.vue -->
<template>
  <div>
    <!-- 子组件内容 -->
  </div>
</template>

<script>
export default {
  mounted() {
    console.log('子组件已挂载');
  },
  updated() {
    console.log('子组件已更新');
  }
};
</script>

4. 使用 Vuex

如果应用足够复杂,可以考虑使用 Vuex 状态管理库。通过 Vuex,可以集中管理应用的状态,并且可以在任何地方触发状态的变化。当状态变化时,所有订阅该状态的组件都会自动更新。

示例
javascript
// Vuex Store
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    appData: {} // 用于存储应用的状态
  },
  mutations: {
    updateAppData(state, payload) {
      state.appData = payload;
    }
  },
  actions: {
    updateAppData({ commit }, payload) {
      commit('updateAppData', payload);
    }
  }
});
子组件
vue
<template>
  <div>
    <!-- 子组件内容 -->
    <button @click="updateAppData">更新应用状态</button>
  </div>
</template>

<script>
export default {
  methods: {
    updateAppData() {
      this.$store.dispatch('updateAppData', { key: 'value' });
    }
  }
};
</script>
父组件
vue
<template>
  <div>
    <Child />
    <div>{{ $store.state.appData }}</div>
  </div>
</template>

<script>
import Child from './Child.vue';

export default {
  components: { Child }
};
</script>

5. 使用 Provide/Inject

Vue 提供了 provide 和 inject 选项,允许组件间进行祖先注入。这种方式可以用来传递数据或方法,使得父组件可以知道子组件何时执行了某些生命周期钩子。

示例
javascript
// 父组件
export default {
  provide() {
    return {
      onLifecycleEvent: this.onLifecycleEvent
    };
  },
  methods: {
    onLifecycleEvent(event) {
      console.log(`子组件生命周期事件: ${event}`);
    }
  }
};
子组件
javascript
export default {
  inject: ['onLifecycleEvent'],
  mounted() {
    this.onLifecycleEvent('mounted');
  },
  updated() {
    this.onLifecycleEvent('updated');
  }
};

总结

  • 自定义事件:通过 $emit 触发事件,父组件通过 v-on 监听。
  • @hook: 前缀:直接监听子组件的生命周期钩子。
  • Refs:父组件通过 ref 引用子组件实例,可以调用子组件的方法。
  • Vuex:集中管理状态,当状态变化时,所有订阅该状态的组件都会更新。
  • Provide/Inject:允许组件间进行祖先注入,传递数据或方法。

每种方法都有其适用场景,选择合适的方法取决于你的具体需求和项目结构。

9. vue组件里写的原生addEventListeners监听事件,要手动去销毁吗?为什么?

答: 要,这是因为 Vue 组件在其生命周期结束时会自动清理自己创建的虚拟 DOM 节点以及与之关联的响应式数据,但并不会自动处理原生 DOM 事件监听器。 避免内存泄漏,避免不必要的事件触发

在 Vue 组件中使用原生的 addEventListener 监听 DOM 事件时,确实需要手动销毁这些事件监听器。这是因为 Vue 组件在其生命周期结束时会自动清理自己创建的虚拟 DOM 节点以及与之关联的响应式数据,但并不会自动处理原生 DOM 事件监听器。如果不手动销毁这些监听器,可能会导致内存泄漏。

为什么需要手动销毁?

  1. 内存泄漏

    • 如果不销毁事件监听器,浏览器会保留对 DOM 节点的引用,即使 Vue 组件已经被卸载或销毁,DOM 节点也可能仍然存在于内存中,导致内存泄漏。
  2. 避免不必要的事件触发

    • 即使 Vue 组件不再存在,事件监听器依然有效,这可能导致在不需要的时候触发事件,造成不必要的计算和渲染。

如何手动销毁?

在 Vue 组件中,通常会在组件的生命周期钩子 beforeDestroy 中移除这些监听器。

示例
javascript
export default {
  methods: {
    setupListeners() {
      this.myListener = event => {
        console.log('事件触发了');
      };
      document.addEventListener('click', this.myListener);
    },
    removeListeners() {
      document.removeEventListener('click', this.myListener);
    }
  },
  mounted() {
    this.setupListeners();
  },
  beforeDestroy() {
    this.removeListeners();
  }
};

最佳实践

  1. 使用 Vue 的内置事件绑定

    • 尽量使用 Vue 的内置事件绑定机制(如 v-on 或 .native 修饰符),这些机制会自动处理事件监听器的销毁。
  2. 使用单文件组件的 <listeners> 标签

    • 在 Vue 3 中,可以使用 <listeners> 标签来传递事件监听器,这样可以更方便地管理事件。
  3. 使用 Composition API

    • 在 Vue 3 的 Composition API 中,可以使用 onUnmounted 钩子来清除事件监听器。
Composition API 示例
javascript
import { onMounted, onUnmounted } from 'vue';

export default {
  setup() {
    let myListener;

    const setupListeners = () => {
      myListener = event => {
        console.log('事件触发了');
      };
      document.addEventListener('click', myListener);
    };

    const removeListeners = () => {
      document.removeEventListener('click', myListener);
    };

    onMounted(setupListeners);
    onUnmounted(removeListeners);

    return {};
  }
};

总结

  • 手动销毁:使用原生 addEventListener 时,必须在 Vue 组件的生命周期钩子 beforeDestroy 或 Vue 3 的 onUnmounted 中手动移除事件监听器。
  • 最佳实践:尽量使用 Vue 的内置事件绑定机制来避免手动管理事件监听器。

通过手动销毁事件监听器,可以避免内存泄漏和其他潜在问题,确保应用程序的稳定性和性能。

10. vue中,推荐在哪个生命周期发起请求?

答: created 这样可以尽早获取数据,减少用户等待时间 created

在 Vue 中发起请求的最佳时机取决于你的具体需求,特别是请求的数据是否影响到组件的初始渲染。以下是推荐的一些生命周期钩子:

1. created

  • 优点

    • 组件实例已经创建完成,所有数据观察开始,但是 DOM 还没有生成。
    • 如果请求的数据不影响首次渲染,那么可以在 created 钩子中发起请求。
    • 这样可以尽早获取数据,减少用户等待时间。
  • 缺点

    • 如果请求的数据是渲染组件所必需的,那么在数据加载完成之前,用户可能会看到空白屏幕或部分渲染的内容。

2. beforeMount

  • 优点

    • 组件即将挂载到 DOM 中,此时可以发起请求。
    • 如果请求的数据对于首次渲染是必要的,那么可以在 beforeMount 钩子中发起请求,确保数据加载完成后再渲染。
  • 缺点

    • 由于 beforeMount 钩子紧接着 mounted 钩子,所以实际中很少单独使用此钩子发起请求。

3. mounted

  • 优点

    • 组件已经挂载到 DOM 中,可以访问和操作真实的 DOM。
    • 如果请求的数据对于首次渲染是必要的,并且你需要在数据加载完成后立即操作 DOM,那么可以在 mounted 钩子中发起请求。
  • 缺点

    • 用户可能会在数据加载完成之前看到空白屏幕或部分渲染的内容。

选择建议

  • 如果请求的数据对于首次渲染不是必需的,或者请求的数据只是用于后续的操作而不是立即显示给用户,那么推荐在 created 钩子中发起请求。
  • 如果请求的数据对于首次渲染是必需的,并且你希望在数据加载完成后立即渲染组件,那么可以在 mounted 钩子中发起请求,或者在 created 钩子中发起请求,并在数据加载完成后设置响应式数据,从而触发视图更新。

实例

在 created 钩子中发起请求
javascript
export default {
  data() {
    return {
      items: []
    };
  },
  created() {
    this.fetchData();
  },
  methods: {
    fetchData() {
      axios.get('/api/data')
        .then(response => {
          this.items = response.data;
        })
        .catch(error => {
          console.error('Error fetching data:', error);
        });
    }
  }
};
在 mounted 钩子中发起请求
javascript
export default {
  data() {
    return {
      items: []
    };
  },
  mounted() {
    this.fetchData();
  },
  methods: {
    fetchData() {
      axios.get('/api/data')
        .then(response => {
          this.items = response.data;
        })
        .catch(error => {
          console.error('Error fetching data:', error);
        });
    }
  }
};

结论

选择在哪个生命周期钩子中发起请求主要取决于数据对于首次渲染的重要性。如果数据不是立即需要的,可以在 created 钩子中发起请求;如果数据对于首次渲染至关重要,则可以在 mounted 钩子中发起请求。无论选择哪一个钩子,都应该确保请求失败时有适当的错误处理措施。

推荐在 mounted 生命周期钩子中发起请求。这样做有几个重要的理由:

  1. 确保 DOM 已经被渲染

    • mounted 钩子在组件的 DOM 已经被插入文档之后调用。这意味着你可以确保所有的 DOM 元素都已经存在,如果你的请求结果需要直接操作或依赖这些 DOM 元素,那么在 mounted 中发起请求是安全的。
  2. 避免不必要的请求

    • 在 created 钩子中发起请求有时会导致在组件还没有挂载时请求数据。如果组件在请求完成之前被销毁,可能会引发内存泄漏或不必要的资源浪费。因此,等待组件挂载完成再发起请求可以减少这些潜在问题。
  3. 处理组件状态

    • 在 mounted 钩子中发起请求,能够确保你有机会在请求开始前处理组件的状态(例如设置加载状态),并且在请求完成后更新组件的状态(例如显示数据或处理错误)。

尽管 mounted 是推荐的生命周期钩子,但也有一些特定场景可能需要在 created 钩子中发起请求,例如:

  • SSR(服务器端渲染) :在服务器端渲染中,Vue 实例的 mounted 钩子不会被调用,因为 DOM 并不会被真正挂载。在这种情况下,你可能需要在 created 钩子中发起请求。
  • 依赖数据初始化:如果组件在挂载之前就需要某些数据来初始化,可以在 created 钩子中发起请求,以确保数据在组件挂载时已经可用。