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属性是一个函数而不是一个对象
-
避免数据污染:
- 当
data
是一个对象时,如果多个 Vue 实例共享同一个构造函数,那么这些实例将会共享同一个data
对象。这意味着改变其中一个实例的数据会影响到其他实例的数据,这显然是不希望发生的。
- 当
-
每次创建新实例时返回新的数据对象:
- 使用函数可以保证每次创建新的 Vue 实例时都会调用该函数,从而返回一个新的数据对象。这样可以确保每个实例都有自己独立的数据副本,避免了数据之间的相互影响。
-
易于理解和维护:
- 将
data
定义为函数使得代码更加清晰,有助于理解 Vue 实例是如何初始化的,并且有助于维护和调试。
- 将
-
性能考虑:
- 如果
data
是一个对象,那么在创建多个实例时,所有实例都将引用同一个对象,这可能会导致意外的行为。使用函数可以避免这种性能上的问题,因为每个实例都拥有自己的数据。
- 如果
总之,Vue.js 设计
data
为一个函数是为了确保每个实例拥有独立的数据绑定,避免实例之间数据的冲突,并且有利于维护和理解代码
4. 说出$nextTick的作用
$nextTick
是 Vue.js 中的一个非常有用的方法,它主要用于确保某些操作在 DOM 更新之后执行。当你修改了数据模型之后,Vue 会异步更新 DOM,以提高性能。如果你需要在数据变化后立即访问更新后的 DOM,就需要使用 $nextTick
。
主要作用
-
等待 DOM 更新:
- 当你改变了组件的状态,但需要确保 DOM 已经完成更新后才能进行某些操作时,可以使用
$nextTick
。
- 当你改变了组件的状态,但需要确保 DOM 已经完成更新后才能进行某些操作时,可以使用
-
确保数据变化后执行回调:
- 如果你需要在数据变化并且视图更新后执行某些操作(比如重新计算布局、触发第三方库的某些功能),可以将这些操作放在
$nextTick
的回调函数中。
- 如果你需要在数据变化并且视图更新后执行某些操作(比如重新计算布局、触发第三方库的某些功能),可以将这些操作放在
总结来说,
$nextTick
的主要作用是在数据变化后确保 DOM 更新完毕,这对于依赖于最新 DOM 状态的操作非常重要。
5. diff算法
diff 算法通常指的是用于比较和更新虚拟 DOM 树的算法。
Diff 算法的核心思想
Diff 算法的主要目标是确定哪些部分需要更新,从而最小化实际 DOM 的操作次数。这是因为直接操作 DOM 是昂贵的操作,而虚拟 DOM 的更新则相对便宜。
Vue.js 的 Diff 算法
Vue.js 也采用了一种高效的 diff 算法来更新 DOM。Vue 的 diff 算法同样关注于最小化 DOM 更新,但它有一些不同的策略:
- 异步更新队列:Vue 将数据变化收集起来,在下一个事件循环开始时一次性更新 DOM,这有助于减少不必要的重绘和重排。
- 局部更新:Vue 只更新发生变化的部分,而不是整个树。
- 利用缓存:Vue 会缓存一些信息,如节点的位置,以便更快地定位需要更新的节点。
关键步骤
无论是 React 还是 Vue.js,diff 算法通常包括以下步骤:
- 标识差异:比较新旧虚拟 DOM 树,找到需要更新的节点。
- 最小化更新:确定最少量的更新操作,如添加、删除、移动或替换节点。
- 应用更新:将这些更新应用到实际 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
-
性能问题:每次循环都会检查
v-if
的条件,这可能导致不必要的计算,特别是在数组较大时。 -
渲染效率低:由于
v-if
会在每次循环时都被评估,即使某些项最终不会被渲染,也会增加渲染时间。 -
可维护性差:同时使用
v-if
和v-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
解释:
- 首先输出 "Start"。
setTimeout
设置了两个延时 0ms 的定时器任务,它们会被放入消息队列。Promise.resolve().then()
是一个微任务,将在当前宏任务(全局执行上下文)结束后立即执行。- 最后输出 "End"。
- 微任务执行,输出 "Promise"。
- 事件循环处理消息队列中的第一个宏任务,输出 "Timer 1"。
- 处理第二个宏任务,输出 "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
闭包的注意事项
-
内存泄漏:
- 闭包可能会导致内存泄漏,因为它们可以让变量一直存在于内存中,即使这些变量不再被需要。
- 请注意清理不再需要的引用,以避免内存泄漏。
-
性能考量:
- 创建大量的闭包可能会消耗较多的内存,尤其是在大型应用程序中。
- 在适当的地方使用闭包,并注意性能优化。