JS:异步与回调函数

同步与异步

JS语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

同步模式

同步模式是指后一个任务等待前一个任务的结束,才开始执行。程序的执行顺序与任务的排列顺序是一致的,同步的。以<script src="xxx.js"></script>为例,它就是同步执行的,在浏览器解析到这句代码时,就会停止下一行的解析,去下载要请求的JS文件,并执行它,然后再开始解析下一步代码。

异步模式

异步模式是指后一个任务不等前一个任务结束就执行,异步模式下的代码,其特点是书写顺序和执行顺序是不同的(反之不一定)。
由于异步模式的代码不是立马执行的,所以你通过赋值根本拿不到结果。以定时器为例:

1
2
var a = setTimeout(function(){return 1314},1000)
console.log(a)

控制台打印的是undefined。因为定时器是异步的。在一秒后,定时器才会执行你传入的函数。在此之前控制台早已将变量a的值打印出来。所以对于异步的代码,其变量赋值是没有意义的。

‘异步模式’编程的几种方法

1. 事件监听

以事件绑定的例子为例:HTML页面里有五个li标签。

1
2
3
4
5
6
let liList = document.querySelectorAll('li')
for(var i=0;i<liList.length;i++){
liList[i].onclick = function(){
console.log(i)
}
}

当我们依次点击这五个li标签时,控制台打印的是

1
2
3
4
5
5
5
5
5
5

这是因为点击事件调用的函数是异步的。在for循环时,li绑定的事件并不会执行,而是待所有代码解析完成,并在用户点击时才会触发。我们知道代码解析完成后,i由0变成了5,所以控制台打印的就是5个5。
在这种异步模式下,我们并不愿意看到这种结果,那该怎样才能拿到异步任务的结果呢?
有两种方法:

  • 用立即执行函数

    1
    2
    3
    4
    5
    6
    let liList = document.querySelectorAll('li')
    for(var i=0;i<liList.length;i++){
    !function(ii){
    liList[ii].onclick = function(){ console.log(ii)}
    }(i)
    }
  • 用let声明变量

    1
    2
    3
    4
    5
    6
    let liList = document.querySelectorAll('li')
    for(let i=0;i<liList.length;i++){
    liList[i].onclick = function(){
    console.log(i)
    }
    }

2. 回调函数

我们以定时器的例子为例:

1
2
3
4
function asyncFn(){
setTimeout(function(){ return Math.random()},2000)
}
asyncFn()

为了拿到随机生成的返回值,需要给asyncFn函数传入一个参数。由于定时器是异步的,所以不能用变量赋值,即可以排除基本类型数据。那么传入一个对象可以吗?试一试吧。

1
2
3
4
5
6
7
8
function asyncFn(obj){
setTimeout(function(){
return obj.value = Math.random()
},2000)
}
var obj = {}
asyncFn(obj)
obj.value

控制台打印的结果为undefined。用对象貌似也不行,因为obj.value,在定时器未执行之前,它并没有赋值,而是在2s之后,obj.value的值才改变。其实我们可以监听obj.value的值是否变化,如果检测到它的值变化了,就打出这个值。这个以后加以讨论
既然对象不行,那就只有函数了。

1
2
3
4
function asyncFn(fn){
setTimeout(function(){return fn(Math.random())},2000)
}
asyncFn(function(xxx)){console.log(xxx)}

最终控制台打出了一个随机数。我们试着分析上面这段代码。首先浏览器会执行asyncFn函数,并传入一个fn函数。2s后,定时器触发了,此时,asyncFn函数返回另外一个函数fn并传入了一个随机数作为参数。那么这个fn函数也会执行。不难知道fn函数就是function(xxx){console.log(xxx)},这个函数的功能是打出你传入的参数。故控制台会打出一个随机数。
所以,把一个函数作为参数传给另外一个函数是可以拿到异步的值的。而这个作为参数传入的函数又称为’回调函数’。
这个回调函数并不会阻塞后面代码的执行。比如,我们在上面代码中再加一个定时器。

1
2
3
4
5
function asyncFn(fn){
setTimeout(function(){return fn(Math.random())},2000)
}
asyncFn(function(xxx)){console.log(xxx)}
setTimeout(function(){console.log('hello')},2000)

2s后,控制台几乎同时打出一个随机数和’hello’字符串。可以看出回调函数并不会影响后面代码的执行。JS会立即执行其它代码,不会等异步任务结束。

一般情况下,异步是与回调函数共同搭配的。

3. AJAX

AJAX请求也属于异步模式。

1
2
3
4
5
6
7
let xhr = new XMLHttpRequest()
xhr.open('GET','xxx.js',true)
xhr.onload = function(){
console.log(xhr.responseText)
}
xhr.send()
其它代码......

控制台并不会立马打出服务器给的响应消息体。因为浏览器创建TCP连接和发送请求需要一定的时间,此外,给服务器发送请求,并不会影响后面代码的执行。因为这个请求的异步的。
这里,我们并不能认为JS引擎同时在做两件事。因为AJAX请求是由浏览器的网络模块执行的,不是由JS引擎执行的。
异步意味不等待任务结束,并没有强制要求两个任务同时进行。
AJAX请求也可以设置成同步,只要把xhr.open('GET','xxx.js',true)改为xhr.open('GET','xxx.js',false)。这里并不推荐使用AJAX的同步请求。假如整个请求响应过程要10s,那么浏览器会等10s,这个期间页面不能进行任何操作。显然这对用户体验极其不友好。

什么情况下用到异步

如果两个任务相互独立,其中一个执行时间较长,那么一般就用异步的方式做这件事。

异步实质

JS本身没有异步,像定时器、AJAX和事件监听都是浏览器提供的接口,这个接口是异步的。windwo.Promise也能产生异步。

参考文章

什么是异步