for循环内使⽤定时器实现循序中展⽰每⼀个的定时器
在学习js的时候,或者⾯试的时候,会经常碰到这⼀道经典题⽬:
for(var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
console.log('a');
熟悉这道题⽬的⼈⽴马就可以说出答案:
'a'
5
5
5
5
5
结果是先打印字符串'a',然后再打印5个数字5。
有⼈会说这个题⽬并不难,⽽且只要你遇到过这个题⽬,下次再见到基本也不会答错了,但其实这段简单的代码⾥⾯包含了很多js知识。
单线程、任务队列以及事件循环(event loop)
第⼀次看到这段代码的时候,会给⼈⼀种错觉:
1. 会先打印for循环⾥⾯的5次i值,然后才会去打印下⾯的字符串'a'
2. for循环⾥⾯的打印结果会是0,1,2,3,4,⽽不是什么5个5这种奇怪的结果
但是实际运⾏结果跟我们预期的不⼀样,原因就是因为这⾥涉及到了js的运⾏机制。
单线程
JavaScript语⾔的⼀⼤特点就是单线程,也就是说,同⼀个时间只能做⼀件事。
为什么不允许js可以实现多线程?因为如果实现了多线程,⼀个线程创建了⼀个div元素,⽽另外⼀个线程删除了这个div元素,那么这个时候浏览器应该听谁的?
所以为了避免出现这种互相冲突的操作,js从⼀开始就是单线程的,这就是它的核⼼特征。
任务队列
单线程就意味着,所有任务需要排队,前⼀个任务结束,才会执⾏后⼀个任务。如果前⼀个任务耗时很长,后⼀个任务就不得不⼀直等着。
如果排队是因为计算量⼤,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输⼊输出设备)很慢(⽐如Ajax操作从⽹络读取数据),不得不等着结果出来,再往下执⾏。
JavaScript语⾔的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运⾏排在后⾯的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执⾏下去。
于是,所有任务可以分成两种,⼀种是同步任务(synchronous),另⼀种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执⾏的任务,只有前⼀个任务执⾏完毕,才能执⾏后⼀个任务;异步任务指的是,不进⼊主线程、⽽进⼊"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执⾏了,该任务才会进⼊主线程执⾏。
1. 所有同步任务都在主线程上执⾏,形成⼀个执⾏栈(execution context stack)。
2. 主线程之外,还存在⼀个"任务队列"(task queue)。只要异步任务有了运⾏结果,就在"任务队列"之中放置⼀个事件。
3. ⼀旦"执⾏栈"中的所有同步任务执⾏完毕,系统就会读取"任务队列",看看⾥⾯有哪些事件。那些对应的异步任务,于是结束等待状
态,进⼊执⾏栈,开始执⾏。
4. 主线程不断重复上⾯的第三步。
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运⾏机制。这个过程会不断重复。
"任务队列"中的事件,除了IO设备的事件以外,还包括⼀些⽤户产⽣的事件(⽐如⿏标点击、页⾯滚动等等)。只要指定过回调函数,这些事件发⽣时就会进⼊"任务队列",等待主线程读取。
事件循环(event loop)
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运⾏机制⼜称为Event Loop(事件循环)。
主线程运⾏的时候,产⽣堆(heap)和栈(stack),栈中的代码调⽤各种外部API,它们在"任务队列"中加⼊各种事件
(click,load,done)。只要栈中的代码执⾏完毕,主线程就会去读取"任务队列",依次执⾏那些事件所对应的回调函数。
定时器
在了解了刚才那些知识之后,再回过头来看看这段代码:
for(var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
为什么明明定时器的时间设置为了0(setTimeout不写延迟时间参数默认值为0)?定时器却在console.log('a')这句代码运⾏了之后才运⾏?
原来在js的任务队列⾥,除了放置异步操作之外,还会放置定时器事件。
当js代码运⾏到有定时器的地⽅的时候,会把定时器操作放在任务队列的尾部,然后跟它说:“你先排队吧,还没有轮到你,因为同步代码还没有执⾏完。”
这⾥所说的同步代码就是指下⾯的console.log('a')。
也就是说,js认为setTimeout是⼀个异步操作,必须让它排队,它只能在同步代码执⾏结束后才能执⾏。
所以这⾥的原因总结就是这样⼀句话:
定时器并不是同步的,它会⾃动插⼊任务队列,等待当前⽂件的所有同步代码和当前任务队列⾥的已有事件全部运⾏完毕后才能执⾏。
这就是为什么字符串'a'在5个5之前就打印出来的原因。
那么为什么是5个5呢?为什么不是0,1,2,3,4?
这是因为在所有同步代码执⾏完毕之后,for循环⾥的i值早已变成了5,循环已经结束。(注意,for循环的圆括号部分也是同步代码)
这就是为什么打印出来5个5,⽽不是0,1,2,3,4。
所以这段代码真实的运⾏情况你可以假想成这样,便于理解:
for(var i = 0; i < 5; i++) {
}
console.log('a');
setTimeout(function () {
console.log(i);
});
setTimeout(function () {
console.log(i);
});
setTimeout(function () {
console.log(i);
});
setTimeout(function () {
console.log(i);
});
setTimeout(function () {
console.log(i);
});
//先循环,i变成了5,然后打印a,然后再打印5次i
//这⾥只是假想,便于理解
作⽤域和闭包
这道题⽬还会引申出来另⼀个问题:
如果想要for循环⾥的定时器打印出0,1,2,3,4,⽽不是5个5,该怎么办?
答案是:使⽤⽴即执⾏函数。
for(var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function () {
console.log(i);
});
})(i)
}
console.log('a');
打印结果:
'a'
1
2
3
4
这⼜是为什么?
这是因为for循环⾥定义的i变量其实暴露在全局作⽤域内,于是5个定时器⾥的匿名函数它们其实共享了同⼀个作⽤域⾥的同⼀个变量。
所以如果想要0,1,2,3,4的结果,就要在每次循环的时候,把当前的i值单独存下来,怎么存下当前的循环i值??
利⽤闭包的原理,闭包使⼀个函数可以继续访问它定义时的作⽤域。⽽这个新⽣成的作⽤域将每⼀次循环的当前i值单独保存了下来。
let关键字、块作⽤域以及atch语句
如果想实现for循环⾥的定时器打印出0,1,2,3,4,除了闭包,还可以使⽤ES6的let关键字。
for(let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
注意for循环定义i的时候把var换成了let,打印出的结果就是0,1,2,3,4
这是问什么呢?
因为let关键字劫持了for循环的块作⽤域,产⽣了类似闭包的效果。并且在for循环中使⽤let来定义循环变量还会有⼀个特殊效果:每⼀次循环都会重新声明变量i,随后的每个循环都会使⽤上⼀个循环结束时的值来初始化这个变量i。
let可以实现块作⽤域的效果,但是它是ES6语法,在低版本语法的时候如何⽣成块作⽤域?
答案是:使⽤atch语句。
看下⾯的效果:
for(var i = 0; i < 5; i++) {
try {
throw(i)
} catch(j) {
setTimeout(function () {
console.log(j);
});
}
}
//打印结果0,1,2,3,4
神奇的效果出现了!
这是因为atch语句的catch后⾯的花括号是⼀个块作⽤域,和let的效果⼀样。所以在try语句块⾥抛出循环变量i,然后在catch的块作⽤域⾥接收到传过来的i,就可以将循环变量保存下来,实现类似闭包和let的效果。
好了,这就是关于这道⾯试题涉及到的知识。
好了,以上都是⽹上通篇的⽂字表述案例展述,下来来展⽰⼀个我⾃⼰做的案例,
案例需求:
列表,循环展⽰每个单个模块,并每个模块内都有展⽰定时器的字段,并且进⾏倒计时
html
<view class="groupPur-div" v-if="datalist.length > 0">
<figure class="groupPur-div-figure"
v-for="(item,index) in datalist"
:key="index"
@click="goSPdetail(item)" >
<!-- 商品图⽚ -->
<div class="groupPur-div-div">
<image class="groupPur-div-img"
mode="widthFix"
:src="item.picUrl" alt />js实现轮播图最简代码
</div>
<figcaption>
<!-- 商品名称 -->
<h1 class="groupPur-div-h1">{{item.name}}</h1>
<view class="progess-div">
<view :class="item.stock == 0 ? 'progess-div1 progess-div1-gray' : 'progess-div1'"
:>
{{ item.braNum }}⼈
</view>
<view v-if="item.braMaxNum-item.braNum != 0"class="progess-div2">{{ item.braMaxNum }}⼈</view> </view>
<div class="groupPur-div-div1">
<h2 class="groupPur-div-h2">
<!-- 商品最低价 -->
<span class="groupPur-div-span">
<span style='color: #3F3F3F;'>最低</span>¥
</span>
{{item.lowPrice || 0}}
<!-- 商品原价 -->
<span class="groupPur-div-span1">¥{{dsPrice || 0}}</span>
</h2>
<!-- @click="goSPdetail(item)" -->
<span class="groupPur-div-span3" >
去购买
</span>
</div>
<view class="countDown">距离结束:
{{dates['date'+index] || 0}}天
{{dates['hour'+index] || '00'}}:
{{dates['minutes'+index] || '00'}}:
{{dates['seconds'+index]|| '00'}}</view>
</figcaption>
</figure>
<rlloading :loading="loading"></rlloading>
<view class="groupPur-bottom">{{text}}</view>
</view>
js 》 data
// 状态⽂字
text:'',
// 时间
dates:{},
// for循环数据
datalist:[],
js 》获取数据,并处理时间
/**
* 获取列表数据
*/
getList(strat,type) {
var that = this;
var data = {
page:this.page,
limit:this.size,
};
// 不需要不⽤写
strat && (data.sort = strat)
type && (data.sortType = type)
homePicList(data).then(response => {
// 轮播图数据
that.adList = response.data.data.adList
// 商品列表数据
that.datalist = response.data.data.items
that.loading = false;
// 每个商品进度和时间
for (var i = 0; i < that.datalist.length; i++) {
/
/ 删除(测试)
// that.datalist[i].braMaxNum = 10
// 计算进度条长度
if(that.datalist[i].braMaxNum-that.datalist[i].braNum === 0) {
that.datalist[i].value = 100
}else if(that.datalist[i].braMaxNum === that.datalist[i].braNum){
that.datalist[i].value = 0
}else{
that.datalist[i].value = that.datalist[i].braNum / that.datalist[i].braMaxNum * 100
}
// console.log(i)
/
/ 结束时间减现在时间
let systemTime = Date.parse(new Date())
// 删除(测试)
// that.datalist[i].endTime = 1621218764000
let maxtime = that.datalist[i].endTime - systemTime/1000;
// let maxdate = maxtime ;
// setInterval(this.fun(i), 1000)
that.CountDown(maxtime,i)
}
})
},
/
**
* 计算时间
*/
CountDown(maxtime,i) {
this.intervalFunc= setInterval(() => {
if (maxtime >= 0) {
this.dates['date'+i] = Math.floor(maxtime / (24 * 3600))
let leave1 = maxtime % (24 * 3600 );
this.$set(this.dates, 'hour'+i, Math.floor(leave1 / 60 / 60))
this.$set(this.dates, 'minutes'+i, Math.floor((leave1 / 60) % 60))
this.$set(this.dates, 'seconds'+i, Math.floor(leave1 % 60))
if (this.dates['hour'+i] < 10) {
this.$set(this.dates, 'hour'+i, 0 + '' + this.dates['hour'+i])
}
if (this.dates['minutes'+i] < 10) {
this.$set(this.dates, 'minutes'+i, 0 + '' + this.dates['minutes'+i])
}
if (this.dates['seconds'+i] < 10) {
this.$set(this.dates, 'seconds'+i, 0 + '' + this.dates['seconds'+i])
}
--maxtime;
} else {
clearInterval(this.intervalFunc);
}
}, 1000);
},
闭包的使⽤可以试⼀试
fun(){
function aaa(){
console.log(1111)
}
}
也可以在定时器那return,打印下看看结果,可能让你有探索闭包的欲望
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论