返回顶部

javascript回调和异步那些事

来源:无 发布时间:2016-04-05

前话

首先我觉得javascript中回调函数和异步编程是这门语言的高级话题了,所以我在表述的过程中,难免有不到位的地方!所以我在这里仅只记录我目前对这些概念的理解!大家在学习的同时,应该更多的集思广益、求同存异,从多个点、角度来学习,这样方能提升自己!另外这边文章不会把相关概念说得很高级化,不会让大家看完之后,然后云里雾里,对相关概念更陌生了!相反,我会从简单的概念理解入手,相关概念会结合生活中一些简单例子做比喻,从而化抽象于具体,方便大家理解!

相关概念术语剖析

同步

我想在这里举个例子来加深对同步概念的理解!大家在大学时,肯定有去食堂排队吃饭的经历吧!这里食堂排队打饭可以看着同步的,大家依次按自己排队的顺序先来先打饭的规则从食堂阿姨那里取得餐食。在javascript中,同步脚本要求写在前面的脚本先执行,并且等执行完之后再执行后面的脚本!这样有个很不好的问题就是,如果写在前面的脚本将要花费很多的时间的话,那么后面的脚本也将被阻塞很长的时间,从而不被执行!

异步

我觉得异步跟同步放在一起来理解更容易,所以决定继续援用上面食堂打饭排队的例子!现在的实际情况是————一位同学已经排了很久的队了,眼看他马上就可以从阿姨那里取得食物了,排在他前面的同学的人数也屈指可数了,可是现在他突然接到他女朋友打过来的电话,说她现在不用去老师那里讨论论文的事情了,要他一定要等她一起吃饭!ok,听到如此信息,当然他会非常兴奋,因为女朋友因为论文的事已经好久没有和他一起吃过饭了。但现在令他更为尴尬的是,他已经排了好久的队了,而且眼看就快到头了,可是女朋友到食堂还要一点时间,他也不想重新排队,他又不能占着位置不让后面的同学打饭,显然不合理!这该怎么办呢?急中生智,他想到一个两全其美的方法,他继续排队,但是到窗口时候,他可以很礼貌的跟排在他后面的同学说:“同学,我在等人,你先打吧”!(交回cpu控制权,严格来讲,他其实把自己打饭排队位置排到后面了!)这样当看到女朋友的时候,他就可以不用排队直接继续打饭了!(说个题外话,这里举的例子有点奇葩,他为什么不打完饭在座位上等女朋友呢,非得傻傻的等她到了再打饭?哈哈。)ok,这里这个例子足以说明异步这个概念了,简单的来说,在javascript,一个花费很长时间的脚本,并不会影响它后面的脚本的执行,而是向系统交回自己的“占位权”,自己却在某个“合适的时间”(这个“合适的时间点”可能是某个事件触发的时候,也有可能是所以其它脚本都运行完了之后。)交回自己的运算结果!这就是异步,相对同步而已的!

回调函数

理解回调函数关键在于几个点!第一:回调函数本质上跟普通的函数是一样的,其实就是一个函数而已,只是它调用的时间点不同于普通函数而已!第二:回调函数主要在于它调用的时间点,理解这个主要看这个“回”字!你可以把这个“回”字理解成“回头”的意思,它说明我们的回调函数在达到某个条件之后,再来调用这个回调函数!可能是在另外一个函数中调用,也有可能在某个超时调用之后再调用!

单线程

我们同样举一个例子来说明js中单线程的含义!假如我们大学毕业旅游去到一个风景圣地,这里有一条河,我们要穿过一条河,很不巧,只有一座独木桥,我们全班同学要过这条河,大家只能一个一个的过了,这里的独木桥就可以理解为javascript中的单线程了!所谓单线程,其实可以理解为在同一个时间点,只能处理一个事情。相对于多线程来说,这里的河流就可能不止一座桥了,可能有很多桥。大家可以一起通过走不同的桥然后一起到达河对面了。也就是说,多线程可以对“每个人过河”另外增开辟一条“桥”!多线程可以提高系统吞吐量,但是耗用系统资源也会到不可控的局面,容易达到瓶颈、导致系统死机!而单线程简单、高效,并且在系统资源耗费上在一个可控范围内!但吞吐量很小!

事件轮询

按我的理解,在javascript里面,所谓的事件,其实就是当脚本触发某种约定的行为之后,再执行后续脚本的一种编程模式!在javascript中,所有的事件都是异步的,也就是说,并不影响其他脚本的执行!这是为什么呢?因为在js中所有的事件都是在js空闲的时候执行的。说的更直接一点就是在js内部,js始终维护一个事件队列,当某个事件触发的时候,然后它会遍历这个事件队列,发现有事件被触发了,而后它会去执行对应的脚本。

超时调用setInterval和间隙调用setTimeout

1、setInterval表示每间隔多少时间执行依次规定的脚本,格式如下:

setInterval(code,interval);

其中code表示周期执行的脚本,而interval表示每次执行的间隔时间。

2、setTimeout表示过了多少时间之后,再执行对应的脚本,格式如下:

setTimeout(code,interval);

其中code表示过了多少时间之后需要执行的脚本,而interval表示间隔的时间。

其实在javascript在js中最基本的异步操作就是setInterval和setTimeout了,它们两个调用的函数都是异步的,不会影响到其它脚本的运行!值得注意的是,setInterval和setTimeout的第二个参数表示间隔多少时间之后把函数添加到js执行线程尾部,这并不代表到那个时间过后代码一定会执行,同样需要排队等待执行,如果到了那个时间,刚好js线程队列里待执行的脚本刚好是空的,那么就立即执行,如果还有很多待执行的脚本,那么就需要排队等待执行,因为js是单线程的!

举例实践

为了加深对上面说到的这些概念的理解,我下面就对应的概念分别举一个对应的例子!文章中有很多代码,读者可以复制测试。

同步执行

我们先看下面的代码:

function a(){
console.log("a");
};
function b(){
console.log("b");
};
a();
b();

我们在浏览器控制台那里发现,顺序打印出“a”、“b”。这说明同步代码是按代码编写的先后顺序执行的,再来看一下下面的代码:

function a(){
//模拟一个10秒的睡眠时间
var startTime = new Date().getTime();
while(new Date().getTime() - startTime<=10000){};
console.log("a");
};
function b(){
console.log("b");
};
a();
b();

我们发现页面等待了10秒钟之后才依次打印了“a”,“b”。打印“a”的时候需要10秒,还符合我们的代码编写需求,因为我们的代码本来就是让它10秒之后打印,但是我们并不希望,“b”也是在10秒之后才打印出来。同时我们还发现一个问题,我们看到浏览器地址栏title旁边一直有一个小圆圈在转动,这是什么原因造成的呢?我经过思考发现,同步方式的javascript不仅阻塞了后面的脚本执行,同时也会对页面的渲染功能也会阻塞,但这里特别注意一点,虽然同步方式阻塞了html的渲染,导致很久都看不到页面,但是并没有阻塞dom树的更新!(注意这里的js脚本是放在页面底部的!)我们可以看下面的代码:

function get_body_innerHTML(){
var str = document.getElementsByTagName('body')[0].innerHTML;
console.log(str);
};
function a(){
//模拟一个10秒的睡眠时间
var startTime = new Date().getTime();
while(new Date().getTime() - startTime<=10000){};
console.log("a");
};
function b(){
console.log("b");
};
get_body_innerHTML()
a();
b();

我们看到这段代码在控制台的输出结果,如果body里面有内容的话,我们发现页面刚打开的时候,能准确打印body的dom结构,但是页面需要等待10秒之后才开始渲染。由此我们可以得到如下结论:

1、同步方式是阻塞的,它会阻塞后续代码的执行,即它没有返回结果之前,后面的代码是不能被执行;2、同步方式是按先后顺序执行的,即写的代码在前就先执行,在后就后执行!同时,如果我们细心的话,我们还可以发现如下结论:

同步方式虽然阻塞了html的渲染,但并没有阻塞dom树的建立。(注:这里的js代码是放在页面底部的,如果放在页面顶部再另外讨论!)为此我们得出这样的一个结论,一个页面从最开始的代码到我们最终在浏览器中看到的效果,大概经过了这样三个步骤:首先简历dom树,然后再解析样式、脚本等,最后再来渲染页面,从而得来我们看到的页面!

异步执行

如果我们把上面的代码稍微经过修改,就能使得打印“a”并不影响“b”的打印,并且也不会阻塞页面的渲染。我们来看过改过的代码:

function a(){
//模拟一个10秒的睡眠时间
setTimeout(function(){
var startTime = new Date().getTime();
while(new Date().getTime() - startTime<=1000){};
console.log("a");
},1000);
};
function b(){
console.log("b");
};
a();
b();

这里我们看到“b”在“a”前面打印出来,并且页面也是正常渲染出来了,可以看到异步方式并没有阻塞后续代码的执行,也没有阻塞代码的渲染。而是到了一个合适的时间点再打印出“a”!由此,我们对js里面的异步概念作如下总结:

1、异步操作是非阻塞的;2、js里面的异步操作本质是脚本把cpu控制权交还给js线程,而把自己添加到js线程尾部排队执行!

回调函数

我们已经说过,回调函数本质就是一个函数,只是调用的点或者说“被调用而已”,我们看下面的一个例子:

function a(arg1,callback){
console.log(arg1);
if(typeof arg1 === "number" && arg1>19){
callback();
};
};
function big(){
console.log("you are older!");
};
a(20,big);

从上面的代码中我们可以看到,我们定义了两个函数,一个是函数a,一个是函数big。而我们把函数big当成参数的形式传到函数a中,从而当函数a的第一个参数是数字并且是大于19的,控制台打印“you are older!”。这里我们就可以把big函数看作是一个回调函数,其实我们可以这样理解:如果一个函数被另外一个函数调用了。那么就可以称这个函数为调用它的那个函数的回调函数,比如这里的big就是a的回调函数。按我自己的理解,我可以认为全部函数某种意义上来说都是回调函数,比如这里a函数我们也可以理解为 全局作用域里这个隐形的函数的回调函数。

如果只是把回调函数照普通函数的含义去理解,那么会大大降低回调函数实用意义。在实际生产过程中,回调用到这样的一个场景中:函数a要执行一个操作,完成之后它想去调用函数b。这一切还要归功于js中函数是一个对象,而函数名仅是指向函数在内存中地址的指针,所以函数可以被当作参数传给其它函数。因为回调函数的使用,使得函数的使用大放异彩!

后面的知识点更新待续……………………