前言刚学前端的时候,总听说JS是单线程的,单线程的,单线程的。其实完整的JS执行引擎在浏览器环境下应该是单线程的。那么什么是线程?为什么JS是单线程的?1.进程和线程进程和线程的主要区别在于它们是管理操作系统资源的不同方式。一个进程有独立的地址空间。一个进程崩溃后,不会影响保护模式下的其他进程,线程只是一个进程中不同的执行路径。在我的理解中,一个程序至少运行一个进程,一个进程至少有一个线程。进程是操作系统分配内存资源的最小单位,线程是cpu调度的最小单位。比如进程就像一个工厂,线程就是里面的工人。工厂里有多个工人,里面的工人可以共享里面的资源。多个worker可以协同工作,类似于多个线程的并发执行。2.浏览器是多进程的。打开windows任务管理器,可以看到浏览器开启了很多进程,每个标签页都是一个单独的进程,所以一个页面崩溃后,不会影响到其他页面。浏览器包含以下进程:浏览器进程:浏览器的主进程(负责协调和主控),只有一个第三方插件进程:每种插件对应一个进程,GPU进程仅在使用插件时创建:最多一个,用于3D绘图和其他浏览器渲染进程(浏览器内核)(Renderer进程,内部多线程):默认情况下,每个标签页有一个进程,3、浏览器渲染过程浏览器渲染过程是多线程的也是前端人最关心的。它包括以下线程:GUI渲染线程负责渲染浏览器界面,解析HTML、CSS,构建DOM树和RenderObject树,布局绘制等。当界面需要重绘(Repaint)或回流(reflow)时是一些操作引起的,线程会执行GUI渲染线程和JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当冻结),GUI更新会保持在一个队列中并且当JS引擎空闲时立即执行。JS引擎线程,又称JS内核,负责处理Javascript脚本程序。(如V8引擎)JS引擎线程负责解析Javascript脚本并运行代码。JS引擎一直在等待任务队列中任务的到来,然后进行处理。任何时候一个Tab页(renderer进程)中只有一个JS线程在运行JS程序。还要注意GUI渲染线程和JS引擎线程是互斥的,所以如果JS的执行时间过长,就会导致页面渲染不连贯,导致页面渲染加载受阻。事件触发线程属于浏览器而非JS引擎,用于控制事件循环(JS引擎本身忙不过来,需要浏览器另开线程辅助,无可厚非)。当JS引擎执行setTimeOut等代码块时(或者来自浏览器内核中的其他线程,比如鼠标点击、AJAX异步请求等),会在事件线程中添加相应的任务。当对应的事件满足触发条件被触发时,线程会将事件添加到pendingqueue的尾部,等待JS引擎的处理。注意,由于JS的单线程关系,这些待处理队列中的事件必须排队等候JS引擎处理(仅当JS引擎空闲时)。传说中的setInterval和setTimeout所在线程的浏览器计时计数器是不会被JavaScript引擎统计的,(因为JavaScript引擎是单线程的,如果处于线程阻塞状态,会影响计时的准确性).所以用一个单独的线程来计时和触发计时(计时结束后加入事件队列,等待JS引擎空闲后执行)注意W3C在HTML标准中规定了一个时间间隔setTimeout中小于4ms要求按4ms计算。当异步http请求线程在连接XMLHttpRequest后通过浏览器开启新的线程请求检测到状态变化时,如果设置了回调函数,异步线程会产生一个状态变化事件,并将回调放入事件队列中。然后由JavaScript引擎执行。4.JS引擎是单线程的。为什么js引擎是单线程的?一个原因是多线程的复杂度会更高。还有一个问题是结果可能不可预知:假设JS引擎是多线程的,有一个div,线程A获取节点并设置属性,线程B删除节点,那又如何呢?多线程并发执行下如??何操作?或许这就是为什么JS引擎是单线程的,代码是从上到下顺序执行的。虽然降低了编程成本,但也存在其他问题。如果一个操作比较耗时,比如一个for循环的计算操作遍历10000万次,就会阻塞下面的代码,导致页面卡死……GUI渲染线程和JS引擎线程是相互的exclusive防止不可预测的渲染结果,因为js是可以获取dom的,如果你修改这些元素属性同时渲染界面(即js线程和ui线程同时运行),那么获取到的元素数据渲染线程前后可能不一致。所以当JS线程正在执行时,渲染线程会被挂起;当渲染线程执行时,JS线程会被挂起,所以JS会阻塞页面加载,这就是为什么JS代码要放在body标签之后,所有html内容之前;为了防止阻塞页面渲染到白屏。5、上面在WebWorker上说了,JS是单线程的,也就是说所有的任务只能在一个线程上完成,一次只能做一件事。前面的任务还没有完成,后面的任务只能等待。随着计算机计算能力的增强,特别是多核CPU的出现,单线程带来了极大的不便,无法充分利用计算机的计算能力。WebWorker为JavaScript创建了一个多线程环境,让主线程创建Worker线程并将一些任务分配给后者运行。主线程运行的同时,Worker线程在后台运行,互不干扰。等到Worker线程完成计算任务,再将结果返回给主线程。这样做的好处是,一些计算密集型或高延迟的任务由Worker线程承担,主线程(通常负责UI交互)会很流畅,不会被阻塞或变慢。WebWorker有几个特点:同源限制:分配给Worker线程运行的脚本文件必须与主线程的脚本文件同源。DOM限制:不能操作DOM通信:工作线程和主线程不在同一个上下文中,不能直接通信,必须通过消息完成。脚本限制:不能执行alert()方法和confirm()方法文件限制:不能读取本地文件6.浏览器渲染过程以下是浏览器渲染页面的简单过程,具体可以另开一篇文章~。~:《从输入 URL 到页面渲染完成发生了什么》用户输入url,DNS解析请求IP地址浏览器与服务器建立连接(tcp协议,三次握手),服务器处理返回html代码块浏览器接受处理,解析html为dom树,解析css为cssobjdom树和cssobj组合成一棵渲染树。JS根据渲染树计算、布局、重绘GPU合成,输出到屏幕。JS事件循环有很多大惊小怪。言归正传1.同步任务和异步任务JS有两种任务:同步任务异步任务同步任务,顾名思义就是代码同步执行,异步代码就是代码异步执行。为什么JS这么分裂?我们假设所有的JS代码都是同步执行的。一个打包好的JS有10000行代码。如果一开始遇到setTimeout,需要等100秒才能执行下面的代码......如果有一些io操作和异步请求等,想想setTimeout(()=>{就很郁闷//todo},100000)//以下10000行代码省略,因为同步执行异步任务比较耗时,而且大部分代码都是同步代码,所以我们可以先执行同步代码,hand将这些异步任务交给其他线程执行,比如定时触发线程、异步http请求线程等,等这些异步任务完成后再执行。这种调度同步和异步任务的策略就是JS事件循环:执行整体代码。如果是同步任务,会直接在主线程上执行,形成执行栈。当遇到网络请求等异步任务时,会交给其他线程去执行,当异步任务执行完毕后,会在事件队列中插入一个回调函数。一旦执行栈中的所有同步任务都执行完毕(即执行栈为空),就会读取事件队列,将一个任务插入到执行栈中,开始执行,重复步骤3。这就是事件循环,保证了同步和异步任务的有序执行。只有当当前所有的同步任务都执行完毕后,主线程才会去读取事件队列,看是否有任务(异步任务执行后的第一个回调)需要执行,一次一个。老讲setTimeoutsetTimeout(()=>{console.log('异步任务');},0);console.log('同步任务');下面的执行结果相信大家很容易理解,主线程扫描整体代码:如果发现一个异步任务setTimeout,将其挂起,交给定时器触发线程(定时器会把结果放到事件中等待指定时间后以回调的形式排队,等待主线程读取执行),找到同步任务控制台,直接塞进执行栈,从上到下执行。执行栈空闲后,查看事件队列中是否有任务(此时定时器执行完毕),取出一个任务塞入执行战执行事件队列清空2.宏-task,micro-task1.Macro-task,micro-task除了广义的同步任务和异步任务,JavaScript单线程中的tasks还可以细分为macro-tasks和micro-tasks:macro-task:script(整体代码),setTimeout,setInterval,setImmediate,I/O,UI渲染过程。nextTick,承诺,对象。观察,MutationObserver2。eventloop和macrotask和microtask的每个执行栈执行的代码是一个macrotask(包括每次从事件队列中获取一个事件回调,放入执行栈执行),检测本次循环是否找到microtask,如果存在则依次从微任务的任务队列中读取并执行所有微任务,然后在宏任务的任务队列中读取任务执行,再执行所有微任务,依次类推。JS的执行顺序是每个事件循环中的macrotask-microtask。在第一个事件循环中,整个代码作为宏任务进入主线程执行。同步代码直接压入执行栈执行。遇到异步代码,就挂起,交给其他线程执行(执行完会回调到事件队列)。同步代码执行后,读取微任务队列。如果所有的微任务都执行完了,微任务就会清空页面并渲染。从事件队列中取出一个宏任务,插入到执行栈中执行。对代码重复此操作以翻译它。#Macrotaskfor(letmacrotaskofmacrotask_list){#执行一个宏任务macrotask();#执行所有微任务for(letmicrotaskofmicrotask_list){microtask();}#UI渲染ui_render();}3.事件循环和页面渲染在ECMAScript中,microtask(微任务)可以称为作业,macrotasks(宏任务)可以称为任务。为了让JS内部任务和DOM任务能够有序执行,浏览器会在一个任务执行完成后,下一个任务执行前重新渲染页面:(任务->渲染->任务->...)我们来看一个例子,我们有一个id为app的div
