javascript 运行机制

123 阅读3分钟

js的单线程

js最大的特点就是单线程,也就是说,同一个时间只能做一件事。那么如何提高效率?所以js语言设计者意识到后把所有任务分成两种:同步任务和异步任务

同步任务

在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务,会阻塞程序执行

同步阻塞示例

console.log('start');
alert('200')
console.log('end');

异步任务

不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行,不会阻塞程序任务

前端使用异步的场景:

  • 定时任务:setTimeout,setInterval
  • 网络请求:ajax请求,动态图片加载
  • 事件绑定
//加载示例
console.log('start');
var body = document.querySelector('body')
var img  = document.createElement('img')
// 异步操作,回头才处理,继续下面的请求,不会阻塞代码执行
img.onload = function() {
    console.log('loaded');
}
img.src = 'https://file.vetscloud.com/awen/local/withdrawal-bg.png'
body.appendChild(img)
console.log('end');

任务队列和事件循环(event loop)

同步和异步任务分别进入不同的执行“场所”,同步进入主线程,异步进入Event Table并注册函数。当指定的事情完成时,Event Table会将这个函数移入任务队列(Event Queue)。主线程内的任务执行完毕,就去任务队列(Event Queue)读取对应的函数,进入主线程执行。 上述过程会不断重复,也就是常说的Event Loop(事件循环)。

宏任务,微任务

宏任务:当前调用栈中执行的代码成为宏任务,主要有:script(整体代码)、setTimeout、setInterval、UI交互事件、MessageChannel 等

微任务:宏任务执行完,在下一个宏任务开始之前需要执行的任务,可以理解为回调函数,主要有:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)等

宏任务,微任务的执行顺序如下图:

整体的运行流程如下图:

现在我们通过一个示例来看宏任务和微任务的执行

var p = new Promise(resolve => {
    console.log(4);
    resolve(5);
});

function fun1() {
    console.log(1)
}

function fun2() {
    setTimeout(() => {
        console.log(2)
    });
    
    fun1();
    
    console.log(3);
    
    p.then(resolved => {
        console.log(resolved)
    })
}
fun2();

第一步:new Promis压放入执行栈中,然后执行里面的代码,打印4,执行resolve(5)注意这里跟new普通函数一样是正常执行的,不会加入到宏任务中;

第二步:执行fun2(), 遇到setTimeout,将它放入宏任务中; 接着执行fun1();打印出1 , 接着console.log(3);打印3

遇到Promise对象执行then()时,这里是异步操作,会将里面回调函数放入微任务中,等待执行 当执行栈被清空时,执行微任务中的console.log(resolved),打印出5, 当微任务清空后,再执行宏任务,即setTimeout到时间后会答应出2, 所以最后答案为:41352

总结

如果把js当作初始的宏任务,那么js在浏览器端的执行过程就是这样:

先执行一个宏任务, 然后执行所有的微任务

再执行一个宏任务,然后执行所有的微任务

反复进行,执行到执行栈和任务队列为空

参考链接

  1. ​什么是 Event Loop?​
  2. ​JavaScript 运行机制详解:再谈Event Loop​
  3. ​浏览器进程/线程模型及JS运行机制​
  4. ​从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理​
  5. ​这一次,彻底弄懂 JavaScript 执行机制​