准备知识1.进程(process)进程是系统资源分配的独立单位,一个程序至少有一个进程。例如:一个工厂代表一个CPU,一个车间代表一个进程。任何时候,只有一个进程在运行,其他进程都处于非运行状态。2、线程(Thread)线程是CPU调度调度的基本单位。一个线程只能属于一个进程,一个进程可以有多个线程且至少有一个。例如,一个车间的工人可以有多个工人一起工作。生活中经常看到某台电脑的CPU是4核4线程,也就是说这个CPU最多只能同时运行4个线程,所以有些线程会处于工作状态,有些线程会被打断。堵塞,睡眠状态。我经常看到许多任务同时进行,一边工作,一边听歌,一边下载电影。那是因为这些线程在以迅雷不及掩耳之势不停的切换主线程,所以人的体验感觉是很多很多的任务在同时进行。3.栈(stack)栈是一种具有后进先出特点的数据结构。最开始进入栈结构的数据只能到最后才出来。4.队列(queue)队列也是一种数据结构。数据只能从一侧进出,先进去的自然先出来。5.同步异步(syncasync)同步异步注意消息通信机制,同步在函数调用中,如果调用者没有得到响应结果,程序会一直等待直到得到结果。异步执行以下代码,等待响应结果再处理响应。6.阻塞和非阻塞(blocking&non-blocking)阻塞和非阻塞关注的是程序在等待调用结果时的状态。阻塞是指在调用结果返回响应之前线程会被挂起,程序无法继续往下走,非阻塞线程不会挂起,后面的代码可以继续执行。举个例子:我去超市买了一袋薯片,老板告诉我货架上没货了,我马上去仓库取货。过程中老板让我站着等他拿货拿给我。这个过程是阻塞的。老板要是跟我说,你先回去吧,等会儿他去仓库拿,拿了就给我打电话。这个过程是非阻塞的,我不用等待,我可以做其他事情。7、执行栈(executionstack)当js代码执行代码时,JS会为调用代码生成一个执行上下文对象,压入执行上下文栈。全局上下文先进入栈底,然后是函数的执行上下文(ExecutionContext),函数执行完后,函数上下文从栈中弹出,全局上下文不从栈底弹出堆栈直到浏览器退出。代码示例:varglobalName="window";varfoo1=function(){console.log("foo1");}varfoo2=function(){console.log("foo2");foo1();}foo2();上图可以大致描述执行上下文栈的实现逻辑。关于执行上下文的知识可以看我之前的文章-《JavaScript 之执行上下文》二、为什么JS是单线程模型?JavaScript的一个非常有趣的特性是事件循环模型,与许多其他语言不同,它从不阻塞。处理I/O通常通过事件和回调来执行——MDN浏览器的主要任务是为用户提供视觉和交互体验。如果页面在使用中,偶尔出现阻塞、挂起、无响应的体验一定是很糟糕的。同时,如果采用多线程同步模型,如何保证同时修改DOM,哪个线程先生效。浏览器执行环境的核心思想在于特殊的任务调度方式:哪个任务的优先级高,就先运行,直到执行完毕才执行下一个,而且只有一个代码片段可以同时执行,也就是所谓的单线程Model。例如,银行只有一个柜台。如果要办理业务,就得先拿号码排队。叫到你的号码才能上去办理业务。多人不能同时在一个柜台办理业务,否则容易出错。3.事件循环事件循环是JS处理各种事件的核心。由于多个线程同时操作DOM,造成不可控的问题,所以JS采用了单线程模型。另外,由于所有的事件都是同步执行的,执行完一个后才能执行下一个,会造成页面渲染阻塞。JS中有异步事件。用户点击页面时,可以请求网络同事响应,也可以进行其他的点击操作,保证页面不会因为网络请求和各种IO响应慢而在代码执行中被阻塞和挂起接口。上升。事件循环的顺序是:进入script标签,创建全局上下文,执行全局上下文中的函数,压入执行调用栈。函数执行完毕后,弹出执行栈,并清除函数上下文中的变量对象和内存空间。判断渲染是否需要更新,必要时更新渲染。如果遇到异步事件,也会被压入执行调用栈,但是浏览器识别出是异步事件后,会将其pop出执行栈,然后将异步事件的回调函数放入事件队列。一直执行到函数调用栈清空,只剩下全局执行上下文。这时JS会检查事件队列中是否有事件。如果有,则事件队列中的一个事件出队,然后压入执行栈执行。当执行栈清空,只剩下全局执行上下文时,会重复第5步。这是JS事件循环。当用户关闭浏览器时,全局执行上下文弹出执行栈,清除相应上下文中的变量对象和内存空间。接下来我们用代码来解释一下:");},1000);functionfoo2(){console.log("foo2");}foo2();console.log("脚本结束!");打印://脚本开始!//foo1//foo2//脚本结束!//setTimeout!然后我们尝试把setTimeout的延迟时间改成0,想马上执行,看会不会马上执行:console.log("scriptstart!");functionfoo1(){console.log("foo1");}foo1();setTimeout(function(){console.log("setTimeout!");},0);functionfoo2(){console.log("foo2");}foo2();console.log("脚本结束!");打印://脚本开始!//foo1//foo2//脚本结束!//设置超时!可以看出setTimeout是一个异步事件,总是会在主线程的任务执行完毕后才开始执行。顺便说一下事件循环的几个原则:一次只处理一个任务,一个任务不被其他任务打断。这两个原则保证了浏览器任务单元的完整性和事件调用的顺序。4.宏任务和微任务事件循环的实现本来应该由一个宏任务队列和一个微任务队列来完成的,这使得事件循环可以根据任务类型进行优先级排序。宏任务:宏任务包括:创建文档对象、解析HTML、执行主线程代码(脚本)执行各种事件:页面加载、输入、点击setTimout、setInterval异步事件宏任务代表离散独立的工作单元,运行后的任务完成后,浏览器可以执行其他任务调度,例如更新渲染或执行垃圾回收。宏任务需要多个事件循环来执行。微任务:微任务包括:Promise回调函数newMutaionObserver()微任务是更小的任务。微任务需要尽可能异步执行。微任务更新浏览器的状态,但必须在浏览器执行其他任务之前执行。微任务使我们能够避免不必要的UI重绘。微任务必须在事件循环中完全执行。macrotasks和microtasks的执行优先级原则是:完成一个macrotask后,在同一个事件周期内执行剩余的microtasks,macrotasks总是先于microtasks执行。好了,知道了优先级原则,我们来看一段代码:console.log(1);setTimeout(function(){console.log(2);newPromise(resolve=>{console.log(3);解决(4);console.log(5);}).then(data=>{console.log(data);});},0);newPromise(resolve=>{console.log(6);解析(7);console.log(8);}).then(data=>{console.log(data);});setTimeout(function(){console.log(9);},0);安慰。log(10);输出:第一个循环://1//6//8//10//7第二个循环://2//3//5//4第三个循环//9让我们分析上面的代码together:进入第一个事件循环,脚本宏任务,输出1。第一个setTimeout函数本身是一个函数调用,属于任务源,setTimeout的回调函数,即第一个参数,是Distributedtask,将任务加入到宏任务队列中,在第二个周期调用。Promise是微任务,但Promise初始化中的代码会立即进行。因此,6和8会立即输出;Promise初始化后的回调放入微任务队列。第二个setTimeout也属于macrotask源,将回调函数的任务放入macrotask队列。当调用第三个事件循环时,调用继续调用栈,输出10。没有问题。第一个事件循环的宏任务执行完,剩下的微任务全部执行完,所以输出7。第二个事件循环找到一个宏任务,也就是第一个setTimeout的回调,输出2,调用PromiseBuild函数的调用栈,直接执行,所以输出3和5,将setTimeout的第一个promise回调放到微任务队列中。第二次事件循环的macrotask调用执行完后,执行上一步Promise创建的microtask,输出4,至此第二次循环执行完毕。进入第三个事件循环,只有一个宏任务,即第二个SetTimeout,所以输出为9;关于事件循环宏任务和微任务的执行流程:首先,两类任务在下一次微任务会前一一执行,在渲染或垃圾回收前全部执行。在事件循环中,只有一个宏任务先被执行,所有的微任务,包括新创建的微任务,都在下一个事件循环之前执行。5、Webworker虽然新的HTML5标准加入了webworker的多线程技术,但是webworker只能用于计算,JS多线程worker不能操作DOM,否则无法控制谁在操作页面.主线程传递给子线程的数据被复制复制,子线程传递给主线程的数据也被复制复制,而不是共享同一个内存空间。上面说明JS没有线程同步,所以JS还是可以看成是单线程模型,webworker可以看成是JS的一种回调机制。总结事件循环是JS和Nodejs事件调用机制的核心,它保证了页面能够有序的、非阻塞的进行处理。事件循环的主要逻辑是先执行调用栈,直到调用栈清空,只剩下全局上下文。然后JS查看宏任务队列,如果有任务就取出一个调用它进行页面渲染和垃圾回收。同时将所有微任务源派发的任务添加到微任务事件队列中,最后执行所有剩余的微任务。microtask执行完后,页面渲染和垃圾回收后进行下一轮事件循环。欢迎关注我的个人公众号“谢南波”,专注分享原创文章。
