确认过眼神 JavaScript是单线程的

简介

起初我是用C#写服务端窗体应用的,所以对JavaScript几乎一窍不通,才开始就是这样的:

momoda

起初JavaScript是否是单线程的,我并不在乎的,才开始我的状态是这样的:

路人甲: JavaScript是单线程的!
Me: 哇哦!,原来他是单线程的,哦,然后呢?

路人已:JavaScript是事件轮询的!
Me: 哦,知道了,然后呢?

路人丙:为什么JavaScript是单线程的呢?
Me: 额……不知道,→_→,JavaScript不就是写用户交互操作的脚本吗?我管他是不是单线程的。

最开始我就是听别人说什么就是什么。但具体是什么原因我是完全不关心的

直到我了解到NodeJS能做服务端程序以后,开始了我的JavaScript之旅,经过了漫长的三个月时间至现在最近一个星期,才算是对JavaScript有了新的认识,就在最近一星期,我对回调函数,异步任务,浏览器内核,JavaScript引擎,有了比较新的认识,而是到了这个点以后,全部衔接在一起了,鸡冻啊…加油!


为什么JavaScript是单线程的?

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

浏览器的主要构成

浏览器的主要构成,先上图 我们再扯淡:

这里写图片描述

我个人大致把浏览器的构成分为三大部分:
  1. 用户界面
  
  2. 浏览器内核(浏览器引擎,渲染引擎,JavaScript引擎(也叫JavaScript解析器),网络)
   其中渲染引擎用来解析执行Html与CSS的
   JavaScript引擎是用来解析执行JavaScript代码
  
  3. 保存类似Cookie的各种数据


浏览器的主要组件包括:
  1. 用户界面 - 包括地址栏、后退/前进按钮、书签目录等,也就是你所看到的除了用来显示你所请求页面的主窗口之外的其他部分。
  2. 浏览器引擎 - 用来查询及操作渲染引擎的接口。
  3. 渲染引擎 - 用来显示请求的内容,例如,如果请求内容为html,它负责解析html及css,并将解析后的结果显示出来。
  4. 网络 - 用来完成网络调用,例如http请求,它具有平台无关的接口,可以在不同平台上工作。
  5. UI后端 - 用来绘制类似组合选择框及对话框等基本组件,具有不特定于某个平台的通用接口,底层使用操作系统的用户接口。
  6. JavaScript解释器 - 用来解释执行JS代码。
  7. 数据存储 - 属于持久层,浏览器需要在硬盘中保存类似cookie的各种数据,HTML5定义了web database技术,这是一种轻量级完整的客户端存储技术
  
 


闲谈-为什么说Chrome运行很快

Chrome的浏览器内核用的是Webkit,JavaScript引擎用的是V8

在Chrome中,只有Html的渲染采用了WebKit的代码,而在JavaScript上,重新搭建了一个NB哄哄的V8引擎。目标是,用WebKit + V8的强强联手,打造一款上网冲浪的法拉利.
(1) V8在执行之前将JavaScript编译成了机器码,而非位元组码或是直译它,以此提升效能。更进一步,使用了如内联缓存(inline caching)等方法来提高性能。有了这些功能,JavaScript程序与V8引擎的速度媲美二进制编译。

(2) 传统的javascript是动态语言.JavaScript继承方法是使用prototype,透过指定prototype属性,便可以指定要继承的目标。属性可以在运行时添加到或从对象中删除,引擎会为执行中的物件建立一个属性字典,新的属性都要透过字典查找属性在内存中的位置。V8为object新增属性的时候,就以上次的hidden class为父类别,创建新属性的hidden class的子类别,如此一来属性访问不再需要动态字典查找了。

(3) 为了缩短由垃圾收集造成的停顿,V8使用stop-the-world, generational, accurate的垃圾收集器

JavaScript引擎(JavaScript解释器)

还是老规矩,先上图 ,再扯淡:

这里写图片描述

(1).JavaScript 引擎的基本工作是把开发人员写的 JavaScript 代码转换成高效、优化的代码。它主要就是,分析、解释、优化、垃圾回收 JavaScript 代码

(2).JavaScript引擎是一个“进程虚拟机”,它给JavaScript代码提供了运行环境,用于执行JavaScript代码

(3).JavaScript引擎是单线程的,维护了一个事件队列(和浏览器事件轮询有关系)

(4).JavaScript引擎根据ECMAScript定义的语言的标准来实现

注: “虚拟机”是指软件驱动的给定的计算机系统的模拟器。有很多类型的虚拟机,它们根据自己在多大程度上精确地模拟或代替真实的物理机器来分类。
“系统虚拟机”提供了一个可以运行操作系统的完整仿真平台
“进程虚拟机”不具备全部的功能,能运行一个程序或者进程

事件轮询-Event Loop

Event Loop:其实也就是JavaScript引擎一直在执行任务队列中的任务。

由于JavaScript引擎是单线程的,单线程意味着所有任务需要排队,前一个任务结束,才会执行下一个任务,假设第一个任务是执行(a++;),第二个任务是通过ajax从网络读取数据-[很耗时的任务],假如直接放在JavaScript引擎中执行,那么JavaScript引擎会一直等待服务端的数据(这时就阻塞线程了),JavaScript引擎会一直等待到数据从服务端传递过来才会执行下一个任务,同时,在JavaScript等待的时候,CPU是空闲的,大大的资源浪费啊!,也会出现界面“假死”状态,那么怎么办呢?

解:浏览器内核是多线程的
1.Ajax操作是由浏览器内核中的浏览器Http异步线程执行的:发送—-等待—-接收
2.JavaScript引擎遇到Ajax操作会交给浏览器内核中的Http异步线程执行,从而自身继续执行下面的任务
3.当Http异步线程接收到数据以后,数据会以回调函数的形式放入任务队列中,JavaScript下次空闲的时候执行该回调函数。

异步任务简单点说 就是不占用当前线程,通过当前线程交给其他线程处理的任务,其他线程处理完毕后,再以回调函数的方式通知当前线程。

这里写图片描述

JavaScript引擎是单线程运行的,浏览器无论在什么时候都只且只有一个线程在运行JavaScript程序,但是浏览器内核是多线程的

(1). 浏览器内核实现允许多个线程异步执行,这些线程在内核制控下相互配合以保持同步.假如某一浏览器内核的实现至少有三个常驻线程:javascript引擎线程,界面渲染线程,浏览器事件触发线程,除些以外,也有一些执行完就终止的线程,如Http请求线程,这些异步线程都会产生不同的异步事件.
(2). 界面渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行.本文虽然重点解释JavaScript定时机制,但这时有必要说说渲染线程,因为该线程与JavaScript引擎线程是互斥的,这容易理解,因为 JavaScript脚本是可操纵DOM元素,在修改这些元素属性同时渲染界面,那么渲染线程前后获得的元素数据就可能不一致了.
在JavaScript引擎运行脚本期间,浏览器渲染线程都是处于挂起状态的,也就是说被“冻结”了.

接下来我们用代码来论证上面所说的:

这里写图片描述

                window.onload = function() {

            var date1 = new Date();
            //异步任务-js引擎线程发现setTimeout这个方法,然后通知浏览器内核启动浏览器定时线程,浏览器定时线程开始定时,js引擎线程执行这个代码块只花了不到1毫秒的时间,然后js引擎就继续往下执行
            //当定时线程到了30s后,就把回调函数放在js引擎队列里面,JS引擎会一直遍历自己的队列,是否有任务要处理,如果js引擎队列正在执行其他方法,那么该回调函数就会等其他任务执行完了再执行,如果js引擎是空闲的,那么就会立即执行
            setTimeout(function() {
                alert("setTimeOut Finish");
            }, 1000 * 30);

            var date2 = new Date();
            var haomiao = date2.getTime() - date1.getTime();
            console.log(date1.getMilliseconds() - date2.getMilliseconds())
//同步任务-立即执行 不会等待30秒后再执行
var a=0;
console.log(a++);
            //同步任务-立即执行,当JS引擎执行在这里的时候,JS引擎是空闲的,JS引擎就立即执行该方法 ,因为每次循环都在占用js线程,所以js引擎不会执行下面的方法
            delayTwentyMilliseconds();

            //异步方法-js引擎 -发现ClickMe() 交给浏览器内核,浏览器内核再交给浏览器事件触发线程,浏览器事件触发线程就会注册点击事件ClickMe,然后Js引擎就不管,然后js引擎就继续往下执行
            function clickMe() {
                var date1 = new Date();
                var date2 = new Date();
                var haomiao = date2.getTime() - date1.getTime();
                alert("点击完成时间执行完成 耗时:" + haomiao / 1000 + '秒');

            }
            //异步方法
            setTimeout(function() {
                alert("setTimeOut Finish");
            }, 1000 * 30);
        }


//定义执行一秒的同步方法
        function delayOneMilliseconds() {
            for (var i = 1; i < 1000; i++) {
                for (var j = 1; j < 10; j++) {
                    for (var k = 1; k < 10; k++) {
                        var b = k * 10;
                    }
                }
            }
        }
//定义执行20秒的同步方法
        function delayTwentyMilliseconds() {
            for (var i = 1; i < 10000; i++) {
                for (var j = 1; j < 1000; j++) {
                    for (var k = 1; k < 2000; k++) {
                        var b = k * 100;
                    }
                }
            }
        }

总结:
1.JavaScript引擎一直在执行任务队列中的任务,当遇到同步任务的时候会立即执行,遇到异步任务会交给浏览器内核的其他线程执行,当其他线程执行完毕,会以回调函数任务的形式放入到JavaScript任务队列中,JavaScript引擎会继续往下执行,当JavaScript引擎空闲时会执行回调函数任务。
2.delayTwentyMilliseconds().这个算是大规模的运算操作,执行时间是20s,[同步任务],会一直阻塞JavaScript线程,从而说明了为什么NodeJS不适合做大规模的运算操作

浏览器与NodeJS

NodeJs和浏览器是差不多的,Node.js也是单线程的Event Loop.只不过浏览器是浏览器内核来管理异步线程,NodeJs是libuv这模块来管理异步线程,同样的NodeJS也是利用V8-JavaScript引擎来进行执行任务队列-Event Loop

这里写图片描述

NodeJS能处理高并发连接并且达到比较良好的吞吐量的真正原因:
以下是年轻的时候的错误理解
实际上是NodeJS中的http模块是异步的,NodeJS只负责接收海量的Http请求连接,而处理这些连接是由libuv来处理,只是把压力转给了libuv。
NodeJS对数据库的操作也是同样的道理。

正解:
其实看上面的论述,可以得知不管是NodeJS还是浏览器的JavaScript引擎都是单线程的,所以所有的请求都会排队 然而,真正提高并发吞吐量的是I/O多路复用(解释:I/O多路复用其意思就是所有的接受请求响应都是用同一个线程来处理),正因为Node接受用户请求时是用一个线程接收所有请求的(不需要开其他线程来处理请求),由于每个进程/线程需要占用不少资源(典型的是内存,一个线程通常需要2M的栈空间),更重要的是,线程/进程创建,切换,销毁时的开销是非常大的。
然而Node的异步事件轮训:
1.异步:
异步表现在于Node在处理比较耗时的I/O(比如请求第三方API,读写文件,读写数据)的时候,Node使用异步回调的形式来处理,这样当遇见比较耗时的I/O时,Node不会等待,而是继续接受其他用户的请求,从而达到更高的并发吞吐量。
2.事件轮训:
事件轮训表现在于,libuv也维护了一个事件队列,所有比较耗时的I/O操作都由libuv来处理,同时libuv一直轮训事件队列的事件是否完成(因为所有事件都是异步的只能轮训),然后以回调函数的方式及时响应给JavaScript解释器

这里写图片描述

啊!,目前还是不知道libuv的工作原理,觉得libuv好强大,有时间我一定要研究一下

window.onload

界面渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行.本文虽然重点解释JavaScript定时机制,但这时有必要说说渲染线程,因为该线程与JavaScript引擎线程是互斥的,这容易理解,因为 JavaScript脚本是可操纵DOM元素,在修改这些元素属性同时渲染界面,那么渲染线程前后获得的元素数据就可能不一致了.

流程:
渲染引擎解析Html和CSS同时,如果这个时候JavaScript引擎在操作Html元素,浏览器是相信渲染引擎的还是JavaScript引擎的?所以window.onload就解决了,当渲染引擎执行完页面渲染,才执行JavaScript引擎来执行JavaScript脚本

回调函数

回调函数 :其实就是函数指针 无异步,无回调,解决了异步函数传值的问题

  //假如A是异步方法。
    function A(callback){
        var a=0;
            //假设执行异步任务是a++,
            a++;
            //当异步任务执行完,调用callback这个函数(也就是B(biu)这个函数),所以只能通过回调函数的方法把a值传递给B,方便B来操作数据
            callback(a);
        }
        //function B(piu)是一个函数,不是函数指针,bb才是函数指针
        var bb=function B(piu){
            piu++;
            alter(piu);
        }

        A(bb);
----------
A(bb){
var a=0;
//假设执行异步任务是a++,
a++;
B(a){
a++;
alter(a);
}
}
//方法B被方法A回调,方法B是方法A的回调函数
//流程:由于A是异步函数,由浏览器内核来执行这个函数,当浏览器内核执行完毕,会把回调函数放入任务队列中,javaScript引擎在空闲的时候就会执行这个回调函数

        //假如A是异步方法。
    function A(callback){
            var a=0;
            //假设执行异步任务是a++,
            a++;
            //如果这样 返回的a将等于0.
            retrun a;
        }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章