之前一直有疑惑过像elm.addEventListener('click', fn)
这样的一行代码怎么就能在用户点击目标元素的时候执行fn()
?今天看了些网上关于“事件循环”、“异步原理”的文章,大体明白了怎么回事,当然也只是大体,毕竟我确定我将要记的这些笔记是模棱两可的,因为还有些东西暂时没弄明白,但又不想像往常一样预想等到完全弄明白时再记而导致最终连好不容易弄明白那些的都慢慢又忘了,所以还是硬着头皮梳理一下。
先从一个问题开始:
- 为什么JS是单线程语言?
直接贴阮一峰的文章内容:
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
这个回答整体来看是能说服我的,JS单线程的是由于它的用途。但是第一句话一开始我是没明白的,他说“JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。”同一时间只能做同一件事,这里面只能做一件事的是谁?网上很多文章都这样,一句话下来总是要少点什么,比如这里的主语。当然,对于一下就能知道这里面的主语是谁的那类读者,省略了也无大碍,但对于其他类的读者,可能就加大了读这篇文章的困难。所以我花了一些时间,猜测这里的主语应该是“JS引擎”。
A JavaScript engine is a program or interpreter which executes JavaScript code
JavaScript引擎是一个专门处理JavaScript脚本的虚拟机
英文的是维基百科上的定义,中文的是百度百科的定义,二者应该等价吧?可能维基的更严谨一点?好吧,我是打算用百度百科的定义的,虽然我也不知道“虚拟机”(维基百科和百度百科的定义也不一样)是什么,只知道大学同学用通过虚拟机在电脑上装了两个系统,所以我想对于JS引擎来讲,不求它装系统,内部自己管理自己的线程应该不成问题吧?(突然觉得通过一个定义来理解一个东西这件事也很难,因为没有统一的标准,每个下定义的人只用负责自己下的定义在自己的理论里自圆其说就可以了)。算了,我也不管了,不然这点笔记怕是开始都开始不了,直接瞎说八道吧。
我的理解是这样的:JS这门编程语言本身是没有所谓单线程、多线程之分的,但是一直以来JS引擎是单线程的,所以我们就称JS语言是单线程。换句话说如果JS一开始用于另外一种用途,人们实现的JS引擎是多线程的,那么我们今天可能就会说JS语言是多线程。如果我这样理解没错的话(为避免麻烦后面的内容就不加这句话了),那“JS语言是单线程的”这种说法多少有点不严谨。
这样一来,就好理解了,“JS引擎是单线程的”这句话是说JS引擎只有一个线程来处理JS代码,所以它在同一时间只能做同一件事。
关于线程:只能说大学里没逃的那一半操作系统课上老师教的东西我都还给她了,当时就没弄懂线程和进程到底是什么,现在也不懂(后面会懂的,今天看文章的经历看来,有必要重新系统学习一下这方面的知识了),不过还好,在这里暂时也没多大影响,前面和后面的用到线程的地方就算是错的应该也不影响整体的理解。
然后下一个问题:
- javascript既然是单线程语言 , 为什么会分主线程和消息线程(event loop) ?
单线程是对于JS引擎内部来讲,因为JS引擎内部只维持一个线程所以没有主次之分;然而对外来讲,JS引擎自己本身也只不过是其宿主环境的一个线程。所以硬要说主线程的话,对于宿主环境来讲,可以指JS引擎本身(JS引擎是不是宿主环境的主线程我也不知道),但是如果问题里的主线程是想指JS引擎里的那个唯一的进程,那又是有欠严谨的。
笔记主要内容,先上图:
这是JavaScript 运行机制详解:再谈Event Loop--阮一峰这篇文章应用的外国一个人的演讲里的图,这里的宿主环境是浏览器,可以看到JS引擎只是浏览器的几个线程之一。JS引擎内部的线程只做一件事,就是按顺序执行每一行JS代码,如果碰到像DOM事件绑定、ajax请求、setTimeout等这样涉及到异步操作的Web APIs时,JS引擎就把它们交给浏览器内核的其他模块进行处理,webkit内核在Javasctipt执行引擎之外,有一个重要的模块是webcore模块。对于图中WebAPIs提到的三种API,webcore分别提供了DOM Binding、network、timer模块来处理底层实现。这些模块处理完这些操作的时候便将回调函数放入任务队列中。当stack为空的时候,JS引擎就会去callback queue里取出队头的callback放到statck里执行,等到stack再次变空之后又继续重复刚才的操作。JS引擎从callback queue里取出callback放到stack去执行这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
不知道为啥叫事件循环,是因为这些回调函数的执行都可以看成是事件驱动的么?不管实实在在的鼠标点击事件还是ajax请求还是计时器都可以把它看成事件?所以,JS异步的本质上都是通过触发某种条件然后去执行对应的回调函数而实现。
来个例子:
(function test() {
setTimeout(function() {console.log(4)}, 0);
new Promise(function executor(resolve) {
console.log(1);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(2);
}).then(function() {
console.log(5);
});
console.log(3);
})()
按理明明setTimeout()
里的console.log(4)
应该是在callback queue的队首的,也就是说它的执行应该要排在.then()
里的console.log(5)
的前面的,而最后的log顺序是12354,然后知道,原来对于图中的callback queue实际上不止一个的,具体见:http://www.jianshu.com/p/12b9f73c5a4f
https://zhuanlan.zhihu.com/p/26229293
现在可以解答elm.addEventListener('click', fn)
这样的一行代码怎么就能在用户点击目标元素的时候执行fn()
的问题了:
JS引擎内部的线程执行到这一行代码的时候,发现它是要进行异步操作的,JS引擎便把它交给浏览器对应的DOM Binding模块处理,从那时起这个模块便开始监听elm这个元素的点击事件,一旦发生这个事件便把对应的fn放到callback queue里,然后这个callback就静静的等待JS引擎把它放到stack里面执行。
大概原理就是这些,细节的后面碰到不明白的再说。
图里的heap涉及到js内存空间的知识,stack涉及到执行上下文(Execution context)和函数调用栈(call stack)的知识,后面完全弄清楚了再说。
碰到疑惑然后想法子去解决固然是好的,但是也仅限于解决,解决之后就很少继续深入了,给自己借口是:要是学了没用上很快就忘了之后又要再重新学一遍怎么办?不知道这种学习方式有无问题。不过无论如何,输入之后再输出总是没错的。