设计师培训多久/seo如何快速排名百度首页
一直说JavaScript是单线程的执行的(当然也可以通过其它其它的方式异步,本篇暂时不聊)。
内核的组成
首先聊一下浏览器的内核组成部分,当然下面也不是全部,而只是说一些常见的。
-
主线线程
- js引擎模块:负责js呈现的编译与运行。
- HTML,CSS文档解析模块:负责页面文本的解析。
- DOM/CSS模块:负责dom/css在内存中的相关处理
-
分线程
- 定时器模块:负责定时器的管理
- 事件响应模块:负责事件的管理
- 网络请求模块:负责ajax请求
这个如果对线程这个名词不太理解,可以看一下我在Java中写的线程,进程甚至协程的文章,虽然实现不同但是对于这几个名词还是很容易理解的:浅解线程
其实这个主线程和分线程的划分,其实就是异步。后面说JavaScript 是单线程,但是单线程不代表是同步,毕竟同步会导致阻塞,而异步却可以解决这个阻塞问题。
看着上面的主线程和分线程,其它的不说,但是看见了定时器模块,如何就不是主线程了,它不是也在js中写的吗,不也是js中的方法吗?
定时器真是定时吗?
这个首先问一下:定时器真的是定时执行的吗?
还是老规矩用代码演示:
var start=Date.now();setTimeout(()=>{console.log('这个定时器执行用了多久',Date.now()- start)},100)
竟然不是100毫秒而是105毫秒,这个还可以解释说一下,毕竟执行的时候又调用了new Date(),自然会占一点时间了,这个还可以说在没想到,但是仔细一想还可以解释。
但是我们可以这样操作:
var start=new Date();setTimeout(()=>{console.log('这个定时器执行用了多久',new Date() - start)},100);for(let i=0;i<100000;i++){};
这个就神奇了,为什么后面的循环会影响定时器模块?毕竟这个不是简单让运行时间提高一点,而是反了几倍,可见这个不是因为定时器中的运行程序影响的了。
当然如果更具神奇的操作那就是用alert:
var start=new Date();setTimeout(()=>{console.log('这个定时器执行用了多久',new Date() - start)},100);alert('alert弹出影响线程');
这个更具会影响,越晚点击alert弹出的对话框,这个定时器的运行时间会越长。
这个演示的时候,记得看一下自己的浏览器,在最新版浏览器在使用alert的时候只会暂停js的运行而不能暂停定时器的定时时间了(在老版本中才可以)。
可以如下看出先执行初始化代码(主线程),然后在执行回调函数如下:
setTimeout(()=>{console.log('定时器'); },0);console.log('定时器测试');
所以说在JS代码中也分两类:初始化代码和回调代码。而正常的流程就是初始化,而定时器的执行却是回调代码(或者说是异步执行代码)。
这个就好奇了为什么JavaScript要用单线程模式。
想回答这个问题,就需要了解JavaScript的用途。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。比如在操作DOM的时候比如一个添加标签一个删除标签,而这个逻辑其通过负责算法自然可以i实现,但是作为一个脚本语言用了最简单暴力的方法,那就是经典的一句话:如果解决问题麻烦,那就解决掉引起麻烦的人。而JavaScript就采用单线程,不出现同步问题,自然也就不用考虑那样复杂的问题了。
当然在H5 的的上海提出了web worker标签,但是它也是有很多限制的,受主线程的控制,而是主线程的子线程,整个也会聊,先不着急,此时先记住整个词即可。
由这可以引出一个问题,那就是既然有初始化代码和回调代码,那自然有一个东西或者说管理员管理这次代码,至少来一个简单的排序吧?所以整个就引出了JavaScript一个概念:事件循环模型。
事件循环模型(Event Loop)
事件循环(vent Loop)其实就是管理调用堆栈运行立即运行程序,以及控制循环回调队列,将整个程序运行下来。
其实整个就是理解一下过程,还是老规矩就是结合代码和图进行聊一下:
console.log('主线程1'); setTimeout(function test_time(){console.log('定时器'); },0);console.log('主线程2');
下图可以理解为初始化:
第一步,执行console.log('主线程1')
第二步:调用这个定时器
这个时定时器,自然有些点击事件对dom的元素绑定事件,或者Ajax等都在web api这个异步中根据等待时间,然后将其放在 callback queue
中,虽然可以循环这个时间,但是因为主线程还在继续,所以这个循环暂时不调用。
第三步: 程序继续运行
第四步: 主线程运行玩了,这个时候会在这个队列(callback queue),然后根据异步中的先后顺序放在这个队列,如果有回调函数,那就执行。
当然如果整体来看,可以通过网站:loupe
而callback queue
中的回调函数,也是可以说收到各自定时或者说dom中的元素事件触发的时间不同进行排序。而控制循环的就是 Event loop
。
这个时候又想到了一个神奇的事情,前面说DOM也是主线,但是DOM如果时自己加载还好,但是如果通过js操作DOM就有两个事情要操作了DOM事件
和DOM渲染
。
所以又引出了event loop
的与两者的关系。
DOM事件
还是老规矩代码演示:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>测试文档</title></head>
<body><button onclick=test() >按钮测试点击</button>
<script>function test() {console.log('点击了图标');}setTimeout(()=>{console.log('定时器');},10000);console.log('定时器测试');</script>
</body>
</html>
然后一个再3秒内点击,一个再三秒后点击。
如下:
三秒内:
三秒后点击:
可见这个点击事件,简单的理解,也是js中的DOM事件也是在web api中等待触发(定时器的触发就是定时事件,而例子时点击事件所以触发就是点击)在callback queue
,然后再根据触发的顺序依次执行。
既然是单线程那为什么定时器运行等待的时候却没有影响主线程的运行呢?其实这个就是通过异步的方式解决的阻塞问题,而不是同步,执行完毕上面再执行下面,而是通过异步也就是将需要需要等待的程序放在其它地方,等待触发的时候回调这个程序即可。
DOM渲染
DOM渲染,这个时通过js操作DOM,而这个不是通过点击等类似的事件触发,而是顺着JS执行。
演示:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>测试文档</title></head>
<body><di id="container"></di>
<script>setTimeout(()=>{alert('定时器')});var container=document.getElementById("container");var p1='<p>第一个p1</p>';var p2='<p>第一个p2</p>';var p3='<p>第一个p3</p>';container.innerHTML=p1+p2+p3;var len=container.children.length;alert('DOM中目标中放了'+len+'个孩子',);</script>
</body>
</html>
可以看看出在执行程序的时候已经把元素放在DOM中了。但是浏览器却没有渲染,不过其后渲染却在定时器回调函数之前,(ps: 如果用编程软件的话,记得不要用软件直接打开浏览器,因为有些编程软件会带一些服务器效果影响观察结果。我用的pycharm,竟然渲染在定时器后面,让我怀疑自己的知识,浪费了一些时间,查询资料。)
注意:我在这里只是说DOM渲染在定时器回调函数之前,为什么不说异步函数之前?
整个就引出了下面:
微任务和宏任务
后面又引出了两个概念宏任务和微任务,而这两个都是在异步里面
。
这个会引出一个对象:Promise
,这个是ES6中新增的。
Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值。这个先了解即可,后面我单独一篇文章聊Promise。
其实这个宏任务和微任务都是分线程,但是这个两者区别有很大。
-
宏任务:
macrotask,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
浏览器为了能够使得JS内部macrotask与DOM任务能够有序的执行,会在一个macrotask执行结束后,在下一个macrotask 执行开始前,对页面进行重新渲染.
宏任务包括:
setTimeout setInterval I/O AJAX DOM事件等 postMessage MessageChannel setImmediate(Node.js 环境)
-
微任务:
microtask,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。
所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。
微任务包括:
Promise.then async/await Object.observe MutationObserver process.nextTick(Node.js 环境)
首先记住一句话:微任务执行时机要比宏任务要早。
宏任务在DOM渲染后触发,而微任务是DOM渲染前触发的。
老规矩代码演示:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>测试文档</title><!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>测试文档</title></head>
<body>
<di id="container"></di>
<script>setTimeout(()=>{alert('定时器')});var container=document.getElementById("container");var p1='<p>第一个p1</p>';var p2='<p>第一个p2</p>';var p3='<p>第一个p3</p>';container.innerHTML=p1+p2+p3;var len=container.children.length;alert('DOM中目标中放了'+len+'个孩子',);// 暂时没必要明白为什么这样用,后面单独一篇会聊,这里就是简单的演示:Promise.resolve().then(()=>{alert('微任务')});</script>
</body>
</html>
既然都是分线程,为什么还要分微任务和宏任务?
因为宏任务是浏览器规定的,而微任务是ES6语法规定的。而这两个事件队列存在两个不同的地方, Event loop
控制的回调宏任务的事件队列。
所以程序运行的顺序应该是如下:
同步代码(主线程)---》微任务--》DOM渲染---》宏任务。
所以整个Event Loop 应该如下看: