js事件循环机制event-loop

348 阅读5分钟
  • 事件循环机制由三部分组成:函数调用栈、微任务队列、消息队列

  • js:单线程,解释性语言

  • event-loop是js的执行机制

  • 任务:同步任务:当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染;异步任务:像加载图片音乐之类占用资源大耗时久的任务。

  • event-loop开始的时候,会从全局一行一行的执行,遇到函数调用,会压入到函数调用栈中,被压入的函数被称之为帧,当函数返回后会从栈中弹出

  • 宏任务:js中的异步操作:比如fetch,setTimeout,setInterval,script,setImmediate压入到函数调用栈中的时候里面的消息会进入到消息队列中去,消息队列会等到函数调用栈清空后再执行

    function func1() {
    	console.log(1)
    }
    function func2(){
    	setTimeout(() => {
    		console.log(2)
    	}, 0)
    	func1()
    	console.log(3)
    }
    func2()
    // func2压入栈中,接着setTimeout压入栈中,但是里面的消息console.log(2)会进入消息队列
    // func1被压入栈中,等函数调用栈全部执行完之后菜会执行消息队列,因此执行顺序是1,3,2
    
  • 微任务:微任务队列:原生Promise(有些实现的promise将then方法放到了宏任务中)、process.nextTick、Object.observe(已废弃)、 MutationObserver、async、await异步操作的时候会加入微任务队列中去,会在函数调用栈清空的时候立即执行,函数调用栈中加入的微任务会立马执行

    var p = new Promise(resolve => {
    	console.log(4) // 加入函数调用栈,立即执行
    	resolve(5) // 加入微任务队列,函数调用栈清空的时候才执行
    })
    // 函数调用栈中加入的微任务会立马执行,所以先输出4
    function func1() {
    	console.log(1)
    }
    function func2() {
    	setTimeout(() => {
    		console.log(2)
    	}, 0)
    	func1()
    	console.log((3)
    	p.then(resolve => {
    		console.log(resolve)
    	})
    }
    func2()
    // func2加入函数调用栈,setTimeout加入消息队列,func1()加入函数调用栈
    // p.then加入微任务队列,先于消息队列
    // 输出 1 3 5 2
    
    console.log('script start');
    
    setTimeout(function() {
      console.log('setTimeout');
    }, 0);
    
    Promise.resolve().then(function() {
      console.log('promise1');
    }).then(function() {
      console.log('promise2');
    });
    
    console.log('script end');
    
    // 顺序是script start, script end, promise1, promise2, setTimeout
    // Promise.resolve.then是已经将内容放入微任务列表
    
  • 过程

    • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
    • 当指定的事情完成时,Event Table会将这个函数移入Event Queue
    • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
    • 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
  • 如何判短主线程执行栈为空:js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

  • 例子

    let data = [];
    $.ajax({
        url:www.javascript.com,
        data:data,
        success:() => {
            console.log('发送成功!');
        }
    })
    console.log('代码执行结束');
    // ajax进入Event Table,注册回调函数success
    // 执行console.log('代码执行结束')
    // ajax事件完成,回调函数success进入Event Queue
    // 主线程从Event Queue读取回调函数success并执行
    
    setTimeout(() => {
        task()
    },3000)
    
    sleep(10000000)
    // task()进入Event Table并注册,计时开始
    // 执行sleep函数,很慢,非常慢,计时仍在继续
    // 3秒到了,计时事件timeout完成,task()进入Event Queue
    // 但是sleep也太慢了吧,还没执行完,只好等着。
    // sleep终于执行完了,task()终于从Event Queue进入了主线程执行。
    // setTimeout这个函数,是经过指定时间后,把要执行的任务(本例中为task())
    // 加入到Event Queue中,又因为是单线程任务要一个一个执行,
    // 如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。
    
    setTimeout(function() {
        console.log('setTimeout');
    })
    
    new Promise(function(resolve) {
        console.log('promise');
    }).then(function() {
        console.log('then');
    })
    
    console.log('console');
    // 这段代码作为宏任务,进入主线程。
    // 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue
    // 接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。
    // 遇到console.log(),立即执行。
    // 整体代码script作为第一个宏任务执行结束
    // then在微任务Event Queue里面,执行。
    // 第一轮事件循环结束了,开始第二轮循环
    // 要从宏任务Event Queue开始。
    // 宏任务Event Queue中setTimeout对应的回调函数,立即执行。
    
    console.log('1');
    
    setTimeout(function() {
        console.log('2');
        process.nextTick(function() {
            console.log('3');
        })
        new Promise(function(resolve) {
            console.log('4');
            resolve();
        }).then(function() {
            console.log('5')
        })
    })
    process.nextTick(function() {
        console.log('6');
    })
    new Promise(function(resolve) {
        console.log('7');
        resolve();
    }).then(function() {
        console.log('8')
    })
    
    setTimeout(function() {
        console.log('9');
        process.nextTick(function() {
            console.log('10');
        })
        new Promise(function(resolve) {
            console.log('11');
            resolve();
        }).then(function() {
            console.log('12')
        })
    })
    // 第一轮事件循环流程分析如下:
    
    // 整体script作为第一个宏任务进入主线程,遇到console.log,输出1。
    // 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1。
    // 遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1。
    // 遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1。
    // 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2。
    // 发现了process1和then1两个微任务。
    // 执行process1,输出6。
    // 执行then1,输出8。
    
    // 第二轮时间循环从setTimeout1宏任务开始
    
    // 首先输出2。
    // 接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。
    // new Promise立即执行输出4,
    // then也分发到微任务Event Queue中,记为then2。
    // 发现有process2和then2两个微任务可以执行。
    // 输出3。
    // 输出5。
    
    // 第三轮事件循环开始,此时只剩setTimeout2了,执行
    
    // 1 7 6 8 2 4 3 5 9 11 10 12