引言
最近在学习vuex的时候遇到一个问题:
vuex的mutation和action到底有什么实质上的不同?
查询官网后给出了一个解答:
Action 类似于 mutation,不同在于:
- Action 提交的是mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
这其中有这么一段:action可以使用任意异步操作,而mutation只能使用同步操作。
本文不考虑这其中的原理,关注的是另一样事情:javaScript中的同步和异步的概念是什么?
javaScript中的同步和异步
先提一道面试题:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log('i: ',i);
}, 1000);
}
console.log(i);
理解这个面试题,需要了解JS的单线程运行原理(heap,stack以及queue),同异步的原理以及定时器(setTimeout),接下来,我对最近几天查询的资料作一下笔记,并记录一下自己的心路历程,方便日后复盘改善。所以有疏漏之处恳请指出。
JS本身是单线程的
js作为浏览器脚本语言,为了避免复杂性,他的一大特点就是单线程。也就是说js中只有一条流水线,在处理中,所有的加工步骤都是跟着这一条线走的,一个任务要等待上一个任务完成之后才能往下执行。就像在商场排队付费一样,排在队列中的人要等待上一个人付好费才轮到自己付费。再想一想,如果JS是多线程的话,那么多个线程同时对一个dom进行操作:一个想要红色,一个想要蓝色,那么浏览器该渲染成哪个线程的?
那么在一个单线程的JS中,是怎么会产生同步和异步的概念呢?
原因是:js处于浏览器的运行环境中,而浏览器中除了js引擎之外还有其他的引擎。所以将js的线程称之为主线程,为主;而其他的线程称之为工作线程,为辅。
那么具体来说在js中,什么是同步?什么是异步?
JS的同步异步概念
简单来说:同步是不需要等待的任务,异步就是需要一段时间才能得到结果的任务
举几个例子:
console.log('hello')
setTimeout(fn,0)
这里前者可以直接得到hello,是不需要等待时间的同步任务,而后者是计时器,即使他设定了参数0(延时0ms返回信息)它依然是异步任务,关于这个函数在后面详述。
用代码将同步异步描述一下:
A(args...)//同步函数
A(args..., callbackFn)//异步函数
args是两个函数的参数,callbackFn是回调函数,比如:
setTimeout(fn,0)
fn是回调函数,0是参数,这句代码的意思是:在0ms后执行回调函数fn。
那么知道了这一点,我们再深入一点:js是按什么流程执行同步和异步的任务?
JS运行的概念
看到这张图,上面对于同步和异步的概念就可以更具象一点:同步任务是进入stack栈中的任务,在这里排排坐的任务都需要等到上一个任务执行完毕后执行;异步任务是排在最下层的Queue任务队列中的任务,他要等stack中的所有任务全都执行完了,才会进入执行阶段。也就是说stack空了,才会读取任务队列,这是JS的运行机制。
具体的运行机制如下:
摘自阮一峰的网站:http://www.ruanyifeng.com/blog/2014/10/event-loop.html
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个”任务队列”(taskqueue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
(3)一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
消息队列和事件循环
执行栈很好理解,但是最值得注意的是上图底下那层消息队列(queue)
值得注意的是:消息队列是一个先进先出的队列,里面存放的是各类消息。也就是说:先进入这个队列的事件会先被调出到执行栈执行;执行的时候,主线程从队列取出消息执行相应回调函数。
事件循环是主线程从消息队列中取消息和执行的过程。也就是说:主线程不断地从queue队列中取出消息,执行回调函数,再取出消息,再执行…的过程就是事件循环
看这张图,会对异步过程有更直观的了解:
打个比方:当在热季去某家很火爆的餐馆时都要排队,但是顾客又不能在等的时候傻站着吧。于是顾客过来吃饭的时候服务员给一个排队号(发起异步任务)这个时候你可以到处走动了(执行异步事件)当你觉得不想到处逛了,就在餐馆门口等候(给消息队列存放消息)当服务员叫到你的时候,你就可以进餐馆吃饭了(主线程执行回调函数),当你吃完饭后就继续让下一位顾客进来吃饭。
这个例子可能不太精确,但是大致过程如此。
js定时器
这个也需要理解。以下出自阮一峰的博客第五章定时器
定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。
就以前者setTimeout()为例。
setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数
console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);
这个输出结果是:
1,3,2
还要注意一点,上面的推迟毫秒数1000改成了0输出结果还是不变。
MDN:零延迟并不是意味着回调会立即执行。在零延迟调用 setTimeout 时,其并不是过了给定的时间间隔后就马上执行回调函数。其等待的时间基于队列里正在等待的消息数量。
另外,我自己理解的一点不知道对不对:setTimeout()进入消息数列的时间点(开始记推迟时间的时间点)是主线程空余之时。
所以如果这里在1000ms内触发个鼠标点击事件输出4,
console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);
document.addEventListener('click', function(){
console.log(4);
})
输出结果会是:
1,3,4,2
回到面试题
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log('i: ',i);
}, 1000);
}
console.log(i);
这里大家看到了setTimeout(),第一反应就是这是异步函数,所以其中的console.log作为回调函数肯定不会先执行,可以在脑中将这个函数放到处于底层的队列中去。
那么,先作为同步函数执行的是:for函数和处于外部的console.log(i);函数。
也就是说:在for循环执行完毕后(i=5)先由外部函数打印,这时候stack中的函数执行完了,再从数列中调取setTimeout()的回调函数(因为for循环了五次,所以这个也是是五次哦)
//输出
5
i: 5
i: 5
i: 5
i: 5
i: 5
如果将这道题改成这样,就更好理解了:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log('i: ',i);
}, 1000);
console.log('x: ',i);
}
console.log(i);
输出是什么?
//输出
x: 5
x: 5
x: 5
x: 5
x: 5
5
i: 5
i: 5
i: 5
i: 5
i: 5
如果对这一类面试题很感兴趣,可以看这篇文章:
破解前端面试(80% 应聘者不及格系列):从 闭包说起
3.08补充:
上面提到的这篇文章又追问:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log('i: ',i);
}, 1000);
}
console.log(i);
如果想让这个题目的输出是:5 0 1 2 3 4,应该怎么修改这道题?里面给出一个解法利用了“JS 中基本类型(Primitive Type)的参数传递是按值传递(Pass by Value)的特征“,这句话有点抽象,我们用代码理解:
var x = function (i){
setTimeout(function(){console.log('i: ',i);}, 1000);
}
for (var i = 0; i < 5; i++) {
x(i);
}
console.log(i);
输出结果是
5
undefined
i: 0
i: 1
i: 2
i: 3
i: 4
接下来解释一下,为什么这么写可以:要实现上面面试官的要求,先输出5,再输出0 1 2 3 4,只需要每次setTimeout的回调函数可以接收到i值就可以了。
所以看上面的代码:x里面是一个异步函数,先不会执行,先执行的是for循环,for循环依次将0到4传递进x(),但是x()是延时1000ms才会有输出结果的,所以for循环完毕后将i=5传给外部函数,之后是外部console.log()打印出5。到这一步,输出:5。
现在同步任务执行完了,js引擎开始在数列中依次读出x(i)(i=0~5)的回调函数,就会有执行结果:
i: 0
i: 1
i: 2
i: 3
i: 4
至于中间为什么会输出undefined,emm…