一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
v8是怎么实现回调函数
回调函数是啥
使用JavaScript的时候,会经常用到大量的回调函数(callback),比如用数组的forEach方法,就要传递一个回调函数,使用定时器setTimeout也需要传递一个回调函数。
//forEach demo
const arr=[1,2,3]
arr.forEach((item)=>{
console.log(item)
})
//setTimeout demo
setTimeout(()=>{console.log(`this is a callback`)},1000)
当然,使用XMLHTTPRequest来异步下载资源,Node用readFile读取文件,也使用了回调函数,这些操作都有共同的特点,调api参数里写入一个回调函数,然后浏览器/Node会将执行结果通过回调的方式触发。
分析一下回调函数
ok,下面就从最常用的forEach来分析一下回调函数
[1,2,3].forEach((i)=>console.log(i))
在使用forEach这个api的时候,需要传入(i)=>console.log(i) 这个回调函数
在forEach方法内部,会遍历这个数组,每遍历一次都会调用一次这个回调函数,不难看出,这个回调函数总是在函数的内部执行,所以这是一个同步回调
有了同步回调,那一定就会有异步回调,比如setTimeout
//setTimeout demo
setTimeout(()=>{console.log(`this is a callback`)},1000)
setTimeout的第一次参数就是得传入一个回调函数,v8执行setTimeout,等同步任务结束完且函数执行栈为空的时候,回调函数才会背v8调用,所以这个回调函数不是在setTimeout内部执行的,所以这是一个异步回调
从上面可以得知,根据回调函数调用的地方,可以将回调分为同步回调和异步回调,同步回调很容易理解,就是在执行api的内部进行调用,那异步回调会在时候时候 什么地方调用呢?
解释这个问题,需要了解v8的线程模型,涉及消息队列,事件循环,这些都是与v8线程模型直接相关,所以有必要分析v8的线程模型
ui线程与消息队列
早期浏览器页面都会运行在单独的ui线程之中,页面引入了js,那么js也要运行在页面相同的线程上,这样才能方便使用js来操控dom。
在页面上,会有各种各样的事件,比如说资源下载,鼠标移动等等。然而鼠标移动每移动一个像素,就会触发鼠标移动这个事件,所以这个事件的发生是相当频繁,我们可以为鼠标移动这个事件添加相应的回调函数,这种情况下,发生了鼠标移动的时候,线程可能在处理别的事情,所以最新的事件就没法立即执行
所以v8为ui线程提供了一个消息队列,并将这些待执行的回调添加到消息队列中,然后ui线程会不断地从消息队列中取出事件,执行事件。
异步回调到调用时机
了解了ui线程的基本架构,下面可以解释一下异步函数的执行时期了,页面主线程遇到了setTimeout(foo,3000),执行setTimeout过程中,宿主就会将foo函数添加到消息队列中,然后setTimeout函数执行结束
主线程不间断的从消息队列取新任务,执行新任务,时机满足,就取出foo函数这个回调,然后就执行foo函数
setTimeout还是比较简单的,就是将回调放到消息队列,主线程在合适时机从消息队列取出来执行
还有一类稍微复杂一点的,比如XMLHttpRequest的回调。
他和setTimeout是有区别的,因为获取网络资源大概率会很费时间,甚至可能是下载大文件,在ui线程中执行,拿他就会阻塞ui线程,所以此时会把这个回调交给专门负责这类的网络线程,这样就不会影响主线程的执行
XMLHttpRequest的执行流程如下概括
执行到XMLHttpRequest 把回调交给消息队列
ui线程取到了这个回调,分析得到这是一个获取网络资源的回调,交给网络线程
网络线程收到请求,与服务端连接,发起下载请求
网络线程不断收到数据
网络线程收到数据,都会将设置的回调和返回的数据信息封装在一起,我称之为下载状态事件,放到消息队列中,
ui线程循环读取消息队列,如果是下载状态事件,ui线程执行回调函数,程序员就可以在回调函数内部编写相应更新下载进度状态的代码
最后收到下载结束这个状态,ui线程会执行下载结束的回调