V8是如何实现回调函数的?

从内部了解回调函数,有助于理解很多问题:

  1. 理解浏览器中的Web API 是如何工作的;
  2. 理解宏任务,微任务以及它们之间的区别;
  3. 回调函数是理解异步编程的基础;

一、回调函数

回调函数其实是一个函数,区别于普通函数,在于它的调用方式。当某个函数作为参数,传递给另一个函数,然后在该函数内部被调用,就称为回调函数。

回调函数有两种不同的形式:

  1. 同步回调:在执行函数内部被执行。
  2. 异步回调:在执行函数外部被执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 同步回调, double函数在map函数内部执行
*/

var arr = [1,2,3]
function double (item) {
return item * 2
}
arr.map(double)

/**
* 异步回调, 执行位置和时间点不在函数内部
* log函数不是在setTimeout函数内部执行的
*/

function log () {
alert("hello world")
}
setTimeout(log, 2000)

由此可知回调函数执行时机:

  • 同步回调:在执行函数内部按代码顺序执行
  • 异步回调:那异步回调呢???在什么位置什么时间点执行???那就需要先了解V8在运行时的线程架构模型

二、V8的线程架构模型

早期浏览器的页面运行在一个单独的UI线程中(运行窗口的线程),要在页面中引入JavaScript,那必须要让JavaScript运行在和页面相同的UI线程中。在页面线程中,当一个事件被触发时,该事件会被提交给UI线程来处理,然而,在大部分情况下,UI线程不能立即响应和处理该事件,因为UI线程可能在处理前一个任务。

针对这种情况,为UI线程提供了一个消息队列,将这些待执行的事件添加到消息队列中,然后UI线程会不断循环地从消息队列中取出事件,并执行。我们把UI线程每次从消息队列取出事件,执行事件的过程称为一个任务

1
2
3
4
5
6
function UIMainThread () {
while(queue.length){ // queue: 消息队列
const task = queue.shift() // 取出一个任务
processTask(task) // 执行该任务
}
}

通用UI线程架构

三、异步回调函数的调用时机

了解了UI线程架构,就可以解释异步回调的执行时机了。在执行setTimeout函数的过程中,会将foo函数封装成一个事件,并添加到消息队列中,然后setTimeout函数执行结束。主线程不断从消息队列中取出任务并执行。所以foo函数的执行位置是在执行函数外部的。

有一类回调和setTimout触发的回调是有区别的,最典型的就是XMLHttpRequest所触发的回调。因为XMLHttpRequest是用来下载网络资源的,下载任务耗时比较久,并不适合在UI线程上执行,所以当主线程从消息队列中取出这类下载任务的任务之后,会将其分配给网络线程,让其在网络线程上执行下载过程。

xhr的回调过程

UI线程处理下载事件过程:

  1. UI线程从消息队列中取出一个任务,并分析该任务,发现该任务是一个下载请求,就会将该任务交给网络线程
  2. 网络线程收到请求之后,便和服务端建立连接,并发出下载请求
  3. 网络线程不断接收服务端传过来的数据。每次接收到数据时都会将回调函数和接收的数据封装成事件,并添加到消息队列中
  4. UI线程循环从消息队列中读取并执行任务。如果是下载状态的事件,那么就可以通过回调函数追踪下载的进度,直到最后接收到下载结束事件,那么下载任务就结束了。

除了XMLHttpRequest所触发的下载任务采用了这种方式外,获取系统设备信息,读取文件等也都采用这种类似方式实现的。

参考链接