前端面试题-问答篇-5万字!
1. 请描述CSS中的层叠(Cascade)和继承(Inheritance)规则,以及它们在实际开发中的应用。
在CSS中,层叠(Cascade)和继承(Inheritance)是两个关键的规则,它们决定了元素的样式如何应用和优先级如何确定。理解这些规则对于编写高效和维护良好的CSS至关重要。
层叠(Cascade)
层叠是CSS处理多个规则冲突的方式。以下是决定哪些规则会应用于元素的几个因素:
-
来源(Origin):
- 浏览器默认样式表(User Agent stylesheets)
- 用户样式表(User stylesheets)
- 作者样式表(Author stylesheets)
其中,作者样式表的优先级高于用户样式表,而用户样式表的优先级高于浏览器默认样式表。
-
重要性(Importance):
!important
声明:具有最高优先级,覆盖所有其他声明。
-
特异性(Specificity):
- 特异性根据选择器的类型计算:
- 内联样式(如
style="..."
)的特异性最高。 - ID选择器(如
#id
)的特异性高于类选择器(如.class
)、属性选择器(如[type="text"]
)和伪类(如:hover
)。 - 类选择器、属性选择器和伪类的特异性高于元素选择器(如
div
)和伪元素(如::before
)。
- 内联样式(如
- 特异性根据选择器的类型计算:
-
源顺序(Source Order):
- 如果特异性和重要性都相同,则后出现的规则覆盖先出现的规则。
示例
<!DOCTYPE html>
<html>
<head>
<style>
#example { color: red; } /* ID选择器 */
.example { color: blue; } /* 类选择器 */
p { color: green; } /* 元素选择器 */
p.example { color: yellow; } /* 元素+类选择器 */
p { color: black !important; } /* !important声明 */
</style>
</head>
<body>
<p id="example" class="example">Hello, World!</p>
</body>
</html>
在这个示例中,<p>
元素的最终文本颜色是黑色,因为 !important
声明具有最高优先级。
继承(Inheritance)
继承是指某些CSS属性会自动从父元素传递给子元素。并不是所有属性都继承,一些常见的继承属性包括:
- 字体属性:
font-family
,font-size
,font-style
,font-weight
- 文本属性:
color
,text-align
,line-height
- 列表属性:
list-style
非继承属性的示例
- 布局属性:
display
,margin
,padding
,border
应用继承的示例
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: Arial, sans-serif;
color: darkblue;
}
.child {
border: 1px solid black;
}
</style>
</head>
<body>
<div>
<p class="child">This text inherits the font-family and color from the body element.</p>
</div>
</body>
</html>
在这个示例中,<p>
元素中的文本将继承 body
元素的字体和颜色,但不会继承 border
属性。
实际开发中的应用
-
优先级控制:通过理解特异性和层叠规则,开发者可以更好地控制样式应用的优先级,避免样式冲突。
-
代码维护:通过适当地使用继承,可以减少冗余代码,提高代码的可读性和维护性。例如,全局设置文本颜色和字体,然后让大部分元素继承这些属性。
-
避免滥用
!important
:滥用!important
会使样式规则难以管理和覆盖。应谨慎使用,仅在必要时使用。 -
特异性管理:应避免使用过高特异性的选择器(如内联样式和ID选择器),以保持样式的灵活性和可重用性。
通过理解和应用CSS的层叠和继承规则,可以更有效地编写和管理样式,提高Web开发的效率和代码质量。
2. flex布局
Flexbox布局(Flexible Box Layout)是一种CSS布局模式,旨在为盒子模型提供更高效的对齐和分布方式,特别是在需要调整和响应不同屏幕尺寸时。Flexbox布局的核心概念是容器(flex container)和项目(flex items)。下面是关于Flexbox布局的详细说明及其实际应用。
基本概念
-
Flex Container(容器):
- 通过设置一个元素的
display
属性为flex
或inline-flex
来定义一个容器。这个容器的子元素将自动成为 flex 项目。
- 通过设置一个元素的
-
Flex Items(项目):
- 容器中的直接子元素自动成为 flex 项目,可以应用各种 flex 属性进行布局。
容器属性
-
display:
display: flex;
将元素设置为块级 flex 容器。display: inline-flex;
将元素设置为行内 flex 容器。
-
flex-direction:
- 定义主轴的方向(项目排列的方向)。
- 取值:
row
(默认值,水平从左到右),row-reverse
(水平从右到左),column
(垂直从上到下),column-reverse
(垂直从下到上)。
-
flex-wrap:
- 决定当项目溢出容器时是否换行。
- 取值:
nowrap
(默认值,不换行),wrap
(换行),wrap-reverse
(换行但方向反转)。
-
justify-content:
- 定义沿主轴(水平)的项目对齐方式。
- 取值:
flex-start
,flex-end
,center
,space-between
,space-around
,space-evenly
。
-
align-items:
- 定义沿交叉轴(垂直)的项目对齐方式。
- 取值:
stretch
(默认值,拉伸),flex-start
,flex-end
,center
,baseline
。
-
align-content:
- 定义多行内容的对齐方式(在有多行的情况下)。
- 取值:
stretch
(默认值,拉伸),flex-start
,flex-end
,center
,space-between
,space-around
,space-evenly
。
项目属性
-
order:
- 定义项目的排列顺序。默认值为
0
,可以为正负整数。
- 定义项目的排列顺序。默认值为
-
flex-grow:
- 定义项目的放大比例。默认值为
0
,即如果存在剩余空间,也不放大。
- 定义项目的放大比例。默认值为
-
flex-shrink:
- 定义项目的缩小比例。默认值为
1
,即项目可以缩小以适应容器空间。
- 定义项目的缩小比例。默认值为
-
flex-basis:
- 定义项目在分配多余空间之前的基准大小。可以是像素值、百分比或
auto
。
- 定义项目在分配多余空间之前的基准大小。可以是像素值、百分比或
-
flex:
- 是
flex-grow
,flex-shrink
和flex-basis
的简写。默认值为0 1 auto
。
- 是
-
align-self:
- 允许单个项目有与其他项目不同的对齐方式。覆盖
align-items
的值。 - 取值:
auto
(默认值,继承自父容器的align-items
),flex-start
,flex-end
,center
,baseline
,stretch
。
- 允许单个项目有与其他项目不同的对齐方式。覆盖
示例
以下是一个使用Flexbox布局的实际示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
.container {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
height: 300px;
border: 1px solid #ccc;
}
.item {
background-color: #f0f0f0;
padding: 20px;
margin: 10px;
flex: 1 1 100px; /* grow, shrink, basis */
}
.item.large {
flex: 2 1 200px; /* larger item */
}
</style>
</head>
<body>
<div class="container">
<div class="item">Item 1</div>
<div class="item large">Item 2</div>
<div class="item">Item 3</div>
</div>
</body>
</html>
在这个示例中:
container
类的容器使用flex
布局,允许项目换行并均匀分布。item
类的项目默认具有相同的伸缩比例,但large
类的项目有更大的基准大小和伸缩比例,使其占据更多空间。
实际开发中的应用
- 响应式布局:Flexbox简化了响应式布局的实现,特别是对于需要动态调整的组件。
- 居中对齐:使用
justify-content
和align-items
可以轻松实现水平和垂直居中对齐。 - 复杂布局:通过组合使用容器和项目属性,可以创建复杂的布局,例如导航栏、卡片布局和网格系统。
- 弹性盒子:通过
flex-grow
和flex-shrink
实现弹性盒子,确保元素在不同屏幕尺寸下均能很好地适应。
Flexbox布局提供了一种强大而灵活的方式来构建现代Web布局,使开发者能够更轻松地实现各种复杂的布局需求。
3. 讲一讲闭包陷阱
闭包是JavaScript中的一个强大特性,但它也带来了一些常见的陷阱,特别是对于初学者而言。了解这些陷阱及其解决方法有助于更好地使用闭包编写健壮的代码。
常见的闭包陷阱
- 循环中的闭包
- 内存泄漏
- 性能问题
1. 循环中的闭包
在循环中创建闭包时,可能会导致所有的闭包共享同一个作用域,从而导致意外的结果。以下是一个常见的示例:
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
预期输出:0, 1, 2
实际输出:3, 3, 3
原因是每个回调函数共享了同一个 i
变量的引用,当 setTimeout
执行时,循环已经结束,i
的值已经变成了 3
。
解决方法:
-
使用IIFE(Immediately Invoked Function Expression)
for (var i = 0; i < 3; i++) { (function(i) { setTimeout(function() { console.log(i); }, 1000); })(i); }
-
使用
let
关键字for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(i); }, 1000); }
let
关键字创建了一个块级作用域,使得每次迭代的i
都是一个新的变量。
2. 内存泄漏
闭包可以导致内存泄漏,因为闭包会保留对外部函数作用域的引用,这可能会导致那些变量无法被垃圾回收。
示例:
function createClosure() {
const largeArray = new Array(1000000).fill('*');
return function() {
console.log(largeArray.length);
};
}
const closure = createClosure();
// closure 变量保持对 largeArray 的引用,导致 largeArray 无法被回收
解决方法:
-
在不再需要闭包时,显式地将其设为
null
,帮助垃圾回收。let closure = createClosure(); closure = null; // 解除引用,允许垃圾回收
3. 性能问题
闭包的频繁创建和销毁可能导致性能问题,特别是在内存敏感的应用中。
示例:
function createHeavyClosure() {
const largeArray = new Array(1000000).fill('*');
return function() {
console.log(largeArray.length);
};
}
for (let i = 0; i < 1000; i++) {
createHeavyClosure();
}
在这种情况下,每次调用 createHeavyClosure
都会创建一个新的大数组,这对性能影响较大。
解决方法:
-
考虑是否可以将需要的变量提升到外部作用域,避免频繁创建闭包。
const largeArray = new Array(1000000).fill('*'); function createLightClosure() { return function() { console.log(largeArray.length); }; } for (let i = 0; i < 1000; i++) { createLightClosure(); }
总结
闭包是JavaScript中非常有用的特性,可以实现数据的私有化和函数工厂等功能。然而,在使用闭包时需要注意一些常见的陷阱:
- 循环中的闭包问题:可以通过使用IIFE或
let
关键字解决。 - 内存泄漏:确保不再需要闭包时,显式地解除引用。
- 性能问题:避免频繁创建大闭包,考虑将变量提升到外部作用域。
通过注意这些问题并采取适当的解决方法,可以更安全和高效地使用闭包。
4. 事件轮询
JavaScript 的事件轮询机制(Event Loop)是理解 JavaScript 异步行为的核心。它是一个负责执行代码、收集和处理事件及子任务的机制。在浏览器和 Node.js 中,事件轮询的工作方式略有不同,但基本概念是相同的。
事件轮询的工作原理
-
调用栈(Call Stack):
- 调用栈是一种数据结构,用于存储程序执行期间的活动子程序。JavaScript 是单线程的,所以一次只能执行一个任务,当函数被调用时,它被添加到调用栈中。当函数执行完毕时,它从调用栈中移除。
-
任务队列(Task Queue):
- 任务队列是一个存放待处理任务的队列。当异步任务(如事件处理程序、定时器、网络请求等)完成时,它们的回调函数被添加到任务队列中。
-
微任务队列(Microtask Queue):
- 微任务队列用于存放微任务(如
Promise
的回调)。这些任务会在当前任务执行结束后立即执行,优先级高于任务队列中的任务。
- 微任务队列用于存放微任务(如
事件轮询的执行过程
-
执行全局代码:
- 当JavaScript引擎开始执行代码时,它首先执行全局上下文中的代码,所有同步代码在调用栈中依次执行。
-
检查微任务队列:
- 当调用栈为空时,事件轮询会检查并执行所有微任务队列中的任务,直到微任务队列为空。
-
执行任务队列中的任务:
- 当微任务队列为空时,事件轮询会从任务队列中取出第一个任务并执行它。
-
重复上述过程:
- 事件轮询不断重复上述过程,确保所有任务都得到处理。
示例
下面是一个展示事件轮询机制的示例:
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('End');
执行顺序解析:
console.log('Start')
被添加到调用栈并执行,输出 “Start”。setTimeout
的回调函数被添加到任务队列。Promise.resolve().then
的回调函数被添加到微任务队列。console.log('End')
被添加到调用栈并执行,输出 “End”。- 调用栈为空,事件轮询检查并执行微任务队列中的任务,输出 “Promise”。
- 微任务队列为空,事件轮询检查并执行任务队列中的任务,输出 “Timeout”。
最终输出顺序为:
Start
End
Promise
Timeout
事件轮询的实际应用
-
处理异步操作:
- 事件轮询使得JavaScript能够处理异步操作,如网络请求、定时器和事件监听器,而不会阻塞主线程。
-
优先处理微任务:
- 微任务(如
Promise
回调)比任务队列中的任务具有更高的优先级,确保关键的异步操作尽快执行。
- 微任务(如
-
保证执行顺序:
- 事件轮询机制保证了任务按照预期顺序执行,避免了竞态条件和不确定行为。
深入理解
-
宏任务和微任务:
- 宏任务(Macrotask)包括:
setTimeout
、setInterval
、I/O
操作等。 - 微任务(Microtask)包括:
Promise
回调、MutationObserver
等。
- 宏任务(Macrotask)包括:
-
浏览器与 Node.js 的区别:
- 在浏览器中,事件轮询包括任务队列和微任务队列。
- 在 Node.js 中,事件轮询更复杂,包含多个阶段(如
timers
、I/O callbacks
、idle
等)。
-
事件循环图解:
- 可以通过图示更直观地理解事件轮询机制的执行过程和各个队列之间的关系。
结论
事件轮询是JavaScript异步编程的核心,理解它可以帮助开发者编写更高效和更可靠的代码。通过结合调用栈、任务队列和微任务队列,事件轮询确保了JavaScript能够处理复杂的异步操作,并保持单线程执行的简洁性。
5. 讲一下 promise (原理, all的缺点, allSelect)
Promise
基本原理
Promise
是 JavaScript 中用于处理异步操作的对象。它代表一个在未来某个时间点可能会完成或失败的操作及其结果值。
-
状态:
pending
:初始状态,既没有被执行,也没有被拒绝。fulfilled
:操作成功完成。rejected
:操作失败。
-
方法:
then(onFulfilled, onRejected)
:添加回调,处理成功和失败两种情况。catch(onRejected)
:添加回调,处理失败情况。finally(onFinally)
:添加回调,不管结果如何都会执行。
示例
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Success');
}, 1000);
});
promise
.then(value => {
console.log(value); // "Success"
})
.catch(error => {
console.error(error);
});
Promise.all 的缺点
Promise.all
方法接受一个 Promise 对象的可迭代对象(例如数组),并返回一个新的 Promise,当所有输入的 Promise 都已成功完成时,该 Promise 才会成功。如果有一个 Promise 失败,该 Promise 会立即失败,并带有第一个被拒绝的原因。
缺点
- 短路行为:如果一个 Promise 被拒绝,
Promise.all
会立即失败,忽略其他 Promise 的状态。 - 性能问题:所有 Promise 都要完成,如果有一个慢的 Promise 会影响整体完成时间。
- 缺乏部分成功处理:无法处理部分成功部分失败的情况。
Promise.allSettled
为了解决 Promise.all
的一些缺点,ES2020 引入了 Promise.allSettled
方法。它接受一个 Promise 对象的可迭代对象,并返回一个新的 Promise,当所有输入的 Promise 都已完成(无论成功还是失败)时,该 Promise 才会成功。
示例
const promises = [
Promise.resolve('Success 1'),
Promise.reject('Error'),
Promise.resolve('Success 2')
];
Promise.allSettled(promises)
.then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log(`Fulfilled: ${result.value}`);
} else if (result.status === 'rejected') {
console.log(`Rejected: ${result.reason}`);
}
});
});
Promise.allSettled 的优点
- 不会短路:所有 Promise 都会执行完成,不会因为某个 Promise 被拒绝而停止。
- 结果包含状态:结果数组包含每个 Promise 的状态和对应的值或原因,可以更好地处理部分成功和部分失败的情况。
输出结果
Fulfilled: Success 1
Rejected: Error
Fulfilled: Success 2
总结
Promise
是 JavaScript 处理异步操作的强大工具。Promise.all
用于并行处理多个 Promise,但如果有一个 Promise 失败,它会立即失败。Promise.allSettled
提供了更好的方式来处理多个 Promise,无论它们是否成功,可以避免Promise.all
的一些缺点,更适合需要处理部分成功和部分失败的场景。
6. ts和js的区别 , 为什么用ts
TypeScript(TS)和JavaScript(JS)虽然在语法和用途上有很多相似之处,但它们之间有一些关键区别,这些区别也是为什么许多开发者选择使用TypeScript的原因。
主要区别
-
静态类型检查:
- JavaScript:是一种动态类型语言,变量的类型是在运行时确定的。这虽然灵活,但也容易导致类型相关的错误。
- TypeScript:是一种静态类型语言,允许在编写代码时指定变量、函数参数和返回值的类型。TypeScript编译器会在编译时检查类型,帮助捕获潜在的错误。
-
编译时错误检查:
- JavaScript:只有在运行时才会发现错误。
- TypeScript:在代码编译阶段就能发现和修复错误,从而在代码进入生产环境前就解决问题,减少运行时错误。
-
更好的开发工具支持:
- JavaScript:现代的IDE和代码编辑器对JS提供了基本的支持,但由于其动态特性,智能提示和自动补全功能有限。
- TypeScript:由于其类型信息,IDE和代码编辑器可以提供更强大的智能提示、代码自动补全和重构支持,提高开发效率。
-
面向对象编程(OOP)支持:
- JavaScript:支持基于原型的继承和OOP,但没有原生的接口、泛型等高级OOP特性。
- TypeScript:支持类、接口、泛型等更丰富的OOP特性,便于构建大型和复杂的应用程序。
-
代码的可维护性和可读性:
- JavaScript:由于缺乏类型信息,大型代码库可能会变得难以维护。
- TypeScript:类型信息和更严格的语法有助于代码的可读性和可维护性,特别是对于大型团队项目。
为什么使用TypeScript
-
提高代码质量和稳定性:通过类型检查和提前发现错误,TypeScript帮助开发者编写更健壮、更稳定的代码。
-
增强的开发体验:TypeScript与现代IDE的紧密集成提供了出色的代码自动补全、导航和重构支持,显著提高开发效率。
-
更易于维护和扩展:TypeScript的类型系统和严格的语法规范使得代码更易于阅读、理解和维护,尤其在大型项目和团队协作中尤为重要。
-
与现代JavaScript兼容:TypeScript是JavaScript的超集,完全兼容现有的JavaScript代码,这意味着你可以逐步将现有的JS项目迁移到TS,而无需一次性重写所有代码。
-
更好的团队协作:类型信息和接口定义使团队成员之间的合作更加明确,减少了沟通成本和错误。
综上所述,TypeScript通过引入静态类型和编译时检查等特性,解决了JavaScript在大型项目中的一些固有问题,使开发过程更加高效、可靠和可维护。
7. weakMap跟map区别,深拷贝? 记忆函数?
WeakMap 与 Map 的区别
WeakMap
和 Map
是 JavaScript 中用于存储键值对的数据结构,但它们有一些重要的区别。
1. 键的类型
-
Map:可以使用任意类型的值作为键,包括对象、原始类型(字符串、数字、布尔值等)。
const map = new Map(); map.set('key', 'value'); map.set(1, 'number'); map.set(true, 'boolean'); map.set({}, 'object');
-
WeakMap:只能使用对象作为键,不能使用原始类型作为键。
const weakMap = new WeakMap(); const objKey = {}; weakMap.set(objKey, 'value'); // weakMap.set('key', 'value'); // 错误:'key' 不是对象
2. 键的弱引用
-
Map:键是强引用,意味着只要 Map 存在,键就不会被垃圾回收。
const map = new Map(); let key = {}; map.set(key, 'value'); key = null; // 即使 key 被设置为 null,Map 仍然保留对原对象的引用
-
WeakMap:键是弱引用,如果没有其他引用指向键对象,该键对象会被垃圾回收。
const weakMap = new WeakMap(); let key = {}; weakMap.set(key, 'value'); key = null; // key 对象被垃圾回收,WeakMap 不再保留对它的引用
3. 迭代与方法
-
Map:可以使用迭代器方法(如
keys()
、values()
、entries()
)进行遍历,且可以使用for...of
循环。const map = new Map(); map.set('key', 'value'); for (const [key, value] of map) { console.log(key, value); }
-
WeakMap:不可以被迭代,无法获取其键、值或条目。
const weakMap = new WeakMap(); weakMap.set({}, 'value'); // 没有迭代方法
深拷贝
深拷贝是指创建一个对象的完全独立的副本,包括所有嵌套的子对象。与浅拷贝不同,深拷贝不会共享引用。
方法
- JSON.parse 和 JSON.stringify
const original = { a: 1, b: { c: 2 } };
const copy = JSON.parse(JSON.stringify(original));
console.log(copy); // { a: 1, b: { c: 2 } }
缺点:不能拷贝函数、undefined
、Symbol
,也无法处理循环引用。
- 递归手动实现
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item));
}
const copy = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepClone(obj[key]);
}
}
return copy;
}
const original = { a: 1, b: { c: 2 } };
const copy = deepClone(original);
console.log(copy); // { a: 1, b: { c: 2 } }
记忆函数(Memoization)
记忆函数是一种优化技术,通过存储函数调用的结果来避免重复计算相同的输入。使用记忆函数可以显著提高计算密集型任务的性能。
示例
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
// 使用示例
function slowFunction(num) {
// 模拟计算密集型任务
for (let i = 0; i < 1e6; i++) {}
return num * 2;
}
const memoizedFunction = memoize(slowFunction);
console.log(memoizedFunction(5)); // 第一次调用,计算并缓存结果
console.log(memoizedFunction(5)); // 第二次调用,从缓存中获取结果
结论
-
WeakMap 与 Map 的区别:
- 键的类型:WeakMap 只能使用对象作为键,Map 可以使用任意类型。
- 键的引用:WeakMap 的键是弱引用,Map 的键是强引用。
- 迭代与方法:WeakMap 不能被迭代,Map 可以。
-
深拷贝:
- JSON 方法简单但有局限性。
- 递归实现可以处理复杂对象,但需要小心循环引用。
-
记忆函数:
- 通过缓存函数调用结果来提高性能。
- 使用 Map 作为缓存容器,可以实现高效的记忆功能。
了解这些概念和技术可以帮助你在 JavaScript 中更好地管理数据和优化代码性能。
8. 什么是异步编程?如何处理异步操作?
异步编程
异步编程是一种编程范式,允许程序在执行某些操作时不阻塞主线程。这对于处理I/O操作、网络请求、定时任务等耗时操作非常重要。在异步编程中,操作可以在后台执行,而不阻塞主线程的继续运行。这使得程序可以在等待这些操作完成时执行其他任务,从而提高程序的响应性和性能。
为什么需要异步编程?
在JavaScript中,异步编程特别重要,因为JavaScript是单线程的。如果不使用异步编程,所有的I/O操作(例如读取文件、网络请求等)都会阻塞整个线程,直到操作完成。这会导致用户界面卡顿或应用程序无响应。
如何处理异步操作?
JavaScript提供了多种方式来处理异步操作,主要包括回调函数、Promise和async/await
。
1. 回调函数
回调函数是最早的一种处理异步操作的方法。一个函数将另一个函数作为参数传递,并在异步操作完成后调用这个回调函数。
示例:
function fetchData(callback) {
setTimeout(() => {
callback('Data fetched');
}, 1000);
}
fetchData((data) => {
console.log(data); // 'Data fetched' after 1 second
});
缺点:回调地狱(Callback Hell),代码变得难以维护和阅读。
2. Promise
Promise 是一种更现代的异步编程解决方案。它代表一个未来可能会完成或失败的操作及其结果值。
示例:
const fetchData = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data fetched');
}, 1000);
});
fetchData
.then((data) => {
console.log(data); // 'Data fetched' after 1 second
})
.catch((error) => {
console.error(error);
});
优点:避免了回调地狱,提供了更清晰的代码结构。
3. async/await
async/await
是基于Promise的语法糖,使得异步代码看起来像同步代码。async
函数总是返回一个Promise,await
只能在 async
函数内部使用,它使得JavaScript等待 Promise
完成并返回其结果。
示例:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data fetched');
}, 1000);
});
}
async function getData() {
try {
const data = await fetchData();
console.log(data); // 'Data fetched' after 1 second
} catch (error) {
console.error(error);
}
}
getData();
优点:使得异步代码更加简洁、易读、易维护。
处理多个异步操作
- Promise.all:等待多个Promise完成。如果其中一个Promise被拒绝,
Promise.all
会立即被拒绝。
示例:
const promise1 = new Promise((resolve) => setTimeout(resolve, 100, 'one'));
const promise2 = new Promise((resolve) => setTimeout(resolve, 200, 'two'));
Promise.all([promise1, promise2])
.then((values) => {
console.log(values); // ['one', 'two'] after 200ms
});
- Promise.allSettled:等待多个Promise完成,无论它们是成功还是失败。
示例:
const promise1 = new Promise((resolve) => setTimeout(resolve, 100, 'one'));
const promise2 = new Promise((_, reject) => setTimeout(reject, 200, 'two'));
Promise.allSettled([promise1, promise2])
.then((results) => {
results.forEach((result) => {
if (result.status === 'fulfilled') {
console.log(`Fulfilled: ${result.value}`);
} else {
console.log(`Rejected: ${result.reason}`);
}
});
});
- Promise.race:返回第一个完成的Promise,无论成功还是失败。
示例:
const promise1 = new Promise((resolve) => setTimeout(resolve, 100, 'one'));
const promise2 = new Promise((_, reject) => setTimeout(reject, 50, 'two'));
Promise.race([promise1, promise2])
.then((value) => {
console.log(value); // 'two' after 50ms
})
.catch((error) => {
console.error(error); // 'two' after 50ms
});
总结
异步编程在JavaScript中至关重要,用于处理耗时的操作而不阻塞主线程。通过回调函数、Promise和async/await
,我们可以有效地处理异步操作。理解和熟练使用这些工具可以显著提高代码的性能和可维护性。
8. 解释模块化开发的概念,并比较CommonJS和ES6模块的区别。
模块化开发的概念
模块化开发是一种软件设计技术,通过将代码分解为独立的、可重用的模块来实现更好的组织和管理。这些模块可以单独开发、测试和维护,然后在需要时组合在一起构建更大的应用程序。模块化开发带来了以下好处:
- 代码复用:模块可以在不同的项目或相同项目的不同部分中重复使用。
- 维护性:代码更易于维护和更新,因为每个模块都封装了特定的功能。
- 隔离性:模块之间的依赖性减少,避免了全局命名冲突。
- 可测试性:模块可以单独测试,确保其功能正确。
CommonJS 和 ES6 模块的区别
CommonJS 模块
CommonJS 是一种模块化标准,主要用于服务器端(如 Node.js)。它使用 require
和 module.exports
进行模块的引入和导出。
示例:
模块定义(math.js
):
// math.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
module.exports = { add, subtract };
模块引入(main.js
):
// main.js
const math = require('./math');
console.log(math.add(2, 3)); // 5
console.log(math.subtract(5, 2)); // 3
特点:
- 同步加载:模块在运行时同步加载,适合服务器端。
- 动态引入:可以在代码的任何地方引入模块,逻辑上非常灵活。
- 单个导出对象:
module.exports
导出一个对象,这个对象包含了模块的所有导出内容。
ES6 模块
ES6(也称为 ES2015)引入了原生的模块系统,适用于浏览器和服务器端。它使用 import
和 export
关键字进行模块的引入和导出。
示例:
模块定义(math.js
):
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
模块引入(main.js
):
// main.js
import { add, subtract } from './math.js';
console.log(add(2, 3)); // 5
console.log(subtract(5, 2)); // 3
特点:
- 静态加载:模块在编译时静态加载,更适合编译工具进行优化。
- 顶层引入:
import
必须在模块的顶层,不能在条件语句或函数内部。 - 多种导出方式:可以导出多个变量和函数,可以使用
export default
导出默认值。
主要区别
-
加载方式:
- CommonJS:同步加载,适用于服务器端。
- ES6 模块:静态加载,适用于现代浏览器和服务器端。
-
导入导出语法:
- CommonJS:使用
require
导入,module.exports
导出。 - ES6 模块:使用
import
导入,export
导出。
- CommonJS:使用
-
顶层引入:
- CommonJS:
require
可以在任何地方使用。 - ES6 模块:
import
必须在模块的顶层。
- CommonJS:
-
动态引入:
- CommonJS:支持动态引入。
- ES6 模块:需要使用动态
import()
函数来实现动态引入。
-
性能优化:
- CommonJS:由于同步加载,适合小规模模块加载,不适合在浏览器端使用。
- ES6 模块:静态分析和优化,更适合现代前端开发。
总结
模块化开发通过将代码分解为独立的模块,提高了代码的复用性、维护性、隔离性和可测试性。CommonJS 和 ES6 模块是两种主要的模块化标准,各自有不同的特点和应用场景:
- CommonJS:主要用于服务器端,使用
require
和module.exports
,支持动态引入。 - ES6 模块:适用于浏览器和服务器端,使用
import
和export
,支持静态加载和顶层引入,更适合现代前端开发。
根据具体的项目需求和环境选择合适的模块化标准,可以有效提高开发效率和代码质量。
9. 说一说浏览器 HTTP 缓存? 项目里你用什么保证来请求到最新资源? 根 index.html 下有没有缓存?如何保证加载到最新的资源?
浏览器第一次加载资源,服务器返回 200,浏览器从服务器下载资源文件,并缓存资源文件与 response header,以供下次加载时对比使用;下一次加载资源时,由于强制缓存优先级较高,先比较当前时间与上一次返回 200 时的时间差,如果没有超过 cache-control 设置的max-age,则没有过期,并命中强缓存,直接从本地读取资源。如果浏览器不支持 HTTP1.1,则使用 expires 头判断是否过期;如果资源已过期,则表明强制缓存没有被命中,则开始协商缓存,向服务器发送带有 If-None-Match 和 If-Modified-Since 的请求;服务器收到请求后,优先根据 Etag 的值判断被请求的文件有没有做修改,Etag 值一致则没有修改,命中协商缓存,返回 304;如果不一致则有改动,直接返回新的资源文件带上新的 Etag 值并返回 200;如果服务器收到的请求没有 Etag 值,则将 If-Modified-Since 和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回 304;不一致则返回新的 last-modified 和文件并返回 200;很多网站的资源后面都加了版本号,这样做的目的是:每次升级了 JS或 CSS 文件后,为了防止浏览器进行缓存,强制改变版本号,客户端浏览器就会重新下载新的 JS 或 CSS 文件 ,以保证用户能够及时获得网站的最新更新。
强缓存
使用强缓存策略时,如果缓存资源有效,则直接使用缓存资源,不必再向服务器发起请求。强缓存策略可以通过两种方式来设置,分别是 http 头信息中的Expires 属性和 Cache-Control 属性。
(1)服务器通过在响应头中添加 Expires 属性,来指定资源的过期时间。在过期时间以内,该资源可以被缓存使用,不必再向服务器发送请求。这个时间是一个绝对时间,它是服务器的时间,因此可能存在这样的问题,就是客户端的时间和服务器端的时间不一致,或者用户可以对客户端时间进行修改的情况,这样就可能会影响缓存命中的结果。
Expires: Wed, 21 Oct 2023 07:28:00 GMT
(2)Expires 是 http1.0 中的方式,因为它的一些缺点,在 HTTP1.1 中提出了一个新的头部属性就是 Cache-Control 属性,它提供了对资源的缓存的更精确的控制。它有很多不同的值,Cache-Control 可设置的字段:
Cache-Control: max-age=3600
-
public:设置了该字段值的资源表示可以被任何对象(包括:发送请求的客户端、代理服务器等等)缓存。这个字段值不常用,一般还是使用 max-age=来精确控制;
-
private:设置了该字段值的资源只能被用户浏览器缓存,不允许任何代理服务器缓存。在实际开发当中,对于一些含有用户信息的HTML,通常都要设置这个字段值,避免代理服务器(CDN)缓存;
-
no-cache:设置了该字段需要先和服务端确认返回的资源是否发生了变化,如果资源未发生变化,则直接使用缓存好的资源;
-
no-store:设置了该字段表示禁止任何缓存,每次都会向服务端发起新的请求,拉取最新的资源;
-
max-age=:设置缓存的最大有效期,单位为秒;
-
s-maxage=:优先级高于 max-age=,仅适用于共享缓存(CDN),优先级高于 max-age 或者 Expires 头;
-
max-stale[=]:设置了该字段表明客户端愿意接收已经过期的资源,但是不能超过给定的时间限制。一般来说只需要设置其中一种方式就可以实现强缓存策略,当两种方式一起使用时,Cache-Control 的优先级要高于 Expires。
no-cache 和 no-store 很容易混淆:no-cache 是指先要和服务器确认是否有资源更新,在进行判断。也就是说没有强缓存,但是会有协商缓存;no-store 是指不使用任何缓存,每次请求都直接从服务器获取资源。
协商缓存
如果命中强制缓存,我们无需发起新的请求,直接使用缓存内容,如果没有命中强制缓存,如果设置了协商缓存,这个时候协商缓存就会发挥作用了。上面已经说到了,命中协商缓存的条件有两个:1. max-age=xxx 过期了 2. 值为 no-store使用协商缓存策略时。协商缓存会先向服务器发送一个请求,如果资源没有发生修改,则返回一个 304 状态,让浏览器使用本地的缓存副本。如果资源发生了修改,则返回修改后的资源。
协商缓存也可以通过两种方式来设置,分别是 http 头信息中的Last-Modified和Etag属性。
(1)服务器通过在响应头中添加Last-Modified
属性来指出资源最后一次修改的时间,当浏览器下一次发起请求时,会在请求头中添加一个If-Modified-Since
的属性,属性值为上一次资源返回时的Last-Modified 的值。当请求发送到服务器后服务器会通过这个属性来和资源的最后一次的修改时间来进行比较,以此来判断资源是否做了修改。如果资源没有修改,那么返回 304 状态,让客户端使用本地的缓存。如果资源已经被修改了,则返回修改后的资源。使用这种方法有一个缺点,就是 Last-Modified 标注的最后修改时间只能精确到秒级,如果某些文件在 1 秒钟以内,被修改多次的话,那么文件已将改变了但是 Last-Modified 却没有改变,这样会造成缓存命中的不准确。
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT
(2)因为 Last-Modified 的这种可能发生的不准确性,http 中提供了另外一种方式,那就是 Etag 属性。服务器在返回资源的时候,在头信息中添加了Etag
属性,这个属性是资源生成的唯一标识符,当资源发生改变的时候,这个值也会发生改变。在下一次资源请求时,浏览器会在请求头中添加一个 If-None-Match
属性,这个属性的值就是上次返回的资源的 Etag 的值。服务接收到请求后会根据这个值来和资源当前的 Etag 的值来进行比较,以此来判断资源是否发生改变,是否需要返回资源。通过这种方式,比 Last-Modified 的方式更加精确。**当 Last-Modified 和 Etag 属性同时出现的时候,Etag 的优先级更高。**使用协商缓存的时候,服务器需要考虑负载平衡的问题,因此多个服务器上资源的 Last-Modified 应该保持一致,因为每个服务器上 Etag 的值都不一样,因此在考虑负载平衡时,最好不要设置 Etag属性。
ETag: "12345"
If-None-Match: "12345"
项目中保证请求到最新资源
在实际项目中,为了保证请求到最新资源,可以采用以下几种策略:
-
版本号:
- 在资源文件的 URL 中添加版本号(如
main.js?v=1.0.1
),当资源更新时,修改版本号以确保浏览器获取最新资源。
<script src="main.js?v=1.0.1"></script>
- 在资源文件的 URL 中添加版本号(如
-
文件名哈希:
- 构建工具(如 Webpack)可以在文件名中添加哈希值,每次构建时生成新的哈希值,确保文件名变化以避免缓存。
<script src="main.abc123.js"></script>
-
配置合理的缓存策略:
- 使用
Cache-Control
和Expires
头控制缓存行为,结合 ETag 和 Last-Modified 实现协商缓存,确保浏览器能够验证资源是否最新。
Cache-Control: no-cache
- 使用
根 index.html
下的缓存
index.html
文件通常是应用程序的入口文件,需要频繁更新以引入新的资源。在保证加载最新资源方面,通常不对 index.html
文件进行长期缓存,而是配置短期缓存或不缓存。
-
设置
Cache-Control
为no-cache
或max-age=0
:Cache-Control: no-cache
-
服务器配置:
- 通过服务器配置文件(如 Nginx、Apache)对
index.html
文件设置合适的缓存策略。
location / { expires -1; add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; }
- 通过服务器配置文件(如 Nginx、Apache)对
保证加载最新资源的最佳实践
- 使用文件名哈希:对于静态资源(如 CSS、JavaScript 文件),使用文件名哈希,确保文件名变化以避免缓存。
- 合理配置缓存头:对于需要频繁更新的文件(如
index.html
),配置合理的缓存头,确保每次都能从服务器获取最新版本。 - 版本管理:在资源 URL 中添加版本号,确保资源更新时,浏览器获取最新版本。
- 服务器配置:通过服务器配置文件,设置合理的缓存策略,确保关键资源能够及时更新。
通过以上策略,可以有效地控制浏览器缓存行为,确保用户始终加载到最新的资源。
总结:
强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求。它们缓存不命中时,都会向服务器发送请求来获取资源。在实际的缓存机制中,强缓存策略和协商缓存策略是一起合作使用的。浏览器首先会根据请求的信息判断,强缓存是否命中,如果命中则直接使用资源。如果不命中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。
10. 讲一讲跨域吧。同源协议和同域协议是什么?不符合同域协议会触发跨域吗?
跨域
跨域指的是浏览器中一项安全策略,即同源策略(Same-Origin Policy)。该策略限制从一个源加载的脚本如何与另一个源的资源进行交互,以防止恶意网站从另一个网站读取敏感数据。
同源策略
同源策略要求一个页面从另一个页面请求资源时,两个页面必须具有相同的协议、域名和端口。这三个部分必须完全一致,才能被认为是同源。
- 协议:例如
http
和https
- 域名:例如
www.example.com
和example.com
- 端口:例如
80
(默认 HTTP 端口)和443
(默认 HTTPS 端口)
示例:
http://www.example.com/page1.html
和http://www.example.com/page2.html
是同源的。http://www.example.com
和https://www.example.com
不是同源,因为协议不同。http://www.example.com
和http://api.example.com
不是同源,因为子域名不同。http://www.example.com:80
和http://www.example.com:8080
不是同源,因为端口不同。
跨域请求
当网页试图向不同源(不同协议、域名或端口)的资源发送请求时,就会触发跨域请求。这种情况下,浏览器会阻止请求,除非响应的服务器明确允许跨域请求。
解决跨域问题的方法
1. CORS(跨域资源共享)
CORS 是一种标准机制,允许服务器指示允许哪些域访问其资源。服务器通过在响应头中包含特定的 HTTP 头来实现。
-
Access-Control-Allow-Origin:指定允许哪些域可以访问资源。如果允许所有域,可以设置为
*
。Access-Control-Allow-Origin: *
-
Access-Control-Allow-Methods:指定允许的 HTTP 方法。
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
-
Access-Control-Allow-Headers:指定允许的自定义请求头。
Access-Control-Allow-Headers: Content-Type, Authorization
示例:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type
2. JSONP(JSON with Padding)
JSONP 是一种非官方的跨域请求方法,只支持 GET 请求。通过动态创建 script
标签,并使用回调函数来处理响应数据。
示例:
客户端:
<script>
function handleResponse(data) {
console.log(data);
}
</script>
<script src="http://example.com/api?callback=handleResponse"></script>
服务器:
// Node.js 示例
app.get('/api', (req, res) => {
const callback = req.query.callback;
const data = { message: 'Hello, world!' };
res.send(`${callback}(${JSON.stringify(data)})`);
});
3. 代理服务器
通过设置代理服务器,使跨域请求在服务器端完成,而不是在浏览器端。
示例:
使用 Node.js 设置一个代理服务器:
const express = require('express');
const request = require('request');
const app = express();
app.get('/proxy', (req, res) => {
const url = 'http://example.com/api';
req.pipe(request(url)).pipe(res);
});
app.listen(3000, () => {
console.log('Proxy server listening on port 3000');
});
客户端:
fetch('/proxy')
.then(response => response.json())
.then(data => console.log(data));
4. 使用 iframe + postMessage
通过在同源的 iframe 中加载跨域资源,然后使用 postMessage
方法进行通信。
同源协议和同域协议
- 同源协议(Same-Origin Policy):限制从一个源加载的脚本如何与另一个源的资源进行交互。
- 同域协议(Same-Domain Policy):常用于描述两个域名是否属于同一个根域,但严格来说这是不准确的,同源策略要求协议、域名和端口都相同。
不符合同源协议是否触发跨域?
是的,如果两个 URL 不符合同源策略的要求(即协议、域名、端口不完全相同),那么任何试图访问另一源资源的请求都会触发跨域请求。浏览器会根据同源策略阻止这些请求,除非目标服务器明确允许通过 CORS 或其他跨域解决方案进行访问。
总结
- 同源策略:用于保护网站数据安全,要求协议、域名和端口相同。
- 跨域:指浏览器阻止来自不同源的请求。
- 解决跨域的方法:
- CORS
- JSONP
- 代理服务器
- iframe + postMessage
通过这些方法,可以有效地解决跨域请求的问题,确保安全和数据的正确传输。
11. 前端安全
https://github.com/brickspert/blog/issues/62
12. url 输入到页面展示?
此时此刻,你在浏览器地址栏输入了百度的网址:
https://www.baidu.com/
网络请求
1. 构建请求
浏览器会构建请求行:
// 请求方法是GET,路径为根路径,HTTP协议版本为1.1
GET / HTTP/1.1
2. 查找强缓存
先检查强缓存,如果命中直接使用,否则进入下一步。
3. DNS解析
由于我们输入的是域名,而数据包是通过IP地址传给对方的。因此我们需要得到域名对应的IP地址
。这个过程需要依赖一个服务系统,这个系统将域名和 IP 一一映射,我们将这个系统就叫做DNS(域名系统)。得到具体 IP 的过程就是DNS
解析。 当然,值得注意的是,浏览器提供了DNS数据缓存功能。即如果一个域名已经解析过,那会把解析的结果缓存下来,下次处理直接走缓存,不需要经过 DNS解析。 另外,如果不指定端口的话,默认采用对应的 IP 的 80 端口。
4. 建立 TCP 连接
这里要提醒一点,Chrome 在同一个域名下要求同时最多只能有 6 个 TCP 连接,超过 6 个的话剩下的请求就得等待。 假设现在不需要等待,我们进入了 TCP 连接的建立阶段。首先解释一下什么是 TCP:
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
建立 TCP连接
经历了下面三个阶段:
- 通过
三次握手
(即总共发送3个数据包确认已经建立连接)建立客户端和服务器之间的连接。 - 进行数据传输。这里有一个重要的机制,就是接收方接收到数据包后必须要向发送方确认,
- 如果发送方没有接到这个确认的消息,就判定为数据包丢失,并重新发送该数据包。当然,发送的过程中还有一个优化策略,就是把大的数据包拆成一个个小包,依次传输到接收方,接收方按照这个小包的顺序把它们组装成完整数据包。 断开连接的阶段。数据传输完成,现在要断开连接了,通过四次挥手来断开连接。
读到这里,你应该明白 TCP 连接通过什么手段来保证数据传输的可靠性,一是三次握手确认连接,二是数据包校验保证数据到达接收方,三是通过四次挥手
断开连接。
当然,如果再深入地问,比如为什么要三次握手,两次不行吗?第三次握手失败了怎么办?为什么要四次挥手等等这一系列的问题,涉及计算机网络的基础知识,比较底层,但是也是非常重要的细节,希望你能好好研究一下,另外这里有一篇不错的文章,点击进入相应的推荐文章,相信这篇文章能给你启发。
5.发送 HTTP 请求
现在TCP连接建立完毕,浏览器可以和服务器开始通信,即开始发送 HTTP 请求。浏览器发 HTTP 请求要携带三样东西:请求行
、请求头
和请求体
。 首先,浏览器会向服务器发送请求行,关于请求行, 我们在这一部分的第一步就构建完了,贴一下内容:
// 请求方法是GET,路径为根路径,HTTP协议版本为1.1
GET / HTTP/1.1
结构很简单,由请求方法、请求URI和HTTP版本协议组成。
同时也要带上请求头,比如我们之前说的Cache-Control、If-Modified-Since、If-None-Match都由可能被放入请求头中作为缓存的标识信息。当然了还有一些其他的属性,列举如下:
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cache-Control: no-cache Connection: keep-alive Cookie: /* 省略cookie信息 */ Host: www.baidu.com Pragma: no-cache Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 > Mobile/15A372 Safari/604.1 最后是请求体,请求体只有在
POST方法
下存在,常见的场景表单提交
。
网络响应
HTTP 请求到达服务器,服务器进行对应的处理。最后要把数据传给浏览器,也就是返回网络响应。 跟请求部分类似,网络响应具有三个部分:响应行
、响应头
和响应体
。 响应行类似下面这样: HTTP/1.1 200 OK 复制代码由HTTP协议版本、状态码和状态描述组成。 响应头包含了服务器及其返回数据的一些信息, 服务器生成数据的时间、返回的数据类型以及对即将写入的Cookie信息。 举例如下:
Cache-Control: no-cache Connection: keep-alive Content-Encoding: gzip Content-Type: text/html;charset=utf-8 Date: Wed, 04 Dec 2019 12:29:13 GMT Server: apache Set-Cookie: rsv_i=f9a0SIItKqzv7kqgAAgphbGyRts3RwTg%2FLyU3Y5Eh5LwyfOOrAsvdezbay0QqkDqFZ0DfQXby4wXKT8Au8O7ZT9UuMsBq2k; path=/; domain=.baidu.com
响应完成之后怎么办?TCP 连接就断开了吗?
不一定。这时候要判断Connection字段
, 如果请求头或响应头中包含Connection: Keep-Alive
,表示建立了持久连接,这样TCP连接会一直保持,之后请求统一站点的资源会复用这个连接。 否则断开TCP连接, 请求-响应流程结束。
总结
到此,我们来总结一下主要内容,也就是浏览器端的网络请求过程: CDN
13. CDN
CDN(内容分发网络)概述
CDN(Content Delivery Network,内容分发网络)是一种分布式网络架构,旨在通过将内容缓存到全球多个节点上,从而加快用户访问网站或应用程序的速度。CDN 节点通常位于用户较近的地理位置,以减少延迟,提高访问速度和可靠性。
为什么使用 CDN?
- 提升访问速度:CDN 将内容分发到全球各地的节点,使用户可以从距离最近的服务器获取资源,从而减少延迟和加载时间。
- 减轻服务器负载:将流量分散到不同的 CDN 节点,减轻了源服务器的压力,提高了整体系统的稳定性和性能。
- 高可用性和容错性:即使某些节点出现故障,CDN 也可以通过其他节点提供内容,提高了网站的可用性。
- 带宽优化:通过缓存和优化传输,减少了带宽消耗,节省了成本。
- 安全性增强:CDN 提供了诸如 DDoS 保护和 SSL/TLS 加密等安全功能。
CDN 资源加载流程
- DNS 解析:当用户请求一个资源(例如图片、CSS、JavaScript 文件)时,浏览器首先通过 DNS 解析获取资源的 CDN 域名对应的 IP 地址。
- 选择最优节点:CDN 服务提供商根据用户的地理位置、网络状况等因素,将请求路由到离用户最近或最优的 CDN 节点。
- 缓存查找:CDN 节点检查缓存中是否存在该资源。如果资源已缓存且未过期,则直接返回给用户。
- 源站回源:如果资源未缓存或缓存已过期,CDN 节点向源服务器请求最新资源,获取后将其缓存并返回给用户。
- 资源传输:用户通过 CDN 节点获取资源,完成资源加载过程。
在项目中使用 CDN 加载资源
在实际项目中,可以通过以下几种方式使用 CDN 加载资源:
1. 静态资源
将静态资源(如图片、CSS、JavaScript 文件等)上传到 CDN,使用 CDN 的 URL 引用这些资源。
示例:
<link rel="stylesheet" href="https://cdn.example.com/styles/main.css">
<script src="https://cdn.example.com/scripts/main.js"></script>
<img src="https://cdn.example.com/images/logo.png" alt="Logo">
2. 配置 Web 服务器
配置 Web 服务器(如 Nginx、Apache)将特定路径的请求重定向到 CDN。
Nginx 示例:
location /static/ {
proxy_pass https://cdn.example.com/static/;
}
3. 使用构建工具
在前端构建工具(如 Webpack)中配置资源路径指向 CDN。
Webpack 示例:
module.exports = {
output: {
publicPath: 'https://cdn.example.com/',
},
};
如何确保加载最新资源
在使用 CDN 时,需要确保加载最新资源。以下是几种常用方法:
1. 缓存控制
通过配置缓存控制头,管理资源的缓存策略。
示例:
Cache-Control: max-age=3600
设置短期缓存时间,并结合版本管理或文件哈希来确保更新。
2. 文件名哈希
在文件名中添加哈希值,每次构建时生成新的哈希值,确保文件名变化,避免缓存问题。
示例:
<link rel="stylesheet" href="https://cdn.example.com/styles/main.abc123.css">
<script src="https://cdn.example.com/scripts/main.abc123.js"></script>
3. 版本管理
在资源 URL 中添加版本号,资源更新时修改版本号。
示例:
<link rel="stylesheet" href="https://cdn.example.com/styles/main.css?v=1.0.1">
<script src="https://cdn.example.com/scripts/main.js?v=1.0.1"></script>
index.html
的缓存处理
对于 index.html
文件,由于它是应用的入口文件,通常需要频繁更新,建议设置较短的缓存时间或不缓存。
1. 设置缓存头
通过服务器配置设置 Cache-Control
为 no-cache
或 max-age=0
,确保每次请求都从服务器获取最新的 index.html
文件。
示例:
Cache-Control: no-cache
2. 配置服务器
通过 Web 服务器配置文件(如 Nginx、Apache)对 index.html
设置合适的缓存策略。
Nginx 示例:
location / {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
}
总结
使用 CDN 可以显著提升资源加载速度、减轻服务器负载、提高可用性和安全性。在项目中使用 CDN 加载资源时,可以通过缓存控制、文件名哈希和版本管理等方法确保加载到最新资源。特别是对于 index.html
文件,建议设置合理的缓存策略,以确保用户始终获取最新的应用入口文件。
14. 哪一些操作会触发重排和重绘 ? 如何减少?
触发重排(Reflow)和重绘(Repaint)的操作
重排(Reflow)
重排是指当页面的布局和几何属性(如大小、位置)发生变化时,浏览器重新计算元素的位置和尺寸的过程。重排会导致页面的所有或部分元素重新计算布局。
触发重排的操作:
-
改变元素的几何属性:
- 设置或修改元素的
width
,height
,padding
,margin
,border
,display
,overflow
,position
,top
,left
,bottom
,right
等属性。
element.style.width = '100px'; element.style.height = '50px';
- 设置或修改元素的
-
添加或删除可见的 DOM 元素:
- 添加、删除或移动 DOM 元素。
document.body.appendChild(newElement); document.body.removeChild(existingElement);
-
读取某些属性值:
- 读取会触发浏览器强制同步布局的属性,如
offsetWidth
,offsetHeight
,clientWidth
,clientHeight
,scrollWidth
,scrollHeight
,getComputedStyle
等。
let height = element.offsetHeight;
- 读取会触发浏览器强制同步布局的属性,如
-
改变窗口大小:
- 浏览器窗口大小变化时,需要重新计算布局。
-
字体大小变化:
- 改变字体大小会影响元素的尺寸和布局。
element.style.fontSize = '16px';
-
CSS 伪类:
- 使用
:hover
,:focus
,:active
等伪类改变样式。
- 使用
重绘(Repaint)
重绘是指当元素的外观发生变化,但布局未发生改变时,浏览器重新绘制元素的过程。重绘的开销相对较小,不会引起重新布局。
触发重绘的操作:
-
改变元素的外观属性:
- 设置或修改元素的
color
,background-color
,border-color
,visibility
,outline
,box-shadow
,text-decoration
等属性。
element.style.backgroundColor = 'red'; element.style.color = 'blue';
- 设置或修改元素的
如何减少重排和重绘
减少重排和重绘可以显著提高页面的性能和响应速度。以下是一些减少重排和重绘的方法:
1. 批量处理 DOM 操作
将多次 DOM 操作合并为一次,减少多次重排和重绘。
示例:
// 批量处理 DOM 操作,避免多次重排
let fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
let newElement = document.createElement('div');
fragment.appendChild(newElement);
}
document.body.appendChild(fragment);
2. 使用 class
进行样式变更
一次性改变多个样式属性时,通过修改 class
来避免多次重排和重绘。
示例:
element.classList.add('new-class');
3. 减少样式查询
避免频繁读取会触发重排的属性,如 offsetWidth
, offsetHeight
,尽可能将它们缓存起来。
示例:
// 缓存读取的值,避免多次访问触发重排
let height = element.offsetHeight;
if (height > 100) {
// Do something
}
4. 避免逐个修改样式
将需要的样式修改放到一起,通过 style.cssText
或修改 class
一次性应用。
示例:
// 使用 cssText 一次性修改样式
element.style.cssText = 'width: 100px; height: 50px; background-color: red;';
5. 离线处理
将元素移出文档流后进行操作,操作完成后再插入文档,减少重排次数。
示例:
// 离线处理元素,减少重排
let parent = element.parentNode;
parent.removeChild(element);
element.style.width = '100px';
element.style.height = '50px';
parent.appendChild(element);
6. 使用 CSS 动画替代 JavaScript 动画
CSS 动画和过渡通常由浏览器的合成线程处理,不会引起重排,只会触发重绘。
示例:
/* 使用 CSS 动画替代 JavaScript 动画 */
@keyframes slide {
from { transform: translateX(0); }
to { transform: translateX(100px); }
}
.element {
animation: slide 1s linear;
}
总结
- 重排(Reflow):布局和几何属性变化时触发,开销较大。
- 重绘(Repaint):外观变化时触发,开销较小。
- 减少重排和重绘的方法:批量处理 DOM 操作、使用
class
变更样式、减少样式查询、避免逐个修改样式、离线处理、使用 CSS 动画等。
通过合理优化,可以显著提升页面性能和用户体验。
15. 聊一聊现在的框架react, vue
React 和 Vue 是目前最流行的两个前端框架(或者说库),它们各有特点和优势,适用于不同的开发需求。以下是对这两个框架的详细介绍和对比。
React
概述
React 是由 Facebook 开发并开源的前端 JavaScript 库,用于构建用户界面。它主要关注视图层,采用组件化开发方式,强调单向数据流和声明式编程。
核心特点
-
组件化:
- 组件是 React 应用的基本单元,可以组合成复杂的 UI。
- 组件的封装性和复用性高。
-
JSX:
- 使用 JSX 语法,可以在 JavaScript 中直接书写 HTML。
- JSX 让代码更具可读性和直观性。
-
单向数据流:
- 数据通过 props 从父组件流向子组件,组件之间通过 props 和 state 进行交互。
- 有利于跟踪数据的变化和调试。
-
虚拟 DOM:
- React 使用虚拟 DOM 来提高性能。每次状态变化时,React 生成新的虚拟 DOM,然后与旧的虚拟 DOM 进行比较,最后只更新实际需要改变的部分。
-
Hooks:
- React 16.8 引入了 Hooks,允许在函数组件中使用 state 和其他 React 特性,使函数组件功能更强大。
-
生态系统:
- 拥有丰富的周边库和工具,如 React Router(路由)、Redux(状态管理)等。
- 强大的社区支持和丰富的学习资源。
应用场景
- 适用于需要高性能和动态交互的复杂应用。
- 企业级应用和大型项目。
- 需要精细化控制组件生命周期和状态的场景。
Vue
概述
Vue 是由尤雨溪开发并开源的前端 JavaScript 框架。Vue 采用渐进式架构设计,既可以作为库使用,也可以通过插件和周边库扩展成完整的框架。
核心特点
-
渐进式框架:
- Vue 可以从简单的库逐步扩展为复杂的框架,适应不同规模的项目需求。
- 可以选择性地使用 Vue 的各个功能模块。
-
模板语法:
- Vue 使用模板语法来声明式地描述 UI,可以选择使用 JSX 或者渲染函数。
- 模板语法简单易学,适合初学者。
-
双向数据绑定:
- Vue 的核心是响应式系统,数据变化会自动更新 DOM。
- 使用 v-model 指令实现表单控件的双向绑定。
-
组件化:
- 和 React 类似,Vue 也采用组件化开发,组件可以组合成复杂的 UI。
- 支持单文件组件(SFC),将模板、脚本和样式封装在一个文件中。
-
指令系统:
- Vue 提供了一系列内置指令(如 v-if, v-for, v-bind),使得模板编写更加简洁。
-
生态系统:
- 拥有丰富的官方插件,如 Vue Router(路由)、Vuex(状态管理)、Vue CLI(脚手架工具)等。
- 活跃的社区和良好的中文支持。
应用场景
- 适用于中小型项目和快速开发的应用。
- 单页应用(SPA)和需要双向数据绑定的表单处理。
- 需要快速上手和开发效率高的项目。
React vs Vue 对比
-
学习曲线:
- Vue:语法简单易学,文档友好,适合初学者快速上手。
- React:需要理解 JSX 和一些 JavaScript 的高级特性(如函数式编程),学习曲线略高。
-
灵活性:
- React:灵活性高,可以选择各种状态管理和路由解决方案,但需要更多的配置。
- Vue:提供了较为完备的解决方案,默认集成了常用功能,减少了选择的困扰。
-
性能:
- 两者在性能上表现都很优异,React 的虚拟 DOM 和 Vue 的响应式系统都能高效地更新视图。
-
生态系统:
- React:生态系统庞大,社区活跃,有大量的第三方库和工具支持。
- Vue:官方插件和工具更完备,中文社区支持好。
-
企业支持:
- React:由 Facebook 维护,企业级支持和稳定性高。
- Vue:开源社区驱动,尤雨溪及其团队维护,近年来在企业中也获得了广泛采用。
总结
- React 适合需要精细控制、性能优化的大型和复杂应用,特别是在企业级项目中有广泛应用。
- Vue 适合快速开发和中小型项目,语法简单、上手快,非常适合初学者和快速迭代的项目。
选择 React 还是 Vue 主要取决于团队的技术背景、项目需求和个人偏好。在实际开发中,这两个框架都可以帮助开发者构建高性能、可维护的前端应用。
16. 谈一下你对虚拟dom , 渲染器的理解
虚拟 DOM (Virtual DOM)
概述
虚拟 DOM 是一种抽象的树结构,它表示了真实 DOM 的结构。虚拟 DOM 是存在于内存中的一棵树,它对真实 DOM 进行了一层抽象,描述了一个 UI 的结构及其状态。
工作原理
-
初次渲染:
- 构建虚拟 DOM 树:在初次渲染时,框架会根据组件的描述构建一棵虚拟 DOM 树。
- 渲染真实 DOM:将虚拟 DOM 树转换为真实 DOM 并插入到页面中。
-
状态更新:
- 构建新虚拟 DOM 树:当组件的状态或属性发生变化时,会构建一棵新的虚拟 DOM 树。
- 比较(Diffing):框架会将新旧两棵虚拟 DOM 树进行比较,找出变化的部分。
- 更新真实 DOM:根据比较结果,最小化地更新真实 DOM。
优势
-
性能优化:
- 通过最小化 DOM 操作,减少了浏览器的重排和重绘,从而提升性能。
-
跨平台能力:
- 虚拟 DOM 是平台无关的,可以用于浏览器端、移动端甚至服务器端渲染。
-
开发体验:
- 通过虚拟 DOM,可以实现组件化开发、状态管理和高效的更新策略,使开发者专注于业务逻辑,而不是手动管理 DOM 操作。
渲染器
渲染器是框架的一部分,它负责将虚拟 DOM 转换为真实 DOM,并将变化反映到页面上。渲染器的核心功能包括初次渲染和后续的更新。
渲染器的工作流程
-
初次渲染:
- 解析组件:渲染器根据组件的描述生成虚拟 DOM 树。
- 生成真实 DOM:将虚拟 DOM 树转换为真实 DOM 并插入页面。
-
状态更新:
- 生成新虚拟 DOM:当组件状态变化时,生成新的虚拟 DOM 树。
- Diff 算法:比较新旧虚拟 DOM 树,找出差异。
- 更新真实 DOM:根据差异,最小化地更新真实 DOM。
Diff 算法
Diff 算法是虚拟 DOM 的核心,用于高效地比较两棵虚拟 DOM 树,找出需要更新的部分。常见的 Diff 算法包括以下几个步骤:
-
分层比较:
- 从根节点开始,逐层比较新旧虚拟 DOM 树,找出节点的差异。
-
节点类型比较:
- 如果节点类型不同,直接替换节点。
- 如果节点类型相同,则继续比较子节点。
-
属性比较:
- 比较节点属性的变化,更新不同的属性。
-
子节点比较:
- 递归比较子节点,找出子节点的差异。
实际应用中的渲染器
不同的框架有不同的渲染器实现,例如 React 和 Vue 的渲染器。
React 渲染器
React 的渲染器主要包括以下几个部分:
-
Reconciler:
- 负责构建和比较虚拟 DOM 树,计算出变化的部分。
-
Renderer:
- 负责将虚拟 DOM 树转换为真实 DOM 并应用更新。
Vue 渲染器
Vue 的渲染器和 React 类似,但在实现上有所不同:
-
Compiler:
- 将模板转换为虚拟 DOM 渲染函数。
-
Reactivity System:
- 负责跟踪依赖和变化,触发更新。
-
Renderer:
- 负责将虚拟 DOM 转换为真实 DOM 并应用更新。
虚拟 DOM 和渲染器的总结
虚拟 DOM 和渲染器是现代前端框架的重要组成部分,它们通过抽象和优化 DOM 操作,提高了性能和开发效率。
- 虚拟 DOM:是对真实 DOM 的抽象,存在于内存中,通过 Diff 算法找出变化部分,最小化 DOM 操作。
- 渲染器:负责将虚拟 DOM 转换为真实 DOM,并应用更新。不同框架有不同的渲染器实现。
通过虚拟 DOM 和渲染器,开发者可以更高效地构建复杂的用户界面,同时保证性能和可维护性。
17. diff 策略是什么?
https://juejin.cn/post/7116326409961734152
18. react fiber 架构解决了什么问题? 为啥, fiber节点的属性
React Fiber 架构解决的问题
React Fiber 是 React 的重新实现,它的目标是解决 React 在大型和复杂应用中遇到的性能和灵活性问题。以下是 React Fiber 解决的主要问题及其原因:
1. 时间分片(Time Slicing)
问题:传统的 React 更新是同步的,一旦开始更新,就会一直进行,直到完成。这在更新较大的组件树时可能导致主线程被长时间占用,阻塞用户交互,导致界面卡顿。
解决:Fiber 引入了时间分片(Time Slicing)技术,将更新工作分成可中断的小任务块,使得 React 可以在任务之间暂停,处理其他高优先级的任务(如用户输入),从而提高响应性。
2. 优先级(Prioritization)
问题:在传统的 React 中,所有更新都有相同的优先级,无法区分紧急的用户交互更新和低优先级的后台数据更新。
解决:Fiber 为不同类型的更新赋予了不同的优先级。这样,紧急的用户交互更新可以优先处理,确保用户体验流畅。
3. 并发(Concurrency)
问题:同步渲染无法充分利用多核 CPU 的并发能力,导致在复杂计算和渲染时性能瓶颈。
解决:Fiber 设计为支持未来的并发渲染,使得 React 可以在多核 CPU 上并行执行不同的渲染任务,提高性能。
4. 细粒度的更新控制
问题:传统的 React 中,无法中途暂停和恢复更新,导致在复杂场景下灵活性不足。
解决:Fiber 通过将更新过程分解为多个小任务块,可以在必要时暂停和恢复更新,从而提高灵活性和控制力。
Fiber 节点的属性
Fiber 节点是 Fiber 架构的基本单位,表示一个组件、DOM 元素或其他节点。每个 Fiber 节点包含许多属性,用于描述节点的状态、结构和更新信息。以下是一些关键属性:
- tag:表示节点的类型,如函数组件、类组件、宿主组件(DOM 元素)等。
- key:唯一标识,用于高效地管理列表中的节点。
- type:组件类型,对应组件的构造函数或 JSX 元素类型。
- stateNode:当前 Fiber 节点对应的实际实例或 DOM 节点。
- child:指向第一个子 Fiber 节点。
- sibling:指向下一个兄弟 Fiber 节点。
- return:指向父 Fiber 节点。
- pendingProps:在更新期间存储新的 props。
- memoizedProps:节点的上一次渲染的 props。
- memoizedState:节点的上一次渲染的 state。
- updateQueue:更新队列,存储待处理的状态和 props 更新。
- effectTag:标记节点的更新类型,如插入、更新、删除等。
- nextEffect:指向下一个需要处理的节点,用于协调和更新阶段。
- expirationTime:标记节点的任务过期时间,用于优先级调度。
结论
React Fiber 架构通过引入时间分片、优先级调度和细粒度更新控制,解决了传统 React 在大型和复杂应用中的性能和灵活性问题。Fiber 节点的多种属性帮助实现了这些改进,使得 React 可以更高效、更灵活地管理和更新组件树,提高用户体验。
19. 为什么 hooks 不能在循环判断等语句内使用?
在 React 中,Hooks 不能在循环、条件语句或者嵌套函数中使用,这是因为 React 需要依赖 Hooks 的调用顺序来正确地管理组件的状态和副作用。如果 Hooks 的调用顺序改变,React 将无法正确地维护这些状态和副作用,从而导致不可预测的行为。以下是详细的解释:
Hooks 的调用顺序
React 通过一种叫做 “Hook List” 的内部机制来追踪 Hooks 的调用顺序。每次组件渲染时,React 需要按照固定的顺序调用 Hooks。这样,React 才能在重新渲染时正确地恢复每个 Hook 对应的状态。
问题所在
如果在循环、条件语句或嵌套函数中使用 Hooks,调用顺序可能会改变。例如:
function MyComponent() {
const [state1, setState1] = useState(0);
if (state1 > 0) {
const [state2, setState2] = useState(1); // 仅在 state1 > 0 时调用
}
// React 不能保证 useState 的调用顺序
// 这会导致状态的混乱
}
在上面的例子中,useState(1)
只有在 state1 > 0
时才会被调用,这会导致每次渲染时 Hooks 的调用顺序不同。React 无法正确地匹配每个 Hook 与其对应的状态,从而导致状态错乱。
规则
为了避免这种情况,React 定义了一些规则,称为 Hook 的“使用规则”(Rules of Hooks):
-
只能在函数组件或自定义 Hook 顶层调用:不要在循环、条件语句或嵌套函数中调用 Hooks。必须确保每次渲染时都以相同的顺序调用 Hooks。
-
只能在 React 函数组件或自定义 Hook 中调用:不能在普通的 JavaScript 函数中调用 Hooks。
解决方法
如果你需要在条件语句中使用不同的逻辑,可以将逻辑拆分到不同的 Hooks 或者组件中。比如:
function MyComponent() {
const [state1, setState1] = useState(0);
useConditionalHook(state1 > 0);
}
function useConditionalHook(condition) {
const [state2, setState2] = useState(1); // 这个 Hook 永远会被调用
if (condition) {
// 在 Hook 内部可以使用条件语句,因为 Hook 本身的调用顺序不变
}
}
通过将条件逻辑放在自定义 Hook 内部,可以确保自定义 Hook 本身的调用顺序不变,而内部的条件逻辑不会影响 Hook 的调用顺序。
总结
Hooks 的调用顺序必须在每次渲染中保持一致,以确保 React 能正确地管理状态和副作用。为了实现这一点,Hooks 不能在循环、条件语句或嵌套函数中使用。遵守 React 的“使用规则”可以帮助你避免常见的错误,并确保你的组件在状态管理方面表现稳定。
20. React Next.js 的服务器端渲染(SSR)
Next.js 是一个基于 React 的流行框架,它提供了服务器端渲染(SSR)和静态站点生成(SSG)的功能,从而优化 React 应用的性能和 SEO。下面详细解释 Next.js 中服务器端渲染的实现及其工作原理。
1. 服务器端渲染的概念
服务器端渲染(SSR)指的是在服务器上生成 HTML 内容,并将其发送给客户端。这与客户端渲染(CSR)不同,后者是在客户端浏览器中使用 JavaScript 生成内容。SSR 的优点包括更快的初始加载时间和更好的搜索引擎优化(SEO)。
2. Next.js SSR 的实现原理
Next.js 提供了几个特性和函数,使得在服务器端渲染 React 组件变得简单。
2.1 getServerSideProps
这是 Next.js 提供的一个特殊函数,用于在服务器端获取数据并传递给页面组件。
-
基本使用:
export async function getServerSideProps(context) { // Fetch data from external API const res = await fetch(`https://api.example.com/data`); const data = await res.json(); // Pass data to the page via props return { props: { data } }; } function MyPage({ data }) { return ( <div> <h1>Data from Server</h1> <pre>{JSON.stringify(data, null, 2)}</pre> </div> ); } export default MyPage;
在上面的例子中,
getServerSideProps
函数在每次请求时都会在服务器端执行,并返回一个包含数据的对象,该数据作为 props 传递给页面组件MyPage
。
2.2 getInitialProps
这是一个用于获取初始数据的函数,适用于页面和自定义的 _app.js
。
-
基本使用:
MyPage.getInitialProps = async (context) => { const res = await fetch('https://api.example.com/data'); const data = await res.json(); return { data }; }; function MyPage({ data }) { return ( <div> <h1>Data from Server</h1> <pre>{JSON.stringify(data, null, 2)}</pre> </div> ); } export default MyPage;
getInitialProps
在页面加载之前运行,可以在服务器端或客户端执行,具体取决于页面是如何导航的。
3. SSR 的工作流程
- 请求到达服务器:当用户请求一个页面时,请求会到达 Next.js 的服务器。
- 执行
getServerSideProps
或getInitialProps
:Next.js 在服务器上调用这些数据获取方法,以获取页面所需的数据。 - 渲染页面:使用获取的数据,Next.js 在服务器上渲染 React 组件,并生成 HTML。
- 发送 HTML:生成的 HTML 发送到客户端。
- 客户端接管:React 在客户端重新初始化,接管 HTML,使其变得可交互。
4. 优化和注意事项
- 缓存:由于每个请求都会触发服务器端渲染,可能导致服务器负载较高。可以使用缓存策略(如 CDN 缓存)来减轻服务器压力。
- 数据获取性能:确保服务器端的数据获取操作尽可能高效,以减少页面的初始加载时间。
- Hydration:在客户端接管服务器渲染的 HTML 时,React 会进行 hydration,确保客户端和服务器端的内容一致。如果内容不一致,会出现警告或错误。
5. 小结
Next.js 通过 getServerSideProps
和 getInitialProps
等特殊函数简化了服务器端渲染的实现。这些功能使得开发者能够在服务器端获取数据并生成 HTML,提高了 React 应用的性能和 SEO。理解这些函数的工作原理和最佳实践,有助于开发出高性能和用户体验良好的应用。
21. 了解哪些全局状态管理库?redux 的设计模式是? redux原理
全局状态管理库
在现代前端开发中,管理应用的全局状态是一个重要且常见的需求。以下是一些流行的全局状态管理库:
-
Redux:
- Redux 是一个流行的 JavaScript 状态管理库,采用单一数据源(single source of truth)和不可变状态(immutable state)。
-
MobX:
- MobX 是一个状态管理库,基于响应式编程,通过观察状态的变化自动更新 UI。
-
Context API:
- React 自带的 Context API 也是一种管理全局状态的方法,适合较简单的状态管理需求。
-
Recoil:
- Recoil 是 Facebook 开发的一种状态管理库,提供了细粒度的原子状态,支持更高效的状态管理。
-
Zustand:
- Zustand 是一个轻量级的状态管理库,采用简单的 API 和高效的性能。
Redux 的设计模式
Redux 的设计模式主要基于 Flux 架构,包含以下几个核心概念:
-
单一数据源(Single Source of Truth):
- 应用的全局状态存储在一个单一的 store 对象中,作为应用状态的唯一数据源。
-
状态是只读的(State is Read-Only):
- 状态不能直接修改,唯一改变状态的方法是触发 action。
-
使用纯函数进行状态更新(Changes are made with Pure Functions):
- Reducers 是纯函数,根据当前状态和 action 返回新的状态,不修改原有状态。
Redux 原理
Redux 的工作原理可以通过以下几个步骤理解:
-
Store:
- Store 是一个对象,包含应用的全局状态。通过
createStore
函数创建。
- Store 是一个对象,包含应用的全局状态。通过
-
Action:
- Action 是一个普通的 JavaScript 对象,用于描述希望对状态进行的操作。每个 action 都有一个
type
属性,表示操作的类型。
- Action 是一个普通的 JavaScript 对象,用于描述希望对状态进行的操作。每个 action 都有一个
-
Reducer:
- Reducer 是一个纯函数,接收当前状态和 action,返回新的状态。Reducer 根据 action 的
type
属性决定如何更新状态。
- Reducer 是一个纯函数,接收当前状态和 action,返回新的状态。Reducer 根据 action 的
-
Dispatch:
dispatch
是一个函数,用于触发 action,传递到 reducer 进行状态更新。
-
Subscribe:
subscribe
是一个函数,用于注册监听器,每当状态更新时,监听器都会被调用。
Redux 的基本使用示例
以下是一个简单的 Redux 使用示例,演示了如何创建 store、定义 action 和 reducer,并触发状态更新:
import { createStore } from 'redux';
// 定义 action 类型
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
// 定义 action creator
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });
// 定义 reducer
const counter = (state = 0, action) => {
switch (action.type) {
case INCREMENT:
return state + 1;
case DECREMENT:
return state - 1;
default:
return state;
}
};
// 创建 store
const store = createStore(counter);
// 订阅 store
store.subscribe(() => console.log(store.getState()));
// 分发 action
store.dispatch(increment()); // 输出: 1
store.dispatch(increment()); // 输出: 2
store.dispatch(decrement()); // 输出: 1
Redux 的高级特性
Redux 还提供了一些高级特性来增强其功能和灵活性:
-
Middleware:
- 中间件用于在 action 被 dispatch 之后、到达 reducer 之前对 action 进行处理。常见的中间件包括 redux-thunk(用于处理异步 action)和 redux-saga(用于更复杂的异步流程)。
-
Redux DevTools:
- Redux DevTools 是一个调试工具,允许开发者查看状态的变化、回溯和重放 action,从而更容易地调试应用。
-
Combine Reducers:
combineReducers
函数用于将多个 reducer 合并为一个主 reducer,从而将状态管理拆分为更小、更易管理的模块。
总结
全局状态管理在现代前端开发中扮演着重要角色,Redux 作为其中的佼佼者,基于 Flux 架构,通过单一数据源、不可变状态和纯函数更新等设计模式,提供了一种可预测的状态管理方法。理解 Redux 的工作原理及其核心概念,对于构建和维护复杂的 React 应用至关重要。
22. Webpack 打包的流程? loader 和 plugin 的区别?
Webpack 打包的流程
Webpack 是一个现代 JavaScript 应用程序的静态模块打包器。它从一个或多个入口文件开始,递归地构建依赖关系图,最终将所有模块打包成一个或多个捆绑包。以下是 Webpack 打包的主要流程:
-
初始化:
- 读取配置文件(webpack.config.js)并合并命令行参数,生成最终配置对象。
-
编译:
- 从入口文件(Entry)开始,调用所有配置的 Loader 对模块进行转换,递归解析模块的依赖。
-
模块化:
- 将每个文件视为一个模块,根据文件类型和规则,使用相应的 Loader 处理模块内容。
-
解析依赖:
- 解析模块中的依赖关系(如
import
或require
),并继续递归处理这些依赖模块。
- 解析模块中的依赖关系(如
-
代码转换:
- 使用 Loader 对代码进行转换,如从 ES6 转换到 ES5,从 TypeScript 转换到 JavaScript,或从 SCSS 转换到 CSS。
-
生成模块图:
- 根据入口文件和依赖关系生成模块图(Module Graph),表示各模块之间的依赖关系。
-
优化:
- 根据配置,进行代码优化,如代码分割(Code Splitting)、去重(Deduplication)、压缩(Minification)等。
-
输出:
- 根据配置,将最终的资源文件输出到指定目录(通常是
dist
目录)。
- 根据配置,将最终的资源文件输出到指定目录(通常是
Loader 和 Plugin 的区别
Loader 和 Plugin 是 Webpack 中用于扩展和定制打包过程的两个重要概念,它们有不同的功能和应用场景。
Loader
Loader 主要用于对模块的源代码进行转换,它们是函数,接受源文件内容作为参数,返回转换后的内容。Loader 用于在编译过程中处理不同类型的文件,使得 Webpack 能够理解和打包这些文件。
-
应用场景:
- 转换文件类型,例如将 ES6 转换为 ES5(babel-loader)。
- 处理样式文件,例如将 SCSS 转换为 CSS(sass-loader)。
- 加载图片或字体文件,例如将图片文件转换为 URL(url-loader)。
-
示例:
module.exports = { module: { rules: [ { test: /\.js$/, use: 'babel-loader', exclude: /node_modules/, }, { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'], }, ], }, };
Plugin
Plugin 用于在整个编译生命周期中执行更广泛的任务,具有更强的功能和灵活性。Plugin 可以访问 Webpack 的编译过程,从而对打包结果进行各种操作和优化。
-
应用场景:
- 资源压缩,例如压缩 JavaScript(TerserPlugin)或 CSS(MiniCssExtractPlugin)。
- 生成额外文件,例如生成 HTML 文件(HtmlWebpackPlugin)。
- 环境变量定义,例如定义全局变量(DefinePlugin)。
- 提取公共模块,例如将多个入口文件的公共代码提取到一个单独的文件(CommonsChunkPlugin)。
-
示例:
const HtmlWebpackPlugin = require('html-webpack-plugin'); const { DefinePlugin } = require('webpack'); module.exports = { plugins: [ new HtmlWebpackPlugin({ template: './src/index.html', }), new DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production'), }), ], };
总结
-
Webpack 打包流程:Webpack 通过初始化配置、编译模块、解析依赖、代码转换、生成模块图、优化和输出等步骤,将入口文件及其依赖打包成最终的资源文件。
-
Loader 和 Plugin 的区别:
- Loader:用于转换模块的源代码,是函数,处理特定类型的文件。
- Plugin:用于执行更广泛的任务,是对象,可以访问编译生命周期,进行更强大的功能扩展和优化。
理解这些概念和流程,有助于更好地配置和优化 Webpack 打包过程,提高构建效率和代码质量。
22. react性能优化
React 的性能优化是前端开发中的一个重要环节,优化得当可以显著提高应用的响应速度和用户体验。以下是一些常见的 React 优化技巧和方法:
1. 使用 PureComponent
和 memo
-
PureComponent
:React.PureComponent
是React.Component
的一种优化版本,它会对 props 和 state 进行浅比较,从而避免不必要的重渲染。class MyComponent extends React.PureComponent { render() { return <div>{this.props.value}</div>; } }
-
React.memo
:React.memo
是一个高阶组件,用于函数组件,类似于PureComponent
,会对 props 进行浅比较。const MyComponent = React.memo(function MyComponent(props) { return <div>{props.value}</div>; });
2. 使用 useMemo
和 useCallback
-
useMemo
:用于缓存计算结果,避免每次渲染时都重新计算。const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
-
useCallback
:用于缓存函数定义,避免在子组件中因为函数引用变化导致的重渲染。const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);
3. 避免匿名函数和对象
每次渲染时创建匿名函数和对象会导致不必要的重渲染。可以将这些函数和对象提取出来,或者使用 useCallback
和 useMemo
进行缓存。
4. 分离和延迟加载组件
-
代码分割:使用 React.lazy 和 Suspense 进行代码分割和延迟加载。
const OtherComponent = React.lazy(() => import('./OtherComponent')); function MyComponent() { return ( <Suspense fallback={<div>Loading...</div>}> <OtherComponent /> </Suspense> ); }
-
动态导入:根据需要动态导入模块,减少初始加载时间。
import('lodash').then(({ default: _ }) => { // 使用 lodash });
5. 避免不必要的状态更新
确保只在需要时更新状态,避免频繁的状态更新导致的重渲染。可以通过以下方法实现:
- 将不相关的状态分离到不同的组件。
- 使用局部状态代替全局状态。
- 通过 shouldComponentUpdate 或 React.memo 进行性能优化。
6. 使用合适的数据结构和算法
选择高效的数据结构和算法,可以显著提高性能。例如:
- 使用哈希表而不是数组进行查找操作。
- 优化算法的时间复杂度。
7. 避免大量 DOM 操作
尽量减少对 DOM 的直接操作,使用 React 提供的虚拟 DOM 机制进行高效的更新。对于需要大量 DOM 操作的情况,可以考虑使用 Web Worker 进行分离处理。
8. 使用 React.StrictMode
React.StrictMode
可以帮助你在开发模式下发现潜在的性能问题和不安全的生命周期方法。
<React.StrictMode>
<App />
</React.StrictMode>
9. 避免 Reconciliation(协调)
通过以下方法可以减少协调的开销:
- 使用
key
属性,确保列表项的唯一性,避免不必要的重排。 - 避免在每次渲染时生成新的引用类型(如数组、对象),可以使用
useMemo
和useCallback
缓存这些值。
10. 性能分析和监控
使用 React DevTools 和性能分析工具(如 Chrome DevTools、Lighthouse)对应用进行性能分析,发现和解决潜在的性能瓶颈。
总结
React 提供了多种优化性能的工具和方法,包括使用 PureComponent
和 memo
进行组件优化,使用 useMemo
和 useCallback
进行值和函数缓存,避免不必要的状态更新和大量 DOM 操作,动态导入和代码分割等。通过这些优化,可以显著提高 React 应用的性能和用户体验。
23. class跟func组件你的理解
在 React 中,组件是构建用户界面的基本单元。React 提供了两种创建组件的方式:类组件(Class Component)和函数组件(Function Component)。两者在编写方式、功能和使用场景上有一些区别。下面详细解释这两种组件的特点和我的理解。
类组件(Class Component)
特点
-
基于 ES6 类:
- 类组件是使用 JavaScript 的 ES6 类语法创建的。
class MyComponent extends React.Component { render() { return <div>Hello, World!</div>; } }
-
生命周期方法:
- 类组件有多个生命周期方法(如
componentDidMount
、componentDidUpdate
、componentWillUnmount
),可以在组件的不同生命周期阶段执行代码。
class MyComponent extends React.Component { componentDidMount() { // 组件已挂载 } componentDidUpdate(prevProps, prevState) { // 组件已更新 } componentWillUnmount() { // 组件将卸载 } render() { return <div>Hello, World!</div>; } }
- 类组件有多个生命周期方法(如
-
状态管理:
- 类组件通过
this.state
管理组件的内部状态,使用this.setState
更新状态。
class MyComponent extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } increment = () => { this.setState({ count: this.state.count + 1 }); } render() { return ( <div> <p>Count: {this.state.count}</p> <button onClick={this.increment}>Increment</button> </div> ); } }
- 类组件通过
-
绑定事件处理程序:
- 由于
this
的上下文问题,需要手动绑定事件处理程序,通常在构造函数中进行绑定,或使用箭头函数避免绑定。
constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { // 处理点击事件 }
- 由于
函数组件(Function Component)
特点
-
基于函数:
- 函数组件是使用 JavaScript 的函数创建的。最初函数组件是无状态的,只接收 props 并返回 JSX。
function MyComponent(props) { return <div>Hello, World!</div>; }
-
使用 Hooks 管理状态和副作用:
- React 16.8 引入了 Hooks,使函数组件可以拥有状态和使用生命周期方法。常用的 Hooks 有
useState
、useEffect
、useContext
等。
import React, { useState, useEffect } from 'react'; function MyComponent() { const [count, setCount] = useState(0); useEffect(() => { // 组件已挂载和更新时执行 return () => { // 组件将卸载时执行 }; }, [count]); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }
- React 16.8 引入了 Hooks,使函数组件可以拥有状态和使用生命周期方法。常用的 Hooks 有
-
简洁且无
this
:- 函数组件没有
this
的概念,状态和事件处理更加简洁明了。
function MyComponent() { const [count, setCount] = useState(0); const increment = () => { setCount(count + 1); }; return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); }
- 函数组件没有
类组件与函数组件的对比
-
编写简洁性:
- 函数组件更简洁,无需编写类结构和绑定
this
,代码更清晰易读。
- 函数组件更简洁,无需编写类结构和绑定
-
性能:
- 函数组件在某些情况下性能更好,因为它们更轻量级,不需要实例化对象和管理生命周期方法。
-
状态和副作用管理:
- Hooks 的引入使得函数组件可以方便地管理状态和副作用,大大增强了函数组件的功能。
-
社区趋势:
- React 社区趋势逐渐偏向于使用函数组件和 Hooks,React 官方也建议优先使用函数组件。
总结
类组件和函数组件各有优缺点,但随着 Hooks 的引入,函数组件的功能和易用性大大增强。函数组件的简洁性、性能优势以及社区趋势,使其成为 React 开发的首选方式。理解这两种组件的特点和适用场景,可以帮助开发者更好地构建和优化 React 应用。
24. React 中是如何处理事件机制的?
React 事件处理机制是其一大特色,与传统的 DOM 事件处理不同。React 采用了合成事件(Synthetic Event)系统,为了解决跨浏览器兼容性问题,并提供更高效的事件处理机制。以下是对 React 事件处理机制的详细介绍。
1. 合成事件(Synthetic Event)
合成事件是 React 自己实现的一套事件系统,它在所有浏览器中都表现一致。React 将原生的浏览器事件封装成合成事件,以便提供跨浏览器的兼容性和性能优化。
- 统一事件接口:
- React 的合成事件提供了一个统一的接口,避免了在处理事件时需要处理不同浏览器的兼容性问题。
- 合成事件对象与原生事件对象具有相同的属性和方法,如
event.target
、event.preventDefault()
等。
2. 事件处理函数
在 React 中,事件处理函数通过 JSX 属性绑定到特定的事件。与传统 DOM 事件处理不同,React 事件处理函数遵循 camelCase 命名规范,并且不需要显式调用 addEventListener
方法。
function MyComponent() {
function handleClick(event) {
console.log('Button clicked', event);
}
return (
<button onClick={handleClick}>
Click Me
</button>
);
}
3. 事件代理
React 使用事件代理(Event Delegation)技术,将所有组件的事件处理函数统一绑定到根元素上。这样可以减少内存消耗,提升事件处理的性能。
- 事件代理的工作原理:
- 当组件中的元素触发事件时,事件会冒泡到根元素(通常是
document
或root
元素)。 - React 在根元素上捕获这些事件,并根据事件目标和绑定的处理函数来调用相应的合成事件处理函数。
- 当组件中的元素触发事件时,事件会冒泡到根元素(通常是
4. 事件绑定
React 中的事件处理函数可以直接在 JSX 中使用,并且可以访问组件的状态和属性。事件处理函数的绑定有多种方式:
- 使用箭头函数绑定:
class MyComponent extends React.Component {
state = { count: 0 };
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<button onClick={this.handleClick}>
Click Me
</button>
);
}
}
- 在构造函数中绑定:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<button onClick={this.handleClick}>
Click Me
</button>
);
}
}
5. 事件对象
React 的合成事件对象会被自动池化(pooled),以提高性能。这意味着事件对象在事件处理函数执行完毕后会被重用。为了在异步代码中使用事件对象,需要调用 event.persist()
方法保留事件对象。
function MyComponent() {
function handleClick(event) {
event.persist(); // 保留事件对象
setTimeout(() => {
console.log('Button clicked', event);
}, 1000);
}
return (
<button onClick={handleClick}>
Click Me
</button>
);
}
6. 常见事件类型
React 支持大多数标准的 DOM 事件,包括鼠标事件、键盘事件、表单事件和触摸事件等。以下是一些常见的事件类型:
- 鼠标事件:
onClick
、onDoubleClick
、onMouseEnter
、onMouseLeave
等。 - 键盘事件:
onKeyDown
、onKeyPress
、onKeyUp
等。 - 表单事件:
onChange
、onSubmit
、onFocus
、onBlur
等。 - 触摸事件:
onTouchStart
、onTouchMove
、onTouchEnd
等。
7. 防止默认行为和事件传播
可以使用 event.preventDefault()
和 event.stopPropagation()
方法来防止默认行为和事件传播:
function MyComponent() {
function handleClick(event) {
event.preventDefault(); // 阻止默认行为
event.stopPropagation(); // 阻止事件传播
console.log('Button clicked');
}
return (
<a href="https://example.com" onClick={handleClick}>
Click Me
</a>
);
}
总结
React 的事件处理机制通过合成事件提供了统一的跨浏览器兼容性,同时通过事件代理提升了性能。事件处理函数可以直接在 JSX 中使用,并且可以访问组件的状态和属性。理解和掌握 React 的事件处理机制,有助于更高效地开发和维护 React 应用。
25. react 事件机制
React 事件处理机制是其一大特色,与传统的 DOM 事件处理不同。React 采用了合成事件(Synthetic Event)系统,为了解决跨浏览器兼容性问题,并提供更高效的事件处理机制。以下是对 React 事件处理机制的详细介绍。
1. 合成事件(Synthetic Event)
合成事件是 React 自己实现的一套事件系统,它在所有浏览器中都表现一致。React 将原生的浏览器事件封装成合成事件,以便提供跨浏览器的兼容性和性能优化。
- 统一事件接口:
- React 的合成事件提供了一个统一的接口,避免了在处理事件时需要处理不同浏览器的兼容性问题。
- 合成事件对象与原生事件对象具有相同的属性和方法,如
event.target
、event.preventDefault()
等。
2. 事件处理函数
在 React 中,事件处理函数通过 JSX 属性绑定到特定的事件。与传统 DOM 事件处理不同,React 事件处理函数遵循 camelCase 命名规范,并且不需要显式调用 addEventListener
方法。
function MyComponent() {
function handleClick(event) {
console.log('Button clicked', event);
}
return (
<button onClick={handleClick}>
Click Me
</button>
);
}
3. 事件代理
React 使用事件代理(Event Delegation)技术,将所有组件的事件处理函数统一绑定到根元素上。这样可以减少内存消耗,提升事件处理的性能。
- 事件代理的工作原理:
- 当组件中的元素触发事件时,事件会冒泡到根元素(通常是
document
或root
元素)。 - React 在根元素上捕获这些事件,并根据事件目标和绑定的处理函数来调用相应的合成事件处理函数。
- 当组件中的元素触发事件时,事件会冒泡到根元素(通常是
4. 事件绑定
React 中的事件处理函数可以直接在 JSX 中使用,并且可以访问组件的状态和属性。事件处理函数的绑定有多种方式:
- 使用箭头函数绑定:
class MyComponent extends React.Component {
state = { count: 0 };
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<button onClick={this.handleClick}>
Click Me
</button>
);
}
}
- 在构造函数中绑定:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<button onClick={this.handleClick}>
Click Me
</button>
);
}
}
5. 事件对象
React 的合成事件对象会被自动池化(pooled),以提高性能。这意味着事件对象在事件处理函数执行完毕后会被重用。为了在异步代码中使用事件对象,需要调用 event.persist()
方法保留事件对象。
function MyComponent() {
function handleClick(event) {
event.persist(); // 保留事件对象
setTimeout(() => {
console.log('Button clicked', event);
}, 1000);
}
return (
<button onClick={handleClick}>
Click Me
</button>
);
}
6. 常见事件类型
React 支持大多数标准的 DOM 事件,包括鼠标事件、键盘事件、表单事件和触摸事件等。以下是一些常见的事件类型:
- 鼠标事件:
onClick
、onDoubleClick
、onMouseEnter
、onMouseLeave
等。 - 键盘事件:
onKeyDown
、onKeyPress
、onKeyUp
等。 - 表单事件:
onChange
、onSubmit
、onFocus
、onBlur
等。 - 触摸事件:
onTouchStart
、onTouchMove
、onTouchEnd
等。
7. 防止默认行为和事件传播
可以使用 event.preventDefault()
和 event.stopPropagation()
方法来防止默认行为和事件传播:
function MyComponent() {
function handleClick(event) {
event.preventDefault(); // 阻止默认行为
event.stopPropagation(); // 阻止事件传播
console.log('Button clicked');
}
return (
<a href="https://example.com" onClick={handleClick}>
Click Me
</a>
);
}
总结
React 的事件处理机制通过合成事件提供了统一的跨浏览器兼容性,同时通过事件代理提升了性能。事件处理函数可以直接在 JSX 中使用,并且可以访问组件的状态和属性。理解和掌握 React 的事件处理机制,有助于更高效地开发和维护 React 应用。
26. 说一下 HTML5 drag API
- dragstart:事件主体是被拖放元素,在开始拖放被拖放元素时触发。
- darg:事件主体是被拖放元素,在正在拖放被拖放元素时触发。
- dragenter:事件主体是目标元素,在被拖放元素进入某元素时触发。
- dragover:事件主体是目标元素,在被拖放在某元素内移动时触发。
- dragleave:事件主体是目标元素,在被拖放元素移出目标元素是触发。
- drop:事件主体是目标元素,在目标元素完全接受被拖放元素时触发。
- dragend:事件主体是被拖放元素,在整个拖放操作结束时触发。
拖拽API(Drag and Drop API)是HTML5提供的一组功能,使得在网页上实现拖放操作变得更加简单和强大。这个API允许开发者为网页元素添加拖拽功能,用户可以通过鼠标将元素拖动并放置到指定的目标区域。以下是对拖拽API的详细介绍:
主要概念和接口
-
Draggable属性
- HTML元素可以通过设置
draggable
属性为true
来启用拖动。例如:<div draggable="true">可拖动的元素</div>
- HTML元素可以通过设置
-
事件类型
拖拽API涉及几个关键的事件类型:dragstart
: 当用户开始拖动元素时触发。drag
: 在拖动过程中不断触发。dragend
: 当拖动操作结束时触发。dragenter
: 当被拖动的元素进入放置目标区域时触发。dragover
: 当被拖动的元素在放置目标区域上方移动时不断触发。dragleave
: 当被拖动的元素离开放置目标区域时触发。drop
: 当被拖动的元素放置到目标区域时触发。
-
DataTransfer对象
- 这些事件中的
event
对象包含一个dataTransfer
属性,用于存储和传递拖拽的数据。例如,可以使用dataTransfer.setData
和dataTransfer.getData
方法来设置和获取拖拽的数据。
- 这些事件中的
实现拖拽的步骤
-
使元素可拖动
设置元素的draggable
属性:<div id="dragItem" draggable="true">拖动我</div>
-
处理拖动开始事件
在dragstart
事件处理函数中,设置拖动数据:const dragItem = document.getElementById('dragItem'); dragItem.addEventListener('dragstart', function(event) { event.dataTransfer.setData('text/plain', event.target.id); event.dataTransfer.effectAllowed = 'move'; });
-
允许放置目标接收拖动元素
在放置目标上阻止默认行为,并在dragover
事件中允许放置:<div id="dropZone">放置到这里</div>
const dropZone = document.getElementById('dropZone'); dropZone.addEventListener('dragover', function(event) { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; });
-
处理放置事件
在drop
事件处理函数中,获取拖动数据,并处理拖放操作:dropZone.addEventListener('drop', function(event) { event.preventDefault(); const data = event.dataTransfer.getData('text/plain'); const draggedElement = document.getElementById(data); dropZone.appendChild(draggedElement); });
例子
综合以上步骤,完整的例子如下:
<!DOCTYPE html>
<html>
<head>
<style>
#dragItem {
width: 100px;
height: 100px;
background-color: lightblue;
margin: 10px;
}
#dropZone {
width: 200px;
height: 200px;
background-color: lightgreen;
margin: 10px;
}
</style>
</head>
<body>
<div id="dragItem" draggable="true">拖动我</div>
<div id="dropZone">放置到这里</div>
<script>
const dragItem = document.getElementById('dragItem');
const dropZone = document.getElementById('dropZone');
dragItem.addEventListener('dragstart', function(event) {
event.dataTransfer.setData('text/plain', event.target.id);
event.dataTransfer.effectAllowed = 'move';
});
dropZone.addEventListener('dragover', function(event) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
});
dropZone.addEventListener('drop', function(event) {
event.preventDefault();
const data = event.dataTransfer.getData('text/plain');
const draggedElement = document.getElementById(data);
dropZone.appendChild(draggedElement);
});
</script>
</body>
</html>
注意事项
-
浏览器支持
- 现代浏览器均支持拖拽API,但在实际开发中还是要注意兼容性问题。
-
用户体验
- 在实现拖放功能时,确保为用户提供良好的视觉反馈,如拖动元素的样式变化和放置区域的高亮显示。
27. webpack4和5的区别
Webpack是一个用于JavaScript应用程序的模块打包工具,能够将多个模块和依赖项打包成一个或多个文件,以优化加载和性能。Webpack 4和Webpack 5是两个主要版本,Webpack 5在Webpack 4的基础上进行了许多改进和新增功能。以下是Webpack 4和Webpack 5之间的主要区别:
性能和优化
-
持久缓存(Persistent Caching)
- Webpack 5 引入了持久缓存功能,大大加快了增量构建速度。通过在磁盘上缓存生成的模块和编译信息,重新构建时只需处理变化的部分,从而提升构建性能。
- Webpack 4 仅支持内存中的缓存,无法持久化到磁盘,增量构建性能较差。
-
树摇优化(Tree Shaking)
- Webpack 5 对于未使用代码的移除更加智能和彻底,包括对循环依赖的更好处理。
- Webpack 4 已经支持树摇优化,但在处理某些复杂依赖场景时效果不如Webpack 5。
模块和解析
-
模块联合(Module Federation)
- Webpack 5 引入了模块联合(Module Federation),允许多个独立的应用程序在运行时共享模块,从而实现微前端架构。
- Webpack 4 没有这种功能。
-
自动持久化缓存(Automatic Persistent Caching)
- Webpack 5 自动为模块和chunk生成持久化缓存键,提高构建性能。
- Webpack 4 不支持自动持久化缓存。
兼容性和弃用
-
Node.js 兼容性
- Webpack 5 不再默认包含Node.js的polyfills。对于那些依赖于Node.js内置模块的代码,需要手动添加相应的polyfill。
- Webpack 4 默认包含一些Node.js的polyfills。
-
插件和Loader的变化
- Webpack 5 中一些插件和Loader的API有变化,可能需要更新或替换为新版本。例如,
terser-webpack-plugin
在Webpack 5中内置。 - Webpack 4 的插件和Loader API相对稳定,但一些老旧插件和Loader可能不再适用于Webpack 5。
- Webpack 5 中一些插件和Loader的API有变化,可能需要更新或替换为新版本。例如,
开发者体验
-
配置和默认值
- Webpack 5 改进了默认配置,使得开箱即用的体验更好。例如,默认情况下更好地支持ESM(ECMAScript Modules)。
- Webpack 4 的默认配置较少,开发者需要更多地进行手动配置。
-
调试和错误信息
- Webpack 5 提供了更详细和友好的错误信息,帮助开发者更快地定位和解决问题。
- Webpack 4 的错误信息相对简略,不够详细。
兼容性与迁移
-
兼容性
- Webpack 5 更加关注对现代JavaScript特性的支持,可能会导致一些旧代码或第三方库需要调整才能兼容。
- Webpack 4 对旧的JavaScript特性和库有更好的兼容性。
-
迁移
- 从Webpack 4 迁移到 Webpack 5 可能需要对配置文件、插件和Loader进行一些调整。官方提供了详细的迁移指南,帮助开发者顺利完成过渡。
示例代码对比
// Webpack 4配置示例
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
mode: 'development',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
}
};
// Webpack 5配置示例
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
mode: 'development',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
},
optimization: {
splitChunks: {
chunks: 'all'
}
},
cache: {
type: 'filesystem',
}
};
总结
Webpack 5 引入了许多新特性和改进,提升了性能、优化能力和开发者体验。然而,升级到Webpack 5 可能需要一些配置和代码上的调整,但这些改进对大型应用程序的构建性能和模块管理能力有显著的提升。
28. 讲一讲webpack和vite,为什么vite编译更快
Webpack 和 Vite 是两种常见的前端构建工具,它们都有助于将现代 JavaScript 项目打包和优化,但在工作原理和性能方面存在显著差异。以下是对 Webpack 和 Vite 的详细介绍以及为什么 Vite 编译更快的原因。
Webpack
工作原理
- 模块打包:Webpack 会递归地解析项目中的所有依赖关系,将代码和资源(如 CSS、图像等)打包成一个或多个 bundle。
- 插件和加载器:Webpack 通过插件和加载器系统提供强大的扩展能力。加载器用于处理不同类型的文件,插件用于在构建过程中执行各种任务。
- 配置灵活:Webpack 的配置文件(
webpack.config.js
)非常灵活,可以根据项目需求进行各种自定义设置。
优势
- 生态系统丰富:Webpack 拥有庞大的插件和加载器生态系统,几乎可以处理任何类型的前端资源。
- 广泛应用:由于其强大的功能和灵活性,Webpack 被广泛应用于各种规模的项目中。
缺点
- 复杂配置:Webpack 的配置文件可能会变得非常复杂,尤其是对于大型项目。
- 较慢的编译速度:由于需要解析整个项目的依赖关系和进行大量的转换和优化,Webpack 在开发模式下的编译速度较慢。
Vite
工作原理
- 原生 ES 模块:Vite 充分利用浏览器对原生 ES 模块的支持,在开发时直接使用 ES 模块加载,使得页面加载速度非常快。
- 按需编译:Vite 采用按需编译的方式,只在浏览器请求时对文件进行编译,而不是预先打包所有文件。
- 热模块替换(HMR):Vite 内置了高效的 HMR,可以快速刷新模块而无需完全重新加载页面。
优势
- 极速启动:由于不需要预先打包所有文件,Vite 可以在几乎瞬间启动开发服务器。
- 快速热更新:Vite 的 HMR 非常高效,代码更改后可以立即反映在浏览器中。
- 简单配置:Vite 的默认配置已经足够强大,通常只需很少的配置即可满足大多数需求。
缺点
- 生态系统相对较新:尽管 Vite 发展迅速,但相对于 Webpack,其生态系统和社区资源尚不如 Webpack 丰富。
- 兼容性问题:由于 Vite 依赖于现代浏览器特性,可能在某些较老的项目或需要支持老旧浏览器的项目中遇到兼容性问题。
为什么 Vite 编译更快?
-
即时编译:Vite 使用原生 ES 模块进行开发,省去了预先打包的过程。文件只有在被请求时才会被编译,这种按需编译大大减少了初始启动时间。
-
高效的依赖处理:Vite 预先扫描并缓存依赖关系,只对源代码中的非依赖部分进行即时编译和热更新。这样可以避免在每次更改时重新编译整个项目的依赖。
-
基于 Rollup 的生产构建:尽管 Vite 的开发模式不使用打包,生产模式下 Vite 依然使用 Rollup 进行打包优化,从而保证了构建的性能和产物的质量。
-
优化的 HMR:Vite 的热模块替换系统非常高效,只更新实际变更的模块,而不是刷新整个页面。这使得开发体验更加流畅。
总结
Webpack 和 Vite 都是强大的前端构建工具,Webpack 以其强大的功能和灵活性适合大型和复杂项目,而 Vite 以其极速的开发体验和简单的配置适合现代前端开发。Vite 通过利用原生 ES 模块和按需编译技术,实现了更快的编译速度和更好的开发体验。
原文地址:https://blog.csdn.net/qq_44704740/article/details/145240896
免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!