javascript到底是如何运行的?

本文在发出后收到了@开发者小蓝,@小水的诸多斧正,我修改了文中不严谨的地方,顺便把他们的原文贴出来供大家参考。

开发者小蓝:致杨先生,关于《javascript到底是如何运行的》

小水的QQ群回复:

  1. JS 引擎不需要区分什么“自己能执行什么”,它只是按各种名字(引用)在运行时作用域中找函数(方法)并调用它

  2. 至于被调用的函数 多久执行完,取决于其内部实现(JS 引擎、宿主环境 或 JS 程序员写的代码)

  3. 只要不断调用的函数 是 JS 同步程序,JS 引擎 函数调用栈 就越来越深,直到内层函数不断返回,调用栈才会最终销毁

  4. 若被调用的函数不是 JS 代码,那调用栈就此封顶,因为接下来执行的是引擎/宿主的代码(C、C++、Java 等),调用栈在其内部继续展开

  5. JS 代码本身都是“同步/顺序执行”,JS 异步能力是由引擎、宿主提供的,所有异步 API 都是内部引用传入的回调函数后直接返回,再在特定条件下,把回调压入异步队列,再由 JS 引擎在空闲时一一取出来同步执行

以下是我的文章:

每次看到别人在讲javascript单线程、事件循环、运行时、执行栈都似懂非懂,今天看了很多相关资料说一说我的理解吧。

javascript的运行机制可抽象为3部分,一部分是[执行栈],一部分为[任务队列],另外一部分为[观察者]。

执行栈

[执行栈]就是我们常说的js的单线程,也叫主线程,js代码在这里被一行一行的执行,[执行栈]遇到它能搞定的语句,包括所有的计算型的语句,以及普通的函数回调,都能在这里直接执行。当[执行栈]遇到自己处理不了的语句(比如setTimeout,ajax,文件读取等),那么它会把这个语句交给[观察者].然后继续执行[执行栈]剩下的语句,直到执行栈为空后,它会轮询[任务队列]里的任务。

观察者

在js中,有多种类型的观察者,包括文件I/O观察者,网络请求观察者等(其实这里的观察者一般来讲就是宿主环境提供的API),[执行栈]会把自己搞不定的事情丢给相应类型的[观察者],[观察者]负责在该事情处理之后会把带着处理结果的回调函数加入[任务队列]。

任务队列

一个JavaScript运行时包含一个待处理的任务队列,该队列是先进先出的,队列里的每一个任务都与一个函数相关联(一般是调用API时指定的回调函数)。当[执行栈]为空时,[执行栈]会从[任务队列]中取出队列里最前面的一个任务进行处理。处理过程包含了调用与这个任务相关联的函数(以及因此而创建的一个初始栈结构)。当[执行栈]再次为空的时候,意味着该任务处理结束, [执行栈]会循环去[任务队列]取出排在最前面的任务,然后执行之,也就是说每一个任务在执行完成后(执行当前任务的执行栈为空的时候),其它任务才会被执行。如果任务队列里没有任务了,那执行栈会同步的等待任务队列的任务到来。

用一张图来表示的话,如下图所示:

举例来讲:


//0. 首先初始化本次执行栈,压入一个初始化的东西
console.log('a1');//1. 把console.log和压入执行栈,执行后就弹出(console.log这种事情执行栈完全可以处理)

var average = function(x,y,callback){
    callback((x+y)/2);
}

var start = function(){

    setTimeout(function(){

        console.log('time!');//13. 把console.log压入执行栈,执行后弹出
        //14. 到这里后,setTimeout的回调函数已执行完毕,弹出该函数

    },1000);//3. 把setTimeout压入执行栈,发现执行栈无法处理setTimeout函数,于是把setTimeout交给时间观察者,执行栈弹出setTimeout,时间观察者会在时间到了之后把回调函数加入任务队列。

    average(3,5,function(result){

        console.log(result);//4.1 这里的回调函数不会加入任务队列,因为这种普通的回调执行栈自己完全可以处理,不需要观察者帮忙。这里会打印出4

    })//4. 把average压入执行栈,执行完average函数后弹出average函数

    //5. 到这里后,start函数已执行完毕,弹出start函数
}

var end = function(){

    console.log('end!');//7. 把console.log压入执行栈,执行后弹出

    //8. 到这里后,end函数已执行完毕,弹出end函数
}

start();//2. 把start压入执行栈

end();//6. 到这里后,执行栈只剩下初始化的那个东西,把end压入执行栈

//9. 到这里后已经没有代码可执行,弹出初始化的那个东西,执行栈为空了
//10. 执行栈为空之后,它便去任务队列读取排在最前面的那个任务,发现任务队列里没有任务,于是同步的等待任务队列,1s过后,任务队列里会出现刚刚setTimeout时指定的回调函数。
//11. 初始化本次执行栈,压入一个初始化的东西
//12. 把setTimeout的回调函数压入执行栈
//15. 到这里后已经没有代码可执行,弹出第二次初始化的那个东西,执行栈为空了
//16. 执行栈为空之后,它便去任务队列读取排在最前面的那个任务,发现任务队列里没有任务,于是同步的等待任务队列。(看这情况,也不会出现别的任务了。。。)