Vue面试

1.Vue响应式原理

整体思路是:数据劫持+观察者模式

通过Object.defineProperty方法和Proxy对象来劫持各个属性的setter、getter,内部Vue追踪依赖,当数据发生变动时发布消息给订阅者,触发相应的监听回调。

Vue2的响应式原理:

Vue2通过Object.defineProperty对data中的属性进行劫持,当属性值发生变化时,会触发对应的更新函数,从而更新视图。

Vue2通过Watcher来实现数据与视图的双向绑定,当数据发生变化时,Watcher会通知对应的视图进行更新。

Vue2的响应式原理存在一些缺陷,例如无法监听数组的变化,需要通过特殊的方法来实现.

Vue3的响应式原理:

Vue3使用Proxy代替了Object.defineProperty,Proxy可以监听到对象的所有属性,包括新增和删除操作。

Vue3使用了WeakMap来存储依赖关系,避免了Vue2中Watcher的内存泄漏问题。

Vue3支持了多个根节点的组件,可以更方便地进行组件的复用和组合。

 原生JS如何实现数据的双向绑定:

步骤 1: 创建HTML结构

首先需要一个HTML元素来展示数据并且允许用户编辑数据。

<input id="a1" oninput="handleChange()" />
<span id="a2"></span>

步骤 2: 编写JavaScript代码

接下来编写JavaScript代码来实现数据和输入框之间的双向绑定

let input = document.getElementById('a1');
    let span = document.getElementById('a2');
    let data = {};

    Object.defineProperty(data, 'val', {
        get: function () {
            return val; 
        },
        set: function (newVal) {
            val = newVal; 
            input.value = val; // 更新 input 的值
            span.innerHTML = val; // 更新 span 的值
        }
    });

    function handleChange() {
        data.val = input.value; // 当输入框值改变时,更新 data.val
    }

2.$route 和 $router的区别

在 Vue.js 框架中,特别是当使用 Vue Router 进行单页应用的路由管理时,$route$router 是两个非常重要的概念。它们都是由 Vue Router 提供的,并且可以在 Vue 组件实例中直接访问。

$router

$router 是一个 Vue Router 实例对象,它包含了操作路由的方法和属性。通过 $router,你可以执行导航、获取所有路由等操作。例如,你可以使用 $router.push() 方法来导航到一个新的路由。

常用方法:

  • push(location): 导航到一个新地址。
  • replace(location): 导航到一个新地址,但不会向 history 添加新记录。
  • go(n): 在 history 中前进或后退 n 步。
  • back(): 返回上一步。
  • forward(): 前进一步。
  • getCurrentRoute(): 获取当前的 route 对象(在 Vue 3 中为 currentRoute)。

$route

$route 是一个只读对象,它代表了当前激活的路由的状态。它包含了一系列与当前 URL 相关的信息,如路径、查询参数等。

常用属性:

  • name: 路由的名称。
  • path: 完整的路径字符串。
  • params: 匹配动态片段和全匹配模式的参数对象。
  • query: 查询参数对象。
  • hash: 哈希部分。
  • meta: 附加元信息的对象。

总结来说,$router 主要用于控制路由跳转和获取路由信息,而 $route 则是用于读取当前路由的信息。

3.为什么data属性是一个函数而不是一个对象

  1. 避免数据污染

    • 当 data 是一个对象时,如果多个 Vue 实例共享同一个构造函数,那么这些实例将会共享同一个 data 对象。这意味着改变其中一个实例的数据会影响到其他实例的数据,这显然是不希望发生的。
  2. 每次创建新实例时返回新的数据对象

    • 使用函数可以保证每次创建新的 Vue 实例时都会调用该函数,从而返回一个新的数据对象。这样可以确保每个实例都有自己独立的数据副本,避免了数据之间的相互影响。
  3. 易于理解和维护

    • 将 data 定义为函数使得代码更加清晰,有助于理解 Vue 实例是如何初始化的,并且有助于维护和调试。
  4. 性能考虑

    • 如果 data 是一个对象,那么在创建多个实例时,所有实例都将引用同一个对象,这可能会导致意外的行为。使用函数可以避免这种性能上的问题,因为每个实例都拥有自己的数据。

总之,Vue.js 设计 data 为一个函数是为了确保每个实例拥有独立的数据绑定,避免实例之间数据的冲突,并且有利于维护和理解代码

4. 说出$nextTick的作用

$nextTick 是 Vue.js 中的一个非常有用的方法,它主要用于确保某些操作在 DOM 更新之后执行。当你修改了数据模型之后,Vue 会异步更新 DOM,以提高性能。如果你需要在数据变化后立即访问更新后的 DOM,就需要使用 $nextTick

主要作用

  1. 等待 DOM 更新

    • 当你改变了组件的状态,但需要确保 DOM 已经完成更新后才能进行某些操作时,可以使用 $nextTick
  2. 确保数据变化后执行回调

    • 如果你需要在数据变化并且视图更新后执行某些操作(比如重新计算布局、触发第三方库的某些功能),可以将这些操作放在 $nextTick 的回调函数中。

总结来说,$nextTick 的主要作用是在数据变化后确保 DOM 更新完毕,这对于依赖于最新 DOM 状态的操作非常重要。

5. diff算法

diff 算法通常指的是用于比较和更新虚拟 DOM 树的算法。

Diff 算法的核心思想

Diff 算法的主要目标是确定哪些部分需要更新,从而最小化实际 DOM 的操作次数。这是因为直接操作 DOM 是昂贵的操作,而虚拟 DOM 的更新则相对便宜。

Vue.js 的 Diff 算法

Vue.js 也采用了一种高效的 diff 算法来更新 DOM。Vue 的 diff 算法同样关注于最小化 DOM 更新,但它有一些不同的策略:

  1. 异步更新队列:Vue 将数据变化收集起来,在下一个事件循环开始时一次性更新 DOM,这有助于减少不必要的重绘和重排。
  2. 局部更新:Vue 只更新发生变化的部分,而不是整个树。
  3. 利用缓存:Vue 会缓存一些信息,如节点的位置,以便更快地定位需要更新的节点。

关键步骤

无论是 React 还是 Vue.js,diff 算法通常包括以下步骤:

  1. 标识差异:比较新旧虚拟 DOM 树,找到需要更新的节点。
  2. 最小化更新:确定最少量的更新操作,如添加、删除、移动或替换节点。
  3. 应用更新:将这些更新应用到实际 DOM 上。

Diff 算法是现代前端框架中的关键技术之一,它通过比较虚拟 DOM 树的变化来最小化实际 DOM 的更新,从而提高应用程序的性能。

 6.Vue3相比Vue2的优势

Vue 3 引入了新的响应式系统,使用 Proxy API 替换了 Object.defineProperty,这使得依赖追踪更加高效且易于实现。

 Vue 3 提供了 Composition API,这是一种新的编程模型,可以帮助开发者更好地组织和复用逻辑代码。这使得组件内部的逻辑更加清晰和模块化。

Vue 3 对 TypeScript 的支持更加完善,提供了更好的类型定义,使得类型检查更加精确,同时降低了使用 TypeScript 的门槛。

7.Vue2里的性能优化

v-for 遍历避免同时使用 v-if
  1. 性能问题:每次循环都会检查 v-if 的条件,这可能导致不必要的计算,特别是在数组较大时。

  2. 渲染效率低:由于 v-if 会在每次循环时都被评估,即使某些项最终不会被渲染,也会增加渲染时间。

  3. 可维护性差:同时使用 v-ifv-for 可能会使代码难以阅读和维护,尤其是当条件变得复杂时。

为了避免这些问题,推荐的做法是提前过滤数据:使用Computed、v-show替代v-if、使用filter()

 巧用计算属性

计算属性是一种处理数据用来解决代码的冗余的方式,当处理不需要常变化的值推荐使用这个。计算属性就有缓存机制,这种机制可以提高我们的性能。

使用防抖与节流控制发送频率
路由守卫处理请求避免重复发送请求

8.扁平化数组代码实现

1. 使用 flat() 方法

ES2019 引入了 flat() 方法,这是一个非常方便的方法来扁平化数组。它可以接受一个可选的深度参数,该参数指定了应该扁平化的深度。

const nestedArray = [1, 2, [3, 4], 5, [6, [7, 8]]];

// 默认只展开一层
const partiallyFlattened = nestedArray.flat();
console.log(partiallyFlattened); // 输出: [1, 2, 3, 4, 5, 6, [7, 8]]

// 展开所有层级
const fullyFlattened = nestedArray.flat(Infinity);
console.log(fullyFlattened); // 输出: [1, 2, 3, 4, 5, 6, 7, 8]
2. 使用递归

如果你无法使用 flat() 方法或者需要更复杂的逻辑,可以使用递归来手动扁平化数组

function flatten(array) {
    return array.reduce((acc, val) => 
        Array.isArray(val) ? acc.concat(flatten(val)) : acc.concat(val), []);
}

const nestedArray = [1, 2, [3, 4], 5, [6, [7, 8]]];
const flattened = flatten(nestedArray);
console.log(flattened); // 输出: [1, 2, 3, 4, 5, 6, 7, 8]
3. 使用 reduce() 和 concat()

你可以使用 reduce() 方法结合 concat() 方法来扁平化数组。

const nestedArray = [1, 2, [3, 4], 5, [6, [7, 8]]];

const flattened = nestedArray.reduce((acc, val) => 
    Array.isArray(val) ? acc.concat(flatten(val)) : acc.concat(val), []);

console.log(flattened); // 输出: [1, 2, 3, 4, 5, 6, 7, 8]

9.数组去重

 1.利用新旧数组遍历对比法
arr=[1,5,1,3,5,4,3,9,8]
 
let newArr = [];
 /*   
    indexOf用于查找数组元素第一次出现的位置,没找到则返回值为-1,参数有两个,第一个为元素项目,参数二(可选)需要查找的位置,负数从-1往前面加 
 */
for (let i=0;i<arr.length;i++) {
    if (newArr.indexOf(arr[i]) === -1) {
	    newArr.push(arr[i]);
    }
  }
console.log(newArr);//[1, 5, 3, 4, 9, 8]
2.利用新语法 new Set()
arr=[1,5,1,3,5,4,3,9,8]
let mySet = new Set(arr); // 非重复的类数组
console.log(mySet,'mySet');//{{1, 5, 3, 4, 9,8}
// let newArr = Array.from(mySet); // set转数组
let newArr = [...mySet]; // 或者是这种解构方法
console.log(newArr);//[1, 5, 3, 4, 9, 8]
3.使用includes()
arr=[1,5,1,3,5,4,3,9,8]
let newArr = [];
for (let i=0;i<arr.length;i++) {
	if (!newArr.includes(arr[i])) {
		newArr.push(arr[i]);
	}
}
console.log(newArr);//[1, 5, 3, 4, 9, 8]

10.事件循环机制

在前端开发中,事件循环(Event Loop)机制是 JavaScript 引擎处理异步任务的关键机制之一。事件循环负责协调同步任务与异步任务的执行顺序,确保 JavaScript 的执行是单线程的同时又能处理异步操作。

事件循环理论先执行同步任务,再去执行我们的异步任务(先执行微任务再执行宏任务)。

实例:

console.log('Start');

setTimeout(() => {
  console.log('Timer 1');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise');
});

setTimeout(() => {
  console.log('Timer 2');
}, 0);

console.log('End');

输出结果:

Start

End

Promise

Timer 1

Timer 2

解释:

  1. 首先输出 "Start"。
  2. setTimeout 设置了两个延时 0ms 的定时器任务,它们会被放入消息队列。
  3. Promise.resolve().then() 是一个微任务,将在当前宏任务(全局执行上下文)结束后立即执行。
  4. 最后输出 "End"。
  5. 微任务执行,输出 "Promise"。
  6. 事件循环处理消息队列中的第一个宏任务,输出 "Timer 1"。
  7. 处理第二个宏任务,输出 "Timer 2"。

11.事件冒泡

事件冒泡(Event Bubbling)是一种重要的事件传播机制。当用户与网页上的某个元素进行交互时(如点击按钮),浏览器会触发相应的事件。事件冒泡描述了事件如何从触发事件的最内层元素开始,逐步向上传播到父元素,直至到达文档的根元素的过程。

事件冒泡的特点

  • 从内到外:事件首先在最内层的元素上触发,然后逐步向上传播到父元素,直到到达文档的根元素(通常是 <html> 元素)。
  • 事件捕获与事件冒泡:与事件冒泡相对应的是事件捕获(Event Capturing),这是一种相反的传播机制,事件首先从根元素开始向下传播,直到到达最内层的元素。不过,事件捕获在实际开发中较少使用。
  • 阻止事件冒泡:可以通过特定的方法来阻止事件的冒泡行为,例如使用 event.stopPropagation()

事件冒泡的用途

  • 事件委托:事件冒泡使得事件委托成为可能,这是一种优化事件处理的常用技术。通过在较高级别的元素上绑定事件处理器,而不是在每个子元素上单独绑定,可以减少事件监听器的数量,从而提高性能。
  • 简化事件处理:事件冒泡简化了事件处理逻辑,因为可以将事件处理器绑定到父元素上,而不必为每个子元素单独绑定。

示例

下面是一个简单的 HTML 示例,演示了事件冒泡的工作原理:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>事件冒泡示例</title>
<script>
document.addEventListener('DOMContentLoaded', function() {
  const div = document.getElementById('outer');
  const button = document.getElementById('button');

  // 为外部 div 绑定事件处理器
  div.addEventListener('click', function(event) {
    console.log('Div clicked');
  });

  // 为按钮绑定事件处理器
  button.addEventListener('click', function(event) {
    console.log('Button clicked');
  });
});
</script>
</head>
<body>
<div id="outer" style="border: 1px solid black; padding: 10px;">
  <button id="button">Click me!</button>
</div>
</body>
</html>

分析

在这个示例中:

  • 当点击按钮时,首先会触发按钮上的点击事件处理器,输出 "Button clicked"。
  • 然后事件会冒泡到外部的 div 元素,触发该元素上的点击事件处理器,输出 "Div clicked"。

阻止事件冒泡

如果你想阻止事件冒泡,可以使用 event.stopPropagation() 方法。下面是一个修改过的示例,展示了如何阻止事件冒泡:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>阻止事件冒泡示例</title>
<script>
document.addEventListener('DOMContentLoaded', function() {
  const div = document.getElementById('outer');
  const button = document.getElementById('button');

  // 为外部 div 绑定事件处理器
  div.addEventListener('click', function(event) {
    console.log('Div clicked');
  });

  // 为按钮绑定事件处理器,并阻止事件冒泡
  button.addEventListener('click', function(event) {
    console.log('Button clicked');
    event.stopPropagation(); // 阻止事件冒泡
  });
});
</script>
</head>
<body>
<div id="outer" style="border: 1px solid black; padding: 10px;">
  <button id="button">Click me!</button>
</div>
</body>
</html>

事件冒泡是前端开发中一个非常重要的概念,它描述了事件如何从触发事件的最内层元素开始,逐步向上传播到父元素的过程。理解事件冒泡有助于优化事件处理逻辑,并且对于实现事件委托等高级功能至关重要。

12.闭包 

闭包是由函数和与其相关的引用环境组合而成的实体。换句话说,闭包就是能够读取其他函数内部变量的函数。在 JavaScript 中,每当一个函数被创建时,它就会形成一个闭包,因为它可以访问其外部作用域中的变量。

function outerFunction() {
  let count = 0;

  function innerFunction() {
    count++;
    console.log(count);
  }

  return innerFunction;
}

const incrementCounter = outerFunction();

incrementCounter(); // 输出: 1
incrementCounter(); // 输出: 2
incrementCounter(); // 输出: 3

闭包的注意事项

  1. 内存泄漏

    • 闭包可能会导致内存泄漏,因为它们可以让变量一直存在于内存中,即使这些变量不再被需要。
    • 请注意清理不再需要的引用,以避免内存泄漏。
  2. 性能考量

    • 创建大量的闭包可能会消耗较多的内存,尤其是在大型应用程序中。
    • 在适当的地方使用闭包,并注意性能优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值