文章目录
- 一、项目背景
- setInterval 的坑(性能问题)
- 延伸拓展
- setTimeout轮询不足
- websocket实现
- 二、http传统轮询(短轮询)
- 长轮询
- http长连接
- websocket
- 三、拓展
- 1、频繁请求接口,如何保证获取的数据是最新的(腾讯一面)
- 2、微信扫码登陆(轮询:每隔一秒发送请求)
你可能不知道的setInterval的坑
js基础之setTimeout与setInterval原理分析
一、项目背景
由于项目中需要定时的请求后端接口获取数据实时更新显示在页面,当我们需要做轮询的时候,自然就想到了定时器,setInterval
可以重复执行,所以轮询的时候理所当然的选择了 setInterval
,刚开始效果杠杠滴,可是过了一段时间如果网络波动就会导致定时器不是那么 ‘准时’ ,当时很奇怪,后面查找资料发现这是 setInterval
的一个弊端,
setInterval 的作用是每隔一段指定时间执行一个函数,但是这个执行不是真的到了时间立即执行,它真正的作用是每隔一段时间将事件加入事件队列中去,只有当当前的执行栈为空的时候,才能去从事件队列中取出事件执行。所以可能会出现这样的情况,就是当前执行栈执行的时间很长,导致事件队列里边积累多个定时器加入的事件,当执行栈结束的时候,这些事件会短时间内连续触发,因此就不能到间隔一段时间执行的效果。并且可能会造成性能问题。这就是setIntervel
的累积效应
setInterval 的坑(性能问题)
1、对自己调用的代码是否报错会无视掉。即自己调用的代码即使错误,也会继续执行下去
2、setInterval无视网络延迟
我们在向服务器轮询数据的时候,如果发生网络卡顿的情况,客户端收到请求响应的时间大于interval循环的时间。而setInterval会无视任何情况下继续定时执行,这就会导致了用户的客户端里充斥着客户端的请求。
3、如果你调用的函数需要花较长时间才能完成,那某些调用会被直接忽略。
解决方案:
使用setTimeout
代替setInterval
。
针对 setInterval 的这个缺点,我们可以使用 setTimeout 递归调用来模拟 setInterval,这样我们就确保了只有一个事件结束了,我们才会触发下一个定时器事件,这样解决了 setInterval 的问题。
var timer;
function func(args){
//函数本身的逻辑
...
//函数执行完后,重置定时器
timer = setTimeout(func, 100, args);
}
timer = setTimeout(func, 100, args);
// 异步请求接口情况
function poll() {
setTimeout(function() {
$.get("/path/to/server", function(data, status) {
console.log(data);
// 前端接收到后端返回的数据时发起下一次请求
poll();
});
}, 10000);
}复制代码
如果确实要保证事件“匀速”被触发,那可以用希望的延迟减去上次调用所花时间,然后将得到的差值作为延迟动态指定给setTimeout。但是实际上由于浏览器的一些机制(垃圾回收、JavaScript是单线程)也无法做到很精确的执行。此外,当前浏览器也会将最小的超时时间固定在4ms到15ms之间。因此不要指望一点误差也没有
ps:setTimeout 会出现 this 指向全局的问题,不过现在使用箭头函数就可以很方便避免这种问题了
延伸拓展
由此需求,自己去查看定时器的一些坑的同时,又接着去了解了js的eventloot
事件循环机制,紧接着发现在vue
中的nextTick
也是运用了异步队列思想实现的
setTimeout应用:
- 防抖节流
setInterval应用:
- 原生js实现简单动画(因为需要函数运行得更频繁才不会卡顿)
setTimeout轮询不足
- 因为客户端以固定的频率请求服务器时,当数据更新较快的时候,无法获取到最新数据。当数据更新较慢的时候,又会进行无意义的请求连接
- 因为每次连接都要建立新的
tcp握手挥手
,因此建立Tcp连接是非常消耗资源。网络负载比较大。
websocket实现
VUE中使用websocket通信
websocket简介:
WebSocket 连接本质上就是一个 TCP 连接,和http协议同属于应用层。浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据,实现了浏览器和客户端双工通信,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。只需要建立一次连接
websocket特点:
- 与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443 ,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
- 建立在TCP协议基础之上,和http协议同属于应用层
- 数据格式比较轻量,性能开销小,通信高效。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信(实现跨域方法之一)
- 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL,如ws://localhost:8023
websocket规范:
为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息Upgrade: WebSocket
、Connection: Upgrade
表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了,双方就可以通过这个连接通道自由的传递信息
客户端到服务端:
GET /demo HTTP/1.1
Host: example.com
Connection: Upgrade
Sec-WebSocket-Key2: 12998 5 Y3 1 .P00
Upgrade: WebSocket
Sec-WebSocket-Key1: 4@1 46546xW%0l 1 5
Origin: http://example.com
[8-byte security key]
服务端到客户端:
HTTP/1.1 101 WebSocket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
WebSocket-Origin: http://example.com
WebSocket-Location: ws://example.com/demo
[16-byte hash response]
vue中使用websocket:
<template>
<div>
</div>
</template>
<script>
export default {
data() {
return {
websock: null,
}
},
created(){
//页面刚进入时开启长连接
this.initWebSocket()
},
destroyed: function() {
//页面销毁时关闭长连接
this.websocketclose();
},
methods: {
//初始化weosocket
initWebSocket(){
const wsuri = process.env.WS_API + "/websocket/threadsocket";//ws地址
this.websock = new WebSocket(wsuri);
this.websocket.onopen = this.websocketonopen;
this.websocket.onerror = this.websocketonerror;
this.websock.onmessage = this.websocketonmessage;
this.websock.onclose = this.websocketclose;
},
websocketonopen() {
console.log("WebSocket连接成功");
},
websocketonerror(e) { //错误
console.log("WebSocket连接发生错误");
},
websocketonmessage(e){ //数据接收
const redata = JSON.parse(e.data);
console.log(redata.value);
},
websocketsend(agentData){//数据发送
this.websock.send(agentData);
},
websocketclose(e){ //关闭
console.log("connection closed (" + e.code + ")");
},
},
}
</script>
跨平台的WebSocket通信库socket.io:
跨平台的WebSocket通信库,具有前后端一致的API,可以触发和响应自定义的事件。socket.io最核心的两个api就是emit 和 on了 ,服务端和客户端都有这两个api。通过 emit 和 on可以实现服务器与客户端之间的双向通信。
- socket.emit(action,data,cb() ) :发射一个事件,第一个参数为事件名,第二个参数为要发送的数据,第三个参数为回调函数(如需对方接受到信息后立即得到确认时,则需要用到回调函数)。
- socket.on(action,function(data)) :监听一个 emit 发射的事件,第一个参数为要监听的事件名(对应发送的事件名),第二个参数为回调函数,用来接收对方发来的数据,该函数的第一个参数为接收的数据。
二、http传统轮询(短轮询)
《Web通信中传统轮询、长轮询和WebSocket简介》 客户端定时向服务器发送请求,服务器接到请求后马上返回响应信息并关闭连接。
长轮询
浏览器只需启动一个HTTP请求,其连接的服务器会hold
住此次连接,直到有新消息才返回响应信息并关闭连接(或到了设定的超时时间关闭连接),客户端处理完响应信息后再向服务器发送新的http请求。
- 优点:减少了很多不必要的http请求次数,相比之下节约了资源。
- 缺点:服务器hold连接会消耗资源,需要同时维护多个线程,服务器所能承载的TCP连接数是有上限的,这种轮询很容易把连接数顶满。
http长连接
HTTP1.1通过使用Connection:keep-alive
进行长连接,HTTP 1.1默认进行持久连接。在一次 TCP 连接中可以完成多个 HTTP 请求,但是对每个请求仍然要单独发header
,Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。
websocket
websocket的长连接,是一个真的全双工的Tcp长连接
。长连接第一次tcp链路建立之后,后续数据可以双方都进行发送,不需要发送请求头header
三、拓展
1、频繁请求接口,如何保证获取的数据是最新的(腾讯一面)
《前端频繁请求接口》、《多个异步请求的执行顺序》
简单一些,定义一个全局Id
,每次请求(是一个闭包)前把全局Id自增(比如Id=setTimeout(0);
),然后存一个到当前闭包里,请求结束后,比较闭包里的Id和全局Id,一致则更新UI,否则就是该请求作废了
一开始回答节流,但是节流适合避免短时间内多次请求,但是并不能保证请求返回后按顺序更新UI
2、微信扫码登陆(轮询:每隔一秒发送请求)