1.基础
1.1 性能优化
-
减少HTTP请求:
- 合并和压缩文件:减少页面所需的CSS、JavaScript和图像文件数量,并通过合并和压缩这些文件来减小它们的大小。
- 使用CSS精灵:将页面上的多个小图标合并为一个精灵图,以减少HTTP请求。
-
优化图像:
- 选择适当的图像格式:根据具体需求选择合适的图像格式,如JPEG、PNG或WebP。
- 图像压缩:使用工具压缩图像,减小文件大小而不损失太多质量。
-
延迟加载(懒加载):
- 图片懒加载:仅在用户滚动到图片可见区域时加载图片,而不是一开始就加载所有图片。
- 延迟加载JavaScript:将不必要的JavaScript代码推迟到页面加载后再执行。
-
浏览器缓存:
- 利用浏览器缓存:通过设置适当的缓存头信息,允许浏览器缓存页面资源,减少重复加载。
-
服务端优化:
- 启用Gzip压缩:服务器端开启Gzip压缩,减小传输的数据量。
- 启用CDN(内容分发网络):通过在全球分布的服务器上存储资源副本,加速用户访问速度。
-
使用字体图标:
- 使用字体图标代替图像:字体图标通常比图像更轻量,且可以通过CSS进行调整和样式更改。
-
异步加载JavaScript:
- 异步加载外部JavaScript:通过async或defer属性异步加载JavaScript,防止它们阻塞页面的渲染。
-
代码优化:
- 精简和优化代码:删除不必要的代码、注释和空格,以及使用更有效的算法。
React性能优化
- 使用 memo、PureComponent 缓存组件
- 使用 useMemo 缓存数据、useCallback 缓存函数
- 使用 lazy 实现组件懒加载、使用 react-visibility-observer 实现懒渲染
- 批量更新
- 利用debounce、throttle 避免重复回调
- 发布者订阅者跳过中间组件 Render 过程
- 状态下放,缩小状态影响范围
小程序优化
围绕 加载性能 跟 渲染性能
- 分包加载
- 去除无用代码,过大的图片应尽量采用网络图片
- 减少启动过程的同步调用
- 精简首屏数据,与视图层无关的数据尽量不要放在 data 中
- onPageScroll 使用 throttle
1.2 浏览器是如何渲染页面的
- 解析HTML文件,创建DOM树
- 解析CSS 文件,创建CSSOM树
- 将CSSOM与DOM合并,构建渲染树
- 布局计算每个对象的精确位置和大小
- 最后一步是绘制,使用最终渲染树将像素渲染到屏幕上
1.3 cookie,localStorage,sessionStorage三者区别
- cookie 数据可在浏览器和服务器间来回传递,而sessionStorage 和 localStorage 不会自动把数据发给服务器,仅在本地保存
- cookie大小不超过 4k,sessionStorage 和 localStorage可达到5M或更大
- cookie在设置的过期时间之前一直有效,即使窗口或浏览器关闭;sessionStorage在当前窗口关闭后自动删除,localStorage永不过期,除非主动删除数据
1.4 cookie、session、token
cookie和session
cookie 是网站用于标记用户的身份的一段数据(加密的字符串),session是另一种记录客户状态的机制
- 客户端发送一个http请求到服务器
- 服务器接受客户端请求后,建立一个Session和一个Session ID用来标识这个唯一 Session,并发送一个http响应到客户端,这个响应头,其中就包含 Set-Cookie 头部(Session ID)
- 客户端再次访问时会将 Cookie 中的 Session ID 放在请求头中一并发送到服务器上
- 服务器从请求中提取出Session ID,并和保存的所有Session ID进行对比,找到这个用户对应的 Session
cookie和session的区别
- cookie 数据存储在客户端(只能存储String对象),session 数据存储在服务端(可以存储任意数据类型)
- cookie 可以伪造,并不是很安全;session 存储在服务器,过多会占用服务器的性能
- cookie 数据不能超过 4k,很多浏览器限制了一个站点最多保存20个cookie;session 没有限制
- 一般重要信息存储在session,其他信息可以放在cookie
cookie 被禁用了怎么办
保持登录的关键不是cookie,而是cookie保存的session id,所以还常用 HTTP 请求头来传输,但需要手动添加
session 弊端
- session 过多时会过度消耗服务器资源
- 某一时间段服务器访问量大时,会导致 session Id 失效
token
token 的认证流程与 session 很相似,无本质区别,使用token的目的是为了减轻服务器的压力
- 用户登录,成功后服务器返回Token给客户端
- 客户端收到数据后保存在客户端
- 客户端再次访问服务器,将token放入headers中,服务器端采用filter过滤器校验。校验成功则返回请求数据,校验失败则返回错误码
token与cookie的区别
-
token 比 cookie 更安全,浏览器不会自动添加到headers里,需要开发者手动添加
-
token 支持跨域访问,cookie 不支持
-
token 在服务器不需要存储 session 信息,本身就包含用户信息,只需要在客户端存储
-
不依赖cookie,不需要防范CSRF
1.5 闭包的理解
含义:
- 函数声明的时候,会生成一个独立的作用域
- 同一作用域的对象可以互相访问
- 作用域呈层级包含状态,形成作用域链,子作用域的对象可以访问父作用域的对象,反之不能;另外子作用域会使用最近的父作用域的对象
作用:
- 延伸局部变量的作用范围
- 提供有限的访问权限
GC回收机制
基本思路:确定确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间就会自动运行。
主要使用两种方式标记未使用的变量:标记清理和引用计数。
-
标记清理
当变量进入上下文(比如在函数内部声明一个变量),这个变量会被加上存在于上下文的标记。在上下文的变量,逻辑上讲永远不应该释放它的内存,因为只要代码在运行,就有可能用到。当变量离开上下文时,也会被加上离开标记。GC回收运行的时候,会标记内存中存储的所有变量(标记方式有很多种,标记过程并不重要,关键是策略)。然后,它会将所有在上下文中的变量的标记去掉。在此之后再被标记的就是待删除的,原因是任何在上下文中的变量都访问不到它们了。随后GC做一次内存清理,销毁带标记的所有值并收回它们的内存。
-
引用计数
思路是对每个值都记录它的引用次数。声明变量并给它赋一个引用值时,这个值的引用数为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参数,技术上是完全行的通的
- 请求区别
- GET在浏览器回退时是无害的,而POST会再次提交请求
- GET产生的URL地址可以被Bookmark,而POST不可以
- GET请求会被浏览器主动缓存,而POST不会,除非手动设置
- GET请求在URL中传送的参数是有长度限制(2kb),而POST没有
- GET只接受ASCII字符,而POST没有限制
- GET参数通过URL传递,POST放在Request body中
- 表单提交区别
- GET是从服务器上获取数据,POST是向服务器传送数据
- GET提交不安全,数据会附在URL后面,POST则不会
- 重大区别
- GET产生一个TCP数据包;POST产生两个TCP数据包
- GET方式的请求,浏览器会把http header和data一并发送出去
- 对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data
- GET产生一个TCP数据包;POST产生两个TCP数据包
总结: GET 与 POST 都有自己的语义,不能随便混用 。POST 请求比 GET 慢,因为 POST 要发两次包,GET 在 request body 中带数据有些服务器可能会忽略
1.7 JavaScript 的 “事件冒泡”?为什么会有"事件冒泡"?
答: 多个元素嵌套,有层次关系,这些元素都注册了相同的事件,如果里边的元素的事件触发了,外面元素的改事件也自动触发;
如果事件涉及到更新HTML节点或者添加HTML节点的时候,就会出现这样的一种情况,新更新的或者新添加的节点无法绑定事件,表现的行为是无法触发事件
比如:有一个需求,需要点击 ul
列表下的 li
标签触发事件,如果给每个 li
都绑定事件,会产生下面 2 个问题
- 当
li
数量非常大的话就会产生性能问题,甚至造成页面卡顿崩溃 - 动态新增的
li
不能绑定事件
如果用事件委托,则会很好的解决这两个问题, 用注册一个事件则能监听子节点的所有事件,所应用的就是事件的冒泡
1.8 url、href 和 src 的区别
- url代表唯一的网上资源链接或者是服务器资源链接的地址,引用资源
- href 目的不是为了引用资源,而是为了建立联系,让当前标签能够链接到目标地址
- src 指向外部资源的位置,指向的内容将会替换当前标签内容
1.9 link 标签引入和 @import 引入的区别
一、相同点
两者都是外部引用CSS的方式
二、区别
- link除了引用样式文件,还可以引用图标等资源文件,而@import只引用样式文件
- link引用样式时,在页面载入时同时加载;@import需要页面网页完全载入以后加载
- link无兼容问题;@import低版本的浏览器不支持
- link支持使用JavaScript控制DOM去改变样式;而@import不支持
1.10 BFC规范(块级格式化上下文:block formatting context)是什么?
BFC规定了内部的Block Box如何布局
定位方案:
- 内部的Box会在垂直方向上一个接一个放置
- Box垂直方向的距离由margin决定,属于同一个BFC的两个相邻Box的margin会发生重叠
- 每个元素的margin box 的左边,与包含块border box的左边相接触
- BFC的区域不会与float box重叠
- BFC是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素
- 计算BFC的高度时,浮动元素也会参与计算
满足下列条件之一就可触发BFC
- float的值不为none(默认)
- overflow的值不为visible(默认)
- display的值为inline-block、table-cell、table-caption
- position的值为absolute或fixed
1.11 从输入URL到页面展示发生了什么
- URL地址解析,判断输入的是一个合法的URL还是一个待搜索的关键词,接着发起真正的URL请求。如果浏览器本地缓存了这个资源,则直接将数据转发给浏览器进程,如果没有缓存,则进行DNS域名解析。
- 查找浏览器有没有DNS缓存(之前有访问记录),如果有则返回IP,没有则寻找本地的host文件,看有没有域名记录,如果有则返回IP,没有则直接向本地DNS服务器请求,直至返回IP。
- 建立TCP连接(三次握手)
- 第一次:客户端发送 ‘SYN’ 数据包给服务端
- 第二次:服务端收到客户端的数据包,返回 ‘SYN/ACK’ 数据包给客户端
- 第三次:客户端收到服务端的返回后,发送 ‘ACK’ 数据包给服务端
- 连接成功,发送http请求,调用后台接口,服务器开始运作起来,准备数据返回
- 服务器处理请求,返回响应结果
- 关闭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状态,完成四次挥手。
- 浏览器渲染页面
1.12 cookie 属性有哪些?
Cookie一共十个属性
- Name: Cookie名
- Value: Cookie值
- Domain: 指定了可以访问该 Cookie 的 Web 站点或域。如果设成.test.com,那么子域名a.test.com和b.test.com,都可以使用.test.com的Cookie。
- Path: 定义了Web站点上可以访问该Cookie的目录。如果设成/path/,则只有路径为/path/的页面可以访问该Cookie。如果设为/,则本域名下的所有页面都可以访问该Cookie。
- Expires / Max-Age:
Cookie的超时时间。若设置其值为一个时间,那么当到达此时间后,此Cookie失效。不设置的话默认值是Session,意思是Cookie会和Session一起失效。当浏览器关闭(不是浏览器标签页,而是整个浏览器)后,此Cookie失效。 - Size: Cookie大小。
- HttpOnly: 若此属性为true,则只有在http请求头中会带有此Cookie的信息,而不能通过document.cookie来访问此Cookie。
- Secure: 设置是否只能通过https来传递此条Cookie。
- SameSite:
用来防止 CSRF 攻击和用户追踪。
可以设置三个值:Strict、Lax 和 None。
Strict: Strict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。
Lax: Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。
None: 关闭SameSite属性,提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。 - Priority: 优先级。定义了三种优先级,Low/Medium/High,当Cookie数量超出时,低优先级的Cookie会被优先清除。
1.13 new 实例化对象过程
- 创建一个新的空对象
- 将新对象的 __proto__ 指向构造函数的prototype
- 将构造函数中this指向新对象(借助 call/apply)
- 判断构造函数的返回值
- 设置了返回值:
若返回值为引用值,则返回引用值
若返回值为原始数据,则返回新对象 - 未设置返回值:返回新对象
- 设置了返回值:
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 面向对象与面向过程的区别
- 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了
- 面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为
优缺点
面向过程:
优点是性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源。而Linux\Unix等一般采用面向过程开发,性能是最重要的因素。缺点是没有面向对象易维护,易复用,易扩展。可维护性差,不易修改。
面向对象:
优点是易维护,易复用,易扩展。由于面向对象由封装,继承,多态性的特性,可以设计出耦合度低的系统,使系统更加灵活,更加易于维护。 缺点是性能比面向过程低
1.15 JS 继承方式有哪些?
-
组合继承
通过原型链和盗用构造函数实现 -
原型式继承
-
寄生式继承
-
寄生式组合继承
-
类继承
1.16 小程序的双线程模式
渲染层和逻辑层
- WXML 模板和 WXSS 样式工作在渲染层,JS 脚本工作在逻辑层
- 渲染层的界面使用了WebView 进行渲染
- 逻辑层采用JsCore线程运行JS脚本
一个小程序存在多个界面,所以渲染层存在多个WebView线程,这两个线程的通信会经由微信客户端(下文中也会采用Native来代指微信客户端)做中转,逻辑层发送网络请求也经由Native转发
为什么小程序不采用浏览器的设计模式?
- 小程序不需要那么多原生dom的标签
- 小程序不需要JS直接操作dom,用目前比较先进的数据驱动,虚拟dom diff更新完全可以
- Web Worker由于它是子线程,在执行JS上性能远不如主线程
知道了原理我们能干什么?
- 在保证功能的前提下尽量使用结构简单的 UI(减少渲染层的工作量)
- 尽量降低 JavaScript 逻辑的复杂度(减少逻辑层的工作量)
- 尽量减少 setData 的调用频次和携带的数据体量(减少数据与事件传递时携带的载荷)
1.17 什么是原型链
原型定义:每个对象都有一个名为__proto__的属性,该属性指向另一个对象(构造函数的prototype属性),这个另一个对象就是该对象的原型。
属性查找:当访问一个对象的属性时,首先会在该对象上查找这个属性。如果没有找到,它会沿着原型向上查找,直到找到该属性或者到达顶端,这种呈链式查找称为原型链。
1.18
2.HTTP 与 HTTPS
2.1 HTTP 状态码
- 100 – 允许客户端继续在后续的请求中发送附件
- 200 – 服务端接收请求,并返回数据成功
- 201 – 请求已经被实现(创建)
- 202 – 服务端接收请求,但还未处理
- 206 – 在客户端请求的资源是部分范围内的内容时返回,例如一个大文件被分割成多个部分,而客户端只请求其中的某一部分
- 301 – 永久性重定向。请求的资源已经永久分配了新的URI ,服务端会自动将该请求重定向到新的位置
- 302 – 临时重定向,希望用户本次使用的新分配的URI
- 304 – 自从上次请求后,请求的网页未修改过
- 400 – 客户端错误,一般是参数错误
- 403 – 拒绝访问
- 404 – 请求资源不存在
- 500 – 服务端错误
- 501 – 服务器无法识别请求
- 503 – 服务器暂时无法使用
2.2 浏览器缓存机制
浏览器缓存机制有两种:
- 强缓存(默认)
浏览器访问网站后会强缓存资源,第二次访问就不会请求服务器(一般会定个时间再去请求服务器) - 协商缓存
强缓存:
通过响应头中的Cache-Control
属性判断 (优先级最高)
- private:客户端可以缓存
- public: 客户端和代理服务器都可缓存
- max-age=xxx : 缓存的内容将在 xxx 秒后失效
- no-cache:需要使用对比缓存来验证缓存数据
- no-store:所有内容都不会缓存,基本不用
协商缓存:
- 第一次请求返回给客户端数据和缓存信息,也就是一个特定的缓存标识
- 客户端把这个缓存标识放到缓存数据库
- 再次请求时,客户端把缓存标识也一起发给服务端,进行对比,判断成功后,返回304状态码,通知客户端比较成功,可以使用缓存数据
两种缓存标识
- Etag:(唯一标识)优先级更高
- Last-Modified/If-Modified-Since:返回给客户端最后这个资源的修改时间,优先级没有Etag高
协商缓存标识不生效时,状态码200,服务端返回body和header
在对比缓存标识生效时,状态码为304,并且报文大小和请求时间大大减少。原因是缓存标识生效只返回header部分,通过状态码通知客户端使用缓存,不再需要将报文主体部分返回给客户端
总结
- 强制缓存的优先级更高,如果没失效,就直接用缓存数据库里的东西
- 如果时间已经失效了,就看用的是哪种标识(Etag服务端生成的唯一标识,还是last-modified资源最后修改时间标识)返回304就用缓存里的,返回200就返回body和新的header
2.3 事件循环
所有任务可以分成两种,一种是同步任务, 另一种是异步任务
- 所有同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,还存在一个"任务队列" , 只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",结束等待状态,进入执行栈,开始执行
- 只要主线程空了,就会去读取”任务队列” , 这个过程会不断重复
宏观任务和微观任务
先执行微观任务,再执行宏观任务
宏观任务主要包含:setTimeout、setInterval、script(整体代码)
微观任务主要包括:Promise、MutaionObserver、process.nextTick(Node.js 环境)
Node 事件循环
当Node.js启动时会初始化event loop, 每一个event loop都会包含按如下六个循环阶段,nodejs事件循环和浏览器的事件循环完全不一样
阶段概览
- timers(定时器) : 此阶段执行那些由 setTimeout() 和 setInterval() 调度的回调函数.
- I/O callbacks(I/O回调) : 此阶段会执行几乎所有的回调函数, 除了 close callbacks(关闭回调) 和 那些由 timers 与 setImmediate()调度的回调.
- idle(空转), prepare : 此阶段只在内部使用
- poll(轮询) : 检索新的I/O事件; 在恰当的时候Node会阻塞在这个阶段
- check(检查) : setImmediate() 设置的回调会在此阶段被调用
- 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.0 中主要使用 header 里的 if-Modified-Since,Expires来做为缓存判断的标准;1.1 则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略
- Host头处理。1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)
- 长连接(Connection: keep-alive)。相较于1.0减少了连接和关闭的延迟,提高了效率,但是若干个请求还是需要串行排队处理,如果一旦某个请求超时,后面的就会被阻塞,也就是常说的线头阻塞。
http2 对比 1.1
- 新的二进制格式传输:二进制即0和1的组合,实现方便健壮,而1.x版本是基于文本,解析存在缺陷
- 多路复用:一个连接可以有多个请求,且可以混杂在一起根据requestid来区分不同的请求,提高了连接的利用率,降低了延迟
- header头部压缩:通讯两方各自缓存了一份 header请求头表,避免了重复的header传输,且缩小了包的体积大小
- 服务端推送功能:可以服务端主动向客户端push消息
2.5 npm run 发生了什么
以 vue 项目 npm run serve 为例
- 实际上就是执行了vue-cli-service serve 这条命令,因为操作系统中没有存在 vue-cli-service 这一条指令,所以直接执行会报错
- 在通过 npm i XXX 安装依赖的时候,例如 npm i @vue/cli-service,就会在 node_modules/.bin/ 目录中创建 好 vue-cli-service 为名的几个可执行文件
- .bin 目录下的文件,表示一个个软链接,打开文件可以看到文件顶部写着 #!/bin/sh ,表示这是一个脚本,相当于执行了 ./node_modules/.bin/vue-cli-service serve(最后的 serve 作为参数传入)
- 执行npm run xxx 的时候,就会到 node_modules/bin中找对应的映射文件,然后再找到相应的js文件来执行
3.Vue
3.1 vue的响应式原理
-
通过
Object.defineProperty
劫持所有data属性,一个属性创建一个Dep
对象 -
解析器(Compile)解析模板中的 Directive(指令),获取到哪里用到了属性(订阅者),比如{{name}} {{message}},创建一个观察者watcher添加到对应的Dep 对象中,同时初始化view,在界面上显示
-
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 的区别
- pinia【同步、异步】统一使用 action 来修改 state 数据
- pinia 没有 modules 配置,每一个独立的仓库都是 definStore 生成出来的
- 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区别
-
响应式原理不同
vue:会遍历data数据对象,使用Object.definedProperty()监听每个属性
react:通过setState()方法来更新状态,状态更新之后,组件也会重新渲染 -
监听数据变化的实现原理不同
vue:使用的是可变数据,通过 getter/setter以及一些函数的劫持,能精确知道数据变化
react:强调数据的不可变,通过比较引用的方式(diff)进行的,如果不优化可能导致大量不必要的VDOM的重新渲染 -
Diff算法不同
vue对比节点,如果节点元素类型相同,但是className不同,认为是不同类型的元素,会进行删除重建,但是react则会认为是同类型的节点,只会修改节点属性。
vue的列表比对采用的是首尾指针法,而react采用的是从左到右依次比对的方式,当一个集合只是把最后一个节点移动到了第一个,react会把前面的节点依次移动,而vue只会把最后一个节点移动到最后一个,从这点上来说vue的对比方式更加高效。 -
数据流不同
vue:组件与DOM之间可以通过v-model双向绑定
react:一直不支持双向绑定,提倡的是单向数据流 -
组合不同功能的方式不同
vue:通过mixin(侵入太强)
react:通过HoC(高阶组件) -
模板渲染方式不同
vue:在和组件JS代码分离的单独的模板中,通过指令来实现的
react:在组件JS代码中,通过原生JS实现模板中的常见语法,比如插值,条件,循环等,都是通过JS语法实现的 -
渲染过程不同
vue:可以更快地计算出Virtual DOM的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树
react:应用的状态被改变时,全部子组件都会重新渲染
3.6 computed 缓存原理
-
响应式
读取 computed 时会触发 get, 设置时会触发 set -
如何控制
某个计算属性C依赖 data 中的 A,每次读取C,C就会去读取A,从而触发A的 get,如果没有缓存多次触发是很消耗性能的;
脏数据标记:dirty,是 watcher 的属性- dirty 是 true 时,读取 computed 时重新计算
- dirty 是 fasle 时,读取 computed 时使用缓存
-
依赖的data发生改变,computed 如何更新
- C 依赖 A,所以A可以收集到C的watcher
- A 发生改变,会把 watcher 的 dirty 设置为 true
- A 发生改变,会通知页面进行更新,页面重新读取计算属性C,此时 dirty 是 true,所以重新计算
- computed 更新完后,会把 dirty 设置为 false
4.React
4.1 setState 为什么是异步的
- 提升性能
- 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的
- 最好的办法应该是获取到多个更新,之后进行批量更新
- 如果同步更新了state,但是还没有执行 render 函数,那么 state 和 props 不能保持同步
setState 在组件生命周期或React合成事件中更新数据是异步的,在setTimeout或者原生dom事件中更新数据是同步的。原因是返回了不同的值做更新判断,同步返回 Sync,批量处理返回 Batch
4.2 setState 的数据为什么要保证原数据不可变性
跟 shouldComponentUpdate 更新界面有关,内部进行的是浅层比较,如果直接在原数据上修改引用型数据类型,比较的时候内存地址是一样的导致不会更新视图,但实际上数据又发生了变化
4.3 React的更新流程
- 同层节点之间相互比较,不会垮节点比较;
- 不同类型的节点,产生不同的树结构;
- 开发中,可以通过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代码进行更加优雅的处理。
- 早期的React有提供组件之间的一种复用方式是mixin,目前已经不再建议使用:
- Mixin 可能会相互依赖,相互耦合,不利于代码维护
- 不同的Mixin中的方法可能会相互冲突
- Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性
- 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 = ''
}
}
优点:
- 兼容性好,老旧浏览器支持,不会发起新的HTTP请求,可以避免跨域问题
- 简单易用,无需服务器配置
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
优势:
-
pushState 设置的 url 可以是同源下的任意 url ;而 hash 只能修改 # 后面的部分,因此只能设置当前 url 同文档的 url
-
pushState 设置的新的 url 可以与当前 url 一样,这样也会把记录添加到栈中;hash 设置的新值不能与原来的一样,一样的值不会触发动作将记录添加到栈中
-
pushState 通过 stateObject 参数可以将任何数据类型添加到记录中;hash 只能添加短字符串
-
pushState 可以设置额外的 title 属性供后续使用
劣势:
-
history 在刷新页面时,如果服务器中没有相应的响应或资源,就会出现404。因此,如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面
-
hash 模式下,仅 # 之前的内容包含在 http 请求中。对后端来说,即使没有对路由做到全面覆盖,也不会报 404
4.7 react@16.4 + 的生命周期
- 挂载阶段:constructor、render、componentDidMount
- 更新阶段:componentDidUpdate
- 卸载阶段:componentWillUnmount
4.8 useEffect 和 componentDidMount 有什么差异?
useEffect 会捕获 props 和 state。所以即便在回调函数里,你拿到的还是初始的 props 和 state。如果想得到“最新”的值,可以使用 ref。
4.9 调用 setState 之后发生了什么?
- 为当前节点创建一个 updateQueue 的更新列队。
- 然后会触发 reconciliation 过程,在这个过程中,会使用名为 Fiber 的调度算法,开始生成新的 Fiber 树, Fiber 算法的最大特点是可以做到异步可中断的执行。
- 然后 React Scheduler 会根据优先级高低,先执行优先级高的节点,具体是执行 doWork 方法。
- 在 doWork 方法中,React 会执行一遍 updateQueue 中的方法,以获得新的节点。然后对比新旧节点,为老节点打上 更新、插入、替换 等 Tag。
- 当前节点 doWork 完成后,会执行 performUnitOfWork 方法获得新节点,然后再重复上面的过程。
- 当所有节点都 doWork 完成后,会触发 commitRoot 方法,React 进入 commit 阶段。
- 在 commit 阶段中,React 会根据前面为各个节点打的 Tag,一次性更新整个 dom 元素
4.10 React有哪些优化性能的手段?
- 使用· memo、PureComponent 包裹组件,优化组件渲染
- 使用 Suspense、lazy 按需加载组件
- 批量更新
- 按优先级更新,及时响应用户
- 利用debounce、throttle 避免重复回调
- 使用 useMemo、useCallback 缓存,稳定 props 值
- 发布者订阅者跳过中间组件 Render 过程
- 状态下放,缩小状态影响范围
- 列表项使用 key 属性
- 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架构
- server端无需等待所有数据(和HTML)都ready再响应客户端,相反地,当应用骨架准备好时你就可以发送给客户端显示,剩余的内容将在它们ready之后流式传输给客户端。
- 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的区别
- 一个重要区别是setTimeout是「延迟」执行,startTransition是立即执行的,传递给startTransition的函数是同步运行,但是其内部的所有更新都会标记为非紧急,React将在稍后处理更新时决定如何render这些updates,这意味着将会比setTimeout中的更新更早地被render。
- 另一个重要区别是用setTimeout包裹的如果是内大面积的更新操作会导致页面阻塞不可交互,直到超时。这时候用户的输入、键盘按下等紧急更新操作将被阻止。而startTransition则不同,由于它所标记的更新都是可中断的,所以不会阻塞UI交互。即使用户输入发生变化,React也不必继续渲染用户不再感兴趣的内容。
- 最后,因为setTimeout是异步执行,哪怕只是展示一个小小的loading也要编写异步代码。而通过transitions,React可以使用hook来追踪transition的执行状态,根据transition的当前状态来更新loading。
4.12 为何要在componentDidMount里面发送请求
- 跟服务器端渲染(同构)有关系,如果在componentWillMount里面获取数据,fetch data会执行两次,一次在服务器端一次在客户端
- 在componentWillMount中fetch data,数据一定在render后才能到达,如果忘记了设置初始状态,用户体验不好
- react16.0以后,componentWillMount可能会被执行多次
5.webpack
5.1 webpack中什么是chunk?什么是bundle?
- 首先告诉 Webpack 一个入口文件,如 index.js 为起点作为打包,将入口文件的所有依赖项引入进来,这些依赖会跟入口文件形成一个文件(代码块),这个文件(代码块)就是 chunk
- 将这个代码块(chunk)进行处理,比如把 less 文件编译成 css,js 资源编译成浏览器能识别的 js 语法等等操作,这些就叫做打包,将打包好的资源再输出出去,这个输出的文件就叫 bundle
5.2 Webpack 五个核心概念分别是什么?
- Entry: 入口(Entry)指示 Webpack 以哪个文件为入口起点开始打包,分析内部构件依赖图
- Output: 输出(Output)指示 Webpack 打包后的资源 bundles 输出到哪里去,以及如何命名
- Loader: Loader 能让 Webpack 处理非 JavaScript/json 文件(Webpack 自身只能处理 JavaScript/json )
- Plugins: 插件(Plugins)可以用于执行范围更广的任务,包括从打包优化和压缩到重新定义环境中的变量
- Mode: 模式(Mode)指示 Webpack 使用相应模式的配置,只有development(开发环境)和production(生产环境)两种模式
5.3 有哪些常见的Loader?它们是解决什么问题的?
- css-loader:将 css 文件变成 CommonJS 模块加载 js 中,里面内容是样式字符串
- style-loader:创建 style 标签,将 js 中的样式资源插入进行,添加到 head 中生效
- url-loader:在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
- ile-loader:打包其他资源(除了css/js/html 资源)
- html-loader:处理 html 文件中的 img
- babel-loader:把 ES6 转换成 ES5
- eslint-loader:通过 ESLint 检查 JavaScript 代码
5.4 有哪些常见的Plugin?它们是解决什么问题的?
- html-webpack-plugin:可以复制一个有结构的html文件,并自动引入打包输出的所有资源(JS/CSS)
- clean-webpack-plugin:重新打包自动清空 dist 目录
- mini-css-extract-plugin:提取 js 中的 css 成单独文件
- optimize-css-assets-webpack-plugin:压缩css
- uglifyjs-webpack-plugin:压缩js
- commons-chunk-plugin:提取公共代码
5.5 webpack 构建流程
- 根据 entry 配置项找出所有的入口文件
- 从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块
- Loader 翻译完所有模块后,根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表
- 确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
5.6 webpack的热更新是什么?
优化打包构建速度,一个模块发生变化,只会重新打包这一个模块(而不是打包所有模块),极大提升构建速度
- 样式文件:可以使用HMR功能,因为style-loader内部实现了
- JS文件:默认没有HMR功能,需要修改js代码,添加支持HMR功能。入口文件做不了HMR功能,只能处理非入口js文件
- HTML文件:默认没有HMR功能,同时会导致 html 文件不能热更新(即修改没有任何反应)
HTML文件不用做HMR功能,因为只有一个html文件
5.7 webpack优化?
开发环境下:
- 开启HMR功能,优化打包构建速度
- 配置 devtool: ‘source-map’,优化代码运行的性能
生产环境下:
- oneOf 优化: 默认情况下,假设设置了7、8个loader,每一个文件都得通过这7、8个loader处理(过一遍),浪费性能,使用 oneOf 找到了就能直接用,提升性能
- 开启 babel 缓存: 当一个 js 文件发生变化时,其它 js 资源不用变
- code split 分割: 将js文件打包分割成多个bundle,避免体积过大
- 懒加载和预加载
- PWA 网站离线访问
- 多进程打包: 开启多进程打包,主要处理js文件(babel-loader干的活久),进程启动大概为600ms,只有工作消耗时间比较长,才需要多进程打包,提升打包速度
- dll 打包第三方库
- code split将第三方库都打包成一个bundle,这样体积过大,会造成打包速度慢
- 是将第三方库打包成多个bundle,从而进行速度优化
5.8 hash、chunkhash、contenthash三者的区别?
浏览器访问网站后会强缓存资源,第二次刷新就不会请求服务器(一般会定个时间再去请求服务器),假设有了bug改动了文件,但是浏览器又不能及时请求服务器,所以就用到了文件资源缓存(改变文件名的hash值)
- hash:不管文件变不变化,每次wepack构建时都会生成一个唯一的hash值
- chunkhash:根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样
问题:js和css同属于一个chunk,修改css,js文件同样会被打包 - contenthash:根据文件的内容生成hash值。不同文件hash值一定不一样
5.9 commonJS和ES6模块化的区别
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
- CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载
- 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的优势
- 提早发现代码中的Bug
- 提高代码的可读性
- 减少了复杂的错误处理逻辑
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 的区别
相同点:
- 都可以用来定义对象或函数的形状
- 都支持继承,并且可以互相继承
不同点:
- type 可以定义基本类型
- type 可以声明联合类型
- interface 可以生命合并
6.4 never,void,unknown 类型
- never
- 一个从来不会有返回值的函数,即死循环(如:如果函数内含有 while(true) {})
- 一个总是会抛出错误的函数(如:function foo() { throw new Error(‘Not Implemented’) },foo 的返回类型是 never)
- void
- 表示没有任何返回值的函数
- 也可以声明一个变量为 void ,但只能将它赋值为 undefined 或 null
- 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
要求:
- 最多同时执行的任务数为10个
- 当前任务执行完成后,释放队列空间,自动执行下一个任务
- 所有任务添加到任务队列后,自动开始执行任务
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)
}