20250108慧能科技前端面试
目录
- ajax 怎么取消请求
- 移动端怎么实现 px 尺寸
- vite 和 webpack 的区别
- 设计模式
- 讲一下什么是原型链
- 讲一下什么是闭包
- 实现 eventbus
- 事件循环
- 项目发布后,如何对项目进行优化,怎么优化
- vue2 的 diff 算法和 vue3 的 diff 算法的区别
1. ajax 怎么取消请求
原生 JavaScript(XMLHttpRequest)
创建XMLHttpRequest
对象发送请求,通过调用其abort()
方法来取消请求。示例代码如下:
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.github.com/");
xhr.send();
// 在需要取消请求的地方
xhr.abort();
jQuery
使用$.ajax()
或$.get()
等方法发送请求,将返回的jqXHR
对象保存起来,再调用abort()
方法取消请求。示例代码如下:
// 用$.ajax发送请求
var jq = $.ajax({
type: "get",
url: "https://api.github.com/",
dataType: "json",
success: function(data) {
console.log(data);
},
error: function(err) {
console.log(err);
}
});
// 取消请求
jq.abort();
// 用$.get发送请求
var request = $.get(url, function(data) {
// 请求成功的回调函数
});
// 停止请求
request.abort();
axios
通过axios.CancelToken.source()
方法创建取消令牌source
,将source.token
添加到请求配置中,在需要取消请求时调用source.cancel()
方法。示例代码如下:
import axios from 'axios';
// 创建取消标记
const source = axios.CancelToken.source();
axios.get('/api/data', { cancelToken: source.token })
.then(response => {
// 请求成功处理
})
.catch(error => {
if (axios.isCancel(error)) {
console.log('请求被取消', error.message);
} else {
console.log('请求出错', error.message);
}
});
// 取消请求
source.cancel('请求取消的原因');
使用fetch API
使用AbortController
和AbortSignal
来中止请求。创建AbortController
实例,将其signal
属性传递给fetch
的配置对象,在需要取消请求时调用controller.abort()
方法。示例代码如下:
const controller = new AbortController();
const { signal } = controller;
fetch('https://jsonplaceholder.typicode.com/todos/1', { signal })
.catch(error => {
if (error.name === 'AbortError') {
console.log('fetch aborted');
} else {
console.error('fetch error:', error);
}
});
// 取消请求
setTimeout(() => {
controller.abort();
}, 1000);
2. 移动端怎么实现 px 尺寸
让我详细解释移动端 rem 的处理方法:
1. 基本概念
rem 是相对于根元素(html)的字体大小的单位。例如,如果根元素的 font-size 是 16px,那么 1rem = 16px。
2. 常用的 rem 适配方案
// 方案1:基础版本
function setRem() {
// 基准大小
const baseSize = 16;
// 设计稿宽度
const designWidth = 375;
// 获取当前视窗宽度
const width = document.documentElement.clientWidth;
// 计算缩放比例
const scale = width / designWidth;
// 设置根元素font-size
document.documentElement.style.fontSize = baseSize * scale + "px";
}
// 初始化
setRem();
// 改变窗口大小时重新设置
window.addEventListener("resize", setRem);
3. 更完善的 rem 方案
// 方案2:完整版本
(function flexible(window, document) {
const docEl = document.documentElement;
const dpr = window.devicePixelRatio || 1;
// 调整body字体大小
function setBodyFontSize() {
if (document.body) {
document.body.style.fontSize = "12px";
} else {
document.addEventListener("DOMContentLoaded", setBodyFontSize);
}
}
setBodyFontSize();
// 设置rem函数
function setRemUnit() {
const rem = docEl.clientWidth / 10;
docEl.style.fontSize = rem + "px";
}
setRemUnit();
// 监听resize事件
window.addEventListener("resize", setRemUnit);
window.addEventListener("pageshow", function (e) {
if (e.persisted) {
setRemUnit();
}
});
// 设置data-dpr属性
if (dpr >= 2) {
const fakeBody = document.createElement("body");
const testElement = document.createElement("div");
testElement.style.border = ".5px solid transparent";
fakeBody.appendChild(testElement);
docEl.appendChild(fakeBody);
if (testElement.offsetHeight === 1) {
docEl.classList.add("hairlines");
}
docEl.removeChild(fakeBody);
}
})(window, document);
4. 配合 CSS 预处理器(以 SCSS 为例)
// _rem.scss
@function rem($px) {
$rem: 37.5px; // 基准值,根据设计稿来定
@return ($px / $rem) + rem;
}
// 使用示例
.container {
width: rem(375); // 10rem
height: rem(100); // 2.67rem
font-size: rem(16); // 0.427rem
margin: rem(10); // 0.267rem
}
5. 使用 PostCSS 插件
// postcss.config.js
module.exports = {
plugins: {
"postcss-pxtorem": {
rootValue: 37.5, // 设计稿宽度的1/10
propList: ["*"], // 需要转换的属性
selectorBlackList: [], // 不需要转换的选择器
replace: true,
mediaQuery: false,
minPixelValue: 0,
},
},
};
6. Vite 配置示例
// vite.config.js
import postcssPxtorem from "postcss-pxtorem";
export default {
css: {
postcss: {
plugins: [
postcssPxtorem({
rootValue: 37.5,
propList: ["*"],
selectorBlackList: [".norem"],
}),
],
},
},
};
7. HTML 模板设置
<!DOCTYPE html>
<html>
<head>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0,
maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
/>
<!-- 引入rem设置脚本 -->
<script src="./flexible.js"></script>
</head>
<body>
<!-- 内容 -->
</body>
</html>
8. 实际使用示例
<!-- Vue组件示例 -->
<template>
<div class="container">
<div class="box">内容</div>
</div>
</template>
<style scoped lang="scss">
.container {
width: rem(375);
padding: rem(15);
.box {
font-size: rem(14);
line-height: rem(20);
margin-bottom: rem(10);
}
}
</style>
9. 注意事项
- 设计稿尺寸
// 常见设计稿宽度
const designWidth = 375; // iPhone 6/7/8
const designWidth = 750; // 2倍图
- 1px 边框问题
// 解决1px边框问题
.border-1px {
position: relative;
&::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 1px;
background: #000;
transform: scaleY(0.5);
}
}
- 字体不使用 rem
// 字体使用px
.text {
font-size: 14px; // 不转换为rem
}
10. 最佳实践建议
- 使用 PostCSS 自动转换
- 配合 CSS 预处理器
- 考虑 1px 边框问题
- 注意字体大小处理
- 合理设置 viewport
- 考虑兼容性问题
这样的 rem 方案可以很好地处理移动端的适配问题,使页面在不同设备上都能保持良好的显示效果。
3. vite 和 webpack 的区别
1. 开发时的启动速度
- Vite:
- 利用现代浏览器原生支持的 ES 模块(ESM),在开发模式下不进行打包操作,而是通过服务器根据请求的文件进行即时编译和转换。这使得开发服务器的启动几乎是瞬间的,无需像 Webpack 那样等待整个项目的构建完成。
- 例如,当你在浏览器中请求一个模块时,Vite 会将该模块及其依赖进行即时编译和传输,避免了 Webpack 中打包整个项目的时间开销。
- Webpack:
- 在开发模式下,会将整个项目打包成一个或多个 bundle 文件,随着项目规模的增大,启动开发服务器的时间会显著增加,因为它需要分析整个项目的依赖树,对每个模块进行处理和打包。
2. 热模块替换(HMR)性能
- Vite:
- 由于采用基于 ESM 的 HMR,只需要对修改的模块进行重新编译和更新,更新速度非常快。并且由于 Vite 是按需编译,不会因为项目规模的增长而显著影响 HMR 的性能。
- 当你修改了一个文件时,Vite 能快速将其编译并更新到浏览器中,提高开发效率。
- Webpack:
- 虽然也支持 HMR,但由于其需要对整个打包的 bundle 进行更新,在处理复杂项目时,HMR 速度会受到影响,并且可能会导致整个页面的刷新,影响开发体验。
3. 构建性能
- Vite:
- 在生产环境中,Vite 利用 Rollup 进行构建,利用 Rollup 的优势,可以生成更优化的构建结果。同时,Vite 的预配置已经考虑了许多优化,减少了手动配置的需要。
- 对于大型项目,Vite 可以利用其按需编译的特点,在一定程度上提高构建速度,尤其是在处理大量模块时。
- Webpack:
- 需要进行大量的配置和优化才能达到较好的构建性能,对于复杂的项目,配置的复杂性会进一步增加,并且需要使用一些插件来优化构建过程,如
terser-webpack-plugin
等。
- 需要进行大量的配置和优化才能达到较好的构建性能,对于复杂的项目,配置的复杂性会进一步增加,并且需要使用一些插件来优化构建过程,如
4. 配置复杂度
- Vite:
- 开箱即用,对于许多常见的前端项目,Vite 的默认配置就可以满足需求,并且配置文件通常比 Webpack 简洁。
- 例如,使用 Vite 开发 Vue 或 React 项目,只需要少量的配置就可以启动开发服务器和进行生产构建。
- Webpack:
- 功能强大,但配置相对复杂,对于新手来说,要达到理想的开发和构建效果,需要学习大量的配置选项和插件。
5. 对现代前端框架的支持
- Vite:
- 对 Vue 3 有很好的原生支持,其开发体验流畅,并且也支持 React 等其他现代框架,其开发服务器和构建过程可以很好地与这些框架的开发流程相结合。
- 在 Vue 3 项目中,Vite 可以更好地利用 Vue 3 的特性,提供更好的开发体验。
- Webpack:
- 可以通过各种插件支持 Vue 3、React 等,但需要更多的配置和插件的配合,而且不同插件之间的兼容性和性能需要开发者自己平衡。
6. 插件生态
- Vite:
- 虽然起步相对较晚,但 Vite 的插件生态正在迅速发展,已经有许多高质量的插件可以满足不同的需求,并且 Vite 的插件 API 相对简洁。
- 可以方便地开发和使用插件来扩展 Vite 的功能,如对 CSS 预处理器的支持、对 TypeScript 的支持等。
- Webpack:
- 拥有庞大的插件生态,几乎可以满足任何需求,但由于其插件 API 的复杂性,开发和使用插件的门槛相对较高。
7. 类型支持
- Vite:
- 对于 TypeScript 的支持较为友好,在开发和构建过程中可以很好地处理 TypeScript 文件,不需要复杂的额外配置。
- 可以在 Vite 项目中轻松地使用 TypeScript 进行开发,而不会增加太多的配置负担。
- Webpack:
- 需要配置
ts-loader
或babel-loader
来处理 TypeScript 文件,并且需要仔细调整配置以确保类型检查和构建的顺利进行。
- 需要配置
综上所述,Vite 在开发时的启动速度、HMR 性能、配置的简洁性、对现代前端框架的支持、插件生态的易用性以及对 TypeScript 的支持等方面都有一定的优势,尤其是对于开发体验和开发效率有更高要求的项目,Vite 是一个很好的选择。然而,Webpack 仍然是一个强大的工具,对于一些复杂的、需要高度定制化的项目,Webpack 的丰富插件和强大的配置能力可以更好地满足需求。在选择时,可以根据项目的具体情况和团队的经验来决定使用哪种工具。
在面试中回答这个问题时,可以结合实际的项目经验,例如:“在我之前的项目中,使用 Vite 开发一个 Vue 3 项目,开发服务器的启动速度非常快,几乎是瞬间完成,而之前使用 Webpack 时,启动时间会随着项目规模的增加而显著增加。而且 Vite 的 HMR 性能很好,修改代码后可以立即看到效果,无需长时间等待,相比之下,Webpack 的 HMR 有时会出现整个页面刷新的情况,影响开发体验。Vite 的配置也更加简洁,对于 TypeScript 的处理也很方便,而在使用 Webpack 时,需要更多的配置来处理 TypeScript 模块和实现类似的开发体验。不过,如果是一个需要高度定制化的大型项目,Webpack 可以通过其丰富的插件和复杂的配置来满足需求,但这也需要更多的时间和精力去配置和维护。”
通过这样的回答,可以向面试官展示你对两种打包工具的深入了解和在实际项目中的应用经验。
4. 设计模式
定义
设计模式即 Software Design Pattern,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。
类型
- 创建型模式:主要关注对象的创建机制,将对象的创建过程进行抽象和封装,使系统创建对象更灵活、高效且可维护。比如单例模式确保一个类只有一个实例,并提供一个全局访问点;工厂模式定义一个用于创建对象的接口,让子类决定实例化哪一个类。
- 结构型模式:主要关注如何将不同的类和对象组合成更大的结构,以实现特定的功能和架构。例如适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作,将一个类的接口转换成客户希望的另外一个接口;桥接模式将抽象部分与它的实现部分分离,使它们都可以独立地变化。
- 行为型模式:主要关注对象之间的交互和职责分配,侧重于描述对象在运行时的行为以及它们之间如何协作、通信和完成特定任务。比如策略模式定义一系列算法,将每个算法封装起来,并让它们可以相互替换,实现同一功能的不同实现;观察者模式定义对象间的一种一对多的依赖关系,当一个对象状态发生改变时,所有依赖它的对象都得到通知并自动更新。
原则
- 单一职责原则:一个类应该只有一个引起它变化的原因,即一个类只负责一项职责。
- 开放-封闭原则:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
- 里氏替换原则:在任何使用父类的地方都可以用其子类来替换。
- 依赖倒置原则:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
- 接口隔离原则:客户端不应该被迫依赖于它不使用的接口。
- 迪米特法则:一个对象应该对其他对象保持最少的了解。
优点
- 提高代码质量:使代码结构更加清晰、合理和健壮。
- 增强可读性和可理解性:遵循特定模式的代码更容易被其他人理解和掌握。
- 促进代码复用:减少重复代码的编写,提高开发效率。
- 提高系统的灵活性和可扩展性:可以方便地进行功能扩展和修改,而对其他部分影响较小。
- 增强系统的稳定性:经过实践验证的模式有助于避免一些常见的错误和问题。
- 便于团队协作:团队成员对设计模式有共同的认知,便于沟通和协作。
5. 讲一下什么是原型链
让我为您详细讲解JavaScript中的原型链(Prototype Chain):
原型链的定义
原型链是JavaScript实现继承的主要方式。每个对象都有一个内部属性 [[Prototype]]
(可以通过__proto__
访问),指向其原型对象。当查找一个对象的属性时,如果对象本身没有这个属性,就会沿着原型链向上查找。
关键概念
- prototype:函数的一个属性,指向原型对象
- proto:对象的内部属性,指向其构造函数的prototype
- constructor:原型对象的属性,指向构造函数本身
代码示例
// 基本示例
function Animal(name) {
this.name = name;
}
Animal.prototype.sayName = function() {
return this.name;
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
// 建立继承关系
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
const myDog = new Dog('旺财', '柴犬');
console.log(myDog.sayName()); // "旺财"
原型链查找顺序
- 查找对象自身的属性
- 查找对象的
__proto__
(即其构造函数的prototype) - 继续查找
__proto__
的__proto__
- 直到找到属性或达到原型链的末端(null)
图解原型链
// 原型链结构示例
Object.prototype.__proto__ === null
Function.prototype.__proto__ === Object.prototype
Array.prototype.__proto__ === Object.prototype
myDog.__proto__ === Dog.prototype
Dog.prototype.__proto__ === Animal.prototype
Animal.prototype.__proto__ === Object.prototype
实际应用示例
// 使用原型链实现继承
function Vehicle() {
this.isVehicle = true;
}
Vehicle.prototype.move = function() {
return '移动中...';
};
function Car(brand) {
Vehicle.call(this);
this.brand = brand;
}
// 设置原型链
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;
// 添加Car特有的方法
Car.prototype.honk = function() {
return '嘟嘟!';
};
const myCar = new Car('Toyota');
console.log(myCar.move()); // "移动中..."
console.log(myCar.honk()); // "嘟嘟!"
console.log(myCar.isVehicle); // true
常见面试问题和要点
- 原型链的作用
- 实现继承
- 共享方法和属性
- 节省内存
- 原型链的性能考虑
- 原型链越长,属性查找越慢
- 应该适度使用继承,避免过深的继承层次
- ES6 class与原型链
// ES6 class语法其实是原型链的语法糖
class Animal {
constructor(name) {
this.name = name;
}
sayName() {
return this.name;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
}
- 常见陷阱
- 原型属性的共享问题(特别是引用类型)
- 构造函数忘记调用super()
- 修改原型后忘记重置constructor
最佳实践
- 优先使用ES6的class语法
- 避免直接操作
__proto__
- 使用
Object.create()
设置原型 - 注意维护constructor属性
- 合理设计继承深度
面试回答技巧
- 先解释概念
- 画图说明原型链结构
- 举例说明实际应用
- 讨论优缺点和最佳实践
- 展示对ES6新特性的了解
这样的回答既有理论深度,又有实践经验,同时还可以引申讨论JavaScript面向对象编程的其他方面。
6. 讲一下什么是闭包
闭包的定义
闭包是指一个函数能够访问并记住其词法作用域中的变量,即使该函数在其原始作用域之外执行时也是如此。简单来说,闭包就是一个函数和其周围状态(词法环境)的引用的组合。
闭包的特点
- 函数嵌套:闭包涉及到函数内部定义的函数
- 数据私有:闭包可以创建私有变量和方法
- 数据持久:闭包可以维持函数内部变量的值,不会随外部函数的调用结束而销毁
代码示例
function createCounter() {
let count = 0; // 私有变量
return {
increment: function() {
count++;
return count;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.getCount()); // 输出: 0
console.log(counter.increment()); // 输出: 1
console.log(counter.increment()); // 输出: 2
闭包的应用场景
- 数据私有化
- 创建私有变量和方法
- 模块化开发
- 函数工厂
- 创建具有特定行为的函数
- 柯里化(Currying)
- 将多参数函数转换为一系列单参数函数
- 事件处理和回调
- 在异步编程中保持状态
闭包的优缺点
优点:
- 可以避免全局变量的污染
- 提供了数据的私有性
- 能够维护状态
缺点:
- 可能造成内存泄露(需要注意及时解除引用)
- 闭包会携带包含它的函数的作用域,因此会比普通函数占用更多内存
实际应用示例
// 实现一个简单的模块模式
const modulePattern = (function() {
// 私有变量
let privateVar = 'I am private';
// 私有方法
function privateMethod() {
return 'This is private';
}
return {
// 公共方法
publicMethod: function() {
return privateVar;
},
anotherPublicMethod: function() {
return privateMethod();
}
};
})();
面试要点提示
- 回答时先给出简洁的定义
- 然后用简单的例子说明
- 说明实际应用场景
- 最后可以谈谈优缺点
- 如果面试官继续追问,可以深入讨论内存管理、作用域链等相关概念
这样的回答既展示了对闭包的理解,又体现了实践经验,同时还可以引申出其他相关的重要概念。
7. 实现 eventbus
实现一个简单的 EventBus 可以帮助你在不同的组件或模块之间进行通信。EventBus 通常用于发布-订阅模式,允许组件之间发送和接收事件,而不需要直接引用彼此。
以下是一个用 JavaScript 实现的简单 EventBus 示例:
class EventBus {
constructor() {
this.events = {};
}
// 订阅事件
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
// 发布事件
emit(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => callback(data));
}
}
// 移除事件监听
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
}
}
}
// 使用示例
const eventBus = new EventBus();
// 订阅事件
eventBus.on('customEvent', (data) => {
console.log('Received data:', data);
});
// 发布事件
eventBus.emit('customEvent', { message: 'Hello, EventBus!' });
// 移除事件监听
eventBus.off('customEvent', (data) => {
console.log('Received data:', data);
});
解释
- 构造函数 (
constructor
): 初始化一个空的事件对象,用于存储事件名称和对应的回调函数数组。 - 订阅事件 (
on
): 将回调函数添加到指定事件名称的数组中。 - 发布事件 (
emit
): 遍历指定事件名称的回调函数数组,并调用每个回调函数,传递数据。 - 移除事件监听 (
off
): 从指定事件名称的回调函数数组中移除指定的回调函数。
这个简单的 EventBus 实现可以满足大多数前端项目中的基本需求。如果你需要更复杂的功能,比如异步处理、错误处理等,可以在此基础上进行扩展。
8. 事件循环
事件循环是 JavaScript 处理异步操作的核心机制,确保 JavaScript 在单线程环境下能够高效地处理各种任务,避免阻塞。以下是事件循环的详细解释:
基本概念
- 单线程执行模型:JavaScript 是单线程的,即在同一时间内只能执行一个任务。这意味着 JavaScript 代码按顺序执行,不会出现多个任务同时执行的情况。但为了处理异步操作,JavaScript 引入了事件循环机制,使它可以在等待某些操作完成时继续执行其他代码。
核心组件
-
执行栈(Call Stack):
- 执行栈是一个后进先出(LIFO)的数据结构,用于存储当前正在执行的函数调用。
- 当一个函数被调用时,它会被压入执行栈;当函数执行完成,它会从栈中弹出。
- 例如:
function first() { second(); } function second() { third(); } function third() { console.log('Hello, World!'); } first();
- 调用
first()
时,first
函数会被压入执行栈;first
函数调用second()
,second
函数会被压入执行栈;second
函数调用third()
,third
函数会被压入执行栈;third
函数执行并打印Hello, World!
,然后third
函数从栈中弹出,接着second
函数弹出,最后first
函数弹出。
-
任务队列(Task Queue):
- 任务队列存储着等待执行的任务,主要是异步操作的回调函数。
- 任务队列可以分为宏任务队列(Macrotask Queue)和微任务队列(Microtask Queue)。
宏任务与微任务
-
宏任务(Macrotasks):
- 常见的宏任务包括
setTimeout
、setInterval
、setImmediate
(Node.js)、I/O 操作、UI 渲染等。 - 宏任务的执行顺序是一个接一个的,即执行完一个宏任务后,才会开始执行下一个宏任务。
- 例如,
setTimeout
函数会将其回调函数添加到宏任务队列中,当达到设定的延迟时间后,该回调函数会等待被执行。
- 常见的宏任务包括
-
微任务(Microtasks):
- 常见的微任务包括
Promise.then()
、Promise.catch()
、process.nextTick
(Node.js)、queueMicrotask
等。 - 微任务的优先级高于宏任务。在当前宏任务执行结束后,会优先执行微任务队列中的所有微任务,直到微任务队列为空。
- 例如,
Promise.resolve().then()
会将其回调函数添加到微任务队列中,该回调函数会在当前宏任务完成后立即执行,而不是等待下一个宏任务。
- 常见的微任务包括
事件循环的执行流程
- 检查执行栈是否为空。
- 如果执行栈不为空,继续执行栈中的函数调用。
- 如果执行栈为空,进入下一步。
- 检查微任务队列是否为空。
- 如果微任务队列不为空,按顺序依次执行微任务队列中的任务,直到微任务队列为空。
- 如果微任务队列也为空,进入下一步。
- 从宏任务队列中取出一个任务,将其添加到执行栈中并执行。
- 重复上述步骤。
示例代码及详细解释
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => {
console.log('Promise inside Timeout 1');
});
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
setTimeout(() => {
console.log('Timeout inside Promise 1');
}, 0);
});
console.log('End');
- 代码执行顺序如下:
- 首先,
console.log('Start')
是同步代码,直接执行,输出Start
。 setTimeout(() => {...}, 0)
是宏任务,其回调函数被添加到宏任务队列中。Promise.resolve().then(() => {...})
是微任务,其回调函数被添加到微任务队列中。console.log('End')
是同步代码,直接执行,输出End
。- 此时执行栈为空,检查微任务队列,发现
Promise.resolve().then(() => {...})
的回调函数,执行该微任务,输出Promise 1
,并将另一个setTimeout
回调添加到宏任务队列。 - 微任务队列已空,从宏任务队列中取出
setTimeout(() => {...})
的回调函数,执行该宏任务,输出Timeout 1
,同时将内部的Promise.then()
微任务添加到微任务队列。 - 再次检查微任务队列,执行内部的
Promise.then()
微任务,输出Promise inside Timeout 1
。 - 最后,执行之前添加到宏任务队列的
setTimeout(() => {...})
回调函数,输出Timeout inside Promise 1
。
- 首先,
事件循环的重要性和应用场景
-
重要性:
- 事件循环使 JavaScript 能够高效处理异步操作,避免因等待某些操作(如网络请求、文件读取等)而阻塞代码执行,保证程序的流畅性。
- 理解事件循环有助于避免一些常见的异步编程错误,如竞态条件、回调地狱等。
-
应用场景:
- 网络请求:当使用
fetch
或XMLHttpRequest
进行网络请求时,请求完成后的回调函数会被添加到任务队列中,等待执行。 - 用户交互:点击事件、输入事件等用户交互的处理函数会被添加到任务队列中,在用户触发事件后等待执行。
- 定时器操作:使用
setTimeout
、setInterval
等定时器,其回调函数会在设定的时间后添加到任务队列中。
- 网络请求:当使用
在面试中,可以这样回答:“JavaScript 事件循环是一种处理异步操作的机制,它基于单线程执行模型。核心组件包括执行栈和任务队列,任务队列又分为宏任务队列和微任务队列。宏任务如 setTimeout
、setInterval
等,微任务如 Promise.then()
等。事件循环的执行流程是先检查执行栈是否为空,若为空,检查微任务队列,若微任务队列不为空,执行微任务直到为空,再从宏任务队列取一个任务执行,不断重复这个过程。这一机制使 JavaScript 可以在等待异步操作时继续执行其他代码,避免阻塞,同时保证了执行顺序。例如在处理网络请求、用户交互和定时器操作等场景中,事件循环能确保这些异步操作的回调函数在适当的时间得到执行,同时避免因等待而影响程序的流畅性。”
通过这样的详细解释和示例,可以清晰地阐述 JavaScript 事件循环的概念、流程、重要性和应用场景,让面试官了解你对该知识点的深入理解和掌握程度。
9. 项目发布后,如何对项目进行优化,怎么优化
项目发布后进行优化是一个持续的过程,旨在提高性能、提升用户体验和减少资源消耗。以下是一些常见的优化策略:
1. 性能优化
1.1 代码压缩和混淆
- 压缩:使用工具如 UglifyJS、Terser 等压缩 JavaScript 代码,移除空格、注释和不必要的字符。
- 混淆:使用工具如 UglifyJS、Terser 等混淆 JavaScript 代码,增加代码的复杂性,防止逆向工程。
1.2 图片优化
- 格式选择:选择合适的图片格式(如 WebP、JPEG、PNG 等),根据图片内容选择最优格式。
- 压缩:使用工具如 TinyPNG、ImageOptim 等压缩图片,减少文件大小。
- 懒加载:使用懒加载技术(如
loading="lazy"
属性)延迟加载图片,减少初始加载时间。
1.3 CSS 和 JavaScript 文件优化
- 拆分文件:将 CSS 和 JavaScript 文件拆分成多个文件,按需加载。
- 合并文件:将多个 CSS 和 JavaScript 文件合并,减少 HTTP 请求次数。
- Tree Shaking:使用工具如 Webpack 进行 Tree Shaking,移除未使用的代码。
1.4 使用 CDN
- 内容分发网络(CDN):将静态资源托管在 CDN 上,利用全球分布的服务器加速资源加载。
1.5 缓存优化
- 浏览器缓存:设置合适的缓存头(如
Cache-Control
、Expires
),利用浏览器缓存减少重复请求。 - Service Worker:使用 Service Worker 实现更复杂的缓存策略,提升离线访问能力。
1.6 减少重绘和回流
- 批量更新 DOM:尽量减少对 DOM 的直接操作,使用批量更新或虚拟 DOM 技术(如 React)。
- 避免强制同步布局:避免在需要重新计算布局的操作后立即读取布局信息,如避免在
offsetHeight
、clientWidth
等属性读取后立即修改样式。
2. 服务器优化
2.1 使用 HTTP/2
- HTTP/2:启用 HTTP/2 协议,支持多路复用、头部压缩等特性,提高传输效率。
2.2 优化服务器响应时间
- 负载均衡:使用负载均衡技术分配请求,提高服务器处理能力。
- 数据库优化:优化数据库查询,使用索引、缓存等技术提高查询效率。
3. 用户体验优化
3.1 首屏加载优化
- 骨架屏:使用骨架屏技术在页面加载时显示占位内容,提升用户体验。
- 预加载关键资源:优先加载关键资源,确保首屏快速显示。
3.2 交互优化
- 动画优化:使用硬件加速的 CSS 属性(如
transform
、opacity
)实现流畅动画。 - 响应时间:优化事件处理逻辑,确保用户操作能够快速响应。
4. 安全优化
4.1 使用 HTTPS
- HTTPS:启用 HTTPS 协议,确保数据传输的安全性。
4.2 防止 XSS 和 CSRF
- XSS:对用户输入进行转义,防止跨站脚本攻击。
- CSRF:使用 CSRF 令牌,防止跨站请求伪造。
5. 监控和分析
5.1 性能监控
- 监控工具:使用工具如 Google Lighthouse、WebPageTest 等监控页面性能,发现瓶颈。
- 日志记录:记录关键性能指标(如加载时间、响应时间),定期分析。
5.2 用户行为分析
- 分析工具:使用工具如 Google Analytics、Mixpanel 等分析用户行为,了解用户需求。
- 反馈收集:收集用户反馈,及时调整优化策略。
6. 持续集成和持续部署(CI/CD)
6.1 自动化测试
- 测试框架:使用测试框架(如 Jest、Mocha)进行自动化测试,确保代码质量。
6.2 自动化部署
- 部署工具:使用工具如 Jenkins、GitLab CI 等实现自动化部署,提高部署效率。
通过以上优化策略,可以显著提高项目的性能和用户体验,确保项目在发布后能够稳定高效地运行。在实际操作中,可以根据项目的具体情况进行针对性的优化。
10. vue2 的 diff 算法和 vue3 的 diff 算法的区别
核心区别
Vue2 和 Vue3 的 diff 算法主要有三个区别:
- 算法实现:
- Vue2 使用双端比较算法
- Vue3 使用快速 diff 算法
- 性能优化:
- Vue3 增加了静态标记(PatchFlag)
- Vue3 增加了静态提升(hoistStatic)
- Vue3 使用最长递增子序列优化了对比流程
- 时间复杂度:
- Vue2 的时间复杂度是 O(n^2)
- Vue3 在理想情况下可以达到 O(n)
双端比较(Vue2)
Vue2 的双端比较会同时从新旧子节点的两端开始比较,会进行以下四种比较:
- 新前与旧前
- 新后与旧后
- 新后与旧前
- 新前与旧后
如果四种都未命中,才会进行遍历查找。
快速diff(Vue3)
Vue3 的 diff 算法步骤:
- 先从头部开始比对
- 再从尾部开始比对
- 如果还有剩余节点:
- 新增:处理新增的节点
- 删除:处理需要删除的节点
- 移动:使用最长递增子序列来优化节点移动
如果面试官继续追问
Vue3 为什么更快?
- 编译优化:
- 静态标记:标记动态内容,只对动态内容进行 diff
- 静态提升:静态节点只会被创建一次
- Block Tree:将动态节点收集到一个数组中
- 算法优化:
- 最长递增子序列算法减少了节点移动次数
- 使用 Map 数据结构优化了节点查找
2. PatchFlag 是什么?
PatchFlag 是 Vue3 新增的一个标记,用于标识节点的动态内容类型:
- 1: 动态文本节点
- 2: 动态 class
- 4: 动态 style
- 8: 动态属性
- 16: 动态事件
等等…这样在 diff 的时候可以只关注动态内容,提高性能。
3. 实际应用影响
这些优化在实际应用中的效果:
- 大型列表渲染更快
- 组件更新性能更好
- 内存占用更少
加分回答
如果想要在面试中脱颖而出,可以补充:
-
Vue3 diff 算法借鉴了 inferno 的算法实现
-
Vue3 还有其他性能优化:
- 事件缓存
- 基于 Proxy 的响应式系统
- 更好的 TypeScript 支持
- 在实践中,我们可以:
- 使用 key 来帮助 diff 算法识别节点
- 避免不必要的节点嵌套
- 合理使用 v-show 和 v-if
记住:
- 回答要由浅入深
- 先说核心区别
- 再解释具体实现
- 最后谈优化和实践
- 如果面试官继续追问,再展开细节