Javascript 异步编程(三)
并行?并发?异步?
同步:synchronous: 指所有任务按出现的先后顺序依次执行 如果出现阻塞的任务,那么线程就会等待这个任务完成,接着执行下一个任务。
异步:asynchronous:不保证所有任务按出现的顺序执行
并发:concurrent:从宏观上,某个时间段里面多个程序都得到了运行,但不是说“同时运行”
并行:parallel:在多核心下,因进程和线程独立运行,且多个线程之间共享数据,程序可以同时运行。
定时器
常用的回调函数有:
- setTimeout
- setInterval
- setImmediate(Node.js)
- requestAnimationFrame
https://zhuanlan.zhihu.com/p/55129100
setTimeout
作用:延迟指定的时间来调用函数或计算表达式。
语法:setTimeout(func /**函数,必选*/,code /**表达式,可选*/ ,milliseconds /**执行需等待的毫秒数,必选*/param1, param2, .../**传递给函数的参数,可选*/)
具体用法参加《Javascript异步编程(一)》
setInterval
作用:按照指定的周期(以毫秒计)来调用函数或计算表达式。
语法:setInterVal(func /**函数,必选*/,code /**表达式,可选*/ ,milliseconds /**每次执行将延迟的毫秒数,必选*/param1, param2, .../**传递给函数的参数,可选*/)
原理
console.log('sync...',1);
setInterval(()=>{
console.log('sync...',2)
},2000);
console.log('sync...',3)
流程分析:
- 主程序调用栈
- 调用webAPI-
setInterVal
- 定时器线程计数2s
- 每隔2s事件触发线程将回调放入任务队列
- 主线程通过Event Loop遍历任务队列执行回调
console.log('sync...',2)
注意
- IE9及以下版本不兼容(
setTimeout
&setInterval
)传递额外参数,可使用polyfill - setInterval,delay最小为10ms。意味着无法设置为0秒间隔,但可以尝试使用postMessage实现
- 使用
clearInterval(timerId)
关闭指定定时器 - 确保回调函数执行时长小于delay时间
- 同setTimeout一样,setInterval回调中的this永远指向global
- 不推荐使用
setInterval(code,delay)
,有安全风险。
为什么不建议使用setInterval
如果任务实际耗时超过delay,会出现同一时间触发多个回调。
有以下场景,每隔1s调用服务
// 时间间隔大于delay
let count = 5
let intervalTimer = setInterval(function () {
if (count <= 0) {
clearInterval(intervalTimer)
return
}
/*模拟延时任务*/
let timeoutTimer = setTimeout(function (count) {
console.log(`the ${count} is running`)
clearTimeout(timeoutTimer)
}, Math.floor(Math.random() * (10000) + 1000),count)
count--
}, 1000)
从上图可知,xhr响应无法按照顺序返回,这样就会导致无法正常处理结果
折中方案
function moreBetterInterval (count) {
// 1s后调用
setTimeout(function (countDown) {
console.log(count +' is begin')
let timeoutTimer = setTimeout(function (times) {
console.log(`the ${times} is running`)
clearTimeout(timeoutTimer)
times--;
if(times>0){
moreBetterInterval(times)
}
}, Math.floor(Math.random() * (10000) + 1000),countDown)
}, 1000,count)
}
moreBetterInterval(5)
可以保证递归之前已执行完回调,但无法保证按照一定的时间间隔。
实际应用场景
- 短信倒计时
let countDown=60;
let timer=setInterval(function () {
countDown--;
if(countDown<0){
clearInterval(timer);
countDown=60
}
},1000)
- 显示当前时间
setInterval(function () {
let d = new Date()
$('#clock')[0].innerHTML = d.toLocaleTimeString()
}, 1000)
setImmediate
仅在Internet Explorer和Node.js下可用
作用:在循环事件任务完成后马上运行指定代码
语法:setImmediate(func /**函数,必选*/,code /**表达式,可选*/,[ param1,param2,...]/**传递给函数的参数,可选*/)
;
setImmediate与setTimeout(func,0)?
setTimeout(func,0)
- 在HTML5中,如果是0,会根据嵌套层数初始化不小于4ms,各浏览器具体实现不同
- 在Node.js中,如果是0,则初始化为1ms
谁先谁后,不一定~
需要考虑
- 是在哪个阶段注册的,如果在定时器回调或者I/O回调里面,setImmediate肯定先执行。如果在最外层或者setImmediate回调里面,哪个先执行取决于当时机器状况。
- 当前的Event Loop的任务队列的情况,如果在队尾,那也要先执行前面的事件,这样也无法保证立即执行
requestAnimationFrame
作用:接受一个动画执行函数作为参数,这个函数的作用是仅执行一帧动画的渲染,并根据条件判断是否结束,如果动画没有结束,则继续调用requestAnimationFrame并将自身作为参数传入
语法:requestAnimationFrame(func /**函数,必选*/)
细节:以60FPS(每帧16.7ms)为目标,浏览器内部会选择渲染的最佳时机
与setTimeout动画区别:
-
setTimeout(func,16.7)
:容易卡顿
原因有两个:
- 实际执行时间晚于设定的延迟时间,出现卡顿
- 与浏览器刷新率有关,不同设备的屏幕刷新频率可能会不同,而setTimeout只能设定固定的时间间隔,无法保证与刷新率同步,容易丢帧
-
requestAnimationFrame
能节省CPU开销,当元素隐藏或不可见时,会停止渲染。而setTimeout仍在后台执行。
用途
- 实现一帧的函数节流
- 动画
注意
- 使用
cancelAnimationFrame(id)
关闭渲染动画 - IE10以下不兼容,可使用setTimeout进行polyfill 从而模拟帧率尽量适配刷新率
结尾
通过以上定时器,最显著的共同点是:回调。
初一看,回调没有问题呀,可以延迟计算。请想下以下情景:
- 嵌套回调
let msg = document.getElementById('msg')
$('#btn').click(function (evt) {
msg.innerHTML += `${new Date()} processing btn click callback... <br>`
setTimeout(function request () {
msg.innerHTML += `${new Date()} processing setTimeout callback...<br>`
$.get('https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js', function (data, status) {
msg.innerHTML += `${new Date()} processing ajax callback...<br>`
})
}, 500)
})
这里我们用了三个函数嵌套,这种代码就被称为“回调地狱(callback hell)”,这样的代码难以编写,难以理解而且难以维护
- 控制反转
$.get('https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js', function (data, status) {
msg.innerHTML += `${new Date()} processing ajax callback...<br>`
})
即自己程序的一部分的执行控制权交由某个第三方。在你不确定这个回调能否按照预期执行时,发生意外时很难定位问题。
为了优雅的的处理回调最大的问题:控制反转,有以下方式:
- 回调分离 -->ES6 Promise
-
error-first
风格-->Node.js中会将回调的第一个参数保留用作error
const fs=require('fs')
fs.readFile(__dirname,function(err,data) {
if (err) console.log(err)
//...
})
但还是不优雅,并没有真正解决我们的控制反转问题,只是将我们之前担心的程序异常暴露了出来。
可能现在你希望有API或其他语言机制来解决这些问题。所幸,ES6会给你带来些干货~