写在前面:本文记录自己的面试经历,用于巩固和复盘。
计算机网络篇
了解https协议吗?
超文本传输安全协议(Hypertext Transfer Protocol Secure,简称:HTTPS)是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,利用SSL/TLS来加密数据包。HTTPS的主要目的是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。
HTTP协议采用明文传输信息,存在信息窃听、信息篡改和信息劫持的风险,而协议TLS/SSL具有身份验证、信息加密和完整性校验的功能,可以避免此类问题发生。
安全层的主要职责就是对发起的HTTP请求的数据进行加密操作和对接收到的HTTP的内容进行解密操作。
https的证书是怎么提供的?
https证书也就是SSL证书,网站通过申请SSL证书将http协议升级为https加密协议,搭建加密传输、身份认证的网络安全通道。
- 选择合适的https安全证书
graph TD
选择SSL证书 --> 保护一个域名 & 保护多个不相关域名 & 保护一个主域名和其子域名
保护一个域名-->单域名SSL证书
保护多个不相关域名 --> 域名不是同一个公司所有 & 域名是同一个公司所有
域名不是同一个公司所有-->OV/EV验证 & DV验证
OV/EV验证 --> 购买多个 --> 单域名SSL证书
DV验证 --> 多域名SSL证书
域名是同一个公司所有 --> 多域名SSL证书
保护一个主域名和其子域名--> 多域名SSL证书 & 通配符证书
- 成功购买合适的证书后,进行以下步骤:
- 生成证书请求文件CSR:生成CSR证书请求文件,系统产生一个公钥(即该CSR文件)和一个私钥(存放在服务器上)。
- 将CSR提交给CA机构认证
- 获取https证书并安装:在收到CA机构签发的https证书后,将https证书部署到服务器上。
- 安装SSL证书
cookie会不会有哪些安全性问题?如果有的话,有哪些有效手段可以保护?
Cookie是一段小小的文本数据(通常单个Cookie的最大长度为4KB),在用户完成“身份认证或会话状态更新”后,它会被服务端放在HTTP响应包的Set-Cookie中,并发送给客户端进行保存。
从功能上来讲:
-
Cookie可用于跟踪用户的会话
SessionID:用户的会话状态信息Session会被存放在服务器,服务端可以根据HTTP请求中Cookie所存储的SessionID来匹配用户当前的会话状态。
-
Cookie可用于记录用户的偏好(下次访问时网站会自动应用这些偏好)
用户的偏好语言、偏好主题等
从安全的角度:
-
Cookie往往会存有服务端在发送Cookie时设定的安全标记
Secure、expires、Domain
Cookie的安全性问题:
-
Cookie窃取:攻击者劫持到用户的Cookie本身
想要维持登陆状态的话,大多数情况下攻击者只需要拿到Cookie中的SessionID
利用XSS注入恶意脚本来收集Cookie、通过网络嗅探和拦截来窃取Cookie
-
Cookie欺骗:篡改或伪造Cookie
-
篡改:
利用XSS注入恶意脚本来修改Cookie
利用CSRF伪造包含恶意Cookie的请求
-
伪造:
查看合法Cookie的结构,并尝试模仿伪造
假如攻击者知道某Web站点的Cookie生成算法,那么他可能会尝试去构造出有效的Cookie
-
-
注入
-
CRLF攻击
假如用户能够控制服务端HTTP响应的Set-Cookie字段的值,且服务端未对即将写入Cookie的值进行恰当的过滤和转义,那么攻击者很有可能通过注入CRLF(回车换行)来截断并控制HTTP响应包的内容
-
SQL注入
同样地,如果服务端没有对Cookie值进行恰当的过滤,那么攻击者很有可能将此作为注入点。
-
Cookie的保护手段:
服务器发送Cookie方面:
-
限制敏感信息写入Cookie:务必不要存储“登录ID、密码”等敏感信息,而是应该在服务端存储用户的会话状态Session,并用对应的SeesionID来替代上述的敏感信息。
SessionID可被设置过期时间,攻击者将更难维持持久的登录状态
SessionID实际不包含有意义的敏感信息,在诸如“需要原密码的密码重置”等场景,攻击者便难以简单地施展攻击。
-
在向Set-Cookie中写入内容前要进行过滤,过滤特殊字符,防止针对Cookie的CRLF攻击
-
做Cookie的访问控制:在HTTP响应的Set-Cookie字段中添加Secure属性,以要求Cookie仅在HTTPS下才能传输
使用HTTPS可以大大提高Cookie在传输过程中的安全性,包括完整性、保密性等。
-
限制Cookie的存储:在HTTP响应的Set-Cookie中添加expires和max-age属性,设置过期时间:
用expires指定绝对时间、用max-age指定存活时间(s)
服务器处理Cookie方面:
-
对收到的Cookie进行安全检查
检查Cookie的安全标记:HttpOnly、Secure、过期时间expires/max-age、可用域Domain、可用路径Path
过滤Cookie中的字符,防范隐藏在Cookie中的SQL注入Payload等
-
强化Cookie的完整性校验和对用户的身份鉴别
完整性校验:使用数字签名+散列(哈希)函数
1.服务端在发送Cookie时,对Cookie进行哈希,得到一个哈希值,并使用私钥对该哈希值进行数字签名,最后将Cookie和签名值一并发送。
2.客户端收到后立马对Cookie进行哈希,得到一个哈希值;并使用服务端提供的公钥对签名值进行特殊运算,如果一切正常也将得到一个哈希值,将两个哈希值进行对比,便可知道Cookie是否被篡改。
强化对用户的身份鉴别:使用多因子验证
有没有了解XSS安全性问题
XSS攻击是跨站脚本攻击,是一种代码注入攻击。攻击者通过网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户信息(如Cookie等)。本质是因为网站没有对恶意代码进行过滤,与正常代码混在一起,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。
XSS攻击分为三种:存储型、反射型和DOM型
-
存储型攻击(持久型):恶意脚本会存储在目标服务器,当浏览器请求数据时,网站服务端会将恶意代码从数据库取出,拼接在HTML中返回给浏览器,浏览器收到响应解析执行时混在其中的恶意代码也会被执行。恶意代码窃取用户数据并发送到攻击者网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。
-
反射型攻击:攻击者诱导用户访问一个带有恶意代码的URL后,服务器端将恶意代码从URL中取出,拼接在HTML中返回给浏览器,浏览器接收到响应后解析执行,混在其中的恶意代码也会被执行。恶意代码窃取用户数据并将其发送到攻击者网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
-
DOM型攻击:攻击者构造出带有恶意代码的URL,用户打开带有恶意代码的URL。用户浏览器接收到响应后解析执行,前端JS取出URL中的恶意代码并执行。恶意代码窃取用户数据并将其发送到攻击者网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
DOM型跟前两种的区别:DOM型XSS攻击中,取出和执行恶意代码由浏览器端完整,属于前端JS自身的安全漏洞,而其他两种属于服务端的安全漏洞。
XSS攻击的防御:
-
从浏览器的执行来进行预防,一种是使用纯前端的方式不用服务端渲染;另一种是对需要插入到HTML中的代码做好充分的转义。对于DOM型攻击主要是前端脚本的不可靠造成的,对于数据获取渲染和字符串拼接的时候应该对可能出现的恶意代码情况进行判断。
-
使用CSP内容安全策略,本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行,从而防止恶意代码的注入攻击。通常有两种方式开启CSP:一种是设置HTTP首部中的Content-Security-Policy,一种是设置meta标签。
-
对于一些敏感信息进行保护。比如Cookie使用http-only,使得脚本无法获取;也可以使用验证码避免脚本伪装成用户执行一些操作。
当设置了http-only属性的Cookie,浏览器将只允许通过HTTP或HTTPS协议来访问该Cookie,而禁止通过JavaScript访问
用vue框架的话会有XSS问题吗
Vue框架本身并不会引起XSS(跨站脚本攻击)问题。然而,XSS问题通常是由于开发者在编写前端代码时未正确处理用户输入而引起的。
在使用Vue框架时,如果开发者不注意对用户输入进行适当的过滤和转义,就有可能造成XSS漏洞。例如,如果直接将用户输入的数据插入到DOM中而不进行过滤,恶意用户就可能通过注入恶意的HTML或JavaScript代码来攻击用户。
为了防止XSS攻击,开发者应该始终对用户输入进行严格的过滤和转义,确保在插入到DOM中之前,所有的用户输入都被当做纯文本处理。Vue框架提供了一些内置的指令和方法来帮助开发者进行XSS防护,例如使用v-html指令时要谨慎,应该尽量避免直接将用户输入的HTML渲染到页面上。
XSS攻击不小心被触发了,可能会导致我们哪一些数据被攻击被盗取呢?恶意代码会干什么?
- 窃取用户Cookie
- 获取键盘记录
- 获取用户信息等
- 后台增删改文章
- XSS钓鱼攻击
- 利用XSS漏洞进行传播和修改网页代码
- XSS蠕虫攻击
- 网站重定向
一旦攻击者成功修改了页面内容,他们可以在受害者的页面上插入包含恶意链接的元素,如按钮、图片或超链接。当用户点击这些链接时,他们可能会被重定向到恶意站点,或者触发进一步的攻击行为。
CSRF是什么?讲一下原理
CSRF攻击指的是跨站请求伪造攻击。攻击者诱导用户进入第三方网站,该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登陆状态,那么攻击者就可以利用这个登陆状态,绕过后台的用户验证,冒充用户向服务器执行一些操作。
本质是利用Cookie会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。
常见的CSRF攻击分为三种:GET类型、POST类型和链接类型
- GET类型:比如在网站中的一个img标签里构建一个请求,当用户打开这个网站时就会自动发起提交。
- POST类型:比如构建一个表单,然后隐藏它,当用户进入页面时,自动提交这个表单。
- 链接类型:比如在a标签的href属性中构建一个请求,然后诱导用户去点击。
链接类型的CSRF攻击和XSS攻击有什么区别
- 目标不同:CSRF 攻击的目标是利用受害者的身份在受信任的网站上执行未经授权的操作。XSS 攻击的目标是向网页中注入恶意的客户端脚本,使其在受害者浏览器中执行。
- 实现方式不同:
- CSRF攻击:攻击者通过诱使受害者访问特定的恶意网站或者点击包含恶意代码的链接,利用受害者在已登录的受信任网站上的会话凭证(如 Cookie),在不知情的情况下执行一些操作,例如发起转账、更改密码等。
- XSS攻击:攻击者通过向受信任的网站提交恶意输入,比如在表单字段中输入恶意脚本,然后当受害者访问包含恶意脚本的页面时,恶意脚本会在受害者浏览器中执行,从而实现对用户的攻击。
CSRF攻击的防御
-
进行同源检测
服务器根据http请求头中的origin或者referer信息来判断请求是否为允许访问的站点,从而对请求进行过滤。当origin或referer信息都不存在时,直接阻止请求。
缺点:有些情况下referer可以被伪造,同时还会把搜索引擎的链接也屏蔽了。所以一般网站会允许搜索引擎的页面请求,但相应的页面请求的这种请求方式也可能被攻击者利用。(referer字段告诉服务器该网站从哪个页面链接过来)
-
使用CSRF Token进行验证
服务器向用户返回一个随机数Token,当网站再次发起请求时,在请求参数中加入服务端返回的token,然后服务器对这个token进行验证。解决了使用cookie单一验证方式时,可能会被冒用的问题。
缺点:需要给网站中的所有请求都添加上这个token,操作比较繁琐。如果请求经过负载平衡转移到了其他服务器,但这个服务器的session中没有保留这个token的话,就没有办法验证了。这种时候可以采用:①可以使用JWT(JSON Web Token)或类似的技术生成Token,并使用服务器共享的密钥进行签名和验证。②将Token存储在分布式存储中,例如Redis、Memcached等。这样所有的服务器都可以访问到相同的Token数据,从而在不同的服务器之间共享Token信息。③将Token存储在共享数据库中,所有的服务器都连接到同一个数据库实例,可以读取和更新Token信息。④可以使用单独的认证服务来生成和验证Token,所有的服务器都与这个认证服务进行通信,从而确保Token的一致性和有效性。
-
对Cookie进行双重验证
服务器在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串,然后当用户再次向服务器发送请求时,从cookie中取出这个字符串,添加到url参数中,然后服务器对cookie中的数据和参数中的数据进行比较,来进行验证。该方法比CSRF Token更加房补,而且不涉及到分布式访问的问题。
缺点:如果网站存在XSS漏洞,这种方式会失效,且这种方式不能做到子域名的隔离。
-
在设置Cookie属性的时候设置Samesite,限制Cookie不能被第三方使用
Samesite有两种模式:严格模式和宽松模式。严格模式下cookie在任何情况下都不可能作为第三方Cookie使用;在宽松模式下cookie可以被请求是GET请求、且会发生页面跳转的请求所使用。
GET请求是一种幂等的操作,通常用于获取资源的请求,而且它不会对服务器上的数据产生任何影响。因此,当浏览器在处理GET请求时,一般不会触发对第三方Cookie的发送,以避免潜在的安全风险。
而POST请求通常用于向服务器提交数据或进行操作,例如表单提交等。由于POST请求可能会对服务器上的数据进行修改或添加,因此浏览器会更加谨慎地处理与POST请求相关的Cookie,通常不会在跨域情况下发送第三方Cookie,以防止CSRF等安全问题。
因此,Samesite属性对GET请求和页面跳转的请求进行了特别的考虑,因为这些请求通常是浏览器自动触发的,可能存在被恶意利用的风险。
通过CSRF形式发起的请求,对方能拿到请求的结果吗?
CSRF攻击者只能利用Cookie,不能访问获取Cookie。
实时协作的过程中前端和后端是用什么协议在沟通?
WebSocket是基于TCP的一种新的应用层网络协议,提供了一个全双工的通道,允许服务器和客户端之间实时双向通信。因此在WebSocket中,浏览器和服务器只需完成一次握手,两者之间就直接可以创建持久性的链接,并进行双向数据传输,客户端和服务器之间的数据交换就变得更加简单。
与HTTP协议相比,WebSocket具有以下优点:
- 更高的实时性能:WebSocket允许服务器和客户端之间实时双向通信,从而提高了实时通信场景中的性能。
- 更高的网络开销:HTTP请求和响应之间需要额外的数据传输,而WebSocket通过在同一个连接上双向通信,减少了网络开销。
- 更灵活的通信方式:HTTP请求和响应通常是一一对应的,而WebSocket允许服务器和客户端之间以多种方式进行通信,例如消息Push、时间推送等。
- 更简洁的API:WebSocket提供了简洁的API,使得客户端开发人员可以更轻松地进行实时通信。
缺点:
- 不支持无连接:websocket的连接不会再一次请求之后立即断开,消除了建立连接的开销,但也可能导致资源泄露的问题
- 不支持广泛:websocket是HTML5中的一种标准协议,一些旧的浏览器可能不支持
- 需要特殊的服务器支持:只有特定的服务器才能够实现Websocket协议,可能会增加系统的复杂性和部署的难度
- 数据流不兼容:websocket的数据流格式与HTTP不同,在不同网络环境下表现可能会有所不同
WebSocket工作原理:
- 握手阶段
- 客户端向服务器发送请求,请求建立WebSocket连接。请求中包含一个Sec-WebSocket-Key参数,用于生成WebSocket的随机密钥。
- 服务端接收到请求后,生成一个随机密钥,并使用随机密钥生成一个新的Sec-WebSocket-Accept参数。
- 客户端接收到服务端发送的新的Sec-WebSocket-Accept参数Sec-WebSocket-Key参数后,使用原来的随机密钥和新的Sec-WebSocket-Accept参数共同生成一个新的Sec-WebSocket-Key参数,用于加密数据传输。
- 客户端将新的Sec-WebSocket-Key参数发送给服务端,服务端收到后,使用该参数加密数据传输。
- 数据传输阶段
建立连接后,客户端和服务端就可以通过WebSocket进行实时双向通信。数据传输阶段包括以下几个步骤:
- 客户端向服务端发送数据,服务端收到数据后将其转发给其他客户端;
- 服务端向客户端发送数据,客户端收到数据后进行处理。
- 关闭阶段
- 客户端向服务端发送关闭请求,请求中包含一个WebSocket的随机密钥
- 服务端接收到关闭请求后,向客户端发送关闭响应,关闭响应中包含服务端生成的随机密钥
- 客户端收到关闭响应后,关闭WebSocket连接
WebSocket对象的属性和方法
WebSocket对象:WebSocket对象表示一个新的WebSocket连接WebSocket.onopen事件处理程序:当WebSocket连接打开时触发WebSocket.onmessage事件处理程序:当接收到来自WebSocket的消息时触发WebSocket.onerror事件处理程序:当WebSocket发生错误时触发WebSocket.onclose事件处理程序:当WebSocket连接关闭时触发WebSocket.send方法:向WebSocket发送数据WebSocket.close方法:关闭WebSocket连接
实时协作时怎么合并多个人的操作?
在使用 WebSocket 进行实时协作时,合并多个人的操作通常采用以下方法:
- 操作队列: 每个客户端在本地维护一个操作队列,将用户的操作按顺序加入队列中。
- 操作序列化: 将用户的操作序列化成特定的格式,例如 JSON 格式,然后通过 WebSocket 发送给服务器。
- 服务器端处理: 服务器端接收到客户端的操作后,按照顺序将操作应用到共享的数据结构中。在这一步,可以使用合适的算法来处理冲突、合并操作。
- 冲突解决: 当多个用户同时对相同数据进行操作时,可能会发生冲突。服务器端可以采用各种算法来解决冲突,例如 OT(操作转换)、CRDT(可复制数据类型)等。这些算法能够自动解决或者标识冲突,并保证数据的一致性。
- 数据同步: 服务器处理完用户的操作后,将最新的数据状态广播给所有客户端,以确保所有用户看到的数据都是同步的。
- 实时更新: 客户端接收到服务器发送的数据更新后,更新本地的界面展示,以实现实时协作的效果。
通过以上方法,可以实现在实时协作场景中合并多个人的操作,保证数据的一致性和协同工作的顺畅进行。
网络协议有哪些?七层协议
七层协议是指OSI(Open Systems Interconnection)参考模型中的七层网络协议模型,也称为OSI模型。它将计算机网络通信的整个过程分为七个层次,每个层次都有自己的功能和责任,便于网络通信的设计、理解和实现。
-
应用层
参考模型中最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网 络服务。
-
表示层
表示层提供各种用于应用层数据的编码和转换功能 , 确保一个系统的应用层发送的数据能被另 一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种 数据格式转换成通信中采用的标准表示形式。数据压缩和加密也是表示层可提供的转换功能之 一。
在项目开发中,为了方便数据传输,可以使用 base64 对数据进行编解码。如果按功能来划 分, base64 应该是工作在表示层。
-
会话层
会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应 用程序之间的服务请求和响应组成。
-
传输层
传输层建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据 传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使 高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的 数据通路。我们通常说的, TCPUDP 就是在这一层。端口号既是这里的“端”。
-
网络层
本层通过 IP 寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由 和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的 IP 层。这一层就是 我们经常说的 IP 协议层。 IP 协议是 Internet 的基础。我们可以这样理解,网络层规定了 数据包的传输路线,而传输层则规定了数据包的传输方式。
-
数据链路层
将比特组合成字节 , 再将字节组合成帧 , 使用链路层地址 (以太网使用 MAC 地址)来访问介质 , 并 进行差错检测。 网络层与数据链路层的对比,通过上面的描述,我们或许可以这样理解,网络 层是规划了数据包的传输路线,而数据链路层就是传输路线。不过,在数据链路层上还增加了 差错控制的功能。
-
物理层
实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电 缆针脚。常用设备有(各种物理设备)集线器、中继器、调制解调器、网线、双绞线、同轴电 缆。这些都是物理层的传输介质。
OSI七层模型通信特点:对等通信对等通信,为了使数据分组从源传送到目的地,源端 OSI 模 型的每一层都必须与目的端的对等层进行通信,这种通信方式称为对等层通信。在每一层通信 过程中,使用本层自己协议进行通信。
对tcp/ip协议有了解吗?
TCP/IP 为网际协议群,是五层协议模型。
- 应用层:直接为应用进程提供服务。应用层协议定义的是应用进程间通讯和交互的规则,不同的应用有着不同的应用层协议,如HTTP协议(万维网服务)、FTP协议(文件传输)、SMTP协议(电子邮件)、DNS(域名查询)等。
- 传输层:负责为两台主机中的进程提供通信服务。有传输控制协议TCP和用户数据报协议UDP。
- 网络层(网际层):负责为两台主机提供通信服务,并通过选择合适的路由将数据传递到目标主机。有IP、ICMP、RIP、IGMP
- 数据链路层:负责将网络层交下来的IP数据报封装成帧,并在链路的两个相邻节点间传送帧,每一帧都包含数据和必要控制信息(如同步信息、地址信息、差错控制等)。
- 物理层:确保数据可以在各种物理媒介上进行传输,为数据的传输提供可靠的环境。
讲一讲TCP三次握手、四次挥手
三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。
三次握手:
- 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN,此时客户端处于 SYN_SEND 状态。
首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
- 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。
在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y
- 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。
确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。
简洁版:
- 第一次握手: 客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。
- 第二次握手: 服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。
- 第三次握手: 当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。
四次挥手:
刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:
- 第一次挥手: 客户端会发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。
即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
- 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。
即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
- 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
- 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。
简洁版:
- 第一次挥手: 若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。
- 第二次挥手:服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。
- 第三次挥手:服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。
- 第四次挥手: 客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。
为什么TCP是三次握手和四次挥手
三次握手的原因: TCP 三次握手的建立连接的过程就是相互确认初始序号的过程,告诉对方,什么样序号的报文段能够被正确接收。 第三次握手的作用是客户端对服务器端的初始序号的确认。如果只使用两次握手,那么服务器就没有办法知道自己的序号是否 已被确认。同时这样也是为了防止失效的请求报文段被服务器接收,而出现错误的情况。
如果是用两次握手,则会出现下面这种情况:
如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。
四次挥手的原因:
TCP 使用四次挥手的原因是因为 TCP 的连接是全双工的,所以需要双方分别释放到对方的连接,单独一方的连接释放,只代表不能再向对方发送数据,连接处于的是半释放的状态。
最后一次挥手中,客户端会等待一段时间再关闭的原因,是为了防止发送给服务器的确认报文段丢失或者出错,从而导致服务器 端不能正常关闭。
你对UDP协议有什么了解?
UDP的全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。
它的特点如下:
- 面向无连接
首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。
具体来说就是:
- 在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
- 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作
- 有单播,多播,广播的功能
UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。
- 面向报文
发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文
- 不可靠性
- 首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。
- 并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。
- 再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。
- 头部开销小,传输数据报文时是很高效的。
UDP 头部包含了以下几个数据:
- 两个十六位的端口号,分别为源端口(可选字段)和目标端口
- 整个数据报文的长度
- 整个数据报文的检验和(IPv4 可选字段),该字段用于发现头部信息和数据中的错误
因此 UDP 的头部开销小,只有8字节,相比 TCP 的至少20字节要少得多,在传输数据报文时是很高效的。
TCP和UDP的明显区别是哪里
| UDP | TCP | |
|---|---|---|
| 是否连接 | 无连接 | 面向连接 |
| 是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输(数据顺序和正确性),使用流量控制和拥塞控制 |
| 连接对象个数 | 支持一对一,一对多,多对一和多对多交互通信 | 只能是一对一通信 |
| 传输方式 | 面向报文 | 面向字节流 |
| 首部开销 | 首部开销小,仅8字节 | 首部最小20字节,最大60字节 |
| 适用场景 | 适用于实时应用,例如视频会议、直播 | 适用于要求可靠传输的应用,例如文件传输 |
http1.x、http2、websocket分别是使用什么协议
HTTP/1.x: 使用的是纯粹的文本协议,请求和响应都是明文传输。它是经典的 HTTP 协议版本,是基于请求-响应模式的协议。每个 HTTP 请求都需要建立一个新的 TCP 连接,并且每个请求都会独立完成,无法复用连接。
HTTP/2: 与 HTTP/1.x 相比,HTTP/2 引入了多路复用、头部压缩、服务器推送等新特性,以提高性能和效率。它仍然使用 TCP 作为传输层协议,但在 TCP 连接上实现了多路复用,可以在一个连接上并行发送多个请求和响应。
WebSocket: WebSocket 是一种双向通信协议,允许客户端和服务器之间建立持久的连接,并通过该连接进行全双工的数据传输。它也是基于 TCP 的协议,但在 HTTP 握手之后,通过切换协议(Upgrade)从 HTTP 协议升级到 WebSocket 协议。
http2相对于http1.x,升级点有哪些
- 二进制协议:HTTP2 是一个二进制协议。在 HTTP1.1 版中,报文的头信息必须是文本(ASCII 编码),数据体可以是文本,也可以是二进制。HTTP2 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为"帧",可以分为头信息帧和数据帧。 帧的概念是它实现多路复用的基础。
- 多路复用: HTTP2 实现了多路复用,HTTP2 仍然复用 TCP 连接,但是在一个连接里,客户端和服务器都可以同时发送多个请求或回应,而且不用按照顺序一一发送,这样就避免了"队头堵塞"的问题。
- 数据流: HTTP2 使用了数据流的概念,因为 HTTP2 的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的请求。因此,必须要对数据包做标记,指出它属于哪个请求。HTTP2 将每个请求或回应的所有数据包,称为一个数据流。每个数据流都有一个独一无二的编号。数据包发送时,都必须标记数据流ID,用来区分它属于哪个数据流。
- 头信息压缩: HTTP2 实现了头信息压缩,由于 HTTP1.1 协议不带状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 Cookie 和 User Agent ,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。HTTP2 对这一点做了优化,引入了头信息压缩机制。一方面,头信息使用 gzip 或 compress 压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就能提高速度了。
- 服务器推送: HTTP2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送。使用服务器推送提前给客户端推送必要的资源,这样就可以相对减少一些延迟时间。这里需要注意的是 http2 下服务器主动推送的是静态资源,和 WebSocket 以及使用 SSE 等方式向客户端发送即时数据的推送是不同的。
浏览器输入一个网址到页面展开,中间经历了多少流程
- 解析URL: 首先会对 URL 进行解析,分析所需要使用的传输协议和请求的资源的路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,如果存在非法字符,则对非法字符进行转义后再进行下一过程。
- 缓存判断: 浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新的请求。
- DNS解析: 下一步首先需要获取的是输入的 URL 中的域名的 IP 地址,首先会判断本地是否有该域名的 IP 地址的缓存,如果有则使用,如果没有则向本地 DNS 服务器发起请求。本地 DNS 服务器也会先检查是否存在缓存,如果没有就会先向根域名服务器发起请求,获得负责的顶级域名服务器的地址后,再向顶级域名服务器请求,然后获得负责的权威域名服务器的地址后,再向权威域名服务器发起请求,最终获得域名的 IP 地址后,本地 DNS 服务器再将这个 IP 地址返回给请求的用户。用户向本地 DNS 服务器发起请求属于递归请求,本地 DNS 服务器向各级域名服务器发起请求属于迭代请求。
- 获取MAC地址: 当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相与,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。
- TCP三次握手: 下面是 TCP 建立连接的三次握手的过程,首先客户端向服务器发送一个 SYN 连接请求报文段和一个随机序号,服务端接收到请求后向服务器端发送一个 SYN ACK报文段,确认连接请求,并且也向客户端发送一个随机序号。客户端接收服务器的确认应答后,进入连接建立的状态,同时向服务器也发送一个ACK 确认报文段,服务器端接收到确认后,也进入连接建立状态,此时双方的连接就建立起来了。
- HTTPS握手: 如果使用的是 HTTPS 协议,在通信前还存在 TLS 的一个四次握手的过程。首先由客户端向服务器端发送使用的协议的版本号、一个随机数和可以使用的加密方法。服务器端收到后,确认加密的方法,也向客户端发送一个随机数和自己的数字证书。客户端收到后,首先检查数字证书是否有效,如果有效,则再生成一个随机数,并使用证书中的公钥对随机数加密,然后发送给服务器端,并且还会提供一个前面所有内容的 hash 值供服务器端检验。服务器端接收后,使用自己的私钥对数据解密,同时向客户端发送一个前面所有内容的 hash 值供客户端检验。这个时候双方都有了三个随机数,按照之前所约定的加密方法,使用这三个随机数生成一把秘钥,以后双方通信前,就使用这个秘钥对数据进行加密后再传输。
- 返回数据: 当页面请求发送到服务器端后,服务器端会返回一个 html 文件作为响应,浏览器接收到响应后,开始对 html 文件进行解析,开始页面的渲染过程。
- 页面渲染: 浏览器首先会根据 html 文件构建 DOM 树,根据解析到的 css 文件构建 CSSOM 树,如果遇到 script 标签,则判断是否含有 defer 或者 async 属性,要不然 script 的加载和执行会造成页面的渲染的阻塞。当 DOM 树和 CSSOM 树建立好后,根据它们来构建渲染树。渲染树构建好后,会根据渲染树来进行布局。布局完成后,最后使用浏览器的 UI 接口对页面进行绘制。这个时候整个页面就显示出来了。
- TCP四次挥手: 最后一步是 TCP 断开连接的四次挥手过程。若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。
http有哪些特点?
HTTP 是超文本传输协议,它定义了客户端和服务器之间交换报文的格式和方式,默认使用 80 端口。它使用 TCP 作为传输层协议,保证了数据传输的可靠性。
HTTP 协议具有以下优点:
- 支持客户端 / 服务器模式
- 简单快速 :客户向服务器请求服务时,只需传送请求方法和路径。由于 HTTP 协议简单,使得 HTTP 服务器的程序规模小,因而通信速度很快。
- 无连接 :无连接就是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接,采用这种方式可以节省传输时间。
- 无状态 :HTTP 协议是无状态协议,这里的状态是指通信过程的上下文信息。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能会导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就比较快。
- 灵活 :HTTP 允许传输任意类型的数据对象。正在传输的类型由 Content-Type 加以标记。
HTTP 协议具有以下缺点:
- 无状态: HTTP 是一个无状态的协议,HTTP 服务器不会保存关于客户的任何信息。
- 明文传输: 协议中的报文使用的是文本形式,这就直接暴露给外界,不安全。
- 不安全:
- 通信使用明文(不加密),内容可能会被窃听;
- 不验证通信方的身份,因此有可能遭遇伪装;
- 无法证明报文的完整性,所以有可能已遭篡改;
http2.0的二进制压缩具体是什么地方压缩?
在 HTTP1.1 版中,报文的头信息必须是文本(ASCII 编码),数据体可以是文本,也可以是二进制。HTTP2 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为"帧",可以分为头信息帧和数据帧。
HTTP2的头部压缩是HPACK算法。在客户端和服务器两端建立“字典”,用索引号表示重复的字符串,采用哈夫曼编码来压缩整数和字符串,可以达到 50%~90% 的高压缩率。 具体来说 :
- 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键值对,对于相同的数据,不再通过每次请求和响应发送;
- 首部表在 HTTP2 的连接存续期内始终存在,由客户端和服务器共同渐进地更新;
- 每个新的首部键值对要么被追加到当前表的末尾,要么替换表中之前的值。
HTTP2 不直接对报文体进行压缩,而是通过使用 TLS 协议进行传输来实现数据的压缩。TLS 本身支持数据的压缩和解压缩功能,因此在使用 HTTP2时,可以在 TLS 层面对数据进行压缩和解压缩,以减少传输的数据量。
url由哪些组成?
URL(统一资源定位符)由以下几个部分组成:
- 协议(Protocol):指定了访问资源所使用的协议或规则,常见的协议有 HTTP、HTTPS、FTP、SMTP 等。
- 域名(或主机名):指定了资源所在的主机名或域名,可以是 IP 地址或域名形式,例如:www.example.com 。
- 端口号(Port):指定了访问资源时使用的端口号,用于区分同一台主机上不同的服务。大多数协议都有默认的端口号,如 HTTP 默认端口号为 80,HTTPS 默认端口号为 443。
- 路径(Path):指定了服务器上资源的具体路径或位置,以斜杠(/)开头,例如:/blog/article1。
- 查询参数(Query Parameters):用于向服务器传递额外的参数信息,以问号(?)开头,多个参数之间使用和号(&)分隔,例如:?page=1&limit=10。
- 片段标识(Fragment Identifier):指定了资源中的具体片段或位置,以井号(#)开头,例如:#section1。
综合起来,一个完整的 URL 格式通常为:协议://域名[:端口号]/路径?查询参数#片段标识。
http和https默认的端口号是什么?
HTTP 协议端口是 80,HTTPS 协议端口是 443。
为什么不使用定时轮询去实现实时协作呢?
定时轮询是一种实现实时协作的方法,但它存在一些缺点,因此不太适合某些情况:
- 实时性差:定时轮询是按照一定的时间间隔进行数据请求,因此无法做到实时更新。数据更新的频率取决于轮询的时间间隔,可能会出现延迟。
- 网络开销大:定时轮询会定期向服务器发送请求,即使数据没有更新也会进行通信,因此可能会造成不必要的网络开销,尤其在数据更新不频繁的情况下更为明显。
- 服务器压力大:定时轮询会导致服务器频繁地处理请求,即使数据没有更新也会消耗服务器资源,从而增加了服务器的负担。
浏览器原理篇
讲一讲进程和线程的差异,你是怎么理解的?
本质上说,进程和线程都是 CPU 工作时间片的一个描述:进程是资源分配的最小单位,线程是CPU调度的最小单位。
进程
操作系统中最核心的概念就是进程,进程是对正在运行中的程序的一个抽象,是系统进行资源分配和调度的基本单位,描述了 CPU 在运行指令及加载和保存上下文所需的时间。一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。
操作系统的其他所有内容都是围绕着进程展开的,负责执行这些任务的是CPU 进程是一种抽象的概念,从来没有统一的标准定义看,一般由程序、数据集合和进程控制块三部分组成:
- 程序用于描述进程要完成的功能,是控制进程执行的指令集
- 数据集合是程序在执行时所需要的数据和工作区
- 程序控制块,包含进程的描述信息和控制信息,是进程存在的唯一标志
线程
线程(thread)是操作系统能够进行运算调度的最小单位,其是进程中的一个执行任务(控制单元),负责当前进程中程序的执行。
进程和线程之间的关系
- 进程中的任意一线程执行出错,都会导致整个进程的崩溃。
- 线程之间共享进程中的数据。
- 当一个进程关闭之后,操作系统会回收进程所占用的内存, 当一个进程退出时,操作系统会回收该进程所申请的所有资源;即使其中任意线程因为操作不当导致内存泄漏,当进程退出时,这些内存也会被正确回收。
- 进程之间的内容相互隔离。 进程隔离就是为了使操作系统中的进程互不干扰,每一个进程只能访问自己占有的数据,也就避免出现进程 A 写入数据到进程 B 的情况。正是因为进程之间的数据是严格隔离的,所以一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,这时候,就需要使用用于进程间通信的机制了。
进程和线程的区别
- 进程可以看做独立应用,线程不能
- 资源:进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位);线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)。
- 通信方面:线程间可以通过直接共享同一进程中的资源,而进程通信需要借助 进程间通信。
- 调度:进程切换比线程切换的开销要大。线程是CPU调度的基本单位,线程的切换不会引起进程切换,但某个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
- 系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存、I/O 等,其开销远大于创建或撤销线程时的开销。同理,在进行进程切换时,涉及当前执行进程 CPU 环境还有各种各样状态的保存及新调度进程状态的设置,而线程切换时只需保存和设置少量寄存器内容,开销较小。
进程之间的通信有哪种方式?
-
管道通信
管道是一种最基本的进程间通信机制。管道就是操作系统在内核中开辟的一段缓冲区,进程1可以将需要交互的数据拷贝到这段缓冲区,进程2就可以读取了。
管道的特点:
- 只能单向通信
- 只能血缘关系的进程进行通信
- 依赖于文件系统
- 生命周期随进程
- 面向字节流的服务
- 管道内部提供了同步机制
-
消息队列通信
消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
使用消息队列进行进程间通信,可能会收到数据块最大长度的限制约束等,这也是这种通信方式的缺点。如果频繁的发生进程间的通信行为,那么进程需要频繁地读取队列中的数据到内存,相当于间接地从一个进程拷贝到另一个进程,这需要花费时间。
-
信号量通信
共享内存最大的问题就是多进程竞争内存的问题,就像类似于线程安全问题。我们可以使用信号量来解决这个问题。信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。例如信号量的初始值是 1,然后 a 进程来访问内存1的时候,我们就把信号量的值设为 0,然后进程b 也要来访问内存1的时候,看到信号量的值为 0 就知道已经有进程在访问内存1了,这个时候进程 b 就会访问不了内存1。所以说,信号量也是进程之间的一种通信方式。
-
信号通信
信号(Signals )是Unix系统中使用的最古老的进程间通信的方法之一。操作系统通过信号来通知进程系统中发生了某种预先规定好的事件(一组事件中的一个),它也是用户进程之间通信和同步的一种原始机制。
-
共享内存通信
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问(使多个进程可以访问同一块内存空间)。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
-
套接字通信
上面说的共享内存、管道、信号量、消息队列,他们都是多个进程在一台主机之间的通信,那两个相隔几千里的进程能够进行通信吗?答是必须的,这个时候 Socket 这家伙就派上用场了,例如我们平时通过浏览器发起一个 http 请求,然后服务器给你返回对应的数据,这种就是采用 Socket 的通信方式了。
线程池的概念你怎么理解
线程池是一种管理和复用线程的机制,用于提高多线程程序的性能和效率。它由线程池管理器、工作队列和一组工作线程组成。
- 线程池管理器(ThreadPool Manager): 负责创建、管理和销毁线程池,通常包括线程池的初始化、线程的动态创建和销毁、线程池状态的管理等功能。
- 工作队列(Work Queue): 用于存储需要执行的任务或工作单元,线程池中的工作线程会从工作队列中获取任务并执行。
- 工作线程(Worker Thread): 线程池中的执行线程,负责执行从工作队列中获取的任务。线程池通常会预先创建一定数量的工作线程,并根据需要动态调整线程数量。
线程池的主要优点包括:
- 减少线程创建和销毁的开销: 线程池预先创建了一定数量的工作线程,并重复利用它们,避免了频繁创建和销毁线程的开销。
- 提高系统性能和资源利用率: 线程池能够有效地利用系统资源,通过合理调度和管理线程,提高系统的并发处理能力和资源利用率。
- 控制并发线程数量: 线程池可以限制并发执行的线程数量,防止系统因过多线程导致资源竞争、内存泄漏等问题。
- 线程池的典型应用场景包括服务器端的并发处理、异步任务的执行、多线程编程中的任务调度等。在实际开发中,使用线程池可以简化多线程编程,提高程序的可维护性和可扩展性,同时降低了并发编程的复杂性。
死锁听说过吗?死锁的条件是什么?怎么避免?
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
系统中的资源可以分为两类:
- 可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU和主存均属于可剥夺性资源;
- 不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。
产生死锁的原因:
-
竞争资源
- 产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞)
- 产生死锁中的竞争资源另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁
-
进程间推进顺序非法
若P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁。例如,当P1运行到P1:Request(R2)时,将因R2已被P2占用而阻塞;当P2运行到P2:Request(R1)时,也将因R1已被P1占用而阻塞,于是发生进程死锁
产生死锁的必要条件:
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个进程——资源的环形链。
预防死锁的方法:
- 资源一次性分配:一次性分配所有资源,这样就不会再有请求了(破坏请求条件)
- 只要有一个资源得不到分配,也不给这个进程分配其他的资源(破坏请保持条件)
- 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
- 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
操作系统中进程的调度策略有哪些
- 先来先服务调度(First-Come, First-Served,FCFS): 按照进程到达的先后顺序进行调度,先到达的进程先执行,直到执行完毕或等待I/O时才执行下一个进程。FCFS调度策略简单易实现,但可能导致长作业等待时间过长,效率较低。
- 最短作业优先调度(Shortest Job First,SJF): 选择估计运行时间最短的进程优先执行。SJF调度策略可以最大程度地减少平均等待时间,但需要准确预测进程的运行时间,对于长作业可能会导致长时间的等待。
- 最短剩余时间优先调度(Shortest Remaining Time First,SRTF): 在SJF的基础上,动态地选择当前剩余执行时间最短的进程执行,即当有新进程到达时,如果其剩余执行时间比当前正在执行的进程的剩余执行时间还短,则立即切换执行。SRTF调度策略可以进一步缩短作业的等待时间,但增加了调度开销。
- 优先级调度(Priority Scheduling): 为每个进程分配优先级,优先级高的进程优先执行。优先级可以是静态的(静态优先级调度)或动态调整(动态优先级调度),动态调度可以根据进程的执行情况和其他因素调整优先级。优先级调度策略可以根据需求调整执行顺序,但可能导致低优先级进程长时间等待。
- 时间片轮转调度(Round-Robin Scheduling): 将CPU时间划分为多个时间片,每个进程被分配一个时间片,当时间片用完时,将进程移到就绪队列尾部,执行下一个进程。时间片轮转调度策略公平且简单,适用于分时系统,但可能导致上下文切换开销过大。
- 多级反馈队列调度(Multilevel Feedback Queue Scheduling): 将进程按照优先级分成多个队列,并按照优先级依次执行,当一个进程用完时间片未执行完时,将其移到下一级队列尾部。多级反馈队列调度策略结合了优先级调度和时间片轮转调度的优点,适用于复杂的多任务系统。
浏览器拿到HTML之后怎么渲染成我们所看到的页面的?
浏览器渲染主要有以下步骤:
- 首先解析收到的文档,根据文档定义构建一棵 DOM 树,DOM 树是由 DOM 元素及属性节点组成的。
- 然后对 CSS 进行解析,生成 CSSOM 规则树。
- 根据 DOM 树和 CSSOM 规则树构建渲染树。渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和 DOM 元素相对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。还有一些 DOM 元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。
- 当渲染对象被创建并添加到树中,它们并没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。
- 布局阶段结束后是绘制阶段,遍历渲染树并调用渲染对象的 paint 方法将它们的内容显示在屏幕上,绘制使用 UI 基础组件。
注意:这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的 html 都解析完成之后再去构建和布局 render 树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。
浏览器里的事件流是怎么理解的?
事件是用户操作网页时发生的交互动作,比如 click/move, 事件除了用户触发的动作外,还可以是文档加载,窗口滚动和大小调整。事件被封装成一个 event 对象,包含了该事件发生时的所有相关信息( event 的属性)以及可以对事件进行的操作( event 的方法)。
事件是用户操作网页时发生的交互动作或者网页本身的一些操作,现代浏览器一共有三种事件模型:
- DOM0 级事件模型 ,这种模型不会传播,所以没有事件流的概念,但是现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义监听函数,也可以通过 js 属性来指定监听函数。所有浏览器都兼容这种方式。直接在 dom 对象上注册事件名称,就是 DOM0 写法。
- IE 事件模型 ,在该事件模型中,一次事件共有两个过程,事件处理阶段和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。这种模型通过 attachEvent 来添加监听函数,可以添加多个监听函数,会按顺序依次执行。
- DOM2 级事件模型 ,在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从 document 一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。后面两个阶段和 IE 事件模型的两个阶段相同。这种事件模型,事件绑定的函数是 addEventListener,其中第三个参数可以指定事件是否在捕获阶段执行。
浏览器中的事件流描述了事件从发生到被处理的整个过程。事件流包括两种:冒泡(bubble)和捕获(capture)。这两种事件流描述了事件如何在DOM树中传播,以及在传播过程中触发事件处理程序的顺序。
浏览器怎么去监听一个事件?不用vue框架,原生地去监听事件,怎么监听?
在原生 JavaScript 中,你可以使用 addEventListener 方法来监听事件。这个方法可以在任何 DOM 元素上调用,用于添加事件监听器,语法如下:
element.addEventListener(event, listener[, options]);
event是要监听的事件类型,比如 "click", "mouseover", "keydown" 等。listener是事件发生时要执行的回调函数。options是一个可选的对象,用于指定事件监听的一些选项,比如{ once: true }表示事件只会被触发一次,{ capture: true }表示在捕获阶段监听事件等。
<button id="myButton">Click me</button>
<script>
// 获取按钮元素
const button = document.getElementById('myButton');
// 添加点击事件监听器
button.addEventListener('click', function(event) {
console.log('Button clicked!');
});
</script>
浏览器的本地存储有哪些?
Cookie
Cookie 是最早被提出来的本地存储方式,在此之前,服务端是无法判断网络中的两个请求是否是同一用户发起的,为解决这个问题,Cookie 就出现了。Cookie 的大小只有 4kb,它是一种纯文本文件,每次发起 HTTP 请求都会携带 Cookie。
Cookie的特性:
- Cookie 一旦创建成功,名称就无法修改
- Cookie 是无法跨域名的,也就是说 a 域名和 b 域名下的 cookie 是无法共享的,这也是由Cookie 的隐私安全性决定的,这样就能够阻止非法获取其他网站的 Cookie
- 每个域名下 Cookie 的数量不能超过 20 个,每个 Cookie 的大小不能超过 4kb
- 有安全问题,如果 Cookie 被拦截了,那就可获得 session 的所有信息,即使加密也于事无补,无需知道 cookie 的意义,只要转发 cookie 就能达到目的
- Cookie 在请求一个新的页面的时候都会被发送过去
如果需要域名之间跨域共享 Cookie,有两种方法:
- 使用 Nginx 反向代理
- 在一个站点登陆之后,往其他网站写 Cookie。服务端的 Session 存储到一个节点,Cookie 存储 sessionId
Cookie 的使用场景:
- 最常见的使用场景就是 Cookie 和 session 结合使用,我们将 sessionId 存储到 Cookie 中,每次发请求都会携带这个 sessionId,这样服务端就知道是谁发起的请求,从而响应相应的信息。
- 可以用来统计页面的点击次数
LocalStorage
LocalStorage 是 HTML5 新引入的特性,由于有的时候我们存储的信息较大,Cookie 就不能满足我们的需求,这时候 LocalStorage 就派上用场了。
LocalStorage 的优点:
- 在大小方面,LocalStorage 的大小一般为 5MB,可以储存更多的信息
- LocalStorage 是持久储存,并不会随着页面的关闭而消失,除非主动清理,不然会永久存在
- 仅储存在本地,不像 Cookie 那样每次 HTTP 请求都会被携带
LocalStorage 的缺点:
- 存在浏览器兼容问题,IE8 以下版本的浏览器不支持
- 如果浏览器设置为隐私模式,那我们将无法读取到 LocalStorage
- LocalStorage 受到同源策略的限制,即端口、协议、主机地址有任何一个不相同,都不会访问
LocalStorage 的常用 API:
localStorage.setItem('key', 'value');localStorage.getItem('key');localStorage.removeItem('key');localStorage.clear();localStorage.key(index)
LocalStorage 的使用场景:
- 有些网站有换肤的功能,这时候就可以将换肤的信息存储在本地的 LocalStorage 中,当需要换肤的时候,直接操作 LocalStorage 即可
- 在网站中的用户浏览信息也会存储在 LocalStorage 中,还有网站的一些不常变动的个人信息等也可以存储在本地的 LocalStorage 中
SessionStorage
SessionStorage 和 LocalStorage 都是在 HTML5 才提出来的存储方案,SessionStorage 主要用于临时保存同一窗口(或标签页)的数据,刷新页面时不会删除,关闭窗口或标签页之后将会删除这些数据。
SessionStorage 与 LocalStorage 对比:
- SessionStorage 和 LocalStorage 都在 本地进行数据存储 ;
- SessionStorage 也有同源策略的限制,但是 SessionStorage 有一条更加严格的限制,SessionStorage 只有在同一浏览器的同一窗口下才能够共享 ;
- LocalStorage 和 SessionStorage 都不能被爬虫爬取 ;
SessionStorage 的常用 API:
sessionStorage.setItem('key', 'value');sessionStorage.getItem('key');sessionStorage.removeItem('key');sessionStorage.clear();sessionStorage.key(index)
SessionStorage 的使用场景
- 由于 SessionStorage 具有时效性,所以可以用来存储一些网站的游客登录的信息,还有临 时的浏览记录的信息。当关闭网站之后,这些信息也就随之消除了。
同域名下在同一个窗口下开新的标签页能否读到SessionStorage
在同源下,同一个窗口打开的不同标签页之间可以共享 SessionStorage 数据。这是因为 SessionStorage 存储的数据是基于浏览器会话(Session)的,而同一浏览器会话下的所有标签页都属于同一个上下文,因此它们可以共享相同的 SessionStorage 数据。
如果在不同的浏览器会话中打开了两个标签页(比如一个标签页在一个浏览器窗口中,另一个标签页在另一个浏览器窗口中),它们将无法共享 SessionStorage 数据,因为它们属于不同的浏览器会话。
什么是事件冒泡,什么是事件捕获?
事件冒泡
微软提出了名为事件冒泡(event bubbling)的事件流。事件冒泡可以形象地比喻为把一颗石头投入水中,泡泡会一直从水底冒出水面。也就是说,事件会从最内层的元素开始发生,一直向上传播,直到document对象。
因此上面的例子在事件冒泡的概念下发生click事件的顺序应该是:
事件捕获
网景提出另一种事件流名为事件捕获(event capturing)。与事件冒泡相反,事件会从最外层开始发生,直到最具体的元素。
事件代理
事件委托也称为事件代理。就是利用事件冒泡,把子元素的事件都绑定到父元素上。如果子元素阻止了事件冒泡,那么委托就无法实现。
原理实现: js复制代码不是每个子节点单独设置事件监听器,而是事件监听器设置在其父节点上,然后利用冒泡原理影响设置每个子节点。
应用场景:1000个button需要注册点击事件
如果循环给每个按钮添加点击事件,那么会增加内存损耗,影响性能;此时可以给button的父元素添加点击事件
数据结构篇
前端使用websocket,在数据结构上有用什么特殊的前端的一个库吗?
前端使用 WebSocket 时通常会借助一些特殊的库来简化数据传输和管理。其中,最常用的库之一是 socket.io。Socket.io 是一个流行的 JavaScript 库,用于实现实时、双向通信的 WebSocket 技术。它提供了一套简单而强大的 API,使得在客户端和服务器之间建立稳定的 WebSocket 连接变得非常容易。Socket.io 还提供了额外的功能,如自动重新连接、消息分组、广播等,这些功能对于构建实时应用程序非常有用。
import io from 'socket.io-client';
// 创建 Socket 实例并连接到服务器
const socket = io('http://localhost:3000');
// 监听连接成功事件
socket.on('connect', () => {
console.log('Connected to server');
});
// 监听接收到的消息
socket.on('message', (data) => {
console.log('Received message:', data);
});
// 发送消息
socket.emit('message', 'Hello, server!');
二叉树的前序中序后序遍历的规则?
前序遍历(Preorder Traversal):
- 根节点 -> 左子树 -> 右子树
- 先访问根节点,然后递归地对左子树进行前序遍历,最后递归地对右子树进行前序遍历。
中序遍历(Inorder Traversal):
- 左子树 -> 根节点 -> 右子树
- 先递归地对左子树进行中序遍历,然后访问根节点,最后递归地对右子树进行中序遍历。
后序遍历(Postorder Traversal):
- 左子树 -> 右子树 -> 根节点
- 先递归地对左子树进行后序遍历,然后递归地对右子树进行后序遍历,最后访问根节点。
队列和栈有什么区别?
栈
栈(stack)又名堆栈,它是一种运算受限的线性表,限定仅在表尾进行插入和删除操作的线性表。 表尾这一端被称为栈顶,相反地另一端被称为栈底,向栈顶插入元素被称为进栈、入栈、压栈,从栈顶删除元素又称作出栈。所以其按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据,具有记忆作用。
关于栈的操作主要的方法如下:
- push:入栈操作
- pop:出栈操作
队列
跟栈十分相似,队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头,当队列中没有元素时,称为空队列。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出。
存在的问题:上述这种入队和出队操作中,头尾指针只增加不减小,致使被删元素的空间永远无法重新利用。当队列中实际的元素个数远远小于向量空间的规模时,也可能由于尾指针已超越向量空间的上界而不能做入队操作,出该现象称为"假溢"。
在实际使用队列时,为了使队列空间能重复使用,往往对队列的使用方法稍加改进:无论插入或删除,一旦rear指针增1或front指针增1 时超出了所分配的队列空间,就让它指向这片连续空间的起始位置,这种队列也就是循环队列
双向链表的结构是怎么样的?
链表(Linked List)是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,由一系列结点(链表中每一个元素称为结点)组成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
链表的结构也十分多,常见的有四种形式:
- 单链表:除了头节点和尾节点,其他节点只包含一个后继指针
- 循环链表:跟单链表唯一的区别就在于它的尾结点又指回了链表的头结点,首尾相连,形成了一个环
- 双向链表:每个结点具有两个方向指针,后继指针(next)指向后面的结点,前驱指针(prev)指向前面的结点,其中节点的前驱指针和尾结点的后继指针均指向空地址NULL
- 双向循环链表:跟双向链表基本一致,不过头节点前驱指针指向尾节点和尾节点的后继指针指向头节点
冒泡排序是稳定的排序吗?时间复杂度是多少?
冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。冒泡排序的思想就是在每次遍历一遍未排序的数列之后,将一个数据元素浮上去(也就是排好了一个数据)。
冒泡排序在每一轮排序中都会使一个元素排到一趟, 也就是最终需要 n-1 轮这样的排序。而在每轮排序中都需要对相邻的两个元素进行比较,在最坏的情况下,每次比较之后都需要交换位置,此时时间复杂度为O(n^2)
只有后一个元素比前面的元素大(小)时才会对它们交换位置并向上冒出,对于同样大小的元素,是不需要交换位置的,所以对于同样大小的元素来说,相对位置是不会改变的,因此,冒泡排序是稳定的。
Vue篇
为什么要加key呢?
vue 中 key 值的作用可以分为两种情况来考虑:
-
第一种情况是 v-if 中使用 key。
由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。
-
第二种情况是 v-for 中使用 key。
用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。
key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速:
- 更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以 避免就地复用的情况。所以会更加准确。
- 更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快
在页面中输入关键字进行查询时,这个key有什么作用呢?
在 Vue 中,使用 key 属性的主要作用是帮助 Vue 识别列表中的每个元素,从而更有效地管理列表的更新和重渲染。具体到搜索框查询的场景中,如果你使用了 key 属性,那么每次输入字符进行查询时,Vue 会根据 key 属性的变化来判断哪些列表项需要重新渲染,哪些列表项可以复用,从而提高页面的性能和用户体验。
如果你没有使用 key 属性,Vue 会默认将列表中的每个元素按照索引顺序进行管理。这意味着当你输入字符进行查询时,即使列表中的内容没有变化,Vue 也会重新渲染所有的列表项,导致性能下降,页面闪烁等问题。
因此,使用 key 属性可以帮助 Vue 更精确地跟踪列表中元素的变化,避免不必要的重渲染,提高页面的性能和用户体验。
在没有使用列表的时候也添加一个key有什么效果?
在没有使用列表的情况下,给一个元素添加 key 属性实际上是没有意义的,因为 key 属性的主要作用是帮助 Vue 识别列表中的每个元素,从而更有效地管理列表的更新和重渲染。
在非列表的情况下,Vue 不会根据 key 属性来进行元素的管理和更新,因为它并不具备列表中元素的排序和重用的场景。因此,给非列表元素添加 key 属性对页面的渲染和性能没有实质性影响。
在一些特殊情况下,你可能会在非列表元素中使用 key 属性,例如在动态组件中或者在 v-for 循环中的模板中使用,这样可以告诉Vue如何唯一地识别和重渲染这些元素。但是在普通的非列表元素中,添加 key 属性通常是不需要的,也不会产生额外的效果。
在没有使用列表时,如果给一个普通的 DOM 元素添加了 key 属性,Vue 会将这个 key 视为元素的特殊属性,用于标识该元素。这样做的效果是可以让 Vue 更加高效地识别和管理这个元素,特别是在动态更新 DOM 结构的情况下,可以减少 Vue 的 diff 算法的复杂度,提高性能。
虽然添加 key 属性可以带来一定的性能优势,但也需要注意一些代价。首先,key 属性会增加 DOM 元素的属性数量,略微增加页面的内存消耗。其次,在某些情况下,如果 key 属性被滥用或者不正确地使用,可能会导致一些意外的问题,比如不必要的组件重渲染等。
因此,在没有列表的情况下添加 key 属性需要权衡利弊。如果你确定该元素在 Vue 的更新中需要保持唯一性或者有特殊的识别需求,那么可以考虑添加 key 属性。但如果没有明确的需要,可以不添加 key 属性。
vue中的data为什么是个函数?
JavaScript 中的对象是引用类型的数据,当多个实例引用同一个对象时,只要一个实例对这个对象进行操作,其他实例中的数据也会发生变化。
而在 Vue 中,更多的是想要复用组件,那就需要每个组件都有自己的数据,这样组件之间才不会相互干扰。
所以组件的数据不能写成对象的形式,而是要写成函数的形式。数据以函数返回值的形式定义,这样当每次复用组件的时候,就会返回一个新的 data,也就是说每个组件都有自己的私有数据空间,它们各自维护自己的数据,不会干扰其他组件的正常运行。
vue中有两个概念,一个叫组件,一个叫插件,有什么区别?比如vue-route和element-ui是什么?
在 Vue 中,组件(Component)和插件(Plugin)是两个不同的概念,它们的作用和使用方式有所不同。
组件(Component):
- 组件是 Vue 应用中的基本构建块,用于封装可复用的 UI 元素和功能模块。
- 组件通常由模板、脚本和样式组成,可以封装特定的功能和样式,并提供接口供其他组件使用。
- 组件具有独立的作用域,可以包含自己的数据、方法和生命周期钩子。
- 在 Vue 中,组件通过 Vue.extend() 方法或单文件组件(.vue 文件)的形式定义,并通过 Vue.component() 全局注册或在父组件中以标签的形式使用。
插件(Plugin):
- 插件是 Vue 的扩展,用于为 Vue 应用添加全局功能或添加特定功能的实现。
- 插件通常封装了一些特定的功能,例如路由管理、状态管理、UI 组件库等。
- 插件可以扩展 Vue 的功能,为 Vue 实例添加全局方法、指令、混入、过滤器等,也可以在应用初始化时执行一些额外的逻辑。
- 插件通常是通过 Vue.use() 方法在 Vue 实例中注册,并在应用初始化时生效。
举例来说,Vue Router(vue-router)和 Element UI(element-ui)都是 Vue 的插件:
- Vue Router 是用于在 Vue 应用中实现路由功能的插件,它提供了路由配置、导航功能、路由参数传递等功能,可以让 Vue 应用实现页面之间的跳转和管理。
- Element UI 是一套基于 Vue.js 的 UI 组件库,提供了丰富的 UI 组件和功能,例如按钮、表单、对话框、布局等,可以用于构建美观、响应式的用户界面。
总之,组件和插件在 Vue 应用中扮演着不同的角色,组件用于封装 UI 和功能模块,而插件用于扩展 Vue 的功能或添加全局功能。
vue中的动态组件一般是什么业务场景?
动态组件是根据数据的变化,可以随时切换不同的组件,比如咱们现在要展示一个文本框和输入框,我们想要根据我们data中定义的值去决定是显示文本框还是输入框。 Vue 中的动态组件通常用于以下业务场景:
- 条件渲染:当需要根据条件动态地渲染不同的组件时,可以使用动态组件。例如,在表单中根据用户的选择显示不同的输入框或者在导航栏中根据用户的权限显示不同的菜单项。
- 路由导航:在路由导航中,可以根据不同的路由动态加载不同的组件。例如,在单页应用中根据不同的路由路径加载相应的页面组件,或者根据用户权限加载不同的权限页面。
- 组件切换:当需要在不同的组件之间进行切换时,可以使用动态组件。例如,在轮播图或者选项卡等组件中切换不同的内容组件。
- 动态表单:在动态生成表单或者表单中的字段时,可以根据不同的条件动态地渲染不同的表单组件。例如,在表单中根据用户选择的选项动态显示或隐藏不同的表单字段。
- 插槽内容:在插槽内容中,可以动态地渲染不同的组件或者内容。例如,在布局组件中动态渲染不同的侧边栏组件或者底部组件。
总之,动态组件能够根据不同的条件或者场景动态地渲染不同的组件,提高了组件的灵活性和可复用性,适用于多种业务场景。
vue2的响应式是怎么实现的?
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
- 需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
- compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
- Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是: ①在自身实例化时往属性订阅器(dep)里面添加自己 ②自身必须有一个update()方法 ③待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
- MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
Vue3中,Proxy 实现的响应式原理与 Vue2 的实现原理相同,实现方式大同小异∶
- get 收集依赖
- Set、delete 等触发依赖
- 对于集合类型,就是对集合对象的方法做一层包装:原方法执行后执行依赖相关的收集或触 发逻辑。
解释一下vue的生命周期函数
Vue 实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是Vue的⽣命周期。
- beforeCreate(创建前) :数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到data、computed、watch、methods上的方法和数据。
- created(创建后) :实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到 $el 属性。
- beforeMount(挂载前) :在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。此时还没有挂载html到页面上。
- mounted(挂载后) :在el被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html 页面中。此过程中进行ajax交互。
- beforeUpdate(更新前) :响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染。
- updated(更新后) :在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
- beforeDestroy(销毁前) :实例销毁之前调用。这一步,实例仍然完全可用, this 仍能获取到实例。
- destroyed(销毁后) :实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。
另外还有 keep-alive 独有的生命周期,分别为 activated 和 deactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 activated 钩子函数。
created阶段能获取到dom节点吗
在Vue的created生命周期钩子中,可以访问到组件实例(this指向当前组件实例),但是此时DOM元素可能尚未完全渲染完成,因此不能保证能够获取到所有DOM节点。通常情况下,建议在mounted生命周期钩子中操作DOM,因为此时组件已经挂载到页面上,DOM元素已经完全渲染完成,可以安全地操作DOM节点。
v-if和v-show的区别是什么?
在 vue 中 v-show 与 v-if 的作用效果是相同的(不含v-else),都能控制元素在页面是否显示。当表达式为true的时候,都会占据页面的位置;当表达式都为false时,都不会占据页面位置。
区别:
- 手段 :v-if 是动态的向 DOM 树内添加或者删除 DOM 元素;v-show 是通过设置 DOM 元素的 display 样式属性控制显隐;
- 编译过程 :v-if 切换有一个局部编译 / 卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show 只是简单的基于 css 切换;
- 编译条件 :v-if 是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译 ; v-show 是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且 DOM 元素保留;
- 性能消耗 :v-if 有更高的切换消耗;v-show 有更高的初始渲染消耗;
- 使用场景 :v-if 适合运营条件不大可能改变;v-show 适合频繁切换。
vue中不建议同时使用v-for和v-if,为什么?
v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 true值的时候被渲染
v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组或者对象,而 item 则是被迭代的数组元素的别名。在 v-for 的时候,建议设置key值,并且保证每个key值是独一无二的,这便于diff算法进行优化。
在进行if判断的时候,v-for是比v-if先进行判断。
-
永远不要把 v-if 和 v-for 同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)
-
如果避免出现这种情况,则在外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环
<template v-if="isShow">
<p v-for="item in items">
</template>
- 如果条件出现在循环内部,可通过计算属性computed提前过滤掉那些不需要显示的项
computed: {
items: function() {
return this.list.filter(function (item) {
return item.isShow
})
}
}
父子组件的通信方式有什么?
props/$emit
父组件通过props向子组件传递数据,子组件通过$emit和父组件通信
- 父组件向子组件传值
- props只能是父组件向子组件进行传值, props使得父子组件之间形成了一个单向下行绑定。子组件的数据会随着父组件不断更新。
- props可以显示定义一个或一个以上的数据,对于接收的数据,可以是各种数据类型,同样也可以传递一个函数。
- props属性名规则:若在 props 中使用驼峰形式,模板中需要使用短横线的形式
- 子组件向父组件传值
- $emit 绑定一个自定义事件,当这个事件被执行的时就会将参数传递给父组件,而父组件通过 v-on 监听并接收参数。
eventBus事件总线($emit / $on)
eventBus事件总线适用于父子组件、非父子组件等之间的通信,使用步骤如下:
(1)创建事件中心管理组件之间的通信
(2)发送事件:假设有两个兄弟组件firstCom和secondCom,在firstCom组件中发送事件。
(3)接收事件:在secondCom组件中接收事件
依赖注入(provide / inject)
这种方式就是Vue中的依赖注入,该方法用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方法来进行传值。就不用一层一层的传递了。
provide / inject是Vue提供的两个钩子,和data、methods是同级的。并且provide的书写形式和data一样。
- provide 钩子用来发送数据或方法
- inject 钩子用来接收数据或方法
ref / $refs
这种方式也是实现父子组件之间的通信。
ref: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法。
$parent/$children
- 使用 $parent 可以让组件访问父组件的实例(访问的是上一级父组件的属性和方法)
- 使用 $children 可以让组件访问子组件的实例,但是, $children 并不能保证顺序,并且访问的数据也不是响应式的。 需要注意:
- 通过 $parent 访问到的是上一级父组件的实例,可以使用 $root 来访问根组件的实例
- 在组件中使用 $children 拿到的是所有的子组件的实例,它是一个数组,并且是无序的
- 在根组件 #app 上拿 $parent 得到的是 new Vue() 的实例,在这实例上再拿 $parent 得到的是 undefined ,而在最底层的子组件拿 $children 是个空数组
- $children 的值是 数组 ,而 $parent 是个对象
$attrs / $listeners 考虑一种场景,如果A是B组件的父组件,B是C组件的父组件。如果想要组件A给组件C传递数据,这种隔代的数据,该使用哪种方式呢?
如果是用props/$emit来一级一级的传递,确实可以完成,但是比较复杂;如果使用事件总线,在多人开发或者项目较大的时候,维护起来很麻烦;如果使用Vuex,的确也可以,但是如果仅仅是传递数据,那可能就有点浪费了。 针对上述情况,Vue引入了$attrs / $listeners,实现组件之间的跨代通信。
先来看一下inheritAttrs,它的默认值true,继承所有的父组件属性除props之外的所有属性;inheritAttrs:false只继承class属性 。
- $attrs :继承所有的父组件属性(除了prop传递的属性、class 和 style ),一般用在子组件的子元素上
- $listeners :该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)
了解vue中的mixin吗?
Mixin是面向对象程序设计语言中的类,提供了方法的实现。其他类可以访问mixin类的方法而不必成为其子类。
Mixin类通常作为功能模块使用,在需要该功能时“混入”,有利于代码复用又避免了多继承的复杂。在Vue中我们可以局部混入跟全局混入。
局部混入:定义一个mixin对象,有组件options的data、methods属性。组件通过mixins属性调用mixin对象。该组件在使用的时候,混合了mixin里面的方法,在自动执行created生命钩子,执行hello方法
全局混入:通过Vue.mixin()进行全局的混入。使用全局混入需要特别注意,因为它会影响到每一个组件实例(包括第三方组件)。全局混入常用于插件的编写。
注意事项:
- 当组件存在与mixin对象相同的选项的时候,进行递归合并的时候组件的选项会覆盖mixin的选项
- 但是如果相同选项为生命周期钩子的时候,会合并成一个数组,先执行mixin的钩子,再执行组件的钩子。
功能:
- Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。
- 如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。
- 然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自己的 hook。
了解nextTick吗?
Vue的nextTick其本质是对JavaScript执行原理EventLoop的一种应用。
nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。
nextTick 不仅是 Vue 内部的异步队列的调用方法,同时也允许开发者在实际项目中使用这个方法来满足实际应用中对 DOM 更新数据时机的后续逻辑处理
nextTick 是典型的将底层 JavaScript 执行原理应用到具体案例中的示例,引入异步更新队列机制的原因∶
- 如果是同步更新,则多次对一个或多个属性赋值,会频繁触发 UI/DOM 的渲染,可以减少一些无用渲染
- 同时由于VirtualDOM的引入,每一次状态发生变化后,状态变化的信号会发送给组件,组件内部使用VirtualDOM进行计算得出需要更新的具体的 DOM 节点,然后对 DOM 进行更新操作,每次更新状态后的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所以异步渲染变得更加至关重要 .
Vue采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作DOM。有时候,可能遇到这样的情况,DOM1的数据发生了变化,而DOM2需要从DOM1中获取数据,那这时就会发现DOM2的视图并没有更新,这时就需要用到了nextTick了。
由于Vue的DOM操作是异步的,所以,在上面的情况中,就要将DOM2获取数据的操作写在$nextTick中。
this.$nextTick(() => { // 获取数据的操作...})
所以,在以下情况下,会用到nextTick:
- 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的DOM结构的时候,这个操作就需要方法在 nextTick() 的回调函数中。
- 在vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在 nextTick() 的回调函数中。
因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在nextTick()的回调函数中。
JavaScript篇
js是单线程还是多线程,为什么设计为单线程?
JavaScript 是单线程的,这意味着在同一时间内只能执行一个任务。JavaScript 之所以被设计为单线程,主要有以下几个原因:
- 简单性: 单线程模型使得 JavaScript 编程变得简单,因为不需要处理多线程同步、死锁等复杂的并发问题。
- 安全性: 多线程可能导致各种竞态条件和内存访问冲突,而单线程可以避免这些问题,使得代码更加可靠和安全。
- 轻量级: 单线程模型使得 JavaScript 在浏览器中的内存消耗更小,运行更加高效。
- 浏览器安全模型: 单线程执行可以有效地防止恶意代码访问和修改页面中的敏感信息,从而增强了浏览器的安全性。
虽然 JavaScript 是单线程的,但通过事件循环机制和异步编程模型,可以实现非阻塞的异步操作,使得 JavaScript 在处理 I/O 操作和其他耗时任务时能够更加高效地利用资源,提高响应性能。
指针和引用有什么区别
在 JavaScript 中,指针和引用是两个不同的概念,它们之间存在一些区别:
- 指针(Pointer): 指针是一个变量,它存储的是另一个变量的内存地址。在 JavaScript 中,我们并不直接操作内存地址,因此 JavaScript 中并没有直接的指针概念。
- 引用(Reference): 引用是一个变量,它存储的是另一个对象的引用(内存地址),而不是对象本身的值。在 JavaScript 中,几乎所有的数据类型都是通过引用来传递的,包括对象、数组、函数等。
区别在于,指针直接指向内存地址,而引用则是指向对象的引用。在 JavaScript 中,对象、数组等复杂数据类型在赋值和传递时,实际上是将引用复制了一份,这意味着两个变量指向的是同一个对象,修改其中一个变量会影响到另一个变量。
宏任务怎么理解的?
宏任务(Macro Task)是异步编程中的一个概念,用于描述一些需要在事件循环中执行的较大的任务单元。在 JavaScript 中,宏任务通常包括整体的 script(即整个代码块)、setTimeout、setInterval、I/O 操作等。
宏任务代表着一些相对耗时的、需要在事件循环中执行的任务,通过将这些任务放入宏任务队列,能够确保它们按照合适的顺序执行,避免阻塞主线程。
ES6有哪些新特性
- 语法增强:包括 let 和 const 声明、箭头函数、模板字符串、解构赋值、默认参数值等。这些语法增强使得代码更加简洁、可读性更高,并且提供了更多的灵活性和表达能力。
- 面向对象:包括类和继承、静态方法等特性。引入了类的概念,使得 JavaScript 更加面向对象,提供了更清晰的代码组织结构和继承关系。
- 模块化:引入了 import 和 export 关键字,支持模块化的文件导入和导出,使得代码的组织和管理更加方便。
- 异步编程:引入了 Promise 对象,解决了回调地狱的问题,使得异步代码更加清晰和易于理解。
- 新的数据结构:包括 Map 和 Set 数据结构,以及 Symbol 数据类型。这些新的数据结构提供了更多的选择和灵活性,可以更好地满足不同场景下的需求。
- 迭代器和生成器:引入了迭代器和生成器的概念,使得处理集合数据更加方便和灵活。
- Proxy 和 Reflect:引入了 Proxy 对象和 Reflect 对象,提供了对对象的拦截和操作,使得对对象的操作更加灵活和可控。
- 字符串和数组新增方法:包括字符串的各种方法(如 startsWith()、endsWith()、includes() 等)和数组的各种方法(如 find()、findIndex()、fill() 等),提供了更多的方法来操作字符串和数组。
promise和async await之间有什么关联吗?
async/await 其实是 Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。从字面上来看,async 是“异步”的简写,await 则为等待,所以很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。当然语法上强制规定 await 只能出现在 asnyc 函数中。
async 函数返回的是一个 Promise 对象。async 函数(包含函数语句、函数表达式、Lambda 表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。
async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,当然应该用原来的方式: then() 链来处理这个 Promise 对象。
在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。
async/await是基于promise的?这个基于怎么理解?
在 JavaScript 中,Promise 是用于处理异步操作的一种方式,它提供了一种更加直观和便捷的方式来处理异步操作的结果。而 async/await 则是在 Promise 的基础上提供了一种更加直观和简洁的编写异步代码的方式,使得异步代码的书写和阅读更加容易和顺畅。async/await 使得异步操作可以像同步操作一样被编写,提高了代码的可读性和可维护性。
一个async函数调用时需要一个await,但是await需要放在async函数内部,是否存在鸡生蛋蛋生鸡的问题?基于promise的话,写出一个aync函数是怎么执行的。
这个问题看起来确实像是鸡生蛋蛋生鸡的问题,但实际上并不是。在 JavaScript 中,async 函数是一个特殊的函数,它返回一个 Promise 对象。当调用 async 函数时,它会立即执行,并返回一个 Promise 对象,而不会等待内部的异步操作完成。
在 async 函数内部,可以使用 await 关键字来等待一个 Promise 对象的解决。这意味着在 async 函数内部,可以使用 await 来等待另一个 async 函数或返回 Promise 的函数执行完成,并获取其结果。这种机制使得异步操作的处理更加直观和顺序,并且不会阻塞主线程。
因此,虽然在调用 async函数时需要等待其返回的 Promise 对象,而在 async 函数内部又可以使用 await 来等待其他异步操作的完成,但这并不是鸡生蛋蛋生鸡的问题,而是 JavaScript 中异步编程机制的一种规范。
async funtion asyncFunction(){
const result =await fn()
}
function fn() {
console.log("hello world")
}
function asyncFunction() {
return new Promise((resolve, reject) => {
fn(); // 调用 fn 函数
resolve(); // 执行完成后,手动调用 resolve
});
}
function fn() {
console.log("hello world");
}
// 调用 asyncFunction 函数
asyncFunction()
.then(() => {
console.log("Async function executed successfully");
})
.catch(error => {
console.error("Error:", error);
});
你了解防抖和节流吗?
函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。
函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll函数的事件监听上,通过事件节流来降低事件调用的频率。
防抖函数的应用场景:
- 按钮提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次
- 服务端验证场景:表单验证需要服务端配合,只执⾏⼀段连续的输⼊事件的最后⼀次,还有搜索联想词功能类似⽣存环境请⽤ lodash.debounce
节流函数的适⽤场景:
- 拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动
- 缩放场景:监控浏览器 resize
- 动画场景:避免短时间内多次触发动画引起性能问题
js有哪些基本数据类型?
JavaScript 共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。
其中 Symbol 和 BigInt 是 ES6 中新增的数据类型:
- Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
- BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
这些数据可以分为原始数据类型和引用数据类型:
- 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
- 堆:引用数据类型(对象、数组和函数)
两种类型的区别在于存储位置的不同:
- 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
- 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:
- 在数据结构中,栈中数据的存取方式为先进后出。
- 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。
在操作系统中,内存被分为栈区和堆区:
- 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
- 堆区内存一般由开发者分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。
怎么去判断变量的数据类型?
- typeof
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof []); // object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object
其中数组、对象、null都会被判断为object,其他判断都正确。
- instanceof
instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
可以看到,instanceof只能正确判断引用数据类型,而不能判断基本数据类型。instanceof运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的prototype属性。
- constructor
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
constructor有两个作用,一是判断数据的类型,二是对象实例通过constrcutor对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了:
function Fn(){};
Fn.prototype = new Array();
var f = new Fn();
console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
- Object.prototype.toString.call()
Object.prototype.toString.call()使用 Object 对象的原型方法 toString 来判断数据类型:
var a = Object.prototype.toString;
console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));
Object.prototype.toString 返回值是什么
Object.prototype.toString 方法返回一个表示对象的字符串。
如果此方法被调用的对象不是一个原始值,它会返回该对象的内部属性 [[Class]] 的值,格式为 [object class]。其中,class 表示对象的类型,比如 Array、Object、Function 等。
举个例子,对于一个数组,调用 Object.prototype.toString 方法会返回 "[object Array]"。
typeof使用时有哪些注意事项呢?
使用 typeof 时需要注意以下几点:
-
返回字符串类型:typeof 返回的是一个表示操作数类型的字符串。
-
无法区分对象和数组:typeof 无法准确区分数组和对象,它们都会返回 "object"。
-
无法区分 null 和对象:typeof null 返回 "object",这是 JavaScript 的历史遗留问题,因此无法准确区分 null 和对象。
-
可以区分函数:typeof 能够准确区分函数,返回 "function"。
-
未声明变量会返回 "undefined":使用 typeof 检查未声明的变量会返回 "undefined"。
-
不是一个函数:typeof 不是一个函数,而是 JavaScript 中的一个操作符。
数组常见的方法有哪些?
数组常见的方法包括:
- push():向数组末尾添加一个或多个元素,并返回新的长度。
- pop():删除数组末尾的元素,并返回被删除的元素。
- shift():删除数组的第一个元素,并返回被删除的元素。
- unshift():向数组的开头添加一个或多个元素,并返回新的长度。
- concat():用于连接两个或多个数组,并返回新数组。
- join():将数组的所有元素连接成一个字符串。
- slice():从数组中截取一部分元素组成新数组,不会修改原数组。
- splice():从数组中添加或删除元素,会修改原数组。
- indexOf():返回数组中指定元素第一次出现的索引,如果不存在则返回 -1。
- lastIndexOf():返回数组中指定元素最后一次出现的索引,如果不存在则返回 -1。
- forEach():对数组中的每个元素执行指定操作。
- map():对数组中的每个元素执行指定操作,并返回操作后的结果组成的新数组。
- filter():根据指定条件过滤数组中的元素,并返回符合条件的元素组成的新数组。
- reduce():对数组中的所有元素执行指定操作,返回一个累加的结果。
- reverse():颠倒数组中元素的顺序,原地修改数组。
- sort():对数组元素进行排序,原地修改数组。
- every():检查数组中的所有元素是否都满足指定条件。
- some():检查数组中是否至少有一个元素满足指定条件。
- find():返回数组中满足指定条件的第一个元素。
- findIndex():返回数组中满足指定条件的第一个元素的索引。
js中常见的异步有哪些?
JavaScript中的异步机制可以分为以下几种:
- 回调函数的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
- Promise的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
- generator的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。
- async函数的方式,async函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await语句的时候,如果语句返回一个 promise对象,那么函数将会等待promise对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。
promise是怎么用的?
Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise的实例有三个状态: Pending(进行中) Resolved(已完成) Rejected(已拒绝)
当把一件事情交给promise时,它的状态就是Pending,任务完成变成Resolved、失败Rejected。
Promise的实例有两个过程: pending -> fulfilled : Resolved(已完成);pending -> rejected:Rejected(已拒绝)
注意:一旦从进行状态变成为其他状态就永远不能更改状态了。
Promise的特点:
- 对象的状态不受外界影响。promise对象代表一个异步操作,有三种状态, pending (进行中)、 fulfilled (已成功)、 rejected (已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是promise这个名字的由来——“ 承诺 ”;
- 一旦状态改变就不会再变,任何时候都可以得到这个结果。promise对象的状态改变,只有两种可能:从 pending 变为 fulfilled ,从 pending 变为 rejected 。这时就称为 resolved (已定型)。如果改变已经发生了,你再对promise对象添加回调函数,也会立即得到这个结果。这与事件(event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的。
Promise的缺点:
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
- 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
总结:Promise 对象是异步编程的一种解决方案,最早由社区提出。Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。
状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。
注意:在构造Promise的时候,构造函数内部的代码是立即执行的
Promise的基本用法
-
创建Promise对象
Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。 Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。
const promise = new Promise(function(resolve, reject) { // ... some code if (/* 异步操作成功 */){ resolve(value); } else { reject(error); } });一般情况下都会使用new Promise()来创建promise对象,但是也可以使用promise.resolve和promise.reject这两个方法
-
Promise方法
Promise有五个常用的方法:then()、catch()、all()、race()、finally。下面就来看一下这些方法。
-
then() :当Promise执行的内容符合成功条件时,调用resolve函数,失败就调用reject函数。then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中第二个参数可以省略。then方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
-
catch():Promise对象除了有then方法,还有一个catch方法,该方法相当于then方法的第二个参数,指向reject的回调函数。不过catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中。
-
all():all方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个promise对象。当数组中所有的promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected。调用all方法时的结果成功的时候是回调函数的参数也是一个数组,这个数组按顺序保存着每一个promise对象resolve执行时的值。
-
race():race方法和all一样,接受的参数是一个每项都是promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。如果第一个promise对象状态变成resolved,那自身的状态变成了resolved;反之第一个promise变成rejected,那自身状态就会变成rejected。当要做一件事,超过多长时间就不做了,可以用这个方法来解决:
-
finally():finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。finally本质上是then方法的特例。
-
Promise.all和Promise.race的区别的使用场景
- Promise.allPromise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
- Promise.all中传入的是数组,返回的也是是数组,并且会将进行映射,传入的promise对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的,除非可迭代对象为空。
- 需要注意,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用Promise.all来解决。
- Promise.race
- 顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决。
我想删掉数组中的第三个元素,可以怎么做?
可以使用数组的 splice 方法来删除数组中的指定元素。
你了解webpack吗?
Webpack 是一个现代 JavaScript 应用程序的静态模块打包工具。它主要用于将多个 JavaScript 文件打包成单个文件,以便在浏览器中加载。Webpack 不仅支持 JavaScript 文件的打包,还可以处理各种类型的文件,如 CSS、图片、字体等,并且支持模块化开发,可以将项目拆分为多个模块进行开发,并通过模块化的方式引用和管理各个模块。
Webpack 的主要特点包括:
- 模块化打包:Webpack 支持将项目拆分为多个模块,并将这些模块打包成一个或多个输出文件。
- 加载器(Loader):Webpack 可以使用各种加载器来处理不同类型的文件,例如使用 Babel 处理 ES6+ 语法、使用 CSS-loader 处理 CSS 文件等。
- 插件系统:Webpack 提供了丰富的插件系统,可以通过插件来完成各种任务,例如优化输出文件、代码分割、压缩等。
- 开发服务器:Webpack 提供了开发服务器,可以在开发过程中实时预览项目,并支持热更新,当源文件发生变化时,浏览器会自动刷新。
- 代码分割:Webpack 支持将代码分割成多个块,可以按需加载,提高应用程序的性能和加载速度。
总之,Webpack 是一个功能强大的静态模块打包工具,可以帮助开发者更高效地管理和打包项目中的各种资源文件,提升开发效率和项目性能。
在本地开发时是如何利用webpack来解决跨域调用的问题?
Webpack 本身并不直接解决跨域调用的问题,而是通过配置开发服务器(Webpack Dev Server)来解决跨域调用问题。
Webpack Dev Server 是一个用于开发环境的轻量级服务器,它具有许多有用的功能,包括实时重新加载、热模块替换(HMR)以及解决跨域问题。
在开发环境中,通常会存在跨域问题,例如前端代码运行在 localhost:8080 上,而后端 API 服务运行在 localhost:3000 上。为了解决这个问题,Webpack Dev Server 提供了一个 proxy 选项,可以将 API 请求代理到后端服务器,从而避免跨域问题。
在 webpack 配置文件中,可以这样配置 proxy 选项:
module.exports = {
// 其他配置...
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000', // 后端API服务的地址
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
},
},
};
以上配置表示将以 /api 开头的请求代理到 http://localhost:3000 ,并将路径重写为空。例如,前端代码发起的请求 http://localhost:8080/api/users 将被代理到 http://localhost:3000/users ,从而解决了跨域问题。
这样,在开发过程中,前端代码就可以通过 webpack 开发服务器代理转发请求到后端服务,避免了跨域问题,方便了开发调试。
webpack热更新是什么?有哪些步骤?
Webpack 热更新(Hot Module Replacement,HMR)是一种开发时的特性,它使得在开发过程中可以在不刷新整个页面的情况下更新模块。这意味着当你修改了代码后,Webpack 会自动将新的代码注入到运行中的应用程序中,从而实现即时的反馈。
可以理解为模块热替换,指在应用程序运行过程中,替换、添加、删除模块,而无需重新刷新整个应用。例如,我们在应用运行过程中修改了某个模块,通过自动刷新会导致整个应用的整体刷新,那页面中的状态信息都会丢失。如果使用的是 HMR,就可以实现只将修改的模块实时替换至应用中,不必完全刷新整个应用。
具体来说,Webpack 热更新的工作原理如下:
- 当你修改了一个模块后,Webpack 会监测到这个变化,并重新编译这个模块,以及其所依赖的其他模块。
- 然后,Webpack 会通过 WebSocket 将更新的模块代码推送给运行中的应用程序。
- 应用程序接收到更新后的模块代码后,会尝试替换旧的模块代码,同时保持应用程序的运行状态不变。
- 最后,如果模块的替换成功,应用程序会自动应用新的模块代码,从而实现热更新的效果。
热更新可以大大提高开发效率,因为它可以让开发者在不刷新页面的情况下立即看到代码修改的效果,而无需重新加载整个页面。这样就可以更快地进行调试和开发,提高了开发效率。
在使用 Webpack 进行开发时,通常会配合开发服务器(如 Webpack Dev Server)一起使用热更新功能。这样在修改代码后,开发服务器会自动触发热更新过程,从而使得修改立即生效。
webpack打包的流程是什么?
Webpack 的打包流程大致可以分为以下几个步骤:
-
解析配置文件: Webpack 会读取项目根目录下的 webpack.config.js 文件或者其他指定的配置文件,解析其中的配置选项,包括入口文件、输出路径、Loader、插件等。
-
解析模块: Webpack 从入口文件开始递归地解析项目中的所有模块,包括 JavaScript 模块、样式文件、图片等。解析过程中,会根据模块的路径、文件后缀等信息确定模块之间的依赖关系。
-
应用 Loader: 对于非 JavaScript 类型的模块,Webpack 会根据配置中指定的 Loader 对其进行转换和处理。比如,对于 CSS 文件,Webpack 会使用 css-loader 和 style-loader 将其转换为 JavaScript 模块,并将样式注入到 HTML 中。
-
应用插件: 在打包过程中,Webpack 会调用配置中指定的各种插件对打包结果进行优化、压缩、分离等操作。比如,使用 UglifyJsPlugin 插件对 JavaScript 代码进行压缩,使用 HtmlWebpackPlugin 插件生成 HTML 文件等。
-
生成输出文件: 在处理完所有模块并应用了 Loader 和插件后,Webpack 将根据配置中指定的输出路径和文件名,生成最终的打包文件。通常情况下,Webpack 会将所有模块打包成一个或多个 JavaScript 文件,并可以选择是否将其他类型的资源文件(如图片、字体等)一并打包输出。
-
完成打包: Webpack 打包流程完成后,会输出一些统计信息(如打包时间、文件大小等),并且通知开发者打包过程是否成功。
这是一个简单的打包流程,实际上,Webpack 的打包过程还包括了更多的细节和优化策略,比如代码分割、懒加载、模块热替换等。Webpack 通过这些功能可以帮助开发者更高效地管理项目代码,提高项目的性能和可维护性。
一个vue文件webpack怎么打包
Vue 文件经过 Webpack 打包时,通常会经历以下几个主要步骤:
- 解析 Vue 文件: Webpack 会读取 Vue 单文件组件(.vue 文件),并使用相应的 Loader 进行解析。通常情况下,会使用 vue-loader 来处理 Vue 文件,vue-loader 负责解析 Vue 文件中的
<template>、<script>和<style>部分,并将它们转换为 JavaScript 模块。 - 解析模块依赖: 在解析 Vue 文件的过程中,如果遇到了其他的模块依赖(如 import 语句),Webpack 会继续递归解析这些依赖模块,并根据配置中的 Loader 来处理这些模块。
- 应用 Loader 和插件: 对于 Vue 文件中的
<template>、<script>和<style>部分,Webpack 会根据配置中指定的 Loader 来处理它们。比如,对于<template>部分,可以使用 vue-template-compiler 来将 Vue 模板编译为渲染函数;对于<style>部分,可以使用 css-loader 和 style-loader 将样式转换为 JavaScript 模块并注入到 HTML 中;对于<script>部分,可以使用 babel-loader 进行转译等。 - 合并和优化: 在处理完所有模块之后,Webpack 会将所有的 JavaScript 模块合并为一个或多个 bundle 文件,并根据配置中的插件对这些文件进行优化、压缩等操作。比如,可以使用 UglifyJsPlugin 对 JavaScript 代码进行压缩,使用 OptimizeCSSAssetsPlugin 对样式文件进行优化等。
- 输出打包结果: 最后,Webpack 将根据配置中指定的输出路径和文件名,将打包结果输出到指定的目录中。通常情况下,会生成一个或多个 bundle 文件,并可能包括一些额外的资源文件(如图片、字体等),这取决于配置中指定的输出规则。
总的来说,Webpack 在打包 Vue 单文件组件时会将其拆分为 <template>、<script> 和 <style> 三个部分,然后根据配置逐一处理这些部分,并最终合并为一个或多个输出文件,从而完成打包过程。
了解js中的闭包吗?如何形成闭包?
闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
闭包有两个常用的用途;
- 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
- 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
形成闭包的关键在于内部函数引用了外部函数的变量,并且这个引用发生在外部函数执行完毕后。
js有哪些新的进展你了解吗?
HTML篇
CSS篇
使用什么进行页面上的布局?(常见的页面布局)
网页布局描述的是浏览器如何对网页中元素进行排版的。目前前端布局方案主要有三种:
- 传统布局方案(标准流、浮动流、定位流)
- flex布局方案
- grid布局方案
标准流
标准流又称为普通流,是浏览器默认的排版方式,元素会自动从左往右、从上往下的流式排列。主要分为块级元素和行内元素。块级元素独占一行,行内元素水平排列直到占满一行然后换行。
根据元素的布局特性可以区分为:块级元素、行内元素、行内块元素。
块级元素:独占一行(width默认为100%,height为0),宽高内外边距都是可以设置的。常见的块级元素有: div h1-h6 ul ol li dl dt dd form p
行内元素:宽高靠内容撑开,一行可以放多个,不能设置宽高,可以设置水平方向的margin和pedding,不能设置垂直方向的margin和pedding。常见的行内元素有:Img input a
行内元素与行内块元素水平排列时,元素之间会出现大约6px的空白间隙,这是由于元素间空白字符(换行符、空格或制表符)导致。
浮动流
半脱标准流的排列方式,主要用来做网页的横向布局。元素设置浮动后,设置宽高起作用,默认在当前包含块左上一排,如果前面有块级元素,会排在元素的下面。左浮动: float:left 和右浮动:float:right
加了浮动的元素盒子是浮起来的,漂浮在其他标准盒子之上。浮动的盒子需要和父级盒子搭配使用,浮动可以使元素显示模式体现为行内块特性。
定位流
浮动是多个块级元素在同一行显示,定位主要是有层叠的概念。定位分为:相对定位、绝对定位和固定定位。
相对定位(relative):占位定位,不脱标;参考点是自身位置。常用于浮动元素上,用于调节占位浮动溢出父盒子,调节浮动后上下的一两像素
绝对定位(absolute):不占位,完全脱标;参考点是离自己最近的设置了(绝对定位、相对定位、固定定位)的父元素。若父元素无定位,默认参考点为初始包含块。行内元素设置了固定定位和绝对定位,会转为块。
固定定位(fixed):占位,完全脱标,参考点永远是浏览器窗口。
元素层级z-index:只有元素设置了position:absolute | relative | fixed z-index属性才会生效。后来居上原则。有position属性的dom会在没有position属性的dom上面;同级dom直接z-index比较,非同级dom,比较父级z-index。
flex布局
flex是FlexibleBox,弹性布局,用来为盒模型提供最大的灵活性。任何一个容器都可以指定为Flex布局。行内元素也可以使用Flex布局。设为Flex布局后,子元素的float、clear和vertical-aligh属性将失效。采用flex布局的元素称为flex容器,其所有子元素自动成为容器成员,称为flex项目。容器默认存在两根轴:水平的主轴和垂直的交叉轴,项目默认沿水平主轴排列。
容器上有6个属性:
flex-direction:决定主轴的方向(即项目的排列方向),属性值有row、row-reverse、column、column-reverseflex-wrap: 如果一条轴线排不下,如何换行,默认为nowrap,还有wrap、wrap-reverseflex-flow:是前两者属性的简写形式,默认值为row nowrapjustify-content: 定义了项目在主轴上的对齐方式- flex-start:项目向主轴起点对齐
- flex-end:项目向主轴终点对齐
- center:项目居中对齐
- space-between:项目沿主轴均匀分布,首尾两个项目分别靠近起点和终点
- space-around:项目沿主轴均匀分布,各项目之间的间隔相等
align-items: 定义了项目在交叉轴上的对齐方式- flex-start:项目向交叉轴起点对齐。
- flex-end:项目向交叉轴终点对齐。
- center:项目在交叉轴上居中对齐。
- baseline:项目以基线对齐。
- stretch:项目在交叉轴上拉伸以适应容器大小(默认值)。
align-content: 定义了多根轴线的对齐方式- flex-start:多根轴线向交叉轴起点对齐。
- flex-end:多根轴线向交叉轴终点对齐。
- center:多根轴线在交叉轴上居中对齐。
- space-between:多根轴线沿交叉轴均匀分布,首尾两根轴线分别靠近起点和终点。
- space-around:多根轴线沿交叉轴均匀分布,各根轴线之间的间隔相等。
- stretch:多根轴线拉伸以适应容器大小。
项目上有6个属性:
order:定义项目的排列顺序。数值越小,排列越靠前,默认为0flex-grow:定义项目的放大比例,默认为0flex-shrink:定义项目的缩小比例,默认为1,若空间不足,该项目将缩小flex-basis:定义了在分配多余空间之前,项目占据的主轴空间。浏览器根据这个属性计算主轴是否有多余空间。默认为auto,即项目本来的大小flex:flex-grow、flex-shrink、flex-basis的简写,默认为0 1 autoalign-self:允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。默认为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch
grid布局
flex是一维布局、grid是二维布局。
通过在元素上声明display:grid或display:inline-grid来创建一个网络容器。该元素的所有直系子元素将成为网格项目。利用grid-template-columns和grid-template-rows来定义网格中的行和列。
.wrapper {
margin: 60px;
/* 声明一个容器 */
display: grid;
/* 声明列的宽度 */
grid-template-columns: repeat(3
, 200px);
/* 声明行间距和列间距 */
grid-gap: 20px;
/* 声明行的高度 */
grid-template-rows: 100px 200px;
}
什么情况下会出现样式冲突的问题?
样式冲突是指在网页开发中,当多个样式规则应用到同一个元素时,它们之间可能发生冲突或产生意外效果的情况。这种情况通常发生在不同的样式规则之间存在相互影响或覆盖的情况下。
样式冲突在以下情况中会出现:
- 选择器权重冲突:如果两个或多个选择器应用于同一个元素,并且它们具有相同的特异性,则可能会出现样式冲突。
- 样式继承与覆盖:如果某个元素同时继承了多个父级元素的样式,且这些样式有冲突或重复,那么可能会出现样式冲突。此外,如果有一个样式声明覆盖了另一个样式声明,也可能导致样式冲突。
- 全局样式与局部样式:如果在全局样式与局部样式中定义了相同的样式规则,或者在不同的局部央视中定义了相同的类或ID选择器,也可能出现样式冲突。
- 浏览器默认样式:不同的浏览器可能具有不同的默认样式,因此在跨浏览器开发时,可能会出现样式冲突的问题。
- 样式表引入顺序:如果多个样式表按照不同的顺序引入,且样式规则之间存在相互影响,可能会导致样式冲突。通常情况下,后引入的样式表会覆盖先引入的样式表。
怎样避免CSS样式冲突?
- 使用命名约定: 使用有意义的、具有描述性的类名和ID名称,以避免不同模块之间的样式冲突。可以采用 BEM(块、元素、修饰符)等命名约定。
- 限定作用域: 将样式规则限制在特定的父级元素内部,使用类似于局部作用域的技术,如 CSS Modules、Scoped CSS、CSS-in-JS 等。
- 提高选择器特异性: 使用更具体的选择器,以增加样式规则的特异性,避免被其他规则覆盖。可以使用元素类型选择器、类选择器、ID选择器、属性选择器等。
- 使用!important声明: 在特定情况下,可以使用!important声明来强制应用某个样式规则,但应谨慎使用,以免滥用导致维护困难。
- 避免全局样式: 尽量避免使用全局样式,而是将样式规则限制在特定的模块或组件内部,以减少样式冲突的可能性。
- 避免使用通配符选择器: 尽量避免使用通配符选择器(如*),因为它们会匹配所有元素,增加了样式冲突的风险。
- 优先级排序: 在编写样式规则时,可以按照一定的优先级顺序组织,使得优先级较高的样式规则排在后面,以确保其生效。
- 合理组织样式表: 合理组织样式表的结构,可以将样式按照模块、组件或页面进行分组,以便更好地管理和维护。
性能优化篇
分页加载主要解决了哪些问题?
分页加载是一种在网页或应用程序中常见的数据加载方式,它主要解决了以下几个问题:
-
减轻服务器负担: 当页面中需要展示大量数据时,一次性将所有数据加载到页面中会增加服务器的负担和网络传输的成本。通过分页加载,可以将数据分成多个页面,每次只加载当前页面所需的数据,从而减轻了服务器的负担。
-
提高页面加载速度: 分页加载可以避免一次性加载大量数据导致页面加载速度过慢的问题。只加载当前页面所需的数据,可以减少页面的加载时间,提升用户体验。
-
节省带宽和资源: 分页加载可以节省带宽和用户设备的资源,因为只加载当前页面所需的数据,而不是将所有数据一次性传输到用户端。
-
提高用户体验: 通过分页加载,用户可以逐步浏览数据,更方便地查找所需内容,提高了用户体验和页面的可用性。
总的来说,分页加载通过将大量数据分成多个页面,每次只加载当前页面所需的数据,解决了服务器负担、页面加载速度、带宽和资源的消耗等问题,提高了用户体验和页面性能。
为什么不分页加载会产生页面卡顿呢?加载的慢一些的时候页面是卡住的吗?服务器那边为什么会卡顿呢?
不分页加载会导致以下一些问题:
- 页面加载缓慢: 当页面需要展示大量数据时,一次性加载所有数据会导致页面加载速度变慢,因为浏览器需要下载和渲染大量的数据,从而影响用户的使用体验。
- 服务器压力大: 一次性加载大量数据会给服务器带来很大的压力,特别是在高并发情况下,可能会导致服务器响应变慢甚至崩溃。
- 带宽占用高: 一次性加载大量数据会占用大量的带宽资源,特别是对于移动端用户来说,可能会耗费用户的流量。
- 用户体验差: 页面加载缓慢会影响用户的浏览体验,用户需要等待很长时间才能看到想要的内容,容易导致用户流失。
- 资源浪费: 用户可能只关心部分数据,而不是所有的数据,一次性加载所有数据会导致资源的浪费。
因此,分页加载通常被用来解决以上问题,通过将大量数据分成多个页面,每次只加载当前页面所需的数据,提高了页面加载速度,减轻了服务器压力,节省了带宽资源,并改善了用户体验。
当页面加载缓慢时,用户可能会感觉到页面卡顿或卡住的现象。这是因为浏览器需要等待所有资源都加载完成后才能完整地渲染页面,如果某些资源加载速度较慢,浏览器就会停止渲染页面,等待资源加载完成后再继续渲染。这样就会导致页面的响应速度变慢,用户操作页面时可能会有延迟或卡顿的感觉。因此,为了提高用户体验,需要尽量优化页面加载速度,减少页面加载时间。
不分页加载可能导致服务器在处理大量数据请求时出现性能瓶颈甚至崩溃的情况,主要原因包括:
- 大量数据请求: 不分页加载会导致一次性请求大量数据,如果数据量庞大,服务器需要同时处理大量请求,容易超出服务器的负载承受能力。
- 数据库压力: 数据库查询是数据请求的主要操作,大量的数据请求会导致数据库负载急剧增加,如果数据库没有合适的优化措施,可能会导致数据库响应变慢甚至崩溃。
- 网络带宽限制: 大量数据请求会占用大量的网络带宽,如果服务器的网络带宽有限,可能无法满足大量请求的传输需求,导致响应变慢或者部分请求超时。
- 资源竞争: 大量数据请求会导致服务器资源(如CPU、内存、磁盘IO等)的竞争,当请求量超过服务器资源的处理能力时,会导致服务器响应变慢或者出现崩溃现象。
图片的静态资源管理,是怎么做到的?图片是如何存储的,如何拿到的?
你的图片的静态资源管理是通过前端和后端配合实现的。让我来解释一下整个流程:
-
前端上传图片:
- 当用户在富文本编辑器中上传图片时,前端调用了
$uploadImg2方法,将图片文件传递给后端。 - 在
$uploadImg2方法中,前端使用FormData对象将图片文件打包成表单数据,然后发送POST请求给后端。
- 当用户在富文本编辑器中上传图片时,前端调用了
-
后端接收图片:
- 后端的Express路由监听POST请求的
/uploadImg路径。 - 当收到上传图片的请求时,后端调用
saveFiles方法,将接收到的图片保存到服务器或者对象存储服务(如MinIO)中。 saveFiles方法返回一个Promise对象,用于异步处理文件上传过程。
- 后端的Express路由监听POST请求的
-
文件存储:
- 在
saveFiles方法中,图片被保存到MinIO对象存储中,文件名使用了当前时间戳。 - MinIO返回一个包含上传图片信息的对象,其中包括上传后的文件名和存储地址。
- 在
-
前端接收图片地址:
- 当图片上传成功后,后端返回一个包含图片地址的JSON对象。
- 前端接收到这个JSON对象后,可以从中提取图片地址,并将其显示在富文本编辑器中。
这样,通过前后端的配合,实现了图片的上传和获取。前端负责将图片传递给后端,并在编辑器中展示,而后端负责接收图片、保存图片到存储服务中,并返回图片的地址供前端使用。
为什么CDN可以加速资源加载?最近的服务器是如何获得图片的?
CDN(内容分发网络)可以加速资源加载的原因主要有以下几点:
- 就近访问: CDN在全球范围内部署了大量的服务器节点,这些节点分布在不同的地理位置,用户可以就近访问离自己地理位置最近的节点。因为数据传输的速度与距离成反比,就近访问可以大大缩短数据传输的时间,从而加速资源加载。
- 负载均衡: CDN节点之间会相互协作,当某个节点的负载较高时,请求会被分发到其他空闲节点上,这样可以实现负载均衡,避免某个节点被过度访问导致性能下降。
- 缓存机制: CDN节点会缓存用户请求过的静态资源,如果有多个用户请求相同的资源,CDN可以直接从缓存中返回资源,而不需要再次向源服务器请求,从而节省了网络带宽和服务器资源,加快了资源加载速度。
最近的服务器是通过路由器和互联网连接到CDN的服务器节点,然后CDN的服务器节点从存储在自己本地的缓存中获取图片并返回给用户。
是否了解前端性能优化问题?
前端常见的性能优化手段包括:
-
减少 HTTP 请求次数: 合并和压缩 CSS、JavaScript 文件,合并小图片为精灵图,减少页面请求次数,从而加快页面加载速度。
-
优化图片: 使用适当的图片格式(如 JPEG、PNG、WebP),压缩图片大小,使用响应式图片,懒加载或延迟加载图片,以减少页面加载时间。
-
使用 CDN 加速: 将静态资源(如 CSS、JavaScript、图片等)部署到 CDN 上,利用 CDN 的分布式服务器加速资源加载,提高页面加载速度。
-
使用缓存机制: 使用浏览器缓存和服务端缓存,减少重复请求,提高页面加载速度。可以使用 HTTP 缓存控制头(如 Cache-Control、Expires、ETag 等)来控制缓存策略。
-
代码优化: 优化 JavaScript 和 CSS 代码,减少不必要的重绘和重排,避免过度绑定事件,减少内存泄漏,提高页面性能。
-
延迟加载或异步加载资源: 将不必要立即加载的资源延迟加载或异步加载,如使用
defer、async属性加载 JavaScript 文件,或者按需加载模块、组件等。 -
优化 CSS 和 JavaScript 执行顺序: 将关键 CSS 放在页面顶部,避免渲染阻塞;将 JavaScript 文件放在页面底部,以减少对页面渲染的影响。
-
使用前端框架和库: 使用优秀的前端框架和库(如 Vue.js、React、Angular 等),它们通常会针对性能进行优化,并提供了很多性能优化的工具和方法。
-
减少重定向和避免重复加载: 减少页面的重定向次数,避免不必要的重定向;避免重复加载相同的资源,合理利用浏览器缓存和本地存储。
-
使用性能监测工具: 使用性能监测工具(如 Lighthouse、WebPageTest、Google PageSpeed Insights 等)对网站进行性能分析和测试,及时发现和解决性能问题。
通过以上性能优化手段,可以提高网站的加载速度、响应速度和用户体验,提升网站的竞争力和用户满意度。
为什么把js放在最下面就不阻塞呢?js为什么会阻塞呢?js和浏览器渲染有什么关系,为什么浏览器渲染遇到js时要停止?
JavaScript阻塞: 当浏览器解析HTML文档时,如果遇到了JavaScript代码,会立即执行该代码。JavaScript的执行可能会涉及到网络请求、DOM操作、计算等耗时操作,如果这些操作耗时较长,就会阻塞HTML文档的解析和其他资源(如CSS、图片)的下载。这就是常说的“JavaScript阻塞页面渲染”。
浏览器渲染与JavaScript关系: 浏览器的渲染过程包括解析HTML、构建DOM树、计算CSS样式、生成渲染树和布局、绘制和重绘等步骤。JavaScript可以对DOM进行修改,而DOM的修改可能会触发页面的重新渲染。当浏览器遇到JavaScript时,为了避免页面渲染的不一致性,会暂停渲染过程,执行JavaScript代码,然后再继续渲染。这就是为什么浏览器在执行JavaScript时会停止渲染的原因。
将JavaScript放在底部的解决方案: 将JavaScript放在HTML文档底部可以减少阻塞,因为在HTML文档解析完毕后,浏览器就会开始下载并执行JavaScript,不会影响HTML文档的解析和其他资源的下载。这样可以使页面更快地展示给用户,提高用户体验。
Git篇
git项目是怎么部署的?
部署 Git 项目通常涉及以下步骤:
-
准备服务器: 首先,需要有一个服务器来部署 Git 项目。这可以是自己维护的服务器,也可以是云服务提供商(如 AWS、Azure、DigitalOcean 等)上的虚拟服务器。
-
安装 Git: 确保服务器上安装了 Git。如果是 Linux 系统,可以使用包管理器(如 apt、yum)来安装 Git。如果是 Windows 系统,可以从 Git 官网下载安装程序进行安装。
-
克隆项目: 在服务器上使用 Git 命令克隆项目的代码仓库到本地。例如:
git clone <repository_url> -
设置环境: 根据项目的需要,配置服务器环境。这可能涉及安装和配置数据库、运行时环境(如 Node.js、Python 等)、安装依赖项等。
-
构建项目: 如果项目需要构建(如前端项目),在服务器上执行构建命令,生成部署所需的静态文件或可执行文件。
-
配置 Web 服务器: 配置 Web 服务器以提供对项目的访问。这通常涉及创建虚拟主机、配置域名和端口、设置 SSL 证书等。常用的 Web 服务器有 Nginx、Apache、Caddy 等。
-
设置自动部署: 为了方便持续集成和部署,可以设置自动部署工具。这可以是 CI/CD 工具(如 Jenkins、GitLab CI/CD、Travis CI 等),也可以是部署脚本(如 Shell 脚本、Ansible 等)。
-
部署项目: 将项目的代码和静态文件部署到 Web 服务器上。这可以通过简单的复制粘贴、使用 Git pull 更新代码、使用部署脚本等方式完成。
-
测试和监控: 部署完成后,进行测试以确保项目能够正常运行。设置监控系统来监测项目的运行状态,及时发现并解决问题。
-
持续改进: 持续改进部署流程,优化项目的性能、安全性和可靠性。
通过以上步骤,可以成功地部署 Git 项目到服务器上,使其可以被访问和使用。在部署过程中,需要注意安全性、稳定性和可扩展性等方面的问题,以确保项目的顺利运行。
git代码控制的话在平时过程中是怎样使用的?
git init: 在当前目录初始化一个新的 Git 仓库。git clone [url]: 克隆一个远程仓库到本地。git status: 查看当前仓库的状态,显示有变更的文件。git add [file]: 将指定文件添加到暂存区。git commit -m "[message]": 将暂存区的文件提交到本地仓库,附带一条提交信息。git push [remote] [branch]: 将本地分支的提交推送到远程仓库。git pull [remote] [branch]: 从远程仓库拉取最新的更改到本地。git branch: 列出本地仓库的所有分支。git checkout [branch]: 切换到指定的分支。git merge [branch]: 合并指定分支到当前分支。git log: 查看提交历史记录。git diff [file]: 显示工作目录和暂存区之间的差异。git reset [file]: 从暂存区中移除指定文件,但保留工作目录中的更改。git rm [file]: 从版本控制中移除指定文件,并且将其从工作目录中删除。git remote -v: 显示远程仓库的信息。
前端项目多个人开发的时候,有问题的代码,怎么找出来是哪个同学开发的?很早的代码怎么看出来是谁改的?
版本控制系统会记录每次提交的作者信息。通过查看提交历史记录,可以找出问题代码最后一次被谁修改的信息。例如,使用 git blame [file] 命令可以逐行显示某个文件的修改记录及作者。
合并冲突一般是怎么解决的?
首先,需要理解冲突是如何发生的。冲突通常是因为同一个文件的不同部分在不同的分支上进行了修改,而这些修改无法自动合并。
- 查看冲突文件:在冲突发生后,版本控制系统会在冲突的文件中标记出冲突的部分。你可以打开这些文件,查看标记的区域以及冲突的内容。
- 决定如何解决冲突:根据冲突的情况和代码的需求,决定采取何种解决方案。可能的解决方案包括接受你的修改、接受其他分支的修改、或者将两者结合起来。
- 手动解决冲突:对于每一个冲突,你需要手动编辑文件,选择要保留的修改内容。你可以根据需要保留自己的修改、接受其他分支的修改,或者进行修改的组合。在解决冲突后,你需要移除冲突标记,并保存文件。
- 提交解决冲突的修改:解决完所有冲突后,将修改后的文件标记为已解决,并提交到版本控制系统中。这样其他团队成员就可以看到冲突已经解决了。
在解决冲突时,一定要仔细审查每一个冲突,确保你的修改不会破坏代码的功能或造成其他问题。此外,及时的沟通和合作也是解决冲突的关键,确保团队成员之间能够有效地协作解决问题。
小程序篇
微信小程序和使用vue或react开发一个页面有什么不一样的地方吗?(开发一个页面有哪些组成、如何运行的、运行原理)
微信小程序和使用 Vue 或 React 开发一个页面有一些不同之处,主要体现在开发方式、架构和运行环境上。
-
开发方式:
- 微信小程序开发使用的是小程序开发框架和语法规范,需要使用特定的开发工具进行开发、调试和发布。
- Vue 或 React 开发页面可以使用常规的 Web 开发工具,如 VS Code、WebStorm 等,并且可以使用大量的第三方库和工具来辅助开发。
-
架构和组成:
- 微信小程序的页面由 JSON 配置文件、WXML 模板文件、WXSS 样式文件和 JavaScript 逻辑文件组成,其中 WXML 类似于 HTML,WXSS 类似于 CSS,JavaScript 逻辑文件负责页面的交互逻辑。
- 使用 Vue 或 React 开发的页面一般由组件组成,每个组件包括模板、样式和 JavaScript 逻辑。Vue 使用的是单文件组件(.vue 文件),其中包括模板、样式和 JavaScript 代码;React 则可以使用 JSX 语法或者纯 JavaScript 来定义组件。
-
运行环境和原理:
- 微信小程序在微信客户端内部运行,使用了微信提供的 JavaScript 引擎和渲染引擎来解析和渲染页面。
- Vue 和 React 开发的页面则是在浏览器中运行,依赖于浏览器提供的 JavaScript 引擎和渲染引擎。Vue 可以直接在浏览器中解析单文件组件,而 React 则需要通过构建工具将 JSX 语法转换为浏览器可识别的 JavaScript 代码。
小程序和h5有什么区别呢?
微信小程序和H5(移动端网页)在很多方面有所不同,主要体现在以下几个方面:
-
运行环境:
- 微信小程序是在微信客户端内部运行的,用户可以在微信中直接打开和使用小程序。
- H5 页面是在浏览器中运行的,用户需要通过浏览器访问链接来打开和使用页面。
-
开发语言和工具:
- 微信小程序使用小程序开发框架和语法规范进行开发,需要使用微信提供的开发工具进行开发、调试和发布。
- H5 页面开发可以使用传统的前端技术,如HTML、CSS 和 JavaScript,并且可以使用各种前端开发工具进行开发。
-
功能和权限:
- 微信小程序具有一定的功能和权限限制,需要符合微信小程序的规范和审核标准,并且受到微信平台的管理和限制。
- H5 页面的功能和权限相对较为自由,可以实现更多复杂的功能和交互,但在某些特殊权限(如获取设备信息、发送通知等)方面受到浏览器的限制。
-
体验和推广:
- 微信小程序在微信生态圈内具有良好的用户体验和推广机制,可以通过微信内置的分享、扫码等方式进行推广和传播。
- H5 页面的推广相对自由,可以通过各种渠道(如搜索引擎、社交媒体、广告等)进行推广,但需要用户自行点击链接访问页面。
-
使用场景和目标用户:
- 微信小程序适用于需要快速打开、简洁操作的应用场景,如小工具、轻应用、特定行业应用等,主要面向微信用户。
- H5 页面适用于需要更丰富交互和功能的应用场景,可以在浏览器中访问,面向更广泛的移动端用户。
综上所述,微信小程序和H5 页面在运行环境、开发方式、功能权限、推广机制等方面有所不同,可以根据具体的业务需求和用户群体选择合适的开发方式。
TypeScript篇
any和unknown有什么区别
在 TypeScript 中,any 和 unknown 都是用来表示不确定类型的值,但它们之间有一些重要的区别:
any(任意类型):
- any 类型可以被赋值为任何类型的值,也可以赋值给任何类型的值,这意味着它可以绕过 TypeScript 的类型检查。
- 使用 any 类型会放弃 TypeScript 对类型的静态检查,因此在编程过程中可能会导致类型错误的风险。
- any 类型对于从 JavaScript 迁移到 TypeScript 的项目或者需要与动态类型交互的情况可能会有用,但应尽量避免在新代码中使用。
unknown(未知类型):
- unknown 类型表示未知类型的值,它类似于一个类型安全的 any,但是更加严格。
- 与 any 不同,unknown 类型不允许对其进行任何操作,除非是与其他类型进行类型检查或类型转换。
- 使用 unknown 类型可以确保在对值进行操作之前进行类型检查,从而增加了类型安全性。
- 在需要从外部代码中接收未知类型的值,但又不确定该值的确切类型时,通常会使用 unknown 类型。
总的来说,any 类型是一种放宽 TypeScript 类型检查的方式,而 unknown 类型则是一种更加安全和严格的方式,适合在需要对未知类型进行类型检查和转换的情况下使用。
TypeScript中常见的数据类型:
-
基本类型:
- number:表示数字,包括整数和浮点数。
- string:表示字符串。
- boolean:表示布尔值,即 true 或 false。
- null:表示空值。
- undefined:表示未定义的值。
- symbol:表示唯一的、不可变的值。
- bigint:表示任意精度的整数。
-
复合类型:
- object:表示非原始类型,即除了 number、string、boolean、null、undefined 之外的类型。
- array:表示数组类型,可以使用
Array<ElementType>或ElementType[]表示。 - tuple:表示元组类型,用于表示固定长度和特定顺序的数组。
- enum:表示枚举类型,用于定义一组命名的常量值。
- function:表示函数类型,包括参数类型和返回值类型。
- class:表示类类型,用于创建对象的蓝图。
-
其他类型:
- any:表示任意类型,可以为任何值赋值,与 JavaScript 的动态类型相似。
- unknown:表示未知类型,类似于 any,但更加严格。
- void:表示没有类型,通常用于表示函数没有返回值。
- never:表示永远不会发生的类型,通常用于表示抛出异常或无限循环等情况。
你觉得ts和js比可能有哪些优势或者不好的地方
TypeScript 是 Microsoft 开发和维护的一种面向对象的编程语言。它是 JavaScript 的超集,包含了 JavaScript 的所有元素,可以载入 JavaScript 代码运行,并扩展了 JavaScript 的语法。
TypeScript 相比于 JavaScript 的显著优势:
- 静态输入
静态类型化是一种功能,可以在开发人员编写脚本时检验错误,查找并修复错误是当今开发团队的迫切需求。有了这项功能,就会允许开发人员编写更健壮的代码并对其进行维护,以便使得代码的质量更好、更清晰,后期维护更方便。
- 大型项目的开发
有时为了改进开发项目,需要对代码库进行小的增量更改,这些小小的变化可能会产生严重的、意想不到的结果,因此有必要撤销这些变化。使用TypeScript工具来进行重构,变得更容易更快捷。
- 更好的协作
当开发大型项目时,会有许多的开发人员,此时乱码和错误的机会也会增加。类型安全是一种在编码期间检测错误的功能,而不是在编译项目时检测错误,这为开发团队创建了一个更高效的编码和调试过程。
- 更强的生产力
干净的ECMAScript6代码,自动完成和动态输入等因素有助于提高开发人员的工作效率。这些功能也有助于编译器创建优化的代码。
相比较TypeScript,JavaScript也有一些非常明显的优势:
- 人气
JavaScript的开发者社区仍然是巨大而活跃的,在社区中可以很方便的找到大量成熟的开发项目和可用资源。
- 学习成本
由于JavaScript 语言发展的比较早,也比较成熟,所以仍有一大批开发人员坚持使用后他们熟悉的脚本语言JavaScript,而不是学习TypeScript。更多小白先接触 JavaScript 的原因也是他的人气更足,资源更多,可以在学习的道路上少走很多弯路少踩很多坑。
- 本地浏览器支持
TypeScript 代码需要被编译,编译后输出 JavaScript 代码,这是 TypeScript 代码执行时的一个额外的步骤。
- 不需要注释
为了充分利用 TypeScript 特性,开发人员需要不断注释他们的代码,这可能会使项目效率降低。
- 灵活性
JavaScript 的灵活性更足一些,一些开发人员更喜欢灵活脚本语言。
JavaScript 和 TypeScript 的主要差异
TypeScript 可以使用JavaScript 中的所有代码和编码概念,TypeScript 是为了使JavaScript 的开发变得更加容易而创建的。例如:TypeScript 使用类型和接口等概念来描述正在使用的数据,这使开发人员能够快速检测错误并调试应用程序。
- TypeScript 比 JavaScript 更具可读性和可维护性。
- TypeScript 支持通过接口进行抽象,而 JavaScript 则不支持。
- TypeScript 允许开发人员使用装饰器注释代码,而 JavaScript 则不允许。
- TypeScript 支持通过使用命名空间来模块化和组织组件的能力,而 JavaScript 不支持这一点。
- 通过使用可选参数和命名参数等语法元素,TypeScript 比 JavaScript 更具表现力。
- TypeScript 支持泛型和 JavaScript 中不可用的类型推断功能。
- TypeScript IDE 具有更多功能,因为为静态类型语言构建插件和工具更容易。
- 随着代码库的扩展,TypeScript 代码更容易调试,因为可以在编译时而不是运行时发现类型错误。
- TypeScript 实现了 JavaScript 所遵守的有限 ECMAScript 规范之外的其他功能。
React篇
react相比vue来说,你认为有什么更好的地方?
相似之处
- 虚拟 DOM:两者都有虚拟 DOM,都只会更新那些已经修改的对象,这样可以节省 DOM 操作的时间和资源。
- 基于组件的开发模式:两者都有大量的组件库,可以提高代码的重用和开发过程的效率。
- 专注于视图:两者都只关心渲染视图,像路由、状态管理等独立的部分都由其他库去完成。
- JavaScript:两者都依赖 JavaScript。
不同之处
- 学习曲线: Vue 的学习曲线相对较低,更适合初学者上手,而 React 的学习曲线较陡,需要更多的学习和实践。
- 渲染方式: Vue 使用模板语法来编写组件的渲染逻辑,而 React 使用 JSX(JavaScript XML)来构建组件,将 HTML 结构直接嵌入到 JavaScript 代码中。
- 状态管理: Vue 提供了 Vuex 来管理应用的状态,而 React 则需要借助第三方库如 Redux 或 MobX 来管理状态。
- 工具链和生态环境: Vue 提供了 Vue CLI 来快速搭建项目,并且有一整套官方推荐的工具和库;React 则需要结合多个工具和库来构建完整的开发环境,例如 Create React App、React Router、Redux 等。
- 语法:Vue 使用模板语法,类似于 HTML,通过将模板嵌入到 JavaScript 中来定义组件的结构和逻辑。
- React 使用 JSX(JavaScript XML),允许开发者在 JavaScript 中编写类似 HTML 的代码,提供了更直观和灵活的组件编写方式。
所以很多初级前端开发者可以很好的学习 Vue,而学习 React 需要对 JavaScript 相对有一定要求。这也是更多新手前端开发者和初级前端开发者更热爱 Vue 的一个原因。
数据库
服务器收到数据后,怎么存进数据库的?
将数据存入数据库通常涉及以下几个步骤:
-
连接数据库: 首先,需要在服务器端建立与数据库的连接。这通常通过使用数据库提供的客户端库或ORM(对象关系映射)工具来实现。具体的连接方式取决于所使用的数据库类型(如MySQL、MongoDB、PostgreSQL等)和服务器端编程语言(如Node.js、Python、Java等)。
-
准备数据: 在将数据存入数据库之前,通常需要对数据进行处理和准备。这可能包括数据验证、格式转换、加密或其他数据处理操作,以确保数据的完整性和准确性。
-
执行数据库操作: 接下来,可以使用数据库客户端库或ORM工具执行数据库操作,如插入(INSERT)、更新(UPDATE)、删除(DELETE)等。通过执行适当的数据库查询或命令,将准备好的数据存入数据库中的特定表格或集合。
-
处理数据库响应: 在执行数据库操作后,服务器端将收到数据库的响应。通常需要对数据库操作的结果进行处理,以便根据情况做出适当的响应,如返回操作结果、记录日志或执行其他后续操作。
-
关闭数据库连接: 最后,在所有数据库操作完成后,应该及时关闭与数据库的连接,以释放资源并避免潜在的连接泄露或资源浪费问题。
总的来说,将数据存入数据库涉及连接数据库、准备数据、执行数据库操作和处理数据库响应等步骤,需要在服务器端编程中谨慎处理,并遵循安全性和性能方面的最佳实践。
从头实现一个后台的接口,应该怎么做,比如用node写一个接口,大概要做什么事情?
要从头实现一个后台的接口,你可以按照以下步骤进行:
-
环境搭建: 首先,确保你的开发环境中已经安装了Node.js和npm(Node.js的包管理工具)。然后,创建一个新的项目文件夹,并在其中初始化一个新的Node.js项目。
mkdir my-backend-api cd my-backend-api npm init -y -
安装依赖: 在项目文件夹中安装所需的Node.js依赖。这通常包括Express.js(用于构建Web服务器和处理HTTP请求)、body-parser(用于解析HTTP请求体)等。
npm install express body-parser --save -
编写代码: 创建一个JavaScript文件(如
app.js),编写后端接口的代码。在这个文件中,你将定义路由、处理HTTP请求和响应,并与数据库进行交互等。const express = require('express'); const bodyParser = require('body-parser'); const app = express(); const PORT = 3000; app.use(bodyParser.json()); // 定义路由和处理HTTP请求 app.get('/api/users', (req, res) => { // 处理获取用户列表的请求 // 从数据库中查询用户数据并返回 res.json({ users: [...] }); }); app.post('/api/users', (req, res) => { // 处理创建新用户的请求 // 从请求体中获取用户数据并保存到数据库 res.status(201).json({ message: 'User created successfully' }); }); // 启动服务器 app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); }); -
启动服务器: 在命令行中运行你的Node.js应用程序,启动Web服务器。
node app.js -
测试接口: 使用工具(如Postman、curl等)发送HTTP请求,测试你的后台接口是否正常工作。你可以发送GET、POST、PUT、DELETE等类型的请求,检查接口的功能和响应是否符合预期。
-
持续开发和维护: 不断改进和扩展你的后台接口,处理更多的业务逻辑和数据操作,确保接口的安全性、性能和可靠性。
通过以上步骤,你可以从头实现一个基本的后台接口,为你的前端应用程序提供数据服务和支持。
前端接口的域名和后台接口的域名是同一个吗?还是说不一样的域名或者是使用ip来请求接口的?
前端接口的域名和后台接口的域名可以是同一个,也可以是不同的。具体取决于你的项目架构和部署方式。
-
同域名部署: 如果前端应用和后台接口部署在同一个域名下,那么前端可以直接通过相对路径或者绝对路径来请求后台接口。例如,前端部署在
www.example.com,后台接口也部署在www.example.com/api下。 -
不同域名部署: 如果前端应用和后台接口部署在不同的域名下,那么前端需要通过跨域请求的方式来访问后台接口。通常情况下,可以通过CORS(跨域资源共享)来实现跨域请求。前端可以通过配置代理服务器或者在后台接口中允许特定域名的请求来解决跨域问题。
-
使用IP来请求接口: 在一些特殊情况下,前端可能会直接使用后台接口的IP地址来请求接口,而不是通过域名。这种情况通常出现在开发阶段或者特定的部署环境中。
总的来说,前端接口的域名和后台接口的域名可以是同一个,也可以是不同的,具体取决于项目的需求和架构设计。
前后端分离项目:
在前后端分离的项目中,前端接口的域名和后台接口的域名通常是不同的。这种设计可以提高项目的灵活性和可维护性,允许前后端分别独立部署和扩展。 在前后端分离的项目中,前端与后端建立连接的方式通常是通过 HTTP 请求。这种情况下,前端通过发送 HTTP 请求到后端的接口来获取数据或执行操作。连接建立的过程通常包括以下几个步骤:
- 确定后端接口的地址: 前端开发人员需要知道后端接口的地址,这通常是后端开发人员提供的。这个地址可以是一个完整的 URL,包括协议、域名、端口和路径,例如 api.example.com/users。
- 发送请求: 前端使用 JavaScript 或其他适当的工具发送 HTTP 请求到后端接口。这通常是使用浏览器内置的 fetch 函数或者是常见的 HTTP 客户端库,如 Axios、Superagent 等。
- 处理响应: 一旦后端接收到请求并处理完成,会返回一个 HTTP 响应。前端收到响应后,需要根据响应的内容进行相应的处理,例如渲染页面、更新状态等。
- 处理错误: 在网络请求过程中可能会出现各种错误,如网络错误、服务器错误等。前端通常需要对这些错误进行适当的处理,例如显示错误信息、重新尝试请求等。
通过这种方式,前端和后端可以实现数据的交互和页面的展示,实现前后端分离项目的功能。在开发过程中,前后端开发人员需要密切合作,确保前端能够正确地发送请求到后端接口,并正确处理后端返回的数据。
前后端分离项目是怎么部署的?
在前后端分离项目中,前端和后端是分开部署的,通常会分别部署到不同的服务器上。下面是部署前后端分离项目的一般步骤:
-
前端部署:
- 前端部署通常是将前端代码打包成静态文件(HTML、CSS、JavaScript)。
- 静态文件可以托管在各种 Web 服务器上,如 Nginx、Apache、CDN 等。
- 部署时需要将打包好的静态文件上传到服务器上,并配置服务器以正确地提供这些文件。
-
后端部署:
- 后端部署通常是将后端代码(例如 Node.js、Java、Python 等)部署到应用服务器上。
- 应用服务器可以是专门用于部署应用程序的服务器,也可以是云服务提供商提供的服务,如 AWS、Azure、Google Cloud 等。
- 部署时需要确保服务器上安装了运行后端代码所需的环境和依赖,并正确配置应用服务器以运行后端应用程序。
-
配置域名和端口:
- 在部署完前后端代码后,需要配置域名和端口以供访问。
- 前端和后端可以使用不同的域名和端口,也可以使用相同的域名但不同的路径来访问。
- 如果使用不同的域名和端口,需要配置跨域访问策略以允许跨域请求。
-
测试和监控:
- 部署完成后,需要进行测试以确保应用程序能够正常运行。
- 可以设置监控系统来监测应用程序的运行状态,及时发现并解决问题。
-
持续集成和持续部署:
- 可以使用持续集成和持续部署工具(如 Jenkins、Travis CI、GitLab CI/CD 等)来自动化部署流程,提高部署效率和可靠性。
通过以上步骤,可以将前后端分离的项目成功部署到生产环境中,实现用户访问和使用。在部署过程中,需要确保前后端的协作配合,以及良好的监控和维护机制,确保项目的稳定运行。