在 JavaScript 中,我个人觉得理解回调函数,就有助于我们理解 JavaScript 中的其它设计,比如异步编程、消息循环、一些常用到的 Web API 以及 Node 中的异步,这些都与回调函数息息相关,所以咱们一步步的来学习和使用。

废话不多说,直接上干货!

什么是回调函数?

回调函数也是函数的一种,回调函数与普通函数的区别在于它的调用时机。所谓的回调函数是把一个函数当作另一个函数的参数,并在这个函数的某个时机进行调用,我们就将这个传入的函数称为回调函数。

回调函数分为同步回调和异步回调。它俩的区别在于调用的位置不同。

1.同步回调

所谓的同步回调函数是在执行的函数内部被调用的。举个例子:

functionfn(){

console.log("我是同步回调函数");

}

functionbar(f){

f();

}

bar(fn);

我们声明了一个回调函数 fn,我们传入 bar 函数,我们在 bar 内部调用了 fn,此时就是同步调用。

2.异步回调

而异步回调函数是在执行的函数外部被调用的。如下代码:

functionfn(){

console.log("我是异步回调函数");

}

settimeout(fn,1000);

上述代码中,settimeout 的第一个参数 fn 是传入的一个回调函数,当程序执行 1000 ms 的时候,此时这个回调函数被调用,而且是在 setTimeout 函数外部被调用的,我们称 fn 为异步回调函数。

JS 线程架构

上述的问题理解起来非常简单,尤其是同步回调函数。但是对于异步回调函数什么时候被调用的,是在什么位置被调用的,我们目前是非常模糊的。

所以要想知道异步回调函数的调用时机和位置,我们需要理解 JS 的线程架构,比如消息队列、事件循环,从根上理解 JS 是如何设计这些东西的。

JS 的最初设计是单线程的,所谓的单线程就是同一时间只能干一件事情,而且 JS 运行的这个单线程就是页面的 UI 线程,毕竟 JS 为了能够更方便的操作 DOM 嘛。

那么问题来了,当我们把 JS 设计到 UI 页面线程,就会出现一个问题,当用户通过页面交互产生一个事件时,如果当前的线程正在处理其他的任务,那么这个交互事件需要等当前 UI 线程的任务处理完毕才能被处理,所以这样设计比较鸡肋。

如果当前 UI 线程一直执行其他任务,那么这个用户的交互事件任务一直不被处理,会出现页面点击无效的假象。

谷歌 V8 团队为了解决这个问题,那么就引入了消息队列。

消息队列

有了消息队列,我们把所有产生的事件,无论是 JavaScript 产生的事件还是用户点击页面交互产生的事件,都统一按照先后循序放到消息队列中,那么 UI 线程就不断的循环这个消息队列,有任务就取出来去执行。

有了消息队列之后,这个主线程就可以有序的执行各种事件任务了。

异步任务什么时候调用?

还是上述的 setTimeout 异步回调函数的例子,假如当前线程在消息队列中取出 setTimeout 这段代码执行,发现 fn 这个任务是在 1000 ms 后执行的,1000 ms 过后,主线程就会将 fn 封装成一个事件任务扔到消息队列中去等待被执行。

等消息队列中的执行到一定的时机,就会取出这个被封装过的 fn 回调函数任务,在主线程中被执行。

这个理解起来并不是很难,但是有一类回调函数比较特殊。当主线程在消息队列中取出一个网络下载的任务时,网络下载比较耗时,我们不可能让它在主线程中阻塞其他任务执行,所以主线程就会交给网络线程去执行这个下载任务,然后主线程回继续有序的在消息队列中取出其他任务执行。

此时网络线程会接受到这个任务执行下载操作,当网络线程把文件下载完毕之后,就将下载的结果封装成一个事件任务,放入到消息队列中。当主线程执行到这个任务时,就知道网络线程已经下载完了,然后将下载的结果呈现给用户。

最后

好了,今天主要和大家分享了 JavaScript 中回调函数的由来和异步回调函数的设计,这也是 V8 引擎中的一小部分,接下来会通过这样一个个的知识点,挖掘 V8 谷歌引擎的各个方面的优秀设计方案。

关键词: 什么是回调函数