HTML
1.HTML5新特性
- 语义化标签:header、nav、main、article、section、aside、footer、figure、figcaption、details、summary、time
- 多媒体:video、audio
- canvas
- 表单控件:search、date、time、email、number
- 地理 Geolocation API
- 本地离线存储:web storage
- 拖拽释放 draggable
- web worker websoket
2. canvas和svg的区别
| svg | canvas |
|---|---|
| 不依赖分辨率(矢量图) | 依赖分辨率(位图) |
每一个图形都是一个 DOM元素 | 单个HTML元素 |
| 支持事件处理器 | 不支持事件处理器 |
| 适合大型渲染区域的应用程序(谷歌地图) | 文本渲染能力差 |
| 可以通过脚本和CSS进行修改 | 只能通过脚本修改 |
对象数量较小 (<10k)、图面更大时性能更佳 | 图面较小,对象数量较大(>10k)时性能最佳 |
| 不适合游戏应用 | 适合图像密集型的游戏应用 |
3. meta viewport
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
- width
- height
- initial-scale:初始缩放比例
- maximum-scale:最大缩放比例
- minimum-scale :最小缩放比例
- user-scalable :是否允许用户缩放(yes/no)
CSS
1. BFC
- 定义:BFC(Block Formatting Context)是 CSS 中的一种布局上下文,它是一个独立的渲染区域,具有一些特定的布局规则。
- 解决问题:
-
- 清除浮动问题,避免父元素塌陷。
-
- 防止外边距重叠问题。
-
- 控制浮动元素在容器内的布局。
-
- 创建独立的渲染环境,避免与浮动元素重叠。
-
- 触发:
- overflow: hidden;
- float
- position: absolute;
- display: flex;
2. 盒模型
- 标准盒模型
- 怪异盒模型(IE盒模型)
3. 选择器优先级
!important > 作为style属性写在元素标签上的内联样式 >id选择器>类选择器>伪类选择器>属性选择器>标签选择器> 通配符选择器(* 应少用)>浏览器自定义
4. CSS3新特性
- 新增选择器
- 属性选择器
- E[att^=value]
- E[att$=value]
- E[att*=value]
- 伪类选择器
-
:root
-
:not
-
:only-child
-
:first-child和:last-child
-
:nth-child(n)和:nth-last-child(n)
-
:nth-of-type(n) 和:nth-last-of-type(n)
-
:target
-
:empty
-
-
边框 border-radius border-shadow border-image
-
文字字体: text-overflow word-wrap @font-face text-shadow font-stretch
-
object-fit object-position
-
多列布局 columns
-
媒体查询 @media
-
弹性布局 flex
-
背景 background-image background-origin background-clip
-
渐变 linear-gradient() radial-gradient()
-
过渡transition
.element {
transition-property: width; /* 过渡的属性 */
transition-duration: 0.3s; /* 过渡持续时间 */
transition-timing-function: ease-in-out; /* 过渡函数 */
transition-delay: 0s; /* 过渡延迟时间 */
}
```
- 变换transform: rotate旋转 translate平移 scale缩放
- 动画animation
11. 变换transform: rotate旋转 translate平移 scale缩放 12. 动画animation
@keyframes slidein {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
.element {
animation-name: slidein; /* 动画名称 */
animation-duration: 1s; /* 动画持续时间 */
animation-timing-function: ease; /* 动画函数 */
animation-delay: 0s; /* 动画延迟时间 */
animation-iteration-count: 1; /* 动画重复次数 */
animation-direction: normal; /* 动画方向 */
animation-fill-mode: forwards; /* 动画结束后保持最后一帧状态 */
}
5. Sass和Less的区别
- 变量
- less: @test: left; margin-@{test}: 5px;
- sass: side: left; border-#{side}-radius: $my-radius;
- 运算
- 嵌套
- 混入(Mixin)
- less: .bordered { border-top: dotted 1px black; border-bottom: solid 2px black; } #menu a { .bordered(); }
- sass: @mixin bordered { border-top: dotted 1px black; border-bottom: solid 2px black; } #menu a { @include bordered; }
- 函数
- less: .rem(@name, @px) { @{name}: unit(@px / 100, rem); }
- sass: @function double(n) { @return n * 2; }
- extend
- less: h2 { &:extend(.style); }
- sass: .success { @extend .message }
- sass (if-else、for、while、each)
6. 移动端1px显示问题的解决方案
原因:设备高分辨率
-
- 使用
viewport缩放
- 使用
<meta name="viewport" content="width=device-width, initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no">
- 2. 使用
transform: scale()
.element {
height: 1px;
background: black;
transform: scaleY(0.5);
}
- 3. 使用
border-image
.element {
border-width: 1px;
border-image: url(border.png) 2 repeat;
}
- 4. 使用
box-shadow
.element {
box-shadow: 0 0 0 0.5px black;
}
- 5. 使用
伪元素 + transform
.element {
position: relative;
}
.element::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 1px;
background: black;
transform: scaleY(0.5);
transform-origin: 0 0;
}
- 6. 使用
min-device-pixel-ratio媒体查询
@media (-webkit-min-device-pixel-ratio: 2) {
.element {
border-width: 0.5px;
}
}
- 7. 使用
svg
<svg width="100%" height="1">
<line x1="0" y1="0" x2="100%" y2="0" stroke="black" stroke-width="1" />
</svg>
7. CSS权重计算
- 内联样式(
style=""):1000 - ID选择器(
#id):100 - 类/伪类/属性选择器(
.class、:hover):10 - 元素/伪元素(
div、::after):1
8. 未定义样式的P元素 样式来源
- 浏览器默认样式(如
margin: 1em 0)。 - 继承样式(如父元素的
color)。 - 全局/重置样式(如
* { margin:0 })。
9. src和href的区别
1. 本质区别
-
href 是引用,建立关系
-
src 是替换,嵌入资源
2. 加载机制
-
href 并行加载,不阻塞页面解析
-
src 加载时会阻塞页面解析
3. 使用场景
-
href:链接、样式表
-
src:图片、脚本、iframe
10. flex:1、0、auto
flex: 1、flex: auto 和 flex: 0 的核心区别在于它们如何处理父容器的可用空间,这三者分别是 flex-grow、flex-shrink 和 flex-basis 的简写。
-
flex: 1(展开为1 1 0%)- 一句话概括: 忽略内容,均分空间。
- 行为: 它会把元素的
basis看作0,让所有设置了flex: 1的元素去平均分配父容器的所有可用空间。结果是大家趋向于变得一样大。
-
flex: auto(展开为1 1 auto)- 一句话概括: 尊重内容,再分空间。
- 行为: 它会先考虑元素自身内容的尺寸,然后让大家去平均分配父容器里**“剩下”的那些空间。结果是元素大小和自身内容相关,通常不一样大**。
-
flex: 0(展开为0 1 0%)- 一句话概括: 不放大,只收缩。
- 行为: 元素完全不参与剩余空间的分配,
flex-grow为0,所以它不会放大。它的尺寸由其内容或width决定,但如果空间不足,它仍然可以被压缩。
总结一下就是:
- 想做等分布局,用
flex: 1。 - 想做自适应内容的布局,用
flex: auto。 - 想让元素保持原始大小,不参与放大,用
flex: 0。
JS
1.事件流
1.定义:在HTML文档中,事件的传播过程,分为3个阶段:
- 捕获阶段:事件从最外层的元素开始向内部进行传播,直到达到触发事件的具体目标元素
- 目标阶段:事件到达目标元素,并在目标元素上触发相应的事件
- 冒泡阶段:事件从目标元素向外部元素进行传播,直到到达页面的最外层元素
- 利用 addEventListener 控制事件流向,第三个参数:
- true 捕获阶段触发
- false或者忽略 冒泡阶段触发
- 阻止事件冒泡
- event.stopPropagation() 阻止事件在DOM树中进一步传播
- event.stopImmediatePropagation() ,并阻止同类型的其他事件调用
- 事件代理(事件委托)
将事件处理程序绑定到父元素而不是每个子元素上,来管理事件
- 性能优化
- 动态元素
- 代码简洁
3. ES新特性
- ES6:
-
let 和 const 声明变量
-
变量的解构赋值
-
模板字符串 ``
-
简化对象写法
-
箭头函数 ()=>{}
-
函数默认值
-
rest参数 function(...args){}
-
扩展运算符 ...
-
String新增方法 includes(), startsWith(), endsWith(), repeat()
-
Number新增方法 Number.isFinite(), Number.isNaN(), Number.parseInt(), Number.parseFloat(),Number.isInteger() Number.isSafeInteger()
-
Math新增方法 Math.trunc(), Math.sign() Math.sqrt() Math.cbrt() Math.hypot()
-
对象新增方法 Object.is(),Object.assign(),Object.setPrototypeOf(),Object.getPrototypeOf()
-
数组新增方法 Array.from() Array.of() find() findIndex() fill() copyWidthin()
-
Symbol 独一无二的值
-
Set 和 Map 数据结构
-
Proxy
-
Promise 对象
-
Iterator 和 for...of 循环
-
Generator
-
class
-
module: import和export
- ES7
- 数组includes
- 指数操作符 ** 实现幂运算
- Math.pow()
- ES8 新特性
-
async和await
-
对象新增方法 Object.values(),Object.entries(),Object.getOwnPropertyDescriptors()
-
字符串新增方法 padStart(), padEnd()
-
尾逗号 Trailing commas
5.常见的前端存储方式
-
Cookies:Cookies是一种在客户端存储小型数据的方式。它们以键值对的形式存储在浏览器中,并在每次请求时自动发送到服务器。Cookies可以用于存储用户的身份验证信息、偏好设置等。
-
Web Storage:Web Storage提供了两个API:localStorage和sessionStorage。它们可以在浏览器中存储较大量的数据,并且在页面刷新后仍然保持存在。localStorage存储的数据没有过期时间,而sessionStorage存储的数据在会话结束后被清除。
-
IndexedDB:IndexedDB是一种在浏览器中存储结构化数据的方式。它提供了一个类似数据库的API,可以进行复杂的查询和事务操作。IndexedDB适用于存储大量数据或需要离线访问的应用程序。
-
Cache API:Cache API允许将请求的响应缓存到浏览器中,以便在离线时仍然可以访问。它可以用于存储静态资源,如HTML、CSS、JavaScript文件等。
-
Service Workers:Service Workers是一种在浏览器后台运行的脚本,可以拦截和处理网络请求。它可以用于缓存数据、离线访问和推送通知等功能。
6. Websocket
-
定义:用户浏览器和服务器之间开启双向交互式通信会话
-
过程:
- 建立连接: WebSocket 协议属于应用层协议,依赖传输层的 TCP 协议。它通过 HTTP/1.1 协议的 101 状态码进行握手建立连接。
- 客户端发送一个 HTTP GET 请求到服务器,请求的路径是 WebSocket 的路径(类似 ws://example.com/socket)。请求中包含一些特殊的头字段,如 Upgrade: websocket 和 Connection: Upgrade,以表明客户端希望升级连接为 WebSocket。
- 服务器收到这个请求后,会返回一个 HTTP 101 状态码(协议切换协议)。同样在响应头中包含 Upgrade: websocket 和 Connection: Upgrade,以及一些其他的 WebSocket 特定的头字段,例如 Sec-WebSocket-Accept,用于验证握手的合法性。
- 客户端和服务器之间的连接从普通的 HTTP 连接升级为 WebSocket 连接。之后,客户端和服务器之间的通信就变成了 WebSocket 帧的传输,而不再是普通的 HTTP 请求和响应
- 数据通信:WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧并将关联的帧重新组装成完整的消息。
- 维持连接:当建立连接后,连接可能因为网络等原因断开,我们可以使用心跳的方式定时检测连接状态。若连接断开,我们可以告警或者重新建立连接。
- 关闭连接:WebSocket 是全双工通信,当客户端发送关闭请求时,服务端不一定立即响应,而是等服务端也同意关闭时再进行异步响应
- 事件
- onopen
- onmessage
- onerror
- onclose
- 心跳机制
- 定时发数据
- 断线重连
7. 原型和原型链
-
原型:在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来访问这个属性,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型。
-
原型:在 JavaScript 中,每个构造函数都拥有一个
prototype属性,它指向构造函数的原型对象,这个原型对象中有一个 constructor 属性指回构造函数;每个实例都有一个__proto__属性,当我们使用构造函数去创建实例时,实例的__proto__属性就会指向构造函数的原型对象。 -
原型链:当访问一个对象的属性/方法时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,这个搜索的轨迹,就叫做原型链。
8.作用域和作用域链
- 作用域:规定了变量的可访问性和生命周期
- 全局作用域
- 局部作用域
- 块级作用域 {}
- 作用域链:是一种嵌套结构,它决定了变量的查找顺序。当在一个作用域内访问变量时,如果找不到该变量,JavaScript 引擎会逐级向外查找,直到找到匹配的变量或者到达全局作用域。
9.执行上下文
- 定义:用于描述代码执行的环境
- 类型:
- 全局执行上下文:创建了全局对象(浏览器是window对象),并将this指向该对象
- 函数执行上下文:每次调用函数都会创建
- eval函数执行上下文
- 组成:
- 变量对象 VO
- 活动对象 AO
- 作用域链
- this
10. 闭包
- 定义:如果一个函数访问了此函数的父级及父级以上的作用域变量,那么这个函数就是一个闭包。闭包会创建一个包含外部函数作用域变量的环境,并将其保存在内存中,这意味着,即使外部函数已经执行完毕,闭包仍然可以访问和使用外部函数的变量。
- 优缺点
- 优点
- 保护变量:将变量封装在函数内部,避免全局污染,保护变量不被外部访问和修改
- 延长变量生命周期
- 实现模块化
- 保护状态
- 缺点
- 内存占用
- 性能损耗
- 应用场景
- 自执行函数
- 节流防抖
- 柯里化
- 链式调用
- 迭代器
- 发布订阅模式
11. 为什么不能使用setInterval做动画
| 特性 | setInterval | requestAnimationFrame |
|---|---|---|
| 时间准确性 | 不准确 | 与刷新率同步 |
| 帧率稳定性 | 不稳定 | 稳定 |
| 页面隐藏时是否暂停 | 不会暂停 | 自动暂停 |
| 回调堆积问题 | 可能发生 | 不会发生 |
| 资源消耗 | 较高 | 较低 |
12. cookie表现
- 主子应用同域:可以携带和共享 Cookie,存在同名属性值被微应用覆盖的风险
- 主子应用跨域同站:默认主子应用无法共享 Cookie,可以通过设置 Domain 使得主子应用进行 Cookie 共享
- 主子应用跨站:子应用默认无法携带 Cookie(防止 CSRF 攻击),需要使用 HTTPS 协议并设置服务端 Cookie 的 SameSite 和 Secure 设置才行,并且子应用无法和主应用形成 Cookie 共享
13. 判断空对象的方法
1. Object.keys(obj).length === 0
- 优点:简单、快速
- 缺点:无法检测不可枚举属性和Symbol属性
2. for...in结合hasOwnProperty
- 优点:可以检测自身的可枚举属性
- 缺点:无法检测不可枚举属性和Symbol属性且需要手动遍历
3. JSON.stringify(obj) === '{}'
- 优点:简单
- 缺点:忽略不可序列化属性,可能不准确
4. 结合Object.getOwnPropertyNames()和Object.getOwnPropertySymbols()
- 优点:检测所有自身属性,包括不可枚举和Symbol属性
- 缺点:性能较差,代码较复杂
```js
function isEmptyObject(obj) {
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
return false;
}
// 检查普通属性
if (Object.getOwnPropertyNames(obj).length > 0) {
return false;
}
// 检查 Symbol 属性
return Object.getOwnPropertySymbols(obj).length === 0;
}
```
14.for...in 和 for...of 的区别
- 区别
- 时间点不同:for in 在js出现之初就有,for of出现在ES6之后
- 遍历的内容不同:for in用于遍历对象的可枚举属性(包括原型链上的可枚举属性),for of用于遍历可迭代对象的值
- 可枚举属性
js里的属性分为数据属性与访问器属性。每一个js对象属性都有一个属性描述符,通过Object.getOwnPropertyDescriptor(obj, property)拿到。enumerable为true代表该属性可枚举
- 可迭代对象
实现了[Symbol.iterator]方法的对象,称为可迭代对象。
-
[Symbol.iterator]方法返回了一个对象,这个对象被称作迭代器对象
-
迭代器对象里面有一个next方法,这个next方法又返回了一个对象,这个对象叫做迭代器结果对象
-
迭代器结果对象里面有两个属性,value和done,value就是每一次迭代返回的值,done为true时迭代停止
-
在for of 进行遍历obj时,[Symbol.iterator]方法会执行一次,之后每一次迭代都执行一次迭代器对象的next方法,返回迭代器结果对象里的value值,直到done为true,停止迭代
// 我们定义一个对象,给这个对象一个range属性,表示范围,我们希望最后迭代的时候从这个范围的开始迭代到这个范围的最后,每次迭代,值就加2
const obj = {
range: [10,100],
[Symbol.iterator]() {
let start = this.range[0]
let end = this.range[1]
return {
next() {
let val = start
start += 2
let done = val > end ? true : false
return {
value: val,
done,
}
},
[Symbol.iterator](){
return this
},
}
}
}
15. 模拟实现async await
生成器函数可以直接返回一个迭代器对象
function* smallNumbers(){
console.log("next()第一次被调用;参数被丢弃")
let y1 = yield 1;
console.log("next()第二次被调用;参数是:", y1)
let y2 = yield 2;
console.log("next()第三次被调用;参数是:", y2)
let y3 = yield 3;
console.log("next()第四次被调用;参数是:", y3)
}
let g = smallNumbers();
console.log("创建了生成器,代码未运行")
let n1 = g.next("a");
console.log("生成器回送", n1.value)
let n2 = g.next("b");
console.log("生成器回送", n2.value)
let n3 = g.next("c");
console.log("生成器回送", n3.value)
let n4 = g.next("d");
console.log("生成器回送", n4.value)
/*
创建了生成器,代码未运行
next()第一次被调用;参数被丢弃
生成器回送 1
next()第二次被调用;参数是: b
生成器回送 2
next()第三次被调用;参数是: c
生成器回送 3
next()第四次被调用;参数是: d
生成器回送 undefined
*/
实现
// 模拟一个异步请求
function getData(str){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(str)
},1000)
})
}
// 然后写一个生成器函数,yield模拟await
function* pretendAsync(){
const res1 = yield getData('111')
const res2 = yield getData('222')
console.log(res1)
console.log(res2)
}
// 最后利用生成器的特性写一个运行这个生成器,将promise里的数据返回给res
function run(asyncFn){
//运行生成器函数,获取迭代器对象
const iterator = asyncFn()
//递归调用exec函数,将请求得到的数据通过迭代器的next方法传参给res
function exec(res){
//第一次.next,参数会丢失,同时res是undefined,但之后每次next的res都是返回的数据
let result = iterator.next(res)
//递归出口
if(result.done){
return
}
//result.value是getData得到的promise,通过这个promise.then(exec),递归调用exec
result.value.then(exec)
}
exec()
}
run(pretendAsync)
16. 让(a == 1 && a == 2 && a == 3) 为 true
内置符号:
Symbol.iterator:用于定义对象的默认迭代行为。Symbol.toPrimitive:用于定义对象到原始值的转换。Symbol.species:用于指定构造函数应返回的构造函数。Symbol.hasInstance:用于自定义instanceof操作符的行为。- 等等。
- 解法一:with
let i = 1
with ({
get a() {
return i++
}
}) {
if (a == 1 && a == 2 && a == 3) {
console.log('前端胖头鱼')
}
}
- 解法二:对象转原始类型的"转换机制"
对象转原始类型,会调用内置的[ToPrimitive]函数,逻辑大致如下:
- 如果有Symbol.toPrimitive方法,优先调用再返回,否则进行2。
- 调用valueOf,如果可以转换为原始类型,则返回,否则进行3。
- 调用toString,如果可以转换为原始类型,则返回,否则进行4。
- 如果都没有返回原始类型,会报错。
const a = {
i: 1,
[Symbol.toPrimitive]() {
return this.i++
}
}
console.log(a == 1 && a == 2 && a == 3)
let a = {
i: 1,
// valueOf替换成toString效果是一样的
// toString
valueOf() {
return this.i++
}
}
console.log(a == 1 && a == 2 && a == 3)
- 解法三:数据劫持
let _a = 1
Object.defineProperty(window, 'a', {
get() {
return _a++
}
})
console.log(a == 1 && a == 2 && a == 3)
let a = new Proxy({ i: 1 }, {
get(target) {
return () => target.i++
}
})
console.log(a == 1 && a == 2 && a == 3)
性能优化
1. 常见手段
-
减少HTTP请求:通过合并和压缩CSS和JavaScript文件,使用CSS Sprites来减少图片请求,以及使用字体图标代替小图标等方式,可以减少页面的HTTP请求次数,从而提高页面加载速度。
-
懒加载:将非关键资源(如图片、视频等)的加载推迟到页面其他内容加载完成后再进行,可以提高页面的首次加载速度。可以使用懒加载技术或Intersection Observer API来实现延迟加载。
-
压缩和缓存:通过压缩CSS、JavaScript和HTML文件,以及启用服务器端的Gzip压缩,可以减小文件的大小,从而加快文件的传输速度。另外,合理设置缓存策略,如设置HTTP缓存头(
Cache-Control和ETag),利用浏览器缓存来减少重复请求,也可以提高性能。
4. 优化图片:使用适当的图片格式(如JPEG、PNG、WebP等),并对图片进行压缩和优化,可以减小图片的文件大小,从而提高页面加载速度。可以使用工具如ImageOptim、TinyPNG等来优化图片。
-
使用CDN:将静态资源(如CSS、JavaScript、图片等)部署到CDN(内容分发网络)上,可以将资源缓存到离用户更近的服务器上,从而提高资源的加载速度。
-
避免重排和重绘:重排(reflow)和重绘(repaint)是浏览器渲染页面时的开销较大的操作。通过合理使用CSS,避免频繁的DOM操作和样式改变,可以减少重排和重绘,提高页面性能。
-
使用延迟、异步加载和执行(defer、async):将不影响页面渲染的脚本标记为异步加载,或将脚本放在页面底部,可以避免阻塞页面的渲染和交互,提高页面加载速度。
-
使用缓存技术:使用缓存技术如LocalStorage、SessionStorage、IndexedDB等,可以将数据缓存到客户端,减少对服务器的请求,提高页面响应速度。
9. 代码优化:优化JavaScript代码,避免使用过多的循环和递归,减少不必要的计算和操作,可以提高代码的执行效率,使用事件委托来减少事件监听器的数量。
10. 网络:使用Keep-Alive和HTTP/2等技术来提高网络性能
11. 使用服务器端渲染(SSR):提高首屏加载速度,从而提高用户体验
- 使用性能分析工具:使用性能分析工具如Chrome开发者工具的Performance面板、Lighthouse等,可以帮助识别性能瓶颈和优化机会,从而改进页面的性能。
2.首屏优化方案
- 代码优化:减少文件大小,压缩和合并CSS、JavaScript文件,按需加载资源(懒加载、按需加载),避免重复的请求和渲染操作,使用图片懒加载等。
- 网络优化:使用CDN加速,开启Gzip压缩,合理设置缓存策略,使用HTTP/2协议提高传输效率,使用预加载和预渲染技术等。
- 缓存优化:使用浏览器缓存和服务器缓存,合理设置缓存头部信息(Cache-Control、Expires等),使用IndexedDB或Web Storage进行数据缓存,使用Service Worker进行离线缓存等。
3.处理内存泄漏问题
- 谨慎使用全局变量: 尽量避免,或使用WeakMap/WeakSet来存储它们
- 在组件销毁时及时清除定时器和事件监听器
- 使用防抖和节流技术,降低内存消耗
- 优化闭包的使用
- 浏览器性能分析
- 内存池技术
4. 大数据量优化
- 虚拟滚动
- 分页
- 数据分片:按需加载(如无限滚动)。
- Web Workers:后台处理计算。
- IndexedDB:本地存储大量数据。
- 服务端过滤:减少传输数据量。
手写还是编程
1.将URL拆解为origin、文件名、hash的示例
const url = "https://example.com/path/to/file.txt#hash";
// 使用URL对象进行拆解
const parsedUrl = new URL(url);
const origin = parsedUrl.origin;
const filename = parsedUrl.pathname.split("/").pop();
const hash = parsedUrl.hash.substring(1); // 去除#符号
console.log("Origin:", origin);
console.log("Filename:", filename);
console.log("Hash:", hash);
2.设置值为数字,输出为百分号格式
const value = 0.75;
// 方法一:使用toFixed方法转换为指定小数位数,并添加百分号
const formattedValue1 = (value * 100).toFixed(2) + "%";
// 方法二:使用Intl.NumberFormat来格式化百分比
const formattedValue2 = new Intl.NumberFormat("en-US", { style: "percent" }).format(value);
console.log(formattedValue1); // 输出:75.00%
console.log(formattedValue2); // 输出:75%
3. 数字转逗号分隔字符串
样例输入:1234567890
样例输出:1,234,567,890
// 法一
function numberToStringWithCommas(number) {
return number.toLocaleString();
}
// 法二
const formattedValue2 = new Intl.NumberFormat("en-US", { style: "standard" }).format(value);
//法三
function toString(num) {
// 这是最简单的写法
// return num.toLocaleString();
const result = [];
const str = `${num}`.split('').reverse();
for (let i = 0; i < str.length; i++) {
if (i && i % 3 === 0) {
result.push(',');
}
result.push(str[i]);
}
return result.reverse().join('');
}
3. 实现 new
- 首先创建一个新的空对象
- 设置原型,将对象的原型设置为函数的prototype对象
- 让函数的this指向这个对象,执行构造函数的代码
- 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象
function myNew(constructor, ...args) {
// 如果不是一个函数,就报错
if (typeof constructor !== "function") {
throw "myNew function the first param must be a function";
}
// 基于原型链 创建一个新对象,继承构造函数constructor的原型对象上的属性
let newObj = Object.create(constructor.prototype);
// 将newObj作为this,执行 constructor ,传入参数
let res = constructor.apply(newObj, args);
// 判断函数的执行结果是否是对象,typeof null 也是'object'所以要排除null
let isObject = typeof res === "object" && res !== null;
// 判断函数的执行结果是否是函数
let isFunction = typeof res === "function";
// 如果函数的执行结果是一个对象或函数, 则返回执行的结果, 否则, 返回新创建的对象
return isObject || isFunction ? res : newObj;
}
4. 实现 call、apply、bind
- call
Function.prototype.myCall = function(context, ...args) {
// 如果传入的上下文为 null 或 undefined,则默认为全局对象
context = context || window;
// 将当前函数作为上下文对象的一个属性
context.fn = this;
// 执行函数,并将传入的参数作为参数
const result = context.fn(...args);
// 删除上下文对象的 fn 属性
delete context.fn;
// 返回函数执行结果
return result;
};
- apply
Function.prototype.myApply = function(context, argsArray) {
// 如果传入的上下文为 null 或 undefined,则默认为全局对象
context = context || window;
// 将当前函数作为上下文对象的一个属性
context.fn = this;
// 执行函数,并将传入的参数数组作为参数
const result = context.fn(...argsArray);
// 删除上下文对象的 fn 属性
delete context.fn;
// 返回函数执行结果
return result;
};
- bind
5. 手写类型判断函数
function getType(value) {
// 判断数据是 null 的情况
if (value === null) {
return 'null';
}
// 判断数据是基本数据类型的情况和函数的情况,使用typeof
if (typeof value !== "object") {
return typeof value;
}
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase()
}
6. 手写instance fo
function myInstanceof(obj, constructor) {
if (obj === null || typeof obj !== 'object') { // 注意不能用于基本类型的检查,如字符串、数字等,直接false。
return false;
}
let proto = Object.getPrototypeOf(obj); // 获取其原型
while (proto !== null) { // 如果原型存在
if (proto === constructor.prototype) { //检查对象的原型是否与目标构造函数的原型相等。
return true;
}
proto = Object.getPrototypeOf(proto); // 沿着原型链查找
}
return false; // 到达原型链末尾也没找到返回false
}
7. 实现ajax
function ajax(url) {
//1.创建XMLHttpRequest对象
const xhr = new XMLHttpRequest();
//2.使用open方法创建一个GET请求
xdr.open('GET', url);
// 3.发送请求
xdr.send();
//4.监听请求成功后的状态变化(readyState改变时触发):根据状态码(0~5)进行相应的处理
xdr.onreadystatechange = function () {
//readyState为4代表服务器返回的数据接收完成
if (xdr.readyState === 4) {
//请求的状态为200或304代表成功
if (xdr.status === 200 || xdr.status === 304) {
handle(this.response)
} else { reject(new Error(this.statusText)); }
}
}
}
8. 防抖节流
// 防抖
function debounce (fn, delay = 500) {
let timer = null;
return function(...arg) {
if (timer) clearTimeout(timer)
timer = setTimeout(() =>{
fn.apply(this, args)
}, delay)
}
}
// 节流
// 定时器版本
function throttle(fn, delay = 500) {
let timer = null;
return function(...arg) {
if (timer) return;
timer = setTimeout(() =>{
fn.apply(this, args)
timer = null
}, delay)
}
}
// 时间戳版本
function throttle(fn, delay = 500) {
let prev = Date.now();
return function(...arg) {
let now = Date.now();
if (now - prev >= delay) {
fn.apply(this, args);
prev = Date.now();
}
}
}
9. 深拷贝,支持循环引用
function deepCopy(obj, map = new WeakMap()) {
if (typeof obj !== 'object' || obj === null) return obj;
let newObject = Array.isArray(obj) ? [] : {};
//利用map解决循环引用
if (map.has(obj)) return map.get(obj);
for(let key in obj){
if(obj.hasOwnProperty(key)) {
newObj[key] = deepCopy(obj[key], map)
}
}
return newObj
}
10. 数组扁平化
-
ES6中的flat
-
递归
function flatten(arr) {
let result = [];
for (let i = 0; i < arr.length; i++) {
//如果当前元素还是一个数组
if (Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]));//递归拼接
} else {
result.push(arr[i]);
}
}
return result;
}
- reduce函数迭代
function flatten(arr) {
return arr.reduce((total, cur) => {
return total.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, []); //传递初始值空数组[],就会从数组索引为 0 的元素开始执行
}
- split和toString
function flatten(arr) {
//先把数组直接转换成逗号分隔的字符串,然后再用 split 方法把字符串重新转换为数组
return arr.toString().split(",").map(Number);
}
11. 柯里化
- 参数定长
// 实现函数柯里化
function curry(fn) {
// 返回一个新函数
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args); // 如果参数够了,就执行原函数,返回结果
} else {
//返回一个新函数,继续递归去进行柯里化,利用闭包,将当前已经传入的参数保存下来
return function (...args2) {
//递归调用 curried 函数
return curried.apply(this, [...args, ...args2]); //新函数调用时会继续传参,拼接参数
};
}
};
}
- 参数不定长
function addCurry() {
// 利用闭包的特性收集所有参数值
let arr = [...arguments]
//返回函数
return function fn() {
// 如果参数为空,则判断递归结束,即传入一个()执行函数
if(arguments.length === 0) {
return arr.reduce((a, b) => a + b) // 求和
} else {
arr.push(...arguments)
return fn //递归
}
}
}
12. 继承
- ES5 寄生组合式继承
Child.prototype = Object.create(Parent.prototype);//这里就是对组合继承的改进,创建了父类原型的副本
Child.prototype.constructor = Child;//把子类的构造指向子类本身
- ES6
class Child extends Parent {
constructor(name, age) {
//使用this之前必须先调用super(),它调用父类的构造函数并绑定父类的属性和方法
super(name);
//之后子类的构造函数再进一步访问和修改
this this.age = age;
}
}
13. 观察者模式
// 被观察者 学生
class Subject {
constructor() {
this.state = "happy";
this.observers = []; // 存储所有的观察者
}
//新增观察者
add(o) {
this.observers.push(o);
}
//获取状态
getState() {
return this.state;
}
// 更新状态并通知
setState(newState) {
this.state = newState;
this.notify();
}
//通知所有的观察者
notify() {
this.observers.forEach((o) => o.update(this));
}
}
// 观察者 父母和老师
class Observer {
constructor(name) {
this.name = name;
}
//更新
update(student) {
console.log(`亲爱的${this.name} 通知您当前学生的状态是${student.getState()}`);
}
}
let student = new Subject();
let parent = new Observer("父母");
let teacher = new Observer("老师");
//添加观察者
student.add(parent);
student.add(teacher);
//设置被观察者的状态
student.setState("刚刚好");
14. 发布-订阅模式
class EventBus {
constructor() {
// 缓存列表,用来存放注册的事件与回调
this.cache = {};
}
// 订阅事件
on(name, cb) {
// 如果当前事件没有订阅过,就给事件创建一个队列
if (!this.cache[name]) {
this.cache[name] = []; //由于一个事件可能注册多个回调函数,所以使用数组来存储事件队列
}
this.cache[name].push(cb);
}
// 触发事件
emit(name, ...args) {
// 检查目标事件是否有监听函数队列
if (this.cache[name]) {
// 逐个调用队列里的回调函数
this.cache[name].forEach((callback) => {
callback(...args);
});
}
}
// 取消订阅
off(name, cb) {
const callbacks = this.cache[name];
const index = callbacks.indexOf(cb);
if (index !== -1) {
callbacks.splice(index, 1);
}
}
// 只订阅一次
once(name, cb) {
// 执行完第一次回调函数后,自动删除当前订阅事件
const fn = (...args) => {
cb(...args);
this.off(name, fn);
};
this.on(name, fn);
}
}
// 测试
let eventBus = new EventBus();
let event1 = function (...args) {
console.log(`通知1-订阅者小陈老师,小明同学当前心情状态:${args}`)
};
// 订阅事件,只订阅一次
eventBus.once("teacherName1", event1);
// 发布事件
eventBus.emit("teacherName1", "教室", "上课", "打架", "愤怒");
eventBus.emit("teacherName1", "教室", "上课", "打架", "愤怒");
eventBus.emit("teacherName1", "教室", "上课", "打架", "愤怒");
15. 使用setTimeout实现setInterval
function mySetInterval(fn, timeout) {
// 控制器,控制定时器是否继续执行
var timer = {
flag: true,
};
// 设置递归函数,模拟定时器执行
function interval() {
if (timer.flag) {
fn();
setTimeout(interval, timeout);//递归
}
}
// 启动定时器
setTimeout(interval, timeout);
// 返回控制器
return timer;
}
let timer = mySetInterval(() => {
console.log("1");
}, 1000);
//3秒后停止定时器
setTimeout(() => (timer.flag = false), 3000);
16. nextTick
function nextTick(fn) {
return new Promise((resolve, reject) => {
if (typeof MutationObserver === "undefined") {
// 如果浏览器不支持 MutationObserver,则回退到 setTimeout 方案
setTimeout(() => {
let res = fn();
if (res instanceof Promise) {
res.then(resolve);
} else {
resolve();
}
}, 0);
} else {
const observer = new MutationObserver(() => {
let res = fn();
if (res instanceof Promise) {
res.then(resolve);
} else {
resolve();
}
// 观察结束,清理 observer
observer.disconnect();
});
// 配置 MutationObserver
observer.observe(document.getElementById("app"), {
attributes: true,
childList: true,
subtree: true
});
}
});
}
17. LRU类
微前端
1.原理
微前端是一种将前端应用程序拆分为多个独立的小型应用,每个应用都可以独立开发、部署和运行的架构模式。它的实现原理可以通过以下几个关键步骤来理解:
-
拆分应用:将整个前端应用拆分为多个独立的子应用,每个子应用负责处理特定的功能或页面。
-
独立开发:每个子应用可以由不同的团队独立开发,使用不同的技术栈和框架。
-
独立部署:每个子应用可以独立部署到不同的服务器或CDN上,可以使用不同的域名或子域名进行访问。
-
路由管理:使用主应用或路由管理器来管理整个微前端应用的路由,将不同子应用的路由进行集成和管理。
-
共享状态:使用状态管理库(如Redux、Vuex)或事件总线来实现子应用之间的状态共享和通信。
-
懒加载:根据需要动态加载子应用,避免一次性加载所有子应用,提高性能和加载速度。
-
样式隔离:使用CSS命名空间或CSS-in-JS等技术来隔离不同子应用之间的样式,避免样式冲突。
8. 通信机制:使用跨域消息传递(Cross-Origin Messaging)或自定义事件等机制来实现子应用之间的通信和交互。
数据结构和算法
1.栈和堆的区别
-
存储方式:栈是一种线性的数据结构,采用后进先出(LIFO)的方式存储数据。堆是一种树状的数据结构,没有特定的存储顺序。
-
内存分配:栈的内存分配是自动的,由编译器或解释器负责管理。当一个函数被调用时,其局部变量和函数参数会被分配到栈上,当函数执行完毕时,这些变量会自动被释放。堆的内存分配是手动的,需要开发人员显式地申请和释放内存。
-
内存管理:栈的内存管理是自动的,无需开发人员手动管理。堆的内存管理需要开发人员手动申请和释放内存,否则可能会导致内存泄漏或内存溢出的问题。
-
存储内容:栈主要用于存储基本数据类型和引用类型的变量的引用。堆主要用于存储对象和复杂数据结构。
-
访问速度:栈的访问速度比堆快,因为栈的数据存储在连续的内存空间中,访问起来更加高效。堆的访问速度相对较慢,因为需要通过指针来访问对象的内存地址。
2.常见的数据结构
-
数组(Array):数组是一种线性数据结构,用于存储一组有序的元素。在JavaScript中,数组可以包含不同类型的元素,并且长度可以动态调整。
-
链表(Linked List):链表是一种动态数据结构,由一系列节点组成,每个节点包含数据和指向下一个节点的指针。链表可以分为单向链表和双向链表。
-
栈(Stack):栈是一种后进先出(LIFO)的数据结构,只允许在栈的一端进行插入和删除操作。常见的应用场景包括函数调用栈和表达式求值。
-
队列(Queue):队列是一种先进先出(FIFO)的数据结构,允许在队列的一端进行插入操作,在另一端进行删除操作。常见的应用场景包括任务调度和消息队列。
-
树(Tree):树是一种非线性的数据结构,由一组节点和边组成。常见的树结构包括二叉树、二叉搜索树和平衡树等。树的应用非常广泛,例如DOM树、文件系统等。
-
图(Graph):图是一种由节点和边组成的非线性数据结构,节点之间的关系可以是任意的。图的应用包括社交网络、地图导航等。
-
哈希表(Hash Table):哈希表是一种根据键(Key)直接访问值(Value)的数据结构,通过哈希函数将键映射到数组中的索引位置。哈希表的查找和插入操作具有常数时间复杂度。
-
堆(Heap) :是一种特殊的树形数据结构,具有堆序性质,即父节点的值大于等于(或小于等于)其子节点的值。堆常用于实现优先队列和堆排序等算法。
网络和浏览器
1.浏览器的工作原理(从网址里输入地址到页面显示的过程)
- 用户输入URL:用户在浏览器地址栏中输入URL(统一资源定位符),例如www.example.com。
- 查找缓存:浏览器先查看浏览器缓存-系统缓存-路由缓存中是否有该地址页面,如果有则显示页面内容。如果没有则进行下一步。
- 浏览器缓存:浏览器会记录DNS一段时间,因此,只是第一个地方解析DNS请求;
- 操作系统缓存:如果在浏览器缓存中不包含这个记录,则会使系统调用操作系统, 获取操作系统的记录(保存最近的DNS查询缓存);
- 路由器缓存:如果上述两个步骤均不能成功获取DNS记录,继续搜索路由器缓存;
- ISP缓存:若上述均失败,继续向ISP搜索。
-
DNS域名解析:浏览器向DNS服务器发起请求,解析该URL中的域名对应的IP地址,然后返回给浏览器
-
建立TCP连接:解析出IP地址后,浏览器根据IP地址和默认80端口,通过TCP三次握手和目标服务器建立TCP连接
-
发起HTTP请求:浏览器发起读取文件的HTTP请求,请求获取网页的内容
-
接收和解析响应:服务器接收到请求后,返回相应的HTML、CSS、JavaScript等资源文件。浏览器接收到响应后,开始解析这些文件。
-
构建DOM树:浏览器解析HTML文件,构建DOM(文档对象模型)树。DOM树表示网页的结构,包括HTML标签、元素和它们的关系。
-
构建CSSOM树:浏览器解析CSS文件,构建CSSOM(CSS对象模型)树。CSSOM树表示网页的样式信息,包括CSS规则、选择器和样式属性。
-
渲染页面:浏览器将DOM树和CSSOM树合并,生成渲染树(Render Tree)。渲染树包含了需要显示在页面上的所有元素和样式信息。
-
布局和绘制:浏览器根据渲染树进行布局(Layout)和绘制(Painting)。布局确定每个元素在页面中的位置和大小,绘制将元素渲染为像素。
-
JavaScript解析和执行:如果页面中包含JavaScript代码,浏览器会解析和执行这些代码。JavaScript可以修改DOM树、CSSOM树和页面的行为。
-
处理用户交互:浏览器监听用户的交互事件,例如点击、滚动等。根据用户的操作,浏览器可能会触发相应的事件处理函数或执行相应的操作。
-
渲染更新:如果页面内容发生变化,浏览器会重新进行布局和绘制,更新页面的显示。
-
关闭TCP连接:通过四次挥手释放TCP连接
2.TCP三次握手和四次挥手
三次握手(Three-Way Handshake)是在建立TCP连接时使用的过程,具体步骤如下:
-
第一步(SYN):客户端向服务器发送一个带有SYN(同步)标志的TCP报文段,表示客户端请求建立连接。
-
第二步(SYN-ACK):服务器收到客户端的请求后,向客户端发送一个带有SYN和ACK(确认)标志的TCP报文段,表示服务器接受连接请求,并发送确认。
-
第三步(ACK):客户端收到服务器的确认后,向服务器发送一个带有ACK标志的TCP报文段,表示客户端确认连接已建立。
四次挥手(Four-Way Handshake)是在终止TCP连接时使用的过程,具体步骤如下:
-
第一步(FIN):当客户端想要关闭连接时,发送一个带有FIN(结束)标志的TCP报文段给服务器,表示客户端不再发送数据。
-
第二步(ACK):服务器收到客户端的结束请求后,发送一个带有ACK标志的TCP报文段给客户端,表示服务器收到了结束请求。
-
第三步(FIN):当服务器也想要关闭连接时,发送一个带有FIN标志的TCP报文段给客户端,表示服务器不再发送数据。
-
第四步(ACK):客户端收到服务器的结束请求后,发送一个带有ACK标志的TCP报文段给服务器,表示客户端收到了结束请求。
- TCP 的连接需要三次握手,三次握手可以保证可靠性并且避免浪费资源。
- TCP 的断开需要四次挥手,四次挥手可以保证全双工通信彻底断开。
3.GET和POST的区别
- 请求参数的位置:GET请求的参数会用&拼接在URL后,POST参数在请求体中
- 请求长度的限制:浏览器对URK长度有限制,POST无
- 安全性:GET的参数暴露在URL中,POST相对安全
- 幂等性:多次执行同一GET请求,服务器将返回相同的结果,POST每次请求都会创建新的资源
- 缓存:GET请求可以被缓存,POST则不会,触发设置响应头
- 后退/刷新按钮的影响:GET请求可以
4. HTTP2相对于HTTP1.x优势
- 二进制分帧层:HTTP/2不再使用文本格式来传输数据,而是将所有传输的信息分割为更小的消息和帧(frame),并以二进制格式进行编码
- 多路复用:允许单个TCP连接中并行处理多个请求和响应
- 头部压缩:通过共享头部信息,减少传输的数据量
- 服务器推送:允许服务器主动向客户端推送数据
- 流量控制:通过流控制、消息控制和窗口控制等机制
5. HTTPS为什么比HTTP安全
- 加密传输:HTTPS使用SSL/TLS协议对HTTP报文进行加密,使得敏感数据在网络传输过程中不容易被窃听和篡改。这种加密过程结合了对称加密和非对称加密,确保数据的保密性和完整性。
- 身份验证:HTTPS通过数字证书进行身份验证,确保通信双方的真实性。在建立HTTPS连接时,服务器会提供数字证书来证明自己的身份。如果验证通过,客户端就可以信任服务器,并继续与其进行安全的数据传输。这有效防止了被恶意伪装的服务器攻击。
- 数据完整性保护:在传输数据之前,HTTPS会对数据进行加密,并使用消息摘要(hash)算法生成一个摘要值。在数据到达接收端后,接收端会使用相同的算法对接收到的数据进行摘要计算,并与发送端的摘要值进行比较。如果两者一致,说明数据在传输过程中没有被篡改。如果不一致,通信双方应重新进行验证或中断连接。
6. HTTP状态码
- 100 Continue:客户端应继续请求
- 200 OK:请求成功
- 201 Created:请求已经是被实现,并创建了一个新的资源
- 204 No Content:服务器成功处理了请求,但没有返回任何内容
- 301 Moved Permanently:请求的资源已经被永久移动到新的URL上
- 302 Found:请求的资源现在临时从不同的URL响应请求
- 304 Not Modified:客户端已经执行了GET请求,但文件未发生变化
- 400 Bad Request:服务器无法理解请求
- 401 Unauthorized: 请求要求进行身份验证
- 403 Forbidden:服务器理解请求,但拒绝执行它
- 404 Not Found:服务器无法找到请求的资源
- 405 Method Not Allowed: 请求中指定的方法不被允许
- 500 Internal Server Error:服务器遇到一个未曾预料的情况,无法处理请求
- 501 Not Implemented:服务器不支持当前请求所需要的某个功能
- 503 Service Unavailable: 由于临时的服务器维护或过载,无法处理请求
- 504 Gateway timeout:指服务器作为网关或代理,不能及时地从远程服务器获得应答
7. post请求为什么多发送一次options请求
HTTP的一种特性,称为预检请求(Preflight request),主要发生在跨域请求中,当请求涉及不太安全的方法(如 PUT、DELETE),或者使用了自定义的HTTP头部等
8. HTTP的keep-alive
- 减少连接建立的开销
- 降低网络负载
- 提高性能和响应时间
- 支持HTTP管道化
9. 浏览器缓存优先级
-
Service Worker 缓存:完全控制网络请求,具有最高优先级,必须使用HTTPS
-
Memory Cache 内存缓存
-
HTTP 缓存:
- 强缓存 Cache-Control > Expires
- 协商缓存 Last-Modified/If-Modified-Since和Etag/If-None-Match。
-
Disk Cache(磁盘缓存)
-
Push Cache(推送缓存):HTTP/2中的
10. 跨域
- 原因:浏览器的同源策略限制
- 同源策略:防止跨站脚本攻击XSS和跨站请求伪造CSRF等安全威胁。协议、域名和端口号任一不匹配,触发跨域。
- 解决方案:
- JSONP:利用script标签不受同源策略限制,通过动态插入请求数据。只支持GET请求,需要服务端配合
- CORS:通过服务端设置相应的HTTP头部信息,如 Access-Control-Allow-Origin
- 代理服务器:转发请求
- window.postMessage:通过监听message事件接收消息
11. 重绘和重排
- 重绘(Repaint):当页面中元素样式的改变不影响布局时,浏览器将只会重新绘制受影响的元素,这个过程称为重绘。
- 回流(Relayout):当页面布局或几何属性发生变化时,浏览器需要重新计算元素的几何属性,并重新构建渲染树,这个过程称为回流。
- 减少:
- 避免频繁操作样式
- 利用css3动画
- 避免使用table布局
- 批量修改DOM:考虑使用DocumentFragment
- 使用绝对定位
- 避免使用内联样式
- 优化图片加载
- 利用浏览器缓存机制
12. 事件循环
- 浏览器
-
宏任务:
- ajax
- setTimeout
- setInterval
- DOM监听
- UI Rendering
- requestAnimationFrame
-
微任务:
- Promise.then
- Mutation Observer API
- queueMicrotask()
-
优先级
- 主线程代码优先执行
- 在执行任一个宏任务之前,都先检查微任务队列中,是否有任务需要执行
-
执行顺序:
- node
- 宏任务
- setTimeout
- setInterval
- IO事件
- setImmediate
- close事件
- 微任务
- Promise.then
- process.nextTick
- queueMicrotask
- 队列
- 微任务队列
- next tick queue: Process.nextTick
- other queue: Promise.then、queueMicrotask
- 宏任务队列
- timer queue: setTimeout、setInterval
- poll queue: IO事件
- check queue: setImmediate
- close queue: close事件
- 微任务队列
13. 跨浏览器标签页通信
- localStorage:监听storage事件
// 在一个标签页中设置localStorage
localStorage.setItem('key', 'value');
// 在另一个标签页中监听storage事件
window.addEventListener('storage', function(event) {
if (event.key === 'key') {
console.log('Value changed to:', event.newValue);
}
});
- BroadcastChannel Api:
// 创建一个新的BroadcastChannel实例
const bc = new BroadcastChannel('my-channel');
// 发送消息
bc.postMessage('Hello, world!');
// 接收消息
bc.onmessage = event => {
console.log('Received', event.data);
};
- Service Workers:独立于网页运行的脚本,用来接受通知、管理缓存
// 注册Service Worker
navigator.serviceWorker.register('/service-worker.js').then(function(registration) {
console.log('Service Worker Registered');
}).catch(function(err) {
console.log('Service Worker Registration Failed: ', err);
});
// 在Service Worker中发送消息
self.clients.matchAll().then(function(clients) {
clients.forEach(function(client) {
client.postMessage('Hello from Service Worker!');
});
});
// 在页面中接收消息
navigator.serviceWorker.onmessage = function(event) {
console.log('Received message from Service Worker:', event.data);
};
- 使用SharedArrayBuffer和Atomics
- 使用WebSocket或Server-Sent Events
14. 网络通信模型(7层、4层和5层模型)
15. TCP和UDP的区别
- 连接与无连接: TCP是面向连接的协议,UDP是无连接的
- 可靠性:TCP可靠
- 效率:UDP效率高
- 流量控制:TCP有流量控制功能
- 应用场景:TCP:可靠的场景如文件传输、电子邮件;UDP:实时性如视频直播、在线游戏。
16. 如何解决浏览器适配、兼容性
- CSS 前缀(Autoprefixer)、CSS Reset(Normalize.css)、响应式设计(flex\grid,媒体查询@media)
- js Polyfills Babel
17. 线程与进程
-
进程 是操作系统分配资源的基本单位,每个进程都有自己的内存空间和资源,适合需要完全隔离、资源消耗较大的任务。
-
线程 是进程中的一个执行单元,是 CPU 调度和执行的基本单位,多个线程共享进程的内存空间,适合任务密集型、需要高效并发的应用。线程间通信较为简单,但需要注意线程同步的问题。
-
浏览器中的进程与线程
-
浏览器进程:每个浏览器标签页(或窗口)通常是一个独立的进程,这样可以避免一个标签页崩溃时影响到其他标签页。例如,在 Chrome 中,每个标签页、插件、渲染进程都是独立的进程。
-
主线程:JavaScript 在浏览器中通常运行在主线程(也叫 UI 线程)中,主线程负责执行 JavaScript 代码、渲染 UI 和处理用户输入。
-
Web Workers:Web Workers 允许在浏览器中创建子线程,使得长时间运行的任务不会阻塞主线程,从而提升页面的响应性。Web Workers 是运行在独立线程中的,能够与主线程通过消息传递进行通信。
-
渲染线程:浏览器渲染页面的工作通常由渲染线程处理,这包括计算样式、布局、绘制、合成等操作。
-
18. 同源策略的安全限制
安全限制主要分三个方面:
- DOM层面:同源策略限制了不同源的js对当前DOM对象的读写操作
- 数据层面:同源策略限制了不同源站点读取当前站点的Cookies、IndexDB、LocalStorage等数据
- 网络层面:同源策略限制了数据发送给非同源站点(比如XML HttpRequest、Fetch等无法请求不同源站点)
安全
1.前端安全方面有哪些攻击方式
-
跨站脚本攻击(XSS):攻击者通过在Web页面中注入恶意脚本,使其在用户浏览器中执行。这可以导致盗取用户敏感信息、篡改页面内容或进行其他恶意操作。
-
跨站请求伪造(CSRF):攻击者通过诱使用户在已登录的状态下访问恶意网站或点击恶意链接,来执行未经授权的操作。这可以导致用户在不知情的情况下执行恶意操作,如更改密码、发起支付等。
-
点击劫持:攻击者通过将恶意网站覆盖在诱导用户点击的合法网站上,使用户在不知情的情况下执行恶意操作。这可以导致用户执行意外的操作,如下载恶意软件、分享敏感信息等。
-
资源加载攻击:攻击者通过篡改或替换Web应用程序中的资源文件(如JavaScript、CSS、图像等),来执行恶意操作。这可以导致恶意代码的执行、页面内容的篡改或其他安全问题。
-
密码攻击:包括密码猜测、密码重放、密码泄露等方式,攻击者试图获取用户的密码或绕过身份验证机制,以获取未经授权的访问权限。
-
不安全的数据存储:如果Web应用程序在客户端或服务器端存储敏感信息时没有采取适当的安全措施,攻击者可以通过窃取或篡改数据来获取敏感信息。
-
不安全的跳转和重定向:攻击者可以通过篡改URL参数或重定向链接,将用户引导到恶意网站或执行未经授权的操作。
前端工程化
1.webpack的配置有哪些
- entry:入口文件
- output:输出目录和文件名称
- module:配置loader
- resolve:如何解析模块依赖,包括别名、扩展名
- plugins:增强功能
- devServer:web服务器
- optimization:splitChunks代码拆分和runtimeChunk运行时代码提取
- externals:排除打包带模块
2. webpack有哪些常见的Loader和Plugin
- loader:
-
babel-loader:将ES6+的代码转换成ES5的代码。
-
css-loader:解析CSS文件,并处理CSS中的依赖关系。
-
style-loader:将CSS代码注入到HTML文档中。
-
file-loader:解析文件路径,将文件赋值到输出目录,并返回文件路径。
-
url-loader:类似于file-loader,但是可以将小于指定大小的文件转成base64编码的Data URL格式
-
sass-loader:将Sass文件编译成CSS文件。
-
less-loader:将Less文件编译成CSS文件。
-
postcss-loader:自动添加CSS前缀,优化CSS代码等。
-
vue-loader:将Vue单文件组件编译成JavaScript代码。
- plugin:
- DefinePlugin:定义全局变量。
- CleanWebpackPlugin:清除输出目录。
- terser-webpack-plugin:压缩ES6的代码(tree-shaking)
- uglifyjs-webpack-plugin:压缩js代码
- ParalleUglifyPlugin:多进程并行压缩js
- mini-css-extract-plugin:将CSS提取为独立文件,支持按需加载
- css-minimize-webpack-plugin:压缩CSS代码
- compression-webpack-plugin:生产环境采用gzip压缩js和css
- 区别
- Loader本质是一个函数,他是一个转换器,webpack只能解析原生js代码,对于其他类型文件就需要loader进行转换
- plugin是一个插件,用于增强webpack功能。webpack在运行的生命周期会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果
3. webpack 热更新HMR原理
- 原理:webpack-dev-server(WDS)和浏览器之间维护了一个websocket服务。当本地资源发生变化后,webpack会先将打包生成新的模块代码放入内存中,然后WDS向浏览器推送更新,并附带上构建时的hash,让客户端和上一次资源进行对比。
- 开启:
- devServer: {hot: true}
4. webpack Tree Shaking原理
- 原理:当Webpack分析代码时,它会标记出所有的import语句和export语句。然后,当Webpack确定某个模块没有被导入时,它会在生成的bundle中排除这个模块的代码。同时,Webpack还会进行递归的标记清理,以确保所有未使用的依赖项都不会出现在最终的bundle中。
- 开启:
- optimization.usedExports: true
- 生产模式
5. 文件指纹
- 定义:文件指纹是指文件打包后的一连串后缀
- 作用:
- 版本管理:在发布版本时,通过文件指纹来区分 修改的文件 和 未修改的文件。
- 使用缓存:浏览器通过文件指纹是否改变来决定使用缓存文件还是请求新文件。
- 种类:
- Hash:和整个项目的构建相关,只要项目有修改(compilation实例改变),Hash就会更新
- Contenthash:和文件的内容有关,只有内容发生改变时才会修改
- Chunkhash:和webpack构架的chunk有关 不同的entry会构建出不同的chunk (不同 ChunkHash之间的变化互不影响)
- 如何使用:
- JS文件:使用Chunkhash
- CSS文件:使用Contenthash
- 图片等静态资源: 使用hash
6. webpack 构建流程
- 初始化阶段: 合并计算配置参数,创建Compiler、Compilation等基础对象,并初始化Plugin,并最终根据entry配置,找到所有入口模块
- 构建模块: 从entry开始,调用loader转译对应的模块,调用 Acorn将代码转换为AST结构, 遍历AST从中 构建出完整的模块依赖关系图(递归操作)
- 生成阶段: 根据entry配置,根据模块生成一个个chunk对象,之后转译Chunk代码并封装为Asset, 最后写出到文件系统。、
7.vite比webpack快在哪里
- 最主要的原因:
vite不需要做全量的打包 vite在解析模块依赖关系时,利用了esbuild,更快- 按需加载:模块之间的依赖关系的解析由浏览器实现。Vite 只需要在浏览器请求源码时进行转换并按需提供源码。
充分利用缓存;Vite 利用 HTTP 头来加速整个页面的重新加载
8.Monorepo的理解
- 介绍:是一种项目代码管理方式,指单个仓库中管理多个项目,有助于简化代码、版本控制、构建和部署等方面的复杂性。
- 缺点:
- 项目庞大而复杂
- 权限管理问题
9.为什么pnpm比npm快
- pnpm使用符号链接来共享依赖项,每个依赖项只需要在磁盘上存储一次,可以节省大量的磁盘空间
- 通过 并行下载和本地缓存加快安装速度
- 解决了幽灵依赖的问题
- 支持monorepo
10. babel原理
-
解析:当 Babel 接收到源代码时,将会调用一个叫做解析器的工具,用于将源代码转换为抽象语法树(AST)。在这个过程中,解析器会识别代码中的语法结构,并将其转换为对应的节点类型。 例如,当解析器遇到一个变量声明语句时,它将会创建一个 “VariableDeclaration” 节点,并将该节点的信息存储在 AST 中。AST 是一个以节点为基础组成的树形结构,每个节点都有相应的类型、属性和子节点等信息。
-
转换:一旦 AST 被创建,Babel 将遍历整个树形结构,对每个节点进行转换。这些转换可以是插件、预设或手动创建的。转换器会检查 AST 中的每个节点,然后对其进行相应的修改或替换,以将新语法转换为旧语法。 例如,如果 Babel 遇到一个包含箭头函数的节点,而你已经启用了转换插件,该插件将会将箭头函数转换为其等效的体函数。代码转换后,Babel 将会生成一个新的 AST。
-
生成:最后,Babel 将基于转换后的 AST 生成代码文本。在这个步骤中,Babel 将遍历转换后的 AST,并创建对应的代码字符串,并将这些字符串组合成一个完整的 JavaScript 文件。如果启用了代码压缩,Babel 还可以将生成的代码进行压缩。 总结来说,Babel 的原理就是将 JavaScript 源代码转换为抽象语法树(AST),然后对 AST 进行转换,生成与源代码功能相同但向后兼容的代码。Babel 提供了一个强大的生态系统,使得开发者可以轻松扩展并自定义转换器,实现自己的功能需求。
11. vite 热模块替换(HMR)机制 原理
1、文件监视:使用 chokidar 来监视文件系统的变化。
2、WebSocket 服务器:在 Vite 启动时被创建,并用于发送 HMR 消息
3、HMR 消息发送:当文件发生变化时,Vite 会通过 WebSocket 向所有连接的客户端发送 HMR 消息。
4、客户端 HMR 处理:客户端监听到onmessage事件,拿到的事件类型type:update、更新文件类型type,更新路径 path,更新时间戳 timestamp
12. 前端编译工具原理
源码首先会被解析成 AST( Abstract Syntax Tree 抽象语法树),然后对 AST 做转换,也就是对这棵树的节点做增删改之后,递归打印,生成新的代码。
并且还会生成 sourcemap,也就是编译后的代码和编译前的代码的映射关系,可以通过这个映射回源码,调试的时候我们就是用 sourcemap 来直接调试源码的。
前端工具的编译流程,都是 parse、transform、generate 三个阶段,只不过在 transform 阶段做的事情不同。
babel 是在 transform 阶段做语法降级,比如 aync、await 换成等价实现,最后 AST 打印成新的代码。
eslint 是 AST 结合 parse 出的 token,来实现代码的逻辑错误、格式错误等检查,然后通过字符串替换实现了 fix。
terser 是在 transform 阶段做一系列以压缩为目的的代码转换,比如 undefined 换成 void 0,最终打印成紧凑的难以阅读的等价 js 代码。
postcss 也是类似 babel 的工具,只不过是针对 css 的,基于它也有 stylelint 等工具。
pretter 实现格式化同样也需要基于 AST 来做。
tsc 也是基于 AST 来实现了类型检查,很多工具比如 babel、swc、esbuild 也可以编译 ts 代码,但它们只是编译不做类型检查,目前类型检查只能通过 tsc。
AST 同样也是有规范的,也就是基于 SpiderMonkey 的 JS 引擎扩展的 estree 规范,但并不是所有 parser 都遵循这个规范。
Vue
1.Vue的Composition API与Options API有什么区别和优势?
1. 逻辑复用:Composition API通过提供可组合的函数,解决了Options API中混入(mixins)的一些问题,实现了更清晰、高效的逻辑复用。通过使用Composition API,我们可以将相关逻辑封装在一个函数中,并在需要时进行复用,提高代码的可维护性和可读性。
-
更灵活的代码组织:Composition API允许我们按照逻辑相关性来组织代码,而不是按照选项的顺序。这使得代码更易于理解和维护,尤其是在处理复杂逻辑和大型组件时。
-
更好的类型推断:Composition API通过使用TypeScript或其他类型检查工具,可以提供更好的类型推断和代码提示。这使得开发者在编写代码时能够更准确地捕获错误和调试问题。
-
更小的生产包和更少的开销:由于Composition API的设计方式,它可以在编译时进行更好的优化,生成更小的生产包,并减少运行时的开销。
2.什么是Vue 3?它与Vue 2有什么区别?
Vue 3是Vue框架的最新版本,它是一个用于构建用户界面的渐进式JavaScript框架。Vue 3相对于Vue 2有一些重要的区别,包括:
-
响应式系统:Vue 3使用Proxy来实现响应式,相比Vue 2的Object.defineProperty(),Proxy具有更高效和灵活的响应式能力。
-
组件实例:Vue 3引入了Composition API,提供了更灵活和可组合的组件逻辑编写方式,相比Vue 2的Options API,Composition API更易于代码组织和逻辑复用。
-
性能优化:Vue 3在虚拟DOM的更新算法上进行了优化,提供了更快的渲染速度和更小的包体积。
3.为什么不用vuex而是pinia
- 更好的团队协作:Pinia提供了更强的团队协作约定,使得多人协作开发更加高效和一致。
2. 与Vue DevTools的集成:Pinia与Vue DevTools完全兼容,包括时间线、组件检查和时间旅行调试等功能,可以帮助开发者更好地调试和监控应用状态的变化。
- 热模块替换(Hot Module Replacement):Pinia支持热模块替换,可以在开发过程中实时更新状态管理逻辑,提高开发效率。
4. 服务器端渲染支持:Pinia对服务器端渲染(Server-Side Rendering)提供了支持,可以在服务器端和客户端之间共享状态。
此外,Pinia是基于对Vuex 5的探索和讨论的结果,它已经实现了大部分Vuex 5中的想法,并且提供了更简洁、灵活的API。因此,Vue官方推荐在新的应用程序中使用Pinia。
4.vue2生命周期

5.在Vue项目中,可以进行以下性能优化:
- 使用合适的Vue指令,如v-if和v-show的选择,避免不必要的DOM操作。
- 合理使用computed属性和watch监听器,减少不必要的计算和渲染。
- 使用异步组件和路由懒加载,按需加载资源,减少初始加载时间。
- 对重复的数据进行缓存,避免重复请求和计算。
- 使用keep-alive组件缓存组件状态,减少重复渲染。
- 对列表数据使用虚拟滚动或分页加载等方式,减少渲染的节点数量。
6.vue3新特性对性能的影响
- 基于代理的响应式系统:初始化和更新性能上更优越
- 静态树提升:可以在编译阶段确定哪些节点是不会改变的,这样在渲染时就会跳过它们
- 静态属性提升:将静态属性移出渲染函数,使得渲染时只需要处理动态属性。
- 基于ES模块的构建:采用ES模块(Native ESM)作为其构建和打包方式,能够更好地利用现代浏览器的能力,提升加载速度
- 更小的库大小
7.Vue3.5新特性
-
重构响应式:采用版本计数和双向链表数据结构优化手段,重构后内存占用减少了56%
-
响应式props支持解构:不会丢失响应性
-
新增 base watch函数:只需导入
reactivity模块,不再需要导入runtime-core -
新增 onEffectCleanup函数:在watchEffect中使用,在组件卸载之前或者下一次
watchEffect回调执行之前会自动调用,不需要在组件的beforeUnmount 统一清理timer了 -
新增 onWatcherCleanup 函数:在watch中使用,作用同上
-
新增pause和resume方法:暂停和恢复,是否执行
watch或者watchEffect中的回调 -
watch的deep支持数字:监听的深度
-
新增
useId函数:生成随机id,解决SSR中服务端和客户端生成的id不一样的警告;也可用于客户端生成唯一键 -
data-allow-mismatch:也可解决上述SSR警告
-
Lazy Hydration 懒加载水合:defineAsyncComponent的hydrate 选项来控制何时进行水合
-
Custom Element 自定义元素改进
-
Teleport组件新增defer延迟属性:能将
target写在Teleport组件的后面 -
useTemplateRef函数:获取DOM元素
React
1.谈谈你对React的理解
-
- 声明式编程
-
- 组件化
-
- 虚拟DOM
-
- 单向数据流
-
- hooks
2.react组件生命周期
- 挂载阶段:constructor、componentWillMount、render、componentDidMount
- 更新阶段:shouldComponentUpdate、componentWillUpdate、render、componentDidUpdate
- 卸载阶段:componentWillUnmount
3. 各种API的性能优化
-
useMemo:用于缓存计算结果,避免在每次组件渲染时都重新计算相同的数据
-
useCallback:用于存储函数引用,确保当组件重新渲染时,传递给子组件的回调函数不会发生变化(除非依赖项发生更改)
-
useState:
- 尽量减少状态更新
- 批量更新状态
- 避免在循环和条件语句引入
- 使用不可变数据结构
4. fiber架构是什么,解决了什么问题
- react16之前问题:
- 不可中断的渲染过程
- 固定的任务优先级
- 解决:
- 任务拆分与中断:Fiber架构将渲染过程拆分成多个小任务,并且可以在任意时间点中断和恢复这些任务。这使得React能够在渲染过程中响应其他高优先级任务,提高了应用的响应性。
- 优先级调度:Fiber架构引入了任务优先级的概念,允许React根据任务的优先级来调度工作。高优先级的任务会优先得到处理,从而确保用户交互等关键任务的流畅执行。
5. React的虚拟DOM和工作原理
- 定义:是一个轻量级js对象,是对真实DOM的抽象
- 原理:当组件的状态或属性发生变化时,React会创建一个新的虚拟DOM树,并与旧的虚拟DOM树进行比较(这个过程称为调和或Reconciliation)。然后,React会根据比较结果计算出最小的DOM操作集合来更新真实DOM,从而实现高效的UI更新。
6. React的diff算法
它采用了一种启发式的方法,基于三个主要策略:按层次比较、按类型比较和按key比较: 首先,React会按层次遍历新旧虚拟DOM树,并比较对应节点的类型。如果类型相同,则进一步比较属性;如果类型不同,则删除旧节点并创建新节点。
对于列表渲染,React使用key属性来识别列表项的身份,以便在列表发生变化时能够准确地移动、添加或删除节点。
通过这些策略,React能够高效地计算出最小的DOM操作集合,从而实现快速的UI更新。
7. React事件原理
- React使用合成事件系统来处理事件,它是对浏览器原生事件的封装和抽象。
- 合成事件提供了与浏览器原生事件相同的接口,但具有更好的跨浏览器兼容性和一致性。
- 合成事件还提供了事件委托机制,允许在组件树的顶层监听所有事件,并根据事件冒泡将事件分发到相应的组件。这有助于减少事件监听器的数量,提高性能。
8. React渲染流程
-
JSX通过babel、tsc等编译器编译成render function,然后执行后产生React Element的树
-
React Element 的树会转成fiber链表,这个过程叫做reconcile,由React 的Scheduler调度
-
reconcile 每次只处理一个fiber节点,通过时间分片把reconcile的过程分到多个任务跑,这样fiber树再大也不会阻塞渲染
-
reconcile + schedule 这个过程叫做 render 阶段,而之后会进入 commit 阶段。
-
commit 阶段会遍历构建好的 fiber 链表,执行 dom 操作,还有函数组件的 effect 等。
-
它按照更新 dom 前后,分了 before mutation、mutation、layout 三个小阶段。
10. react 18 新特性
- 并发模式(Concurrent Mode:是一种新的渲染策略,它允许React在渲染过程中中断和恢复工作,以便更好地响应用户输入和其他高优先级任务。
- hooks:
useDeferredValueuseTransition
- 更新 render API
- setState 自动批处理
- flushSync退出批量更新
- SSR 流式渲染
- Suspense 不再需要 fallback 来捕获;Suspense 能够让 React SSR 流式渲染 html 片段,并且根据用户行为,自主的选择 hydrate 的顺序
11. React 19 新特性
- Hook
- useActionState:简化ajax代码
- useFormStatus:优化 submit button
- useOptimistic:优化多loading体验
- use:优化组件数据加载
- 服务器组件
- 服务器操作 server action
- 自动记忆化
- forwardRef被移除,ref可以作为props传入组件
12. React Context 实现原理
- createCotnext 就是创建了一个 _currentValue、Provider、Consumer 的对象
- _currentValue: 保存 context 的值的地方
- Provider 是一种 jsx 类型,之后会转为对应的 fiber 类型,来修改 _currentValue
- Consumer 和 useCotnext 就是读取 _currentValue,也就是读取 context 值
- Provider 处理每个节点之前会入栈 context,处理完会出栈,这样就能保证 context 只影响子组件。
13. React Context性能缺点,及解决方案
- context 中如果是一个对象,不管任意属性变了,都会导致依赖其它属性的组件跟着重新渲染。
- 方案
- 拆分 context,每种数据放在一个 context 里
- 用 zustand 等状态管理库,因为它们不是用 context 实现的,自然没有这种问题
- 用 memo 包裹子组件,它会对比新旧 props,没变就不会重新渲染
14. react调度Scheduler原理
- 调度入口 - scheduleCallback:
- 创建一个新的任务 newTask。
- 通过任务的开始时间( startTime ) 和 当前时间( currentTime ) 比较:当 startTime > currentTime, 说明未过期, 存到 timerQueue,当 startTime <= currentTime, 说明已过期, 存到 taskQueue。
- 如果任务过期,并且没有调度中的任务,那么调度 requestHostCallback。本质上调度的是 flushWork。
- 如果任务没有过期,用 requestHostTimeout 延时执行 handleTimeout,通过advanceTimers 将 timeQueue 中过期的任务转移到 taskQueue 中。然后调用 requestHostCallback 调度过期的任务。
-
开始调度-找出调度者和执行者 非浏览器环境是setTimeout,浏览器环境是port.postMessage。而两个环境的执行者也显而易见,前者是
_flushCallback,后者是performWorkUntilDeadline,执行者做的事情都是去调用实际的任务执行函数 -
任务执行 - 从performWorkUntilDeadline说起
- 中断:剩余时间不足 break掉while循环
- 恢复:currentTask 不是null ; workLoop return true
- 任务完成状态判断:currentTask.callback 是函数,则未完成;为null,则完成
- 取消调度 currentTask从taskQueue剔除
15. transition对比防抖、节流、定时器
- 使用 useTransition 会触发 Concurrent 模式,可中断渲染,让浏览器在空闲时间下执行,所以渲染进程不会长时间被阻塞,使得其他操作得到及时响应,从而使用户体验得到了极大的提升;
- 其次,定时器的本质是异步延时执行,而 useTransition 属于同步执行,通过标记 transition 来决定是否完成此次更新。所以 useTransition 要比定时器更新得要早,整体的效果要好很多;
- 对于防抖、节流(不好控制防抖和节流的延时时间)、setTimeout 来说,相当于合并渲染的次数,简单地说,就是控制了 render 的渲染次数,而 useTransition 并没有减少渲染的次数。
- transition 在处理慢的计算机上效果更加明显
typescript
1. type 和 interface的区别
可以这样理解:interface 是一个开放的契约定义,而 type 是一个封闭的类型别名。
1. 相同点
首先,它们都可以用来描述一个对象的形状或者一个函数的签名。
// 描述对象
interface User {
name: string;
age: number;
}
type UserType = {
name: string;
age: number;
};
// 描述函数
interface SayHello {
(name: string): void;
}
type SayHelloType = (name: string) => void;
在上述例子中,User 和 UserType 的作用几乎是完全等价的。
2. 核心区别
a) 扩展性(Declaration Merging)
这是 interface 和 type 最本质的区别。
-
interface是开放的: 你可以多次声明同名的interface,它们会自动合并成一个单一的接口。这个特性叫做“声明合并”(Declaration Merging)。这在你想扩展一个已存在的接口时非常有用,比如为第三方库的类型定义添加自定义属性。// 原始声明 interface Person { name: string; } // 在其他地方再次声明,添加新属性 interface Person { age: number; } // Person 接口现在同时拥有 name 和 age 属性 const p: Person = { name: "Alice", age: 30, }; -
type是封闭的: 你不能多次声明同名的type。type一旦被定义,就是最终的,无法再向其中添加新的属性。type Animal = { name: string; }; // 再次声明会直接报错: Duplicate identifier 'Animal'. // type Animal = { // legs: number; // };
b) 继承方式
两者都可以被扩展,但语法不同。
-
interface使用extends关键字,这和类(class)的继承非常相似,符合面向对象的直觉。interface Point2D { x: number; y: number; } interface Point3D extends Point2D { z: number; } const p3d: Point3D = { x: 1, y: 2, z: 3 }; -
type使用交叉类型(Intersection Types&) 来实现扩展。type Point2DType = { x: number; y: number; }; type Point3DType = Point2DType & { z: number; }; const p3dType: Point3DType = { x: 1, y: 2, z: 3 };
c) 类型定义的能力范围
-
type的能力更广泛:type可以为任何类型创建别名,包括联合类型(Union Types)、元组类型(Tuple Types)、原始类型(Primitives)以及更复杂的映射类型(Mapped Types)和条件类型(Conditional Types)。// 联合类型 type Status = "success" | "error" | "pending"; // 元组类型 type UserInfo = [string, number]; // [name, age] // 映射类型:将一个对象的所有属性变为可选 type Partial<T> = { [P in keyof T]?: T[P]; }; // interface 无法直接定义这些复杂的类型结构 // 下面的写法是错误的 // interface Status extends "success" | "error" {} // Error -
interface只能声明对象形状: 它的主要职责是定义一个对象的结构契约。
3. 何时使用?总结与实践建议
基于以上的区别,我们可以得出一个清晰的实践指南:
-
优先使用
interface定义对象形状:- 公共API定义:当你在编写一个库或者一个公共模块时,优先使用
interface来定义对外暴露的类型。它的“声明合并”特性允许库的使用者通过模块增强(module augmentation)的方式来安全地扩展你的类型定义,而不会破坏原始类型。 - 团队协作:
interface的可扩展性也使得它在大型项目中更具灵活性。 - 面向对象:如果你习惯于使用类(Class),
interface在语义上与implements关键字结合得更好。
- 公共API定义:当你在编写一个库或者一个公共模块时,优先使用
-
在需要复杂类型时使用
type:- 当你需要定义联合类型、元组类型时,只能使用
type。 - 当你需要使用工具类型(Utility Types)如
Partial,Pick,Omit等,或者自定义复杂的映射、条件类型时,type是你的不二之un択。 - 当你只是想给一个已经存在的(可能是复杂的)类型起一个更易读的别名时。
- 当你需要定义联合类型、元组类型时,只能使用
我的个人偏好和建议:
- 一致性是关键。在团队项目中,最重要是遵循统一的规范。
- 我的个人习惯是:“如果可以,就用
interface,不行再用type”。这意味着当我定义一个对象或类的结构时,我总是先用interface。只有当我需要用到interface无法表达的功能(如联合类型)时,我才会使用type。我认为这种方式兼顾了interface的扩展性和type的灵活性,使得代码意图更加清晰。
2. 泛型与其用途
TypeScript 中的泛型,本质上是一种“类型变量”。它允许我们在定义函数、接口或类的时候,不预先指定具体的类型,而是在使用的时候再传入类型。这样做最大的好处是,我们能够创建出既可重用、又类型安全的组件。
什么是泛型?(用一个例子说明)
假设我们要写一个 identity 函数,它会返回传入的任何值。
没有泛型的糟糕做法 (使用 any):
function identity(arg: any): any {
return arg;
}
这虽然能用,但我们完全丢失了类型信息。identity("hello") 的返回值是 any 类型,而不是我们期望的 string 类型,这就失去了 TypeScript 的意义。
使用泛型的完美做法:
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("myString"); // output 的类型是 string
let output2 = identity(123); // TS 会做类型推断,output2 的类型是 number
这里的 <T> 就是类型变量。它像一个占位符,捕获了我们传入的参数类型(比如 string),然后把它作为函数的返回值类型。这样,类型安全和代码的灵活性就兼得了。
泛型的主要用途
在实际工作中,泛型主要有以下三个核心用途:
1. 增强函数的可重用性(泛型函数)
这是最常见的用途。比如,我们需要一个函数来获取任何类型数组的第一个元素。
function getFirstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const names = ["Alice", "Bob", "Charlie"];
const firstNumber = getFirstElement([1, 2, 3]); // 类型是 number
const firstString = getFirstElement(names); // 类型是 string
我们不需要为 number[] 和 string[] 分别写两个函数,一个泛型函数就解决了所有类型数组的问题,并且保持了完整的类型信息。
2. 定义通用的数据结构(泛型接口和类)
当我们的数据结构可以容纳不同类型的数据时,泛型就非常有用了。一个典型的例子是API返回的数据格式。
// 定义一个通用的 API 响应接口
interface ApiResponse<T> {
code: number;
message: string;
data: T; // data 的类型是不确定的
}
// 实际使用时,传入具体的类型
type User = { id: number; name: string };
type Articles = { id: number; title: string }[];
let userResponse: ApiResponse<User>;
let articlesResponse: ApiResponse<Articles>;
// userResponse.data.id 是合法的
// articlesResponse.data[0].title 也是合法的
在 React 中,我们定义组件的 props 时也经常使用泛型,比如创建一个通用的列表组件。
3. 增强类型的灵活性(泛型约束)
有时候,我们希望泛型代表的类型必须满足某些条件,比如“它必须包含一个 length 属性”。这时就需要泛型约束。
我们使用 extends 关键字来添加约束。
interface WithLength {
length: number;
}
// T 必须是拥有 length 属性的类型
function logLength<T extends WithLength>(arg: T): void {
console.log(arg.length);
}
logLength("hello"); // 正确, string 有 length 属性
logLength([1, 2, 3]); // 正确, array 有 length 属性
// logLength(123); // 错误! number 没有 length 属性
// logLength({ a: 1 }); // 错误! 对象没有 length 属性
泛型约束让我们的泛型函数更加健壮和智能。
3. 枚举
它允许我们为一组相关的常量定义一个清晰的、带名字的集合。
主要可以分为两类,外加一种优化:
-
数字枚举 (Numeric Enum):它的值是数字,并且会默认从
0开始自增。它最大的一个特点是支持反向映射,也就是说我们不仅能通过Enum.Key得到值,也能通过Enum[Value]反向查到键名。 -
字符串枚举 (String Enum):它的值必须是字符串。它没有反向映射,这让它在运行时行为更可预测,可读性也更强。在大多数场景下,为了代码的清晰和易于调试,字符串枚举是更被推荐的选择。
-
常量枚举 (Const Enum):它是一种性能优化。用
const关键字声明的枚举会在编译阶段被完全移除,所有使用枚举成员的地方都会被替换为具体的值(即“内联”),这样可以减少运行时的代码体积。
核心使用场景,主要是为了增强代码的可读性和可维护性,避免硬编码的“魔术字符串”或“魔术数字”:
-
定义状态集合:这是最常见的用法。比如,一个API请求的生命周期状态(
Loading,Success,Error),或者用户的角色权限(Admin,Editor,Guest)。使用枚举能让这些状态一目了然。 -
定义固定的配置或类型:比如,一个组件库里的按钮有几种固定的主题色(
Primary,Success,Warning),或者一个应用支持的几种主题模式(LightTheme,DarkTheme)。 -
替代魔法值:在代码逻辑中,
if (status === RequestStatus.Success)这样的写法,远比if (status === 1)要清晰得多,也更容易维护和重构。
重点难点
1. 自动国际化cli
- 代码转换:自动将代码中的中文内容替换为
intl.formatMessage。- 提取国际化:扫描文件或文件夹中的中文内容并生成
zh.json和en.json。- 初始化配置:一键创建
react-intl所需的初始化文件。- 文件格式互转:支持 JSON 与 Excel 文件的双向转换,方便翻译工作流。
- 高效递归处理:支持对目录及子目录的递归处理。
-
- 使用 commander 构建命令行工具,新增 transform命令 ,接收一个路径参数
-
- 文件处理策略
-
stat、isDirectory()、stats.isFile() 首先判断输入路径是文件还是目录
-
如果是目录,则递归遍历处理
-
只处理 .tsx、.ts、.js、.jsx 文件
-
排除 .d.ts 类型文件
-
- 核心转换实现
- readFile 读取源代码
- 通过babel parse 解析成 AST,插件选择 jsx、typescript
- 使用自定义babel插件 转换
- prettier 格式化输出
-
- 写入文件writeFile,通过chalk打印错误日志
2. 自定义 babel 转换插件的实现
自动识别代码中的中文文本
将中文文本转换为使用 react-intl 的 formatMessage 调用
自动收集所有中文文案并生成 messages 定义
- 在 visitor Program访问器,调用path.traverse在JSXText、StringLiteral、TemplateLiteral收集中文文本,注入必要的 import 语句,生成 defineMessages 定义
2.JSXText、StringLiteral、TemplateLiteral访问器中,替换中文文本为intl.formatMessage()格式
3.自己实现脚手架:封装一些项目模版,开箱即用
- 基于pnpm workspace和changeset搭建monorepo项目
- inquirer让用户选择项目模版、输入项目名,通过npminstall下载到 home 目录的一个临时目录下,拷贝到项目目录
4.vite插件 支持在react项目中组件引入svg
import { Plugin } from 'vite';
import * as fs from 'fs';
import { transform as svgrTransform } from '@svgr/core';
import { transformWithEsbuild } from 'vite';
interface SvgrOptions {
defaultExport: 'url' | 'component';
}
export default function viteSvgrPlugin(options: SvgrOptions): Plugin {
const { defaultExport = 'url' } = options;
return {
name: 'vite-plugin-svgr',
async transform(code, id) {
// 1. 根据 id 入参过滤出 svg 资源;
if (!id.endsWith('.svg')) {
return code;
}
// 2. 读取 svg 文件内容;
const svg = await fs.promises.readFile(id, 'utf8');
// 3. 利用 `@svgr/core` 将 svg 转换为 React 组件代码
const svgrResult = await svgrTransform(
svg,
{ typescript: true, plugins: ['@svgr/plugin-jsx'], icon: true },
{ componentName: 'ReactComponent' }
);
// 4. 处理默认导出为 url 的情况
let componentCode = svgrResult;
if (defaultExport === 'url') {
// 加上 Vite 默认的 `export default 资源路径`
componentCode += code;
componentCode = componentCode.replace('export default ReactComponent', 'export { ReactComponent }');
}
// 5. 利用 esbuild,将组件中的 jsx 代码转译为浏览器可运行的代码;
const result = await transformWithEsbuild(componentCode, id, {
loader: 'tsx',
});
return {
code: result.code,
map: null // TODO
};
}
};
}
5. qiankun集成vite子应用
- 背景 vite的模块加载方式是esm,而qiankun通过import-html-entry解析入口文件html时,依次用eval执行script时,如果script里使用了type="module"或者静态的import语句就会报错,为了绕过这一点只能不使用import语句。
- 开发环境 借鉴了vite-plugin-qiankun,将移除script上的 type=module,静态导入import改用动态import()
- 生产环境 使用systemjs加载,把所有的module格式的sciprt脚本全部注释,同时把legacy sciprt标签上的nomodule全部去掉。
6.虚拟列表
什么是虚拟列表
虚拟列表的核心思想是只渲染用户可见区域内的元素,而不是一次性渲染所有数据。这对于有成千上万条数据的长列表尤为重要。
实现原理
虚拟列表主要基于三个关键点:
- 可视区域计算:根据容器高度和列表项高度,计算可见区域能容纳的元素数量
- 滚动位置监听:监听滚动事件,动态计算当前应该显示哪些元素
- 位置模拟:使用CSS定位技术(通常是绝对定位加transform)模拟完整列表的滚动效果
实现步骤
如果需要实现一个基础的虚拟列表,我会这样做:
- 创建一个固定高度的容器作为可视区域
- 计算并设置一个撑满所有数据高度的"幽灵div",提供滚动条
- 通过滚动位置计算起始索引和结束索引
- 只渲染这个索引范围内的元素,并通过绝对定位放在正确的位置
性能优化要点
在实际项目中,我会考虑以下优化点:
- 缓冲区设计:在可视区域上下额外渲染一些元素,避免滚动时出现空白
- 滚动事件节流:减少滚动事件触发频率,避免性能问题
- 高度缓存:对于动态高度的元素,可以缓存已测量的高度,避免重复计算
- 分组渲染:对非常大的数据集,可以考虑分组策略进一步优化
代码示例
import React, { useState, useEffect, useRef } from 'react';
import './VirtualList.css';
interface VirtualListProps {
data: any[]; // 列表数据
itemHeight: number; // 每项的高度
windowHeight: number; // 可视区域高度
renderItem: (item: any, index: number) => React.ReactNode; // 渲染函数
}
const VirtualList: React.FC<VirtualListProps> = ({
data,
itemHeight,
windowHeight,
renderItem
}) => {
// 容器的ref
const containerRef = useRef<HTMLDivElement>(null);
// 当前滚动位置
const [scrollTop, setScrollTop] = useState(0);
// 计算可见区域的起始和结束索引
const startIndex = Math.floor(scrollTop / itemHeight);
// 多渲染几个项以实现平滑滚动
const visibleCount = Math.ceil(windowHeight / itemHeight) + 2;
const endIndex = Math.min(startIndex + visibleCount, data.length);
// 可见的数据
const visibleData = data.slice(startIndex, endIndex);
// 监听滚动事件
useEffect(() => {
const handleScroll = () => {
if (containerRef.current) {
setScrollTop(containerRef.current.scrollTop);
}
};
const currentContainer = containerRef.current;
currentContainer?.addEventListener('scroll', handleScroll);
return () => {
currentContainer?.removeEventListener('scroll', handleScroll);
};
}, []);
// 总高度,用于模拟完整的滚动区域
const totalHeight = data.length * itemHeight;
return (
<div
ref={containerRef}
className="virtual-list-container"
style={{ height: `${windowHeight}px`, overflow: 'auto' }}
>
<div
className="virtual-list-phantom"
style={{ height: `${totalHeight}px`, position: 'relative' }}
>
<div
className="virtual-list-content"
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${startIndex * itemHeight}px)`
}}
>
{visibleData.map((item, index) => (
<div
key={startIndex + index}
className="virtual-list-item"
style={{ height: `${itemHeight}px` }}
>
{renderItem(item, startIndex + index)}
</div>
))}
</div>
</div>
</div>
);
};
export default VirtualList;