JavaScript单线程、同步异步任务、事件循环的那些事儿

最近看到一些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);

结果:
clearInterval()执行结果图
注意以下两种写法都可以,都是传入一个匿名函数当回调函数
前者用了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对象)的异步问题(优先级啊之类的),这里先不研究。


参考资料:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值