最近看到一些js的问题,稍微研究了一下,可能还是有些地方理解比较肤浅不到位,可能也有错误。
单线程 同步任务 异步任务 执行栈 任务队列
js是单线程的,意味着所有任务都像排长队一样排成一条,执行完一个再执行下一个,这些任务就叫同步任务(synchronous)。
同步任务执行的环境就是主线程,这条长队就是执行栈(execution context stack)。
而这样就导致有时有些任务如果停留很久(比如等待一个打字很慢的人输入他的账号密码),其他的任务就被延迟执行,这会影响到运行效率。
所以就有了异步任务,将这些需要等待的任务,移出执行栈让他自己等待,在某些时候再将其放入另外一条长队中。
这些需要等待或者需要延迟执行的任务就是异步任务(asynchronous),这个另外一条长队就叫任务队列(taskqueue)。
将这些异步任务移出之后,系统可以继续执行主线程执行栈中的同步任务。
就好比你泡面,加水之后等泡面好的这段时间,你可以先去上个厕所什么的,等上完厕所泡面也差不多泡好了,可以直接吃,就提高了效率。
事件与回调函数
任务队列,里面排队的是一个个事件(event)。
当某些异步任务等待的事情结束了(比如那个打字很慢的人终于输完了自己的账号密码),就会产生一个事件,放入队列中。
等于说告诉主线程这个任务可以继续做接下来的事了,不用再等了。
主线程执行完执行栈中的同步任务,就过来检查任务队列,看看有哪些异步任务是可以继续执行的,如果有,就将其放入执行栈继续执行。
回调函数(callback)是什么呢,就是指那些异步任务,等待完了之后要执行那些事情
比如假设刚才那个打字很慢的人,主线程告诉他,他输入完用户名密码就给他一百万,
但是他打字真的很慢,所以主线程就把他当成一个异步任务踢出执行栈,等他自己慢慢输入,主线程自己就做自己的事情去了
过了十年,他终于输入完了,然后就产生了一个事件,放入任务队列
主线程过会儿检查任务队列的时候,一看woc这货终于输入完了,然后把它调回执行栈,给他发钱
这个给他一百万的函数,就是回调函数
回调函数就是这些被主线程暂时搁置的代码,每个异步任务都要指定对应的回调函数,不然主线程不知道等你半天之后要干嘛。
事件循环
事件循环(event loop)又是什么呢?
其实很简单,主线程不断重复的执行执行栈中的代码,然后去读取任务队列中的事件的过程就是事件循环。
主线程执行执行栈中的代码->检查任务队列中的事件->执行对应事件的回调函数->又执行执行栈中的代码->又去检查任务队列->…loop…
注意主线程会先执行完执行栈中的代码再去检查任务队列。
setTimeout()/setInterval()
二者都为定时器,一般理解为延迟若干时间后执行某些事。
setTimeout()只执行一次,setInterval()每隔若干时间都会执行一次。二者其他类似,这里拿setTimeout作说明。
setTimeout(回调函数,延迟时间)函数有两个参数。
第一个参数为回调函数,标明时间到达后执行什么事情。
第二个参数为延迟时间,标明延迟多少时间执行。
两个函数都有返回值,返回一个数字,可理解为该计时器的标识(用于之后清除这个计时器)。
var timer = setTimeout(() => {
console.log('你浪费了人生的5秒时间');
}, 5000);
var timecount = 0;
var timer1 = setInterval(() => {
timecount++;
console.log('你的时间正在流逝 +' + timecount + 's');
}, 1000);
执行结果:
还在一直继续执行下去。
注:这里应该就能看出一些异步执行的特点了,队列里有先后顺序。
clearTimeout()和clearInterval()
用于停止一个正在执行的计时器
传入参数为前文说的计时器标识
如:
var timecount = 0;
var timer1 = setInterval(() => {
timecount++;
if(timecount>5){
clearInterval(timer1);
return
}
console.log('你的时间正在流逝 +' + timecount + 's');
}, 1000);
结果:
注意以下两种写法都可以,都是传入一个匿名函数当回调函数
前者用了ES6的箭头表达式
setTimeout(() => {
//内容
}, timeout);
setTimeout(function() {
//内容
}, timeout);
几个例子
第一个例子:
先自己想想输出结果
console.log(1);
setTimeout(() => {
console.log(2);
}, 1000);
setTimeout(() => {
console.log(3);
}, 0);
console.log(4);
执行结果顺序为1432
因为1和4的输出都在执行栈中,2和3在任务队列中。
所以先执行执行栈中的代码,再检查任务队列中的事件。
1和4 按照先后顺序输出,
2和3 由于3设置的延迟时间是0,所以早于2,在消息队列中排在2前面。
于是最终的输出顺序是1432。
第二个例子:
先自己想想输出的结果
function foo() {
console.log('first');
setTimeout(() => {
console.log('second');
}, 5);
}
for (var i = 0; i < 20; i++) {
foo();
}
以上输出结果为,先输出了全部(20次)的first,然后输出全部的second
所有的first都在执行栈中,所有的second都在任务队列中,所以所有的first都在second之前输出。
还有两个有关的经典题:
思考一下两者的执行结果
for (var i = 0; i < 3; i++) {
setTimeout(function() {
i += i;
console.log(i);
}, 1000)
}
var i = 1;
console.log(i);
以及
for (var i = 0; i < 3; i++) {
setTimeout(function() {
i += i;
console.log(i);
}, 1000)
}
console.log(i);
前者结果 1 2 4 8
后者结果 3 6 12 24
首先结果与定时器设置的延迟时间无关,设置成0ms也是一样的结果
执行结果源于异步执行
然后作用域问题,js中for不存在私有作用域,下面执行的console.log(i)中的i继承于for循环的i,循环执行完后i为3
前者将i重新赋值为1,后者i仍为3
按照执行顺序,循环之下的console.log在主线程执行栈中,先执行,然后再执行任务队列中的三次function
所以前者结果为 1,2,4,8 后者结果为3,6,12,24
其他还有些关于其他(比如ES6中的Promise对象)的异步问题(优先级啊之类的),这里先不研究。