一、先了解javascript为什么是单线程
|--javascript语言的特点:单线程。
|--线程和进程
|--进程:运行的程序就是一个进程,比如正在运行的浏览器就是一个进程。
|--线程:程序中独立运行的代码段,一个进程由单个或多个线程组成,线程是负责执行代码的。
|--JS为什么单线程?多线程效率多高啊?
|--1.首先决定单线程的主要原因是js的用途:用户交互和操作DOM
|--2.举个例子:两个线程,一个线程在DOM节点添加内容,另一个线程删除了这个节点。
|--3.上述例子浏览器应该以哪个线程为准?
|--综上:产生了问题。
|--为了避免复杂性,单线程成为了javascript的核心特征。
|--但是为了利用多核CPU的计算能力,HTML5提出了Web Worker标准,允许js创建多个线程,但是子线程
完全受主线程控制,且不得操作DOM,新标准并没有改变javascript单线程本质。
二、同步执行和异步执行
|--同步执行:因为JS语言特点是单线程,即任务是串行的,后一个任务要等前一个任务执行完,才能执行,这样在主线程按照顺序,串行执行的任务称为同步执行任务。
|--异步执行:由于类似于Ajax网络请求、setTimeout时间延迟、DOM事件的用户监护等,这些任务并不消耗CPU,是一种空等,资源浪费,因此出现了异步。通过将任务交给异步处理模块去处理,主线程的效率能大大的得到提升,可以并行的处理其他操作。当异步处理完成,主线程空闲时间,主线程读取相应的callback,进行后续操作,最大程度的利用CPU。因此异步执行就是CPU跳过等待,先处理后续任务(CPU和网络模块、timer模块等并行进行任务)。
|--而为了协调主线程和异步模块之间性的工作,就产生了任务队列和事件循环。
三、Event Loop事件循环机制
|--逐步分析事件循环机制:
|--1.主线程运行js代码,产生了堆和执行栈。
|--堆:对象被分配在堆中,即用来表示大部分非机构化的内存区域。
|--执行栈:execution context stack,运行同步代码。执行栈中的代码(同步任务),总是在读取
task queue(异步任务)之前。
|--2.主线程遇到异步任务,指给对应的异步处理模块(异步进程)进行处理(web API)
|--3.异步进程处理完毕(Ajax返回、DOM事件处理、Timer到时等),将相应的异步任务推入任务队列。
|--任务队列:任务队列是一个事件队列,IO设备完成一项任务,就在task queue里添加一个事件,表示
相关的异步任务可以进入“执行栈”了。主线程读取任务队列,就是读取里面的那些事件(数据结构是
先进先出)。主线程的读取过程是自动的,只要执行栈一清空,任务队列中的第一个事件就会自动
进入主线程。
|--回调函数:任务队列也被称为callback queue(回调队列),即异步任务必须指定回调函数,当主线程
开始执行异步任务时,就是执行对应的回到函数。
|--异步进程包括:
|--类似于onclick,由浏览器内核的DOM binding模块处理,事件触发时,回调函数添加到任务队列。
|--setTimeout,由浏览器内核的Timer模块处理,事件到达,回调函数添加到任务队列。
|--Ajax,由浏览器内核的Network模块处理,网络请求返回后,添加到任务队列。
|--4.主线程循环的去查找、执行任务队列中的事件,就形成是事件循环(event loop)。
四、宏任务和微任务
|--将任务进行更精细的定义,分为宏任务和微任务。
|--宏任务队列(macrotask queue)
|--不唯一,存在一定的优先级(用户I/O部分优先级更高),异步执行,同一个事件循环中只执行一个,
包括:整体代码script,setTimeout、setInterval、ajax、dom操作。
|--微任务队列(microtask queue)
|--整个事件循环当中仅存在一个,执行为同步,同一个事件循环中的microtask会按照队列顺序,
串行执行。微任务指的是ES6中的Promise。
|--宏任务和微任务区别:
|--微任务中所有的callback处在同一个事件循环中,宏任务中的callback有自己的事件循环。
|--利用微任务可以形成一个同步执行环境,但是如果微任务太长,将导致宏任务等待太久,长时间
执行不了,最终导致用户的I/O无响应,所以慎用使用。
|--加入宏任务和微任务概念的js运行机制:
|--1.“执行栈”最先执行所有的同步代码(宏任务)。执行完毕。
|--2.检查是否有微任务(microtask),如果有执行所有微任务。
|--3.取出“任务队列”中的事件对应的回调函数(宏任务)进入执行栈。执行完毕。
|--4.再检查是否有微任务,有的话执行多有微任务。
|--5.主线程不断重复执行3,4形成事件循环。
|--示例:
|--运行机制分析:
|--同步环境:1 -> 2 -> 3
|--事件循环(微任务):5
|--事件循环(宏任务):4
五、宏任务之setTimeout和setInterval
|--两个都是定时器,内部运行机制完全相同,区别在意setTimeout一次性执行,而setInterval反复执行。
|--setTimeout和setInterval产生的任务都是异步任务,且是宏任务。
|--两个定时器都是接受两个参数
|--fn:callback 函数
|--time:推迟执行的毫秒数、反复执行的毫秒数。
|--注意:如果第二个参数为0,并不是立即执行,而是指定某个任务在主线程最早可得的空闲时间执行,
也就是“尽早执行”。它在任务队列的“尾部”添加一个事件,因此要等同步任务和“任务队列”现有的事件处理完
才能得到执行。
|--setTimeout(function(){},3000)是异步任务,先被放入event table,3秒后才被推入到task queue,
而task queue任务队列里的任务,只有主线程空闲时,才会执行。这样一来如果同步任务超出了
推迟执行的事件3000s,那么这个3000秒就没有什么意义了,setTimeout(function(){},3000)就等同于
setTimeout(function(){},0)。
六、微任务之Promise
|--当new Promise(function(){... ...}).then(microTask),Promise里的function会立即执行,但是then方法里的
函数是在执行栈后,任务队列之前执行的,即微任务。
七、nodeJs中的process.nextTick
|--node.js中提供的和“任务队列”有关的方法,他产生的任务是放在执行栈的尾部,并不属于宏任务或
微任务。因此它的任务总是发生在异步任务的前面。
八、nodeJs中的setImmediate
|--他产生的任务在“任务队列”的尾部。
九、总结
|--任务执行的优先级:
|--1.同步代码(宏任务)
|--2.process.nextTick(node.js中的方法)
|--3.Promise(微任务)
|--4.setTimeout(fn)、setInterval(fn)等(宏任务)
|--5.setImmediate(nodejs)
|--6.setTimeout(fn,time>0)、setInterval (fn,time>0)
|--示例1:同步任务>异步任务 [优先级]
|--示例2:证明任务队列中,先进先出。
|--示例3:在fn()之前增加一个微任务,Promise里的内容同步执行,先输出8,9后才是6。
|--示例4:Promise形成了同步执行环境。但是then里的内容会在同步后,任务队列前执行。
十、扩展
|--案例:for循环+setTimeout问题
|--如何让其输出0~4?
|--解决办法1:var该为let
|--解决办法2:通过闭包
|--解决办法3:添加立即执行的函数