前端面试题总结----

2023-11-13

1.基础

1.1 性能优化

  1. 图片压缩,小图使用base64
  2. 数据缓存,避免重复请求
  3. 合理使用标签,避免创建无用的DOM结构
  4. 减少DOM操作,减少重排,尽可能重绘
  5. 避免空的src或者href值
  6. 启用 gzip 压缩
  7. css 放顶部,js 放底部
  8. 减少 http 请求

React性能优化

  1. 使用 memo、PureComponent 缓存组件
  2. 使用 useMemo 缓存数据、useCallback 缓存函数
  3. 使用 lazy 实现组件懒加载、使用 react-visibility-observer 实现懒渲染
  4. 批量更新
  5. 利用debounce、throttle 避免重复回调
  6. 发布者订阅者跳过中间组件 Render 过程
  7. 状态下放,缩小状态影响范围

小程序优化
围绕 加载性能 跟 渲染性能

  1. 分包加载
  2. 去除无用代码,过大的图片应尽量采用网络图片
  3. 减少启动过程的同步调用
  4. 精简首屏数据,与视图层无关的数据尽量不要放在 data 中
  5. onPageScroll 使用 throttle

1.2 浏览器是如何渲染页面的

  1. 解析HTML文件,创建DOM树
  2. 解析CSS 文件,创建CSSOM树
  3. 将CSSOM与DOM合并,构建渲染树
  4. 布局计算每个对象的精确位置和大小
  5. 最后一步是绘制,使用最终渲染树将像素渲染到屏幕上

1.3 cookie,localStorage,sessionStorage三者区别

  1. cookie 数据可在浏览器和服务器间来回传递,而sessionStorage 和 localStorage 不会自动把数据发给服务器,仅在本地保存
  2. cookie大小不超过 4k,sessionStorage 和 localStorage可达到5M或更大
  3. cookie在设置的过期时间之前一直有效,即使窗口或浏览器关闭;sessionStorage在当前窗口关闭后自动删除,localStorage永不过期,除非主动删除数据

1.4 cookie、session、token

cookie和session

cookie 是网站用于标记用户的身份的一段数据(加密的字符串),session是另一种记录客户状态的机制

  1. 客户端发送一个http请求到服务器
  2. 服务器接受客户端请求后,建立一个Session和一个Session ID用来标识这个唯一 Session,并发送一个http响应到客户端,这个响应头,其中就包含 Set-Cookie 头部(Session ID)
  3. 客户端再次访问时会将 Cookie 中的 Session ID 放在请求头中一并发送到服务器上
  4. 服务器从请求中提取出Session ID,并和保存的所有Session ID进行对比,找到这个用户对应的 Session

cookie和session的区别

  1. cookie 数据存储在客户端(只能存储String对象),session 数据存储在服务端(可以存储任意数据类型)
  2. cookie 可以伪造,并不是很安全;session 存储在服务器,过多会占用服务器的性能
  3. cookie 数据不能超过 4k,很多浏览器限制了一个站点最多保存20个cookie;session 没有限制
  4. 一般重要信息存储在session,其他信息可以放在cookie

cookie 被禁用了怎么办

保持登录的关键不是cookie,而是cookie保存的session id,所以还常用 HTTP 请求头来传输,但需要手动添加

session 弊端

  1. session 过多时会过度消耗服务器资源
  2. 某一时间段服务器访问量大时,会导致 session Id 失效

token

token 的认证流程与 session 很相似,无本质区别,使用token的目的是为了减轻服务器的压力

  1. 用户登录,成功后服务器返回Token给客户端
  2. 客户端收到数据后保存在客户端
  3. 客户端再次访问服务器,将token放入headers中,服务器端采用filter过滤器校验。校验成功则返回请求数据,校验失败则返回错误码

token与cookie的区别

  1. token 比 cookie 更安全,浏览器不会自动添加到headers里,需要开发者手动添加

  2. token 支持跨域访问,cookie 不支持

  3. token 在服务器不需要存储 session 信息,本身就包含用户信息,只需要在客户端存储

  4. 不依赖cookie,不需要防范CSRF

1.5 闭包的理解

含义:

  1. 函数声明的时候,会生成一个独立的作用域
  2. 同一作用域的对象可以互相访问
  3. 作用域呈层级包含状态,形成作用域链,子作用域的对象可以访问父作用域的对象,反之不能;另外子作用域会使用最近的父作用域的对象

作用:

  1. 延伸局部变量的作用范围
  2. 提供有限的访问权限

GC回收机制

基本思路:确定确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间就会自动运行。

主要使用两种方式标记未使用的变量:标记清理和引用计数。

  1. 标记清理
    当变量进入上下文(比如在函数内部声明一个变量),这个变量会被加上存在于上下文的标记。在上下文的变量,逻辑上讲永远不应该释放它的内存,因为只要代码在运行,就有可能用到。当变量离开上下文时,也会被加上离开标记

    GC回收运行的时候,会标记内存中存储的所有变量(标记方式有很多种,标记过程并不重要,关键是策略)。然后,它会将所有在上下文中的变量的标记去掉。在此之后再被标记的就是待删除的,原因是任何在上下文中的变量都访问不到它们了。随后GC做一次内存清理,销毁带标记的所有值并收回它们的内存。

  2. 引用计数
    思路是对每个值都记录它的引用次数。声明变量并给它赋一个引用值时,这个值的引用数为1。如果同一个值又被赋给另一个变量,那么引用数加1。如果对该值得引用得变量被其他值给覆盖了,那么引用数减1。当一个值得引用数为0时,就说明没办法再访问到这个值了,就可以安全的收回其内存了。

1.6 GET与POST的区别

GET和POST是HTTP协议中的两种发送请求的方法, HTTP的底层是TCP/IP( 数据如何在万维网中通信的协议),所以GET和POST的底层也是TCP/IP 。 GET和POST能做的事情是一样的。给GET加上request body,给POST带上url参数,技术上是完全行的通的

  1. 请求区别
    • GET在浏览器回退时是无害的,而POST会再次提交请求
    • GET产生的URL地址可以被Bookmark,而POST不可以
    • GET请求会被浏览器主动缓存,而POST不会,除非手动设置
    • GET请求在URL中传送的参数是有长度限制(2kb),而POST没有
    • GET只接受ASCII字符,而POST没有限制
    • GET参数通过URL传递,POST放在Request body中
  2. 表单提交区别
    • GET是从服务器上获取数据,POST是向服务器传送数据
    • GET提交不安全,数据会附在URL后面,POST则不会
  3. 重大区别
    • GET产生一个TCP数据包;POST产生两个TCP数据包
      • GET方式的请求,浏览器会把http header和data一并发送出去
      • 对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data

总结: GET 与 POST 都有自己的语义,不能随便混用 。POST 请求比 GET 慢,因为 POST 要发两次包,GET 在 request body 中带数据有些服务器可能会忽略

1.7 JavaScript 的 “事件冒泡”?为什么会有"事件冒泡"?

答: 多个元素嵌套,有层次关系,这些元素都注册了相同的事件,如果里边的元素的事件触发了,外面元素的改事件也自动触发;

如果事件涉及到更新HTML节点或者添加HTML节点的时候,就会出现这样的一种情况,新更新的或者新添加的节点无法绑定事件,表现的行为是无法触发事件

比如:有一个需求,需要点击 ul 列表下的 li 标签触发事件,如果给每个 li 都绑定事件,会产生下面 2 个问题

  1. li 数量非常大的话就会产生性能问题,甚至造成页面卡顿崩溃
  2. 动态新增的 li 不能绑定事件

如果用事件委托,则会很好的解决这两个问题, 用注册一个事件则能监听子节点的所有事件,所应用的就是事件的冒泡

1.8 url、href 和 src 的区别

  1. url代表唯一的网上资源链接或者是服务器资源链接的地址,引用资源
  2. href 目的不是为了引用资源,而是为了建立联系,让当前标签能够链接到目标地址
  3. src 指向外部资源的位置,指向的内容将会替换当前标签内容

1.9 link 标签引入和 @import 引入的区别

一、相同点
两者都是外部引用CSS的方式

二、区别

  1. link除了引用样式文件,还可以引用图标等资源文件,而@import只引用样式文件
  2. link引用样式时,在页面载入时同时加载;@import需要页面网页完全载入以后加载
  3. link无兼容问题;@import低版本的浏览器不支持
  4. link支持使用JavaScript控制DOM去改变样式;而@import不支持

1.10 BFC规范(块级格式化上下文:block formatting context)是什么?

BFC规定了内部的Block Box如何布局

定位方案:

  1. 内部的Box会在垂直方向上一个接一个放置
  2. Box垂直方向的距离由margin决定,属于同一个BFC的两个相邻Box的margin会发生重叠
  3. 每个元素的margin box 的左边,与包含块border box的左边相接触
  4. BFC的区域不会与float box重叠
  5. BFC是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素
  6. 计算BFC的高度时,浮动元素也会参与计算

满足下列条件之一就可触发BFC

  1. float的值不为none(默认)
  2. overflow的值不为visible(默认)
  3. display的值为inline-block、table-cell、table-caption
  4. position的值为absolute或fixed

1.11 从输入URL到页面展示发生了什么

  1. URL地址解析,判断输入的是一个合法的URL还是一个待搜索的关键词,接着发起真正的URL请求。如果浏览器本地缓存了这个资源,则直接将数据转发给浏览器进程,如果没有缓存,则进行DNS域名解析。
  2. 查找浏览器有没有DNS缓存(之前有访问记录),如果有则返回IP,没有则寻找本地的host文件,看有没有域名记录,如果有则返回IP,没有则直接向本地DNS服务器请求,直至返回IP。
  3. 建立TCP连接(三次握手)
    • 第一次:客户端发送 ‘SYN’ 数据包给服务端
    • 第二次:服务端收到客户端的数据包,返回 ‘SYN/ACK’ 数据包给客户端
    • 第三次:客户端收到服务端的返回后,发送 ‘ACK’ 数据包给服务端
  4. 连接成功,发送http请求,调用后台接口,服务器开始运作起来,准备数据返回
  5. 服务器处理请求,返回响应结果
  6. 关闭TCP连接(四次挥手)
    • 第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
    • 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
    • 第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
    • 第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1, Server进入CLOSED状态,完成四次挥手。
  7. 浏览器渲染页面

1.12 cookie 属性有哪些?

Cookie一共十个属性

  1. Name: Cookie名
  2. Value: Cookie值
  3. Domain: 指定了可以访问该 Cookie 的 Web 站点或域。如果设成.test.com,那么子域名a.test.com和b.test.com,都可以使用.test.com的Cookie。
  4. Path: 定义了Web站点上可以访问该Cookie的目录。如果设成/path/,则只有路径为/path/的页面可以访问该Cookie。如果设为/,则本域名下的所有页面都可以访问该Cookie。
  5. Expires / Max-Age:
    Cookie的超时时间。若设置其值为一个时间,那么当到达此时间后,此Cookie失效。不设置的话默认值是Session,意思是Cookie会和Session一起失效。当浏览器关闭(不是浏览器标签页,而是整个浏览器)后,此Cookie失效。
  6. Size: Cookie大小。
  7. HttpOnly: 若此属性为true,则只有在http请求头中会带有此Cookie的信息,而不能通过document.cookie来访问此Cookie。
  8. Secure: 设置是否只能通过https来传递此条Cookie。
  9. SameSite:
    用来防止 CSRF 攻击和用户追踪。
    可以设置三个值:Strict、Lax 和 None。
    Strict: Strict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。
    Lax: Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。
    None: 关闭SameSite属性,提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。
  10. Priority: 优先级。定义了三种优先级,Low/Medium/High,当Cookie数量超出时,低优先级的Cookie会被优先清除。

1.13 new 实例化对象过程

  1. 创建一个新的空对象
  2. 将新对象的 __proto__ 指向构造函数的prototype
  3. 将构造函数中this指向新对象(借助 call/apply)
  4. 判断构造函数的返回值
    • 设置了返回值:
      若返回值为引用值,则返回引用值
      若返回值为原始数据,则返回新对象
    • 未设置返回值:返回新对象
function newFn (Fn, params) {
    // 创建一个新的空对象 instance
    // const instance = {}

    // 将 instance 的 __proto__ 属性指向构造函数的原型(Fn.prototype)
    // instance.__proto__ = Fn.prototype
    
    const instance = Object.create(Fn.prototype)
    // 以 instance 来调用执行构造函数(借助 call/apply)
    const result = Fn.apply(instance, params)

    // 判断构造函数的返回值,返回 instance 或函数返回值(当构造函数返回值为 object 时)
    return (result && (typeof result === 'object' || typeof result === 'function')) ? result : instance
}

1.14 面向对象与面向过程的区别

  1. 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了
  2. 面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为

优缺点

面向过程:

优点是性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源。而Linux\Unix等一般采用面向过程开发,性能是最重要的因素。缺点是没有面向对象易维护,易复用,易扩展。可维护性差,不易修改。

面向对象:

优点是易维护,易复用,易扩展。由于面向对象由封装,继承,多态性的特性,可以设计出耦合度低的系统,使系统更加灵活,更加易于维护。 缺点是性能比面向过程低

1.15 JS 继承方式有哪些?

  1. 组合继承
    通过原型链和盗用构造函数实现

  2. 原型式继承

  3. 寄生式继承

  4. 寄生式组合继承

  5. 类继承

1.16 小程序的双线程模式

渲染层和逻辑层

  • WXML 模板和 WXSS 样式工作在渲染层,JS 脚本工作在逻辑层
  • 渲染层的界面使用了WebView 进行渲染
  • 逻辑层采用JsCore线程运行JS脚本

一个小程序存在多个界面,所以渲染层存在多个WebView线程,这两个线程的通信会经由微信客户端(下文中也会采用Native来代指微信客户端)做中转,逻辑层发送网络请求也经由Native转发

为什么小程序不采用浏览器的设计模式?

  1. 小程序不需要那么多原生dom的标签
  2. 小程序不需要JS直接操作dom,用目前比较先进的数据驱动,虚拟dom diff更新完全可以
  3. Web Worker由于它是子线程,在执行JS上性能远不如主线程

知道了原理我们能干什么?

  1. 在保证功能的前提下尽量使用结构简单的 UI(减少渲染层的工作量)
  2. 尽量降低 JavaScript 逻辑的复杂度(减少逻辑层的工作量)
  3. 尽量减少 setData 的调用频次和携带的数据体量(减少数据与事件传递时携带的载荷)

2.HTTP 与 HTTPS

2.1 HTTP 状态码

  • 100 – 允许客户端继续在后续的请求中发送附件
  • 200 – 服务端接收请求,并返回数据成功
  • 201 – 请求已经被实现(创建)
  • 202 – 服务端接收请求,但还未处理
  • 301 – 永久性重定向。请求的资源已经永久分配了新的URI ,服务端会自动将该请求重定向到新的位置
  • 302 – 临时重定向,希望用户本次使用的新分配的URI
  • 304 – 自从上次请求后,请求的网页未修改过
  • 400 – 客户端错误,一般是参数错误
  • 403 – 拒绝访问
  • 404 – 请求资源不存在
  • 500 – 服务端错误
  • 501 – 服务器无法识别请求
  • 503 – 服务器暂时无法使用

2.2 浏览器缓存机制

浏览器缓存机制有两种:

  1. 强缓存(默认)
    浏览器访问网站后会强缓存资源,第二次访问就不会请求服务器(一般会定个时间再去请求服务器)
  2. 协商缓存

强缓存:
通过响应头中的Cache-Control属性判断 (优先级最高)

  • private:客户端可以缓存
  • public: 客户端和代理服务器都可缓存
  • max-age=xxx : 缓存的内容将在 xxx 秒后失效
  • no-cache:需要使用对比缓存来验证缓存数据
  • no-store:所有内容都不会缓存,基本不用

协商缓存:

  1. 第一次请求返回给客户端数据和缓存信息,也就是一个特定的缓存标识
  2. 客户端把这个缓存标识放到缓存数据库
  3. 再次请求时,客户端把缓存标识也一起发给服务端,进行对比,判断成功后,返回304状态码,通知客户端比较成功,可以使用缓存数据

两种缓存标识

  1. Etag:(唯一标识)优先级更高
  2. Last-Modified/If-Modified-Since:返回给客户端最后这个资源的修改时间,优先级没有Etag高

协商缓存标识不生效时,状态码200,服务端返回body和header

在对比缓存标识生效时,状态码为304,并且报文大小和请求时间大大减少。原因是缓存标识生效只返回header部分,通过状态码通知客户端使用缓存,不再需要将报文主体部分返回给客户端

总结

  1. 强制缓存的优先级更高,如果没失效,就直接用缓存数据库里的东西
  2. 如果时间已经失效了,就看用的是哪种标识(Etag服务端生成的唯一标识,还是last-modified资源最后修改时间标识)返回304就用缓存里的,返回200就返回body和新的header

2.3 事件循环

所有任务可以分成两种,一种是同步任务, 另一种是异步任务

  1. 所有同步任务都在主线程上执行,形成一个执行栈
  2. 主线程之外,还存在一个"任务队列" , 只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",结束等待状态,进入执行栈,开始执行
  4. 只要主线程空了,就会去读取”任务队列” , 这个过程会不断重复

宏观任务和微观任务
先执行微观任务,再执行宏观任务

宏观任务主要包含:setTimeout、setInterval、script(整体代码)
微观任务主要包括:Promise、MutaionObserver、process.nextTick(Node.js 环境)

Node 事件循环

当Node.js启动时会初始化event loop, 每一个event loop都会包含按如下六个循环阶段,nodejs事件循环和浏览器的事件循环完全不一样

阶段概览

  1. timers(定时器) : 此阶段执行那些由 setTimeout() 和 setInterval() 调度的回调函数.
  2. I/O callbacks(I/O回调) : 此阶段会执行几乎所有的回调函数, 除了 close callbacks(关闭回调) 和 那些由 timers 与 setImmediate()调度的回调.
  3. idle(空转), prepare : 此阶段只在内部使用
  4. poll(轮询) : 检索新的I/O事件; 在恰当的时候Node会阻塞在这个阶段
  5. check(检查) : setImmediate() 设置的回调会在此阶段被调用
  6. close callbacks(关闭事件的回调): 诸如 socket.on(‘close’, …) 此类的回调在此阶段被调用

如果event loop进入了 poll 阶段,且代码未设定timer,将会发生下面情况:

  • 如果poll queue不为空,event loop将同步的执行queue里的callback,直至queue为空,或执行的callback到达系统上限;
  • 如果poll queue为空,将会发生下面情况:
    • 如果代码已经被setImmediate()设定了callback, event loop将结束poll阶段进入check阶段,并执行check阶段的queue (check阶段的queue是 setImmediate设定的)
    • 如果代码没有设定setImmediate(callback),event loop将阻塞在该阶段等待callbacks加入poll queue,一旦到达就立即执行

如果event loop进入了 poll阶段,且代码设定了timer:

  • event loop将检查timers,如果有1个或多个timers时间时间已经到达,event loop将按循环顺序进入 timers 阶段,并执行timer queue

2.4 http1.0、http1.1及http2.0的区别

http1.0:每次请求都需要重新建立tcp连接,请求完后立即断开与服务器连接,这很大程度造成了性能上的缺陷,http1.0被抱怨最多的就是连接无法复用。

http1.1 对比 1.0

  1. 缓存处理。1.0 中主要使用 header 里的 if-Modified-Since,Expires来做为缓存判断的标准;1.1 则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略
  2. Host头处理。1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)
  3. 长连接(Connection: keep-alive)。相较于1.0减少了连接和关闭的延迟,提高了效率,但是若干个请求还是需要串行排队处理,如果一旦某个请求超时,后面的就会被阻塞,也就是常说的线头阻塞。

http2 对比 1.1

  1. 新的二进制格式传输:二进制即0和1的组合,实现方便健壮,而1.x版本是基于文本,解析存在缺陷
  2. 多路复用:一个连接可以有多个请求,且可以混杂在一起根据requestid来区分不同的请求,提高了连接的利用率,降低了延迟
  3. header头部压缩:通讯两方各自缓存了一份 header请求头表,避免了重复的header传输,且缩小了包的体积大小
  4. 服务端推送功能:可以服务端主动向客户端push消息

2.5 npm run 发生了什么

以 vue 项目 npm run serve 为例

  1. 实际上就是执行了vue-cli-service serve 这条命令,因为操作系统中没有存在 vue-cli-service 这一条指令,所以直接执行会报错
  2. 在通过 npm i XXX 安装依赖的时候,例如 npm i @vue/cli-service,就会在 node_modules/.bin/ 目录中创建 好 vue-cli-service 为名的几个可执行文件
  3. .bin 目录下的文件,表示一个个软链接,打开文件可以看到文件顶部写着 #!/bin/sh ,表示这是一个脚本,相当于执行了 ./node_modules/.bin/vue-cli-service serve(最后的 serve 作为参数传入)
  4. 执行npm run xxx 的时候,就会到 node_modules/bin中找对应的映射文件,然后再找到相应的js文件来执行

3.Vue

3.1 vue的响应式原理

  1. 通过Object.defineProperty劫持所有data属性,一个属性创建一个 Dep 对象

  2. 解析器(Compile)解析模板中的 Directive(指令),获取到哪里用到了属性(订阅者),比如{{name}} {{message}},创建一个观察者watcher添加到对应的Dep 对象中,同时初始化view,在界面上显示

  3. Watcher属于Observer和Compile桥梁,将接收到的Observer产生的数据变化,并根据Compile提供的指令进行视图渲染,使得数据变化促使视图变化

3.2 toast 模块封装

import Toast from './Toast'

export default {
  install(Vue) {
    // 1. 创建组件构造器
    const toastConstructor = Vue.extend(Toast)

    // 2. new 一个组件对象
    const toast = new toastConstructor()

    // 3. 手动挂载某一盒子上
    toast.$mount(document.createElement('div'))

    // 4. toast.$el 对应的就是 div
    document.body.appendChild(toast.$el)

    // 5. 原型上添加属性
    Vue.prototype.$toast = toast
  }
}

3.3 vuex 跟 pinia 的区别

  1. pinia【同步、异步】统一使用 action 来修改 state 数据
  2. pinia 没有 modules 配置,每一个独立的仓库都是 definStore 生成出来的
  3. pinia 有完整的 TypeScript 支持

缺点:pinia 不支持调试

3.4 MVC 跟 MVVM 的区别

一、MVVM

在MVVM下视图和模型是不能直接通信的,只能通过ViewModel进行交互,它能够监听到数据的变化,然后通知视图进行自动更新,而当用户操作视图时,VM也能监听到视图的变化,然后通知数据做相应改动,这实际上就实现了数据的双向绑定。

优点:

  • 低耦合: View可以独立于Model变化和修改,一个ViewModel可以绑定到不同的View上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
  • 可重用性: 可以把一些视图逻辑放在一个ViewModel里面,让很多View重用这段视图逻辑。
  • 独立开发: 开发人员可以专注于业务逻辑和数据的开发,设计人员可以专注于页面的设计。

二、MVC

分为:Model(模型)、View(视图)、Controller(控制器)。View和Model不直接联系,必须通过Controller来承上启下。

优点:

  • 低耦合
  • 重用性高
  • 可维护性高

区别:

  • MVVM通过数据来显示视图层而不是节点操作
  • MVVM主要解决了MVC中大量的dom操作使页面渲染性能降低,加载速度变慢,影响用户体验

3.5 vue和react区别

  1. 响应式原理不同
    vue:会遍历data数据对象,使用Object.definedProperty()监听每个属性
    react:通过setState()方法来更新状态,状态更新之后,组件也会重新渲染

  2. 监听数据变化的实现原理不同
    vue:使用的是可变数据,通过 getter/setter以及一些函数的劫持,能精确知道数据变化
    react:强调数据的不可变,通过比较引用的方式(diff)进行的,如果不优化可能导致大量不必要的VDOM的重新渲染

  3. Diff算法不同
    vue对比节点,如果节点元素类型相同,但是className不同,认为是不同类型的元素,会进行删除重建,但是react则会认为是同类型的节点,只会修改节点属性。
    vue的列表比对采用的是首尾指针法,而react采用的是从左到右依次比对的方式,当一个集合只是把最后一个节点移动到了第一个,react会把前面的节点依次移动,而vue只会把最后一个节点移动到最后一个,从这点上来说vue的对比方式更加高效。

  4. 数据流不同
    vue:组件与DOM之间可以通过v-model双向绑定
    react:一直不支持双向绑定,提倡的是单向数据流

  5. 组合不同功能的方式不同
    vue:通过mixin(侵入太强)
    react:通过HoC(高阶组件)

  6. 模板渲染方式不同
    vue:在和组件JS代码分离的单独的模板中,通过指令来实现的
    react:在组件JS代码中,通过原生JS实现模板中的常见语法,比如插值,条件,循环等,都是通过JS语法实现的

  7. 渲染过程不同
    vue:可以更快地计算出Virtual DOM的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树
    react:应用的状态被改变时,全部子组件都会重新渲染

3.6 computed 缓存原理

  1. 响应式
    读取 computed 时会触发 get, 设置时会触发 set

  2. 如何控制
    某个计算属性C依赖 data 中的 A,每次读取C,C就会去读取A,从而触发A的 get,如果没有缓存多次触发是很消耗性能的;
    脏数据标记:dirty,是 watcher 的属性

    • dirty 是 true 时,读取 computed 时重新计算
    • dirty 是 fasle 时,读取 computed 时使用缓存
  3. 依赖的data发生改变,computed 如何更新

    • C 依赖 A,所以A可以收集到C的watcher
    • A 发生改变,会把 watcher 的 dirty 设置为 true
    • A 发生改变,会通知页面进行更新,页面重新读取计算属性C,此时 dirty 是 true,所以重新计算
    • computed 更新完后,会把 dirty 设置为 false

4.React

4.1 setState 为什么是异步的

  1. 提升性能
    1. 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的
    2. 最好的办法应该是获取到多个更新,之后进行批量更新
  2. 如果同步更新了state,但是还没有执行 render 函数,那么 state 和 props 不能保持同步

setState 在组件生命周期或React合成事件中更新数据是异步的,在setTimeout或者原生dom事件中更新数据是同步的。原因是返回了不同的值做更新判断,同步返回 Sync,批量处理返回 Batch

4.2 setState 的数据为什么要保证原数据不可变性

跟 shouldComponentUpdate 更新界面有关,内部进行的是浅层比较,如果直接在原数据上修改引用型数据类型,比较的时候内存地址是一样的导致不会更新视图,但实际上数据又发生了变化

4.3 React的更新流程

  1. 同层节点之间相互比较,不会垮节点比较;
  2. 不同类型的节点,产生不同的树结构;
  3. 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定;

情况一:对比不同类型的元素

当一个元素从 <div> 变成 <p> 时, <div> 树下面的子节点会全部销毁,重新渲染 <p> 树

会调用 <div> 树的 componentWillUnmount(), <p> 树的 componentDidMount() 方法

情况二:对比同一类型的元素

会保留 DOM 节点,仅比对及更新有改变的属性

  • 会更新该组件的props
  • 下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归

情况三:对子节点进行递归

当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个mutation

4.4 受控组件与非受控组件

在 HTML 中,表单元素(如<input>、<textarea> 和 <select>)之类的表单元素通常自己维护 state,并根据用户输入进行更新。

受控组件

而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”

非受控组件

不通过 React 控制,当提交表单时通过 ref 来从DOM节点中获取表单数据,这种表单元素叫做“非受控组件”

4.5 高阶组件(HOC)的意义

可以针对某些React代码进行更加优雅的处理。

  1. 早期的React有提供组件之间的一种复用方式是mixin,目前已经不再建议使用:
    • Mixin 可能会相互依赖,相互耦合,不利于代码维护
    • 不同的Mixin中的方法可能会相互冲突
    • Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性
  2. HOC也有自己的一些缺陷:
    • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难;
    • HOC可以劫持props,在不遵守约定的情况下也可能造成冲突;

4.6 SPA应用中的hash路由与history路由

优点:

  • 用户体验好,用户操作更方便
  • 完全的前端组件化

缺点:

  • 首次加载大量资源 -->解决:按需加载
  • 对SEO不友好 -->解决:服务端渲染 nuxt(next)

特点:当有不同的请求时,在同一个页面渲染不同的组件
原理:前后端分离(后端专注数据,前端专注交互与可视化)+ 前端路由

4.6.1 hsah 路由

Hash 路由(也就是锚点#),本质是是改变location的hash属性

利用 URL 上的 hash,当 hash 改变不会引起页面刷新,可以触发相应的 hashchange 回调函数配置路由

 window.onhashchange = function() {
   // 更新页面内容
   switch (location.hash) {
     case '#/home': 
       app.innerHTML = '首页'
       break
     case '#/about': 
       app.innerHTML = '关于'
       break
     default:
       app.innerHTML = ''
   }
}

4.6.2 history 路由

本质是通过h5新增的history.pushState()或history.replaceState()改变路径

pushState()、replaceState() 方法接收三个参数:state、title、url

history.pushState({color: 'red'}, '', '/red') // 设置状态,生成 /red
window.onpopstate = function(event) { // 监听状态
  if (event.state && event.state.color) {
    document.body.style.color = event.state.color  // 更新页面内容
  }
}

4.6.3 history 对比 hash

优势:

  1. pushState 设置的 url 可以是同源下的任意 url ;而 hash 只能修改 # 后面的部分,因此只能设置当前 url 同文档的 url

  2. pushState 设置的新的 url 可以与当前 url 一样,这样也会把记录添加到栈中;hash 设置的新值不能与原来的一样,一样的值不会触发动作将记录添加到栈中

  3. pushState 通过 stateObject 参数可以将任何数据类型添加到记录中;hash 只能添加短字符串

  4. pushState 可以设置额外的 title 属性供后续使用

劣势:

  1. history 在刷新页面时,如果服务器中没有相应的响应或资源,就会出现404。因此,如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面

  2. hash 模式下,仅 # 之前的内容包含在 http 请求中。对后端来说,即使没有对路由做到全面覆盖,也不会报 404

4.7 react@16.4 + 的生命周期

  1. 挂载阶段:constructor、render、componentDidMount
  2. 更新阶段:componentDidUpdate
  3. 卸载阶段:componentWillUnmount

4.8 useEffect 和 componentDidMount 有什么差异?

useEffect 会捕获 props 和 state。所以即便在回调函数里,你拿到的还是初始的 props 和 state。如果想得到“最新”的值,可以使用 ref。

4.9 调用 setState 之后发生了什么?

  1. 为当前节点创建一个 updateQueue 的更新列队。
  2. 然后会触发 reconciliation 过程,在这个过程中,会使用名为 Fiber 的调度算法,开始生成新的 Fiber 树, Fiber 算法的最大特点是可以做到异步可中断的执行。
  3. 然后 React Scheduler 会根据优先级高低,先执行优先级高的节点,具体是执行 doWork 方法。
  4. 在 doWork 方法中,React 会执行一遍 updateQueue 中的方法,以获得新的节点。然后对比新旧节点,为老节点打上 更新、插入、替换 等 Tag。
  5. 当前节点 doWork 完成后,会执行 performUnitOfWork 方法获得新节点,然后再重复上面的过程。
  6. 当所有节点都 doWork 完成后,会触发 commitRoot 方法,React 进入 commit 阶段。
  7. 在 commit 阶段中,React 会根据前面为各个节点打的 Tag,一次性更新整个 dom 元素

4.10 React有哪些优化性能的手段?

  1. 使用· memo、PureComponent 包裹组件,优化组件渲染
  2. 使用 Suspense、lazy 按需加载组件
  3. 批量更新
  4. 按优先级更新,及时响应用户
  5. 利用debounce、throttle 避免重复回调
  6. 使用 useMemo、useCallback 缓存,稳定 props 值
  7. 发布者订阅者跳过中间组件 Render 过程
  8. 状态下放,缩小状态影响范围
  9. 列表项使用 key 属性
  10. Hooks 按需更新

4.11 React 18 新特性

一、自动批处理state更新

跳过批处理

import { flushSync } from 'react-dom';

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  // React has updated the DOM by now
  flushSync(() => {
    setFlag(f => !f);
  });
  // React has updated the DOM by now
}

二、新的Suspense SSR架构

  1. server端无需等待所有数据(和HTML)都ready再响应客户端,相反地,当应用骨架准备好时你就可以发送给客户端显示,剩余的内容将在它们ready之后流式传输给客户端。
  2. client端来说,不再需要等待所有 JavaScript 加载完毕才能开始渲染。可以结合code spliting与SSR一起使用,在server端的HTML(片段)将被保留(在server),当相关代码加载时,React将对其进行合成。不再需要等待所有组件都加载完成才运行用户交互。相反,可以依靠Selective Hydration 来确定用户与之交互的组件的优先级。

三、startTransition 非紧急更新

React将状态更新分为两类

紧急更新(Urgent updates):反映直接的交互,如输入、点击、按键按下等等。
过渡更新(Transition updates):将UI从一个视图过渡到另一个视图。

setInputValue(input);

// 标记为非紧急更新
startTransition(() => {
  React.setSearchQuery(input);
});

和setTimeout的区别

  1. 一个重要区别是setTimeout是「延迟」执行,startTransition是立即执行的,传递给startTransition的函数是同步运行,但是其内部的所有更新都会标记为非紧急,React将在稍后处理更新时决定如何render这些updates,这意味着将会比setTimeout中的更新更早地被render。
  2. 另一个重要区别是用setTimeout包裹的如果是内大面积的更新操作会导致页面阻塞不可交互,直到超时。这时候用户的输入、键盘按下等紧急更新操作将被阻止。而startTransition则不同,由于它所标记的更新都是可中断的,所以不会阻塞UI交互。即使用户输入发生变化,React也不必继续渲染用户不再感兴趣的内容。
  3. 最后,因为setTimeout是异步执行,哪怕只是展示一个小小的loading也要编写异步代码。而通过transitions,React可以使用hook来追踪transition的执行状态,根据transition的当前状态来更新loading。

4.12 为何要在componentDidMount里面发送请求?

  1. 跟服务器端渲染(同构)有关系,如果在componentWillMount里面获取数据,fetch data会执行两次,一次在服务器端一次在客户端
  2. 在componentWillMount中fetch data,数据一定在render后才能到达,如果忘记了设置初始状态,用户体验不好
  3. react16.0以后,componentWillMount可能会被执行多次

5.webpack

5.1 webpack中什么是chunk?什么是bundle?

  1. 首先告诉 Webpack 一个入口文件,如 index.js 为起点作为打包,将入口文件的所有依赖项引入进来,这些依赖会跟入口文件形成一个文件(代码块),这个文件(代码块)就是 chunk
  2. 将这个代码块(chunk)进行处理,比如把 less 文件编译成 css,js 资源编译成浏览器能识别的 js 语法等等操作,这些就叫做打包,将打包好的资源再输出出去,这个输出的文件就叫 bundle

5.2 Webpack 五个核心概念分别是什么?

  1. Entry: 入口(Entry)指示 Webpack 以哪个文件为入口起点开始打包,分析内部构件依赖图
  2. Output: 输出(Output)指示 Webpack 打包后的资源 bundles 输出到哪里去,以及如何命名
  3. Loader: Loader 能让 Webpack 处理非 JavaScript/json 文件(Webpack 自身只能处理 JavaScript/json )
  4. Plugins: 插件(Plugins)可以用于执行范围更广的任务,包括从打包优化和压缩到重新定义环境中的变量
  5. Mode: 模式(Mode)指示 Webpack 使用相应模式的配置,只有development(开发环境)和production(生产环境)两种模式

5.3 有哪些常见的Loader?它们是解决什么问题的?

  1. css-loader:将 css 文件变成 CommonJS 模块加载 js 中,里面内容是样式字符串
  2. style-loader:创建 style 标签,将 js 中的样式资源插入进行,添加到 head 中生效
  3. url-loader:在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
  4. ile-loader:打包其他资源(除了css/js/html 资源)
  5. html-loader:处理 html 文件中的 img
  6. babel-loader:把 ES6 转换成 ES5
  7. eslint-loader:通过 ESLint 检查 JavaScript 代码

5.4 有哪些常见的Plugin?它们是解决什么问题的?

  1. html-webpack-plugin:可以复制一个有结构的html文件,并自动引入打包输出的所有资源(JS/CSS)
  2. clean-webpack-plugin:重新打包自动清空 dist 目录
  3. mini-css-extract-plugin:提取 js 中的 css 成单独文件
  4. optimize-css-assets-webpack-plugin:压缩css
  5. uglifyjs-webpack-plugin:压缩js
  6. commons-chunk-plugin:提取公共代码

5.5 webpack 构建流程

  1. 根据 entry 配置项找出所有的入口文件
  2. 从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块
  3. Loader 翻译完所有模块后,根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表
  4. 确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

5.6 webpack的热更新是什么?

优化打包构建速度,一个模块发生变化,只会重新打包这一个模块(而不是打包所有模块),极大提升构建速度

  1. 样式文件:可以使用HMR功能,因为style-loader内部实现了
  2. JS文件:默认没有HMR功能,需要修改js代码,添加支持HMR功能。入口文件做不了HMR功能,只能处理非入口js文件
  3. HTML文件:默认没有HMR功能,同时会导致 html 文件不能热更新(即修改没有任何反应)

HTML文件不用做HMR功能,因为只有一个html文件

5.7 webpack优化?

开发环境下:

  1. 开启HMR功能,优化打包构建速度
  2. 配置 devtool: ‘source-map’,优化代码运行的性能

生产环境下:

  1. oneOf 优化: 默认情况下,假设设置了7、8个loader,每一个文件都得通过这7、8个loader处理(过一遍),浪费性能,使用 oneOf 找到了就能直接用,提升性能
  2. 开启 babel 缓存: 当一个 js 文件发生变化时,其它 js 资源不用变
  3. code split 分割: 将js文件打包分割成多个bundle,避免体积过大
  4. 懒加载和预加载
  5. PWA 网站离线访问
  6. 多进程打包: 开启多进程打包,主要处理js文件(babel-loader干的活久),进程启动大概为600ms,只有工作消耗时间比较长,才需要多进程打包,提升打包速度
  7. dll 打包第三方库
    • code split将第三方库都打包成一个bundle,这样体积过大,会造成打包速度慢
    • 是将第三方库打包成多个bundle,从而进行速度优化

5.8 hash、chunkhash、contenthash三者的区别?

浏览器访问网站后会强缓存资源,第二次刷新就不会请求服务器(一般会定个时间再去请求服务器),假设有了bug改动了文件,但是浏览器又不能及时请求服务器,所以就用到了文件资源缓存(改变文件名的hash值)

  1. hash:不管文件变不变化,每次wepack构建时都会生成一个唯一的hash值
  2. chunkhash:根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样
    问题:js和css同属于一个chunk,修改css,js文件同样会被打包
  3. contenthash:根据文件的内容生成hash值。不同文件hash值一定不一样

5.9 commonJS和ES6模块化的区别

  1. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
  2. CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载
  3. CommonJS是对模块的浅拷⻉,ES6 Module是对模块的引⽤,即ES6 Module只存只读,不能改变其值,具体点就是指针指向不能变,类似const 。

5.10 webpack 和 vite 的区别

webpack是先打包再启动开发服务器,vite是直接启动开发服务器,然后按需编译依赖文件。

由于现代浏览器本身就支持ES Modules,会主动发起请求去获取所需文件。vite充分利用这点,将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像webpack先打包,交给浏览器执行的文件是打包后的

由于vite使用的是ES Module,所以代码中不可以使用CommonJs

6.TypeScript

6.1 对比JS的优势

  1. 提早发现代码中的Bug
  2. 提高代码的可读性
  3. 减少了复杂的错误处理逻辑

6.2 什么是泛型

定义函数,接口或类时,不预先指定具体类型,而是在使用的时候指定具体类型

// 定义类型的时候 动态指定值 
interface KeyPair<T,U> {
  key: T;
  value: U;
}

let kp1: KeyPair<number,string> = { key: 123, value: 'str' }
let kp2: keypair<string,number> = { key: 'str', value: 123 }

6.3 type 跟 interface 的区别

相同点

  1. 都可以用来定义对象或函数的形状
  2. 都支持继承,并且可以互相继承

不同点

  1. type 可以定义基本类型
  2. type 可以声明联合类型
  3. interface 可以生命合并

6.4 never,void,unknown 类型

  1. never
    • 一个从来不会有返回值的函数,即死循环(如:如果函数内含有 while(true) {})
    • 一个总是会抛出错误的函数(如:function foo() { throw new Error(‘Not Implemented’) },foo 的返回类型是 never)
  2. void
    • 表示没有任何返回值的函数
    • 也可以声明一个变量为 void ,但只能将它赋值为 undefined 或 null
  3. unknown
    • 用于描述类型不确定的变量
    • 必须确定类型才能做后续操作
    • 只能赋值给any和unknown类型
    • unknown 除了与 any 以外, 与其它任何类型组成的联合类型最后都是 unknown 类型

6.5 交叉类型(&)和接口继承(extends)对比

  • 相同点:都可以实现对象类型的组合
  • 不同点:两种方式实现类型组合时,对于同名属性之间,处理类型冲突的方式不同
    在这里插入图片描述

7. 代码题

7.1 数组拉平

// 第一种方式
function myFlat(arr, newArr = []) {
  for (let x of arr) {
    if (Array.isArray(x)) {
      myFlat(x, newArr)
    } else {
      newArr.push(x)
    }
  }
  return newArr
}
console.log(myFlat([1, [2, 3], [4, [5, 6]]]))

// 第二种方式
function flat(arr) {
  return arr.reduce((result, item) => {
    return result.concat(Array.isArray(item) ? flat(item) : item)
  }, [])
}
console.log(flat([1, [2, 3], [4, [5, 6]]]))

7.2 深拷贝

function deepCopy(obj) {
  if (typeof obj === 'object') {
    var result = Array.isArray(obj) ? [] : {}
    for (let i in obj) {
      result[i] = typeof obj[i] === 'object' ? deepCopy(obj[i]) : obj[i]
    }
  } else {
    var result = obj
  }
  return result
}
const arr = [1, [2, 3], [4, [5, 6]]]
const newArr = deepCopy(arr)
newArr[1][0] = 4
console.log(arr, newArr)

7.3 手写 bind

Function.prototype.bind = function (context, ...args) {
  context = context || window
  const fnSymbol = Symbol('fn')
  context[fnSymbol] = this

  return function (..._args) {
    args = args.concat(_args)

    context[fnSymbol](...args)
    Reflect.deleteProperty(context, fnSymbol)
  }
}

7.4 防抖与节流

// 防抖
function debounce(fn, wait = 300) {
  let timer = null
  return function (...args) {
    timer && clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, wait)
  }
}

// 节流
function throttle(f, wait = 1000) {
  let timer = null
  let flag = true
  return (...args) => {
    if (timer) return
    if (flag) {
      f(...args)
      flag = false
    }
    timer = setTimeout(() => {
      flag = true
      timer = null
    }, wait)
  }
}

window.onclick = throttle(() => {
  console.log('点击')
})

7.5 手写题Promise

class MyPromise {
  constructor(fn) {
    this.resolvedCallbacks = []
    this.rejectedCallbacks = []
    
    this.state = 'PENDING'
    this.value = ''
    
    fn(this.resolve.bind(this), this.reject.bind(this))
    
  }
  
  resolve(value) {
    if (this.state === 'PENDING') {
      this.state = 'RESOLVED'
      this.value = value
      
      this.resolvedCallbacks.map(cb => cb(value))
    }
  }
  
  reject(value) {
    if (this.state === 'PENDING') {
      this.state = 'REJECTED'
      this.value = value
      
      this.rejectedCallbacks.map(cb => cb(value))
    }
  }
  
  then(onFulfilled, onRejected) {
    if (this.state === 'PENDING') {
      this.resolvedCallbacks.push(onFulfilled)
      this.rejectedCallbacks.push(onRejected)
    }
    
    if (this.state === 'RESOLVED') {
      onFulfilled(this.value)
    }
    
    if (this.state === 'REJECTED') {
      onRejected(this.value)
    }
  }
}

7.6 手写发布订阅模式

class Event {
  // 首先定义一个事件容器,用来装事件数组(因为订阅者可以是多个)
  #handlers = {}

  // 事件添加方法,参数有事件名和事件方法
  addEventListener(type, handler) {
    // 首先判断handlers内有没有type事件容器,没有则创建一个新数组容器
    if (!(type in this.#handlers)) {
      this.#handlers[type] = []
    }
    // 将事件存入
    this.#handlers[type].push(handler)
  }

  // 触发事件两个参数(事件名,参数)
  dispatchEvent(type, ...params) {
    // 若没有注册该事件则抛出错误
    if (!(type in this.#handlers)) {
      return new Error('未注册该事件')
    }
    // 便利触发
    this.#handlers[type].forEach(handler => {
      handler(...params)
    })
  }

  // 事件移除参数(事件名,删除的事件,若无第二个参数则删除该事件的订阅和发布)
  removeEventListener(type, handler) {
    // 无效事件抛出
    if (!(type in this.#handlers)) {
      return new Error('无效事件')
    }
    if (!handler) {
      // 直接移除事件
      delete this.#handlers[type]
    } else {
      const idx = this.#handlers[type].findIndex(ele => ele === handler)
      // 抛出异常事件
      if (idx === -1) {
        return new Error('无该绑定事件')
      }
      // 移除事件
      this.#handlers[type].splice(idx, 1)
      if (this.#handlers[type].length === 0) {
        delete this.#handlers[type]
      }
    }
  }
}

7.7 设计控制并发请求任务队列

场景:前端页面zh
要求:

  1. 最多同时执行的任务数为10个
  2. 当前任务执行完成后,释放队列空间,自动执行下一个任务
  3. 所有任务添加到任务队列后,自动开始执行任务
function createTask(i) {
  return () => {
    // 模拟网络请求
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(i)
      }, 2000)
    })
  }
}

class TaskQueue {
  constructor() {
    this.max = 10 // 最大数 10
    this.taskList = [] // 存储任务,模拟队列先进先出
    setTimeout(() => { // 自动执行
      this.run()
    })
  }
  addTask(task) {
    this.taskList.push(task)
  }
  run() {
    const length = this.taskList.length
    if (!length) return // 没有任务了
    let count = Math.min(this.max, length) // 最大并发数
    for (let i = 0; i < count; i++) {
      const task = this.taskList.shift() // 取出第一个任务
      task().then(res => {
        console.log(res)
      }).finally(() => {
        count--
        if (count === 0) { // 并发请求队列空了,继续下一轮
          this.run()
        }
      })
    }
  }
}

const taskQueue = new TaskQueue()
for (let i = 0; i < 20; i++) {
  const task = createTask(i)
  taskQueue.addTask(task)
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

前端面试题总结---- 的相关文章

  • 事件委托Tab栏切换

  • 浏览器缓存相关面试题一网打尽,理论结合实践,用代码学习缓存问题,建议关注+收藏,(含项目源代码)

    前言 浏览器缓存的问题是面试中关于浏览器知识的重要组成部分 也是性能优化题目的一部分 但是不要被吓到 我话放到这里 就那么点东西 我这一篇文章基本上就涵盖了所有相关的知识点 认真看一遍 所有的问题都是纸老虎 一 准备工作 1 1 拉取仓库
  • 2024年最热门的15个科技工作岗位

    1 系统安全管理员 系统安全管理员的任务是确保公司的网络 数据和系统免受网络安全威胁 方法是确保有适当的安全战略并保持最新的合规性和策略 要求 应聘者应具有网络安全职位的工作经验 并对合规性和安全协议的最佳实践有坚实的基础 这个职位通常需要
  • Element-Puls中el-upload组件结合vue-draggable-plus实现上传支持拖拽排序(并保留el-upload原有样式、预览、删除)等功能

    展示效果 需求 需求想要一个可拖拽排序的图片列表 但是发现el upload虽然可以实现照片墙 但是没办法拖拽 实现思路 使用 vue draggable plus 拖拽插件 隐藏Upload原有的已上传文件列表 自定义上传后文件列表的样式
  • 每天10个前端小知识 <Day 1>

    前端面试基础知识题 1 将数组的length设置为0 取第一个元素会返回什么 设置 length 0 会清空数组 所以会返回 undefined 2 e target 和 e currentTarget 有什么区别 e target 触发事
  • 前端必备的 web 安全知识手记

    前言 安全这种东西就是不发生则已 一发生则惊人 作为前端 平时对这方面的知识没啥研究 最近了解了下 特此沉淀 文章内容包括以下几个典型的 web 安全知识点 XSS CSRF 点击劫持 SQL 注入和上传问题等 下文以小王代指攻击者 话不多
  • 每天10个前端小知识 <Day 7>

    前端面试基础知识题 1 什么是尾调用优化和尾递归 尾调用的概念非常简单 一句话就能说清楚 就是指某个函数的最后一步是调用另一个函数 function f x return g x 上面代码中 函数f的最后一步是调用函数g 这就叫尾调用 尾调
  • Android SDK开发艺术探索(五)安全与校验

    一 前言 本篇是Android SDK开发艺术探索系列的第五篇文章 介绍了一些SDK开发中安全方面的知识 包括资源完整性 存储安全 权限校验 传输安全 代码混淆等知识 通过基础的安全配置为SDK保驾护航 探索SDK开发在安全方面的最佳实践
  • 新入职一个00后卷王,每天加班到2点,太让人崩溃了····

    在程序员职场上 什么样的人最让人反感呢 是技术不好的人吗 并不是 技术不好的同事 我们可以帮他 是技术太强的人吗 也不是 技术很强的同事 可遇不可求 向他学习还来不及呢 真正让人反感的 是技术平平 却急于表现自己的人 每天加班到12点 在老
  • 基于java的饮食分享平台系统设计与实现

    基于java的饮食分享平台系统设计与实现 I 引言 A 研究背景和动机 近年来 随着人们生活水平的提高和健康意识的增强 饮食健康已经成为越来越多人的关注焦点 因此 一个方便快捷的饮食分享平台就显得尤为重要 基于Java的饮食分享平台系统设计
  • HTML概述、基本语法(表格整理、标签、基本结构)

    一 HTML概述 HTML指的是超文本标记语言 超文本 是指页面内可以包含图片 链接 声音 视频等内容 标记 标签 通过标记符号来告诉浏览器页面该如何显示 我们可以打开浏览器 右击页面 点击 查看网页源代码 来方便了解HTML标签通过浏览器
  • 点击存储到固定时间清除存储

    这段代码 无意间想到的 随便写了下来 运行 根据点击之后传递一个参数 将他存入本地存储 方便测试为10秒 10秒后触发下一个事件 清除本地存储 结束
  • 低代码配置-组件列表设计

    过滤字段功能 配置了api 启用 输出配置 filter type Array default gt
  • 课设:NFA确定化和最小化程序的设计与实现(html+css+js实现)

    文章目录 问题描述 待解决问题 1 如何存储NFA或者是DFA 2 NFA多初态问题 3 子集化过程思路 4 分割法过程思路 使用方法 下载链接 问题描述
  • 新手也能看懂的【前端自动化测试入门】!

    前言 最近在网上搜索前端自动化测试相关的文档 但是发现网上的文章都是偏使用 没有把一些基础概念说清楚 导致后续一口气遇到一些 karma Jasmine jest Mocha Chai BDD 等词汇的时候很容易一头雾水 这次一方面整理一下
  • 做好这几件事,30岁的你也能转行鸿蒙(HarmonyOS)?

    当你年过30 不管你愿不愿意承认 你的精力都在走下坡路 25岁熬一个通宵能写出来的代码 30岁有可能需要一整天 当然你也可以选择不拼精力和体力 当自身的一线经验积累到一定程度后 就会选择慢慢过渡到管理者的角色 通过经验分享及任务分配来参与项
  • vue实现 marquee(走马灯)

    样式 代码 div class marquee prompt div class list prompt span class prompt item span div div data return listPrompt xxx xxxx
  • chrome浏览器无法在地址栏输入内容搜索问题解决--图文

    关于日常遇到的小问题解决记录一下 1 导航栏录入信息后跳转错误 2 解决办法 默认百度搜索引擎地址错误 百度正确的搜索格式是 http www baidu com s wd s chrome浏览器中百度的搜索格式是 http www bai
  • 每天10个前端小知识 <Day 14>

    前端面试基础知识题 1 CSSOM树和DOM树是同时解析的吗 浏览器会下载HTML解析页面生成DOM树 遇到CSS标签就开始解析CSS 这个过程不会阻塞 但是如果遇到了JS脚本 此时假如CSSOM还没有构建完 需要等待CSSOM构建完 再去
  • 如何在 Python 脚本中使用 Google OAuth2

    在使用 Python 脚本将视频上传到 YouTube 频道时 若希望将视频上传到第二个频道 需要解决 OAuth2 授权的问题 解决方案 创建新的 Google Cloud 项目 from google oauth2 import ser

随机推荐

  • LangChain 的聊天模型

    各位人工智能爱好者 大家好 今天 我们就来详细了解一下 LangChain 聊天模型 LangChain是一个很棒的工具 它提供了与各种语言模型交互的标准接口 包括基于文本的大型语言模型 LLM 和聊天模型 LangChain模型的概念 模
  • 【Linux命令详解

    文章标题 简介 一 参数列表 二 使用介绍 1 使用基本模式搜索 2 忽略大小写匹配 3 反向匹配 4 递归搜索目录 5 显示文件名 6 显示行号 7 显示上下文行 8 启用扩展正则表达式 9 将模式视为固定字符串 10 使用颜色高亮显示匹
  • Docker部署服务

    部署Nginx 寻找镜像 docker search nginx 默认最新版 官网查看不同的版本信息 下载镜像 docker pull nginx root iZwz9hv1phm24s3jicy8x1Z docker images REP
  • C语言打印输出斐波那契数前20项案例讲解

    我们先看什么是斐波那契数 斐波那契数列指的是这样一个数列 1 1 2 3 5 8 13 21 34 55 89 我们通过观察可以得出斐波那契数列的特点 1 第一项和第二项都是1 2 这个数列从第3项开始 每一项都等于前两项之和 思路分析 1
  • 2023年第二届全国大学生数据统计与分析竞赛题目B:电影评分的大数据分析第二问

    详细代码 企鹅2869955900 import pandas as pd import matplotlib pyplot as plt import numpy as np plt rcParams font sans serif Si
  • 【大数据】Hadoop 生态系统及其组件

    Hadoop 生态系统及其组件 1 Hadoop 生态系统的组成 2 Hadoop 生态系统简介 2 1 HDFS 2 2 MapReduce 2 3 YARN 2 4 Hive 2 5 Pig 2 6 HBase 2 7 HCatalog
  • python3 模块、import、from import

    模块 1 模块就是 py后缀的文件 2 py文件类似于一个类 包含以下部分 1 导入 一般的类都有导入 2 变量 对应类的属性 3 函数 对应类的方法 4 类 对应内部类 5 if name main 对应主函数 6 顶格写的代码段 对应构
  • OpenCV学习笔记(17)双目测距与三维重建的OpenCV实现问题集锦(二)双目定标与双目校正

    三 双目定标和双目校正 双目摄像头定标不仅要得出每个摄像头的内部参数 还需要通过标定来测量两个摄像头之间的相对位置 即右摄像头相对于左摄像头的三维平移 t 和旋转 R 参数 图6 要计算目标点在左右两个视图上形成的视差 首先要把该点在左右视
  • vue2+高德地图web端开发使用

    创建vue2项目 我们创建一个vue2项目 创建vue2项目就不用再多说了吧 使用 vue create 项目名 创建即可 注册高德地图 高德地图官网地址 https lbs amap com 如果是第一次使用 点击注册然后进入我们的控制台
  • idea快捷键最全最新最好

    持续更新 如果文档中没有的 麻烦在评论中添加 常用快捷键 返回最顶头 home 返回最末尾 end Alt Insert 可以新建类 文件 get或set方法 此快捷键又名创造一切 编辑区和文件区的跳转 alt 1 编辑区跳转至文件区 es
  • 272. 最长公共上升子序列(lcis,dp)

    首先是lis的状态划分图 然后是lcs 结合lis和lcs两种dp问题的分析方法 我们就可以得出lcis的状态分析图 1 首先上升子序列的分析方法 以某个数字为结尾 2 其次公共子序列的分析方法 有4种状态 00 01 10 11 双关键字
  • 128、函数接口类---Consumer

    一 概念 java util function Consumer
  • Git常用命令fetch和pull和push

    Git常用命令pull和push 1 fetch 从远程获取代码库 会将所有远程分支都拉到本地 并不会合并代码 git fetch 下载远程仓库的所有变动 git fetch remote git fetch origin 指定拉取远程re
  • 百度App Objective-C/Swift 组件化混编之路(二)- 工程化

    作者丨张渝 郭金 来源丨百度App技术 前文 百度App Objective C Swift 组件化混编之路 已经介绍了百度App 引入 Swift 的影响面评估以及落地的实施步骤 本文主要以依赖管理工具为支撑 介绍百度App 如何实现组件
  • Socket -- udp

    接收者 完成System out println UDPProvider Started 作为接收者 指定一个端口用于数据接收 DatagramSocket ds new DatagramSocket 20000 构建接收实体 final
  • 网吧管理系统mysql_网吧管理系统数据库课程设计.doc

    网吧管理系统数据库课程设计 doc 大型数据库课程设计 设计报告 题 目 网吧管理系统数据库 学 号 学生姓名 指导教师 提交时间 2013 11 23 第1章 需求分析 1 1 需求分析任务 1 2 需求分析过程 1 3 数据字典和流程图
  • Java面向对象(1) —— 封装

    目录 一 封装的概念 二 类的封装以及使用 三 访问修饰符 四 属性封装的实现 五 方法封装的实现 六 UML类图 七 类的构造方法与可重载 八 this关键字 九 static关键字 十 方法重载 overload 十一 包 packag
  • unity制作和输出摄像机环绕动画

    1 需要用到两个官方插件Cinemachine以及Recorder 2 第一步设置摄像机 创建一个虚拟相机CM vcam1 这时主摄像机会自动加载Cinemachinebrain组件 并指定CM1 3 设置CM1的参数 创建空物体命名为ta
  • 4 朴素贝叶斯法

    文章目录 4 朴素贝叶斯法 4 1 朴素贝叶斯的学习与分类 4 1 1 基本方法 4 1 2 后验概率最大化的含义 4 2 朴素贝叶斯法的参数估计 4 2 1 极大似然估计 4 2 2 学习与分类算法 4 2 3 贝叶斯估计 4 3 代码
  • 前端面试题总结----

    1 基础 1 1 性能优化 图片压缩 小图使用base64 数据缓存 避免重复请求 合理使用标签 避免创建无用的DOM结构 减少DOM操作 减少重排 尽可能重绘 避免空的src或者href值 启用 gzip 压缩 css 放顶部 js 放底