【本周主题】第一期:JavaScript单线程与异步
相信下边这个图一定都不陌生,本周就围绕这张图深入了解下js代码执行时的来龙去脉。
一、JavaScript是单线程的
2018-11-19 21:21:21 周一
js本质是单线程的。这一特性是javascript的核心特征。一定牢记于心。
js两大特性:单线程与非阻塞。
单线程是指,js在执行的时候,都只有一个线程来处理所有任务。这个线程就是js的主线程。
非阻塞是指,当代码里有一段任务是要花一定时间才能返回时,主线程会挂起这个任务。在异步任务达到条件时派出回调函数依次执行这些代码。比如非阻塞I/O。
进程和线程
进程:一个正在运行的程序就是一个进程。
线程:独立运行的代码段
进程和线程的关系和作用?
一个进程由一个或多个线程组成。
进程只负责资源的调度和分配。
线程才是程序真正的执行单元,负责代码的执行。
进程和线程的区别?
一个程序至少有一个进程,一个进程至少有一个线程.
线程的划分尺度小于进程,使得多线程程序的并发性高。
另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
为什么js是单线程的?
js单线程是js语言的一大特点,这与他的用途有关。
作为浏览器脚本语言,他最初的主要执行环境是浏览器,而他(专指js)的主要用途是和用户互动、操作dom。
试想一下,如果是多线程的js,用户一边左移dom的时候,dom还因为某一段代码的执行自己向右移动,客户还不以为自己见了个鬼啊!
单线程的缺点:
因为只有一个线程,代码需要排队按顺序执行,前一个任务执行完毕,后一个任务才会被执行
排队等待就需要大量的计算。耗费CPU。限制了js的执行效率。
还有,假如有一段代码报错,就会导致代码阻塞,页面假死。
js主线程及其作用
每个程序要运行,至少包含一个线程,就是主线程。
js是单线程的,所以js的线程就是主线程。也就只有一个主线程。此线程被称为js引擎线程,也是js代码的执行栈。
而主线程负责执行程序的所有代码。这就导致这些代码只能顺序执行,不能并发执行。
js执行栈
上边图中,heap和stack翻译过来分别是:
heap: 堆
stack: 栈
他们是js代码在执行时,用于储存变量的两个内存位置。
堆内存空间:里边放引用类型的变量值,比如对象、数组
栈内存空间:里边放基本类型的变量值,比如字符串、数字。另外还存放引用类型变量名以及指向堆空间引用类型值存放地址的指针。
对应上图画了个便于理解的草图:
思考:执行栈和栈空间是一回事儿吗?
js的执行栈
执行栈,也叫调用栈。
特点:就是先进后出(学术上都叫后进先出 - LIFO),就像数组的push+pop。从栈顶推入,从栈顶拿出。
执行栈作用:用于存储代码执行期间创建的所有执行上下文。
js代码首次运行时,执行栈中被推入一个全局(global)执行上下文,
后期js按照代码顺序执行同步代码。
每当遇到函数调用,js会创建新的函数执行上下文,并推入到执行栈的栈顶。
当执行环境代码运行完毕,js退出这个执行环境并销毁这个执行环境。(这也就是一个函数运行完毕后会被销毁)
执行栈LIFO规则:后进先出。依旧看上图,funcC执行完后会率先被pop出栈。
执行上下文:
当我们在执行栈调用一个函数方法的时候,js会生成一个“执行上下文”,这个执行上下文就是这个函数的私有作用域、就是执行环境context。
这个执行环境中,初始化(又叫预编译,具体见后边预编译篇章)时期,会包含以下内容:
上层作用域的指向(父级作用域)、this对象:指向window、形参、arguments、变量:undefined...
栈溢出:一个函数被运行,他的执行上下文被推入执行栈,函数在执行环境中还有可能调用其他方法,甚至是自己。
而当其调用自己时 ,就会再次向栈中添加执行环境。如此往复下去,内存不断增大,直到超出内存最大值,造成栈溢出。(内存溢出)
HTML5的web Worker多线程与js的单线程矛盾吗?
html5提出了web Worker,这个功能可以允许js独立于其他脚本在后台运行,感觉上去让js有了多线程的能力。即同时有好几段js脚本可以同时执行了。
但是,web Worker在外部文件中,无法访问到js对象中的Window对象、document和parent对象。所以web Worker也就不能操作DOM。
并且,web Worker创建的线程,子线程需要受主线程的控制,且子线程没有执行I/O操作的权限。只能辅助主线程做一些计算任务。
所以,他本质意义上,没有改变js单线程的本质。未来的js也可能会一直是单线程的。
js是单线程的。这使得js引擎每次只能处理一个任务。即同一时间只能做一件事。
所有的任务会有一个先后的执行顺序。
但有的时候,前边的代码要等待一段时间才能执行,你总不能让所有代码都堵在他后边等着他执行完再执行吧。
(试想一下进地铁安检的场景,如果一个人包里需要被检查,会把他堵在门口检查也不让后边的人接受检查吗?正确的处理方法就是把他拉到一边检查,让出安检口好让后边的人先顺利通过。)
代码也是这个道理,前边等待的代码需要先给后边的代码让路,要让其先执行。自己先挂起。
按书写顺序执行的就算是同步任务,被挂起的任务成为异步任务。具体这俩见后边的同步和异步。
这里,我们先区分下,在什么情况让代码挂起:
二、浏览器多线程和WebAPIs
2018-11-20 19:02:21 周二
我们前边说,js的出现,起初是为了操作dom用的。
那你可以试想一下,在单线程的js世界里,假如不小心将“修改”按钮的填充写到了 修改按钮点击事件之后。如果用户不点击修改按钮,修改按钮就不会被填充到页面中。
那这不是一个死循环了吗?
用户看不到按钮没法点击,而不点击按钮又排队在点击代码之后不被执行也就不能把点击按钮加入到页面中... ...
所以,这种(还有其他几种)情况下,我们就需要一些规则,将一类代码暂时挂起,等到他的条件满足时才被触发。从而不会影响其后的其他任务。
可喜可贺的是,js的宿主环境(浏览器等)是多线程的。在宿主环境的协同帮助下,我们上边说的那些耗时、或者需要事件驱动的代码就人来处理了:
什么是多线程?
多线程就是不是单线程呗。单线程一次只能运行一个任务。
多线程就是一个程序可以同时开辟多个不同的线程、执行多个不同的任务。
好处就是:
提高cpu的利用率,当遇到耗时需等待的任务时,也可以绕过先执行其他的。
就行我们工作中,一个浏览器tab标签页正在加载中,我们可以先打开别的页面干别的,大大提高工作效率。
浏览器都由哪些部分组成?
1. 用户界面
2. 浏览器引擎(the browser engine)— 用来查询及操作渲染引擎的接口;
3. 渲染引擎(the rendering engine 浏览器内核)— 显示请求的内容;
4. 网络 — 用来完成网络调用,例如 HTTP 请求;
5. UI 后端 — 用来回执类似组合框以及对话框等基本组件;
6. JS 解释器 — 解释执行JS 代码;
7. 数据存储 — 属于持久层 ,浏览器或许会在本地保存各种数据,比如cookie
---------------------
来源:https://blog.csdn.net/zhoujie_zhoujie/article/details/53025202
渲染引擎:
渲染引擎负责解析HTML文档和渲染页面,主流的渲染引擎有:IE的Trident,火狐的Gecko,Safari和Chrome的WebKit等。渲染引擎首先通过网络获得请求文档的内容,之后进行文档内容的解析和页面的渲染,一般流程如下:
解析html构建DOM tree —-> 结合样式规则构建render tree —> 布局render tree —> 绘制render tree
---------------------
来源:https://blog.csdn.net/zhoujie_zhoujie/article/details/53025202
具体这部分的研究见另一篇文章。输入url按下回车后,都发生了什么?
浏览器的多进程
浏览器是多进程的。系统分配给浏览器资源,如cpu、内存等,使之可以运行。
每打开一个浏览器tab页面,就相当于建立了一个独立的浏览器进程。
浏览器内的进程:(以下图转自)
浏览器作为js的宿主环境,是多线程的。
浏览器的线程有哪些【浏览器内核中的多线程】?
浏览器内核:
就是渲染进程。js的执行、页面的渲染、事件处理等。
渲染进程下有多个线程。这些线程共同合作完成渲染任务。
浏览器内核中的线程有:
通常一个浏览器至少有3个常驻线程:
1、图形用户界面GUI渲染进程 (浏览器页面渲染)
2、JS引擎线程 (处理js)
3、事件触发线程 (事件触发控制)
还有两个线程是:
4、定时器触发线程【定时触发器线程】
5、异步HTTP请求线程【http网络请求线程】
这五条共同组成浏览器的内核。
浏览器内核中各线程描述:
1. GUI渲染线程:
作用:负责渲染浏览器界面、解析HTML,CSS、构建DOM树和RenderObject树、布局和绘制等。
触发条件:当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
注意点:与JS引擎线程互斥。
原因是当浏览器界面重绘或者回流时(重绘不一定触发回流,但是回流一定触发重绘),由于js可以操作dom,从而改变整个dom tree。
所以当JS引擎执行时GUI线程会被挂起(相当于被冻结了,js执行时就阻塞页面的渲染了),
GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
重绘、回流。请看浏览器的渲染流程部分。以后贴出。
2. JS引擎线程(js内核)
作用:负责解析、处理JavaScript脚本程序,运行代码(用户输入、网路请求等)。有名的就是V8引擎。
触发条件:JS引擎是基于事件驱动的单线程。他会一直等待任务队列中任务的到来并作处理。一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序。
注意点:GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
3. 事件触发线程
作用:当一个事件被触发时,该线程会把这个事件添加到待处理队列的队尾,然后排队等待js引擎线程来处理。归属于浏览器而不是JS引擎。用来控制事件循环。
触发条件:当JS引擎执行代码块如如鼠标点击等事件时,会将对应任务添加到事件线程中。
注意点:由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理,当JS引擎空闲时才会去执行。
4. 定时触发器线程(setInterval
与setTimeout
所在线程)
作用:对于需要长时间等待的任务,该线程会把这个任务添加到待处理队列的队尾。防止阻塞后边的任务。
触发条件:通过单独线程来计时并触发定时,计时完毕后,添加到事件队列中,等待JS引擎空闲后执行。
注意点:
- 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
- W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
5. 异步http请求线程
作用:同定时器触发。因为请求也需要时间,不会立马被执行完毕。需要等待。这就要求http请求线程将其异步处理。
触发条件:XMLHttpRequest等ajax请求。在连接后通过浏览器新开一个线程请求。当检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
注意点:
浏览器内核中线程之间的关系
转自https://segmentfault.com/a/1190000013083967#articleHeader2
什么情况下的任务会被浏览器开辟新的线程(WebAPIs)?
浏览器为几个明显的耗时任务或者需等待的事件任务单独开辟线程,以解决耗时问题导致的页面假死现象。
总结来说,就是以下这几种情况的任务:
事件监听 - 浏览器需要用户触发的事件
比如:chick事件,表单事件。
http网络请求请求
比如:ajax请求等
定时触发器
比如:setTimeout和setInteval
阅读推荐:
[1]: 浅谈浏览器多进程和js线程
[2]: js的单线程和异步
总结:
浏览器的渲染进程是多线程的。js是阻塞单线程的。
通过浏览器开辟的多线程任务,使得js拥有了异步的属性。
三、同步和异步
2018-11-21 21:32:22 周三
同步、异步的名词解析:
特别注意、跟我们生活中理解的同步完全相反。
我们理解“同步”就是左手画圆、右手画方同时执行吧。但是js中的同步指的是左手画完圆、再换右手去画方。就是一个一个来。
就像在一个管道里,任务们顺着下来。大家同在一条路上排队前进。
而“异步”才是左手画圆、右手画方。就是两个任务可以并排执行。你可以理解多开辟了一条道路让他执行第二个任务,简称异步任务。
“异”:不同、另一个。。。
在js中,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
什么是同步任务?
同步任务指的是,在js主线程上排队执行的任务(js也只有一个线程,那就是主线程),只有前一个任务执行完毕,才能执行后一个任务;原文
什么是异步任务?
异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。原文
什么样的代码会被异步处理?
总的来说就是不能被立即执行的代码。
但是有个例外是
setTimeout(function(){ //daima },0);
这段定时器代码,等待时间设置为0,看上去等待0秒就是立即执行啊,其实不是的。他的代码流程并不是我们看上去的那样。这个下边详细解说。
只是这个当时我也被他的外表坑过。
严谨的说,因为异步是浏览器的两个或者两个以上线程共同完成的。所以触发浏览器多线程的代码会被异步处理。
即:
- ajax异步请求、
- setTimeout、setInterval定时器、
- click等事件代码
以上这些带 回调函数 的代码都是会触发异步线程(WebAPIs)的。
也就是当引擎遇到这几种情况的代码,会把代码放入到异步线程中,然后继续向下执行其他的代码。
当这些代码在异步线程中达到条件后(比如定时器时间到了以后)。就会有把代码里边的回调函数加入到任务队列中,排队到主线程中去执行。
举个例子:
1 console.log(1); 2 setTimeout(function(){ 3 console.log(2); 4 },0); 5 console.log(3)
以上代码中,第1行、第3行的代码就是同步代码。引擎读到这里可以立即执行的。
比如第1行的时候,立马在控制台打印出1。
而到了第2-4行的setTimeout代码,就是异步任务。引擎看见他们并不是立即开始倒计时。因为引擎倒计时的话,线程就会被阻塞了。
想象一下一个捂着眼睛在从100倒数到0的可爱小朋友。数不到0他不能睁开眼去抓其他捉迷藏的小伙伴。那在他数数期间是不是就不能动不能干别的事儿了?
代码也是这个道理,加入单线程的js真的去数数的话,他一次只能干一件事的单弦特性,会让整个页面的逻辑代码阻塞,造成页面假死情况的。
所以倒计时这个任务被浏览器内核的定时触发器线程接收并处理。相当于js一碰到这种代码,立即对其他线程说:“xx线程,这是你的任务,赶紧领走,别耽误我的进度。”
然后其他线程赶紧接盘,js引擎继续下边的代码。
这时都到了第5行,立马在控制台打印出了3。
当定时器在异步线程中到达时间时,异步线程将定时器内部的回掉函数抛出,加入所有回掉函数排队组成的一个任务队列(vip会员专享通道)。
回掉函数等待js主线程空闲时(所有同步任务执行完毕),被推进入执行栈被执行。此时被打印出 2。。
如果js主线程的任务还没有被处理完,即使异步线程条件成立(比如倒计时完毕),也不会被执行。这就说明了,为什么0毫秒不存在延迟的第3行代码,还是最后被执行、输出。
假如还有第二个回掉函数,js主线程在执行完一个回掉函数时,不是立马执行下一个,而是再排查一遍还有没有同步任务需要处理,没有了再执行第二个回调。第三个回调执行前也会再检查一遍。
而在任务队列里边的叫宏任务,在js主线程里边的叫微任务
四、任务队列
2018-11-22 11:04:49 周四
任务队列的形成:
js引擎是单线程的,在处理同步任务的时候,会在执行栈中立即顺序执行这些代码。
而当js遇到一个异步任务时,并不会立即执行并等待其返回结果。而是会将其挂起,转而继续执行其他的任务。
(就好像你正在处理几个项目bug,测试又提出一个需要耗时寻找病因的bug,你可能就会将这个耗时的bug先放到一边,重点解决可以快速处理的bug一样)
当异步事件达到条件返回结果后(比如定时器到时,比如ajax请求返回了数据),js会将这些事件的回调函数加入到一个“管道”中,这个管道就是任务队列。
进入管道后也不是会立马被执行的,在管道里大家都遵循先来后到,排队前进。前边的回调没执行完,后边的也就不会被轮到。
另外,管道里的任务还需要等待执行栈中的所有任务都执行完毕、执行栈空闲的状态下才会被执行。因为只有主线程处于空闲状态时,主线程才会立马去任务队列(管道)里查找回调函数,如果有排队等待在这里的回调函数,会按照他们的排队顺序依次取出来执行。且执行的还是其内部的同步代码。
任务队列规则:先进先出
与执行栈的规则不同的是,任务队列属于先进先出的数据结构。先排队的代码排在队列的最前边、也优先被主线程读取、被执行栈执行。
异步事件和回调函数
造成异步的代码上边已经说过了,大致是那三类:
而他们在异步线程里达到触发条件时,怎么加入的任务队列呢?
答案是靠的回调函数。
现在你细想一下,这三类代码是不是都有回调函数?
伪代码:
div.onclick = function(){ console.log("我就是回调函数") }
$.ajax({ url: '', type: 'get', success: function(){ console.log("我是成功的回调") } fail: function(){ console.log("我是失败的回调") } })
setTimeout(function(){ console.log('我是定时器的回调') },1000)
是不是细思极恐?
原来奥妙在这里。整个代码程序是有事件驱动的(点击事件、页面滚动、请求事件、定时器事件等)。
每个事件上都有一个回调函数。只要指定过回调函数,当事件触发或成立时,就会把回调函数放到任务队列里,等待主线程“翻牌子”。
主线程空闲时就会来任务队列里取回调函数执行。整个程序就被一个一个事件的驱动起来了。
换句话说,主线程执行异步任务其实就是执行对应的回调函数。
宏任务和微任务
值的注意的是,像定时器这些任务都属于宏任务(macro-task),回调函数被推入执行栈之前,js引擎都会先扫描一遍看还有没有微任务(micro-task)没有被执行,当所有微任务被执行完毕后,才会开始执行宏任务。
并且每下一次执行下一个宏任务之前,都会再检查一下还有没有微任务。之后再执行宏任务。
就像
常见的宏任务有:
setTimeout
常见的微任务有:
Promise.then
宏任务队列和微任务队列
任务队列中其实是双轨车道。
任务队列其实有两条:一条放宏任务队列,一条放微任务队列。
并且,微任务比宏任务的优先级高。
主线程比较偏向微任务。执行宏任务之前,他会先去微任务那里问问,你有没有任务让我执行啊?
如果有,主线程把微任务接走到执行栈执行。
然后主线程又问微任务,你还有任务要我执行吗?
微任务回答:没了。
直到微任务处理完毕,主线程开始翻牌宏任务。
并且每执行完一个宏任务,都会再问一遍微任务是否有任务需要插队处理。
setTimeout(() => console.log(4)) new Promise(resolve => { resolve() console.log(1) }).then(()=> { console.log(3) }) console.log(2)
以上,代码执行顺序:1、2、3、 4
解析见今日面试题第四题:【本周面试题】第2周 - 看上去和实际上的代码执行顺序、连等赋值问题、运算符优先级
Event Loop 事件循环
主线程不断的从任务队列中读取事件的过程。就是事件循环机制。也是计算机系统的一种运行机制。
有了Event Loop,使得js的单线程能够和浏览器提供的异步多线程有机组合、规律运行,形成一个完善运转的机器。
其实具体他是怎么样的,上边已经一直重复的就是了。
画一个自己理解的中文版本的:
图中js主线程、webAPIs异步线程和任务主队列三个组合成一个生态圈,不停的轮转,就是事件循环了。
非阻塞js(non-blocking javascript)
js文件在浏览器中的加载顺序
2018-12-12 19:37:36
js阻塞浏览器的某些处理过程:http请求、界面刷新
性能优化:js压缩变小,限制请求数、像页面中逐步添加js、
非阻塞:页面加载完成后,再加载js源码,即window的load事件发生后再开始下载代码
三种方法:
1. 延期脚本
2. 动态脚本元素
3. XMLHttpRequest(XHR)对象
一、延期脚本:defer属性
defer:script标签的一个属性,指明代码可以在dom树建立后执行
遇到script外部js,并且设置有defer,浏览器创建异步线程加载,并继续解析文档。不阻碍文档解析。
对于有defer属性的脚本,需要等到脚本解析完才会执行(即触发了window.onload事件后才执行)。因此可以放到页面的任何位置,就像一个内联脚本写了window.onload = function(){//主程序}一样,她被放到哪里都不会阻塞页面解析。
不足:
兼容性只有:ie4+,firefox 3.5+。其他浏览器忽略,js还会默认阻塞dom页面解析
二、动态脚本元素:document.createElement('script')+appendChild
使用js动态的创建HTML的文档内容。一个<script>标签和页面其他标签没什么区别。
1 var script = document.createElement ("script"); 2 script.type = "text/javascript"; 3 script.src = "non_blocking.js"; 4 document.body.appendChild(script);
只有当元素添加到页面之后,文件才开始下载。无论什么时候下载和运行,都不会阻塞页面其他处理过程。且除Firefox和Opera之外,返回的代码会立即执行。
并且可通过script标签的load事件监听脚本是否准备完毕。
IE中需要特殊方式:监听readyState == “complete" 才行。
readyState 有五种取值:
- "uninitialized"默认状态
- "loading"下载开始
- "loaded"下载完成
- "interactive"下载完成但尚不可用
- "complete"所有数据已经准备好
来自 <http://www.cnblogs.com/jenry/archive/2011/02/13/1953211.html>
兼容性写法:
来自 <http://www.cnblogs.com/jenry/archive/2011/02/13/1953211.html>
1 function loadScript(url, callback) { 2 var script = document.createElement("script") 3 script.type = "text/javascript"; 4 if (script.readyState) { //IE 5 script.onreadystatechange = function () { 6 if (script.readyState == "loaded" || script.readyState == "complete") { 7 script.onreadystatechange = null; 8 callback(); 9 } 10 }; 11 } else { //Others 12 script.onload = function () { 13 callback(); 14 }; 15 } 16 script.src = url; 17 document.getElementsByTagName("head")[0].appendChild(script); 18 }
动态脚本加载是非阻塞JavaScript 下载中最常用的模式,因为它可以跨浏览器,而且简单易用。
来自 <http://www.cnblogs.com/jenry/archive/2011/02/13/1953211.html>
三、XHR对象
说白了就是ajax加载一个js脚本,然后append到页面中,<script>标签添加到文档,代码将被执行,并准备使用。大型网站通常不采用。
优点:可以下载不立即执行的js代码,可推迟执行。
缺点:必须同源,不能跨域,不能在cdn下载?
总结+推荐:
一、动态加载js所需的代码,然后加载页面初始化所需的js之外的部分。初始代码准备完毕后,加载其余js。
二、script标签放置在body标签之前。1保证js运行不影响页面其他部分显示,2js文件完成下载,所有应用程序所必须的dom就已经创建完毕,并做好被访问的准备。
三、避免使用window.onload来监听页面是否已经准备好了。
定时器setTimeout 和 setInterval
setTimeout(function(){ //daima },0);