JavaSript的事件执行机制

写在前面:弄懂JavaScript事件执行机制必须要先知道的两点。
  • 1.JavaScript是一门单线程语言。
  • 2.Event Loop(事件循环)是JavaScript的执行机制。
  • 首先什么是单线程语言呢?简单通俗的解释就是代码的执行顺序是从上到下依次执行的。那么问题来了,当我运行下面代码,自信满满,以为会输出1,2,3,4时,却发现结果和我想象的不太一样啊,这是为什么呢?
setTimeout(()=>{
  console.log('1')
},0)
new Promise((resolve,reject)=>{
  console.log('2')
  resolve()
}).then(()=>{
  console.log('3')    
})
 console.log('4');
image
  • 是不是觉得和js是按照语句出现的顺序执行/产生的冲突呢?这时我们就要引入js的事件执行机制了。
关于JavaScript
  • javascript是一门单线程语言,在最新的HTML5中提出了Web-Worker, 但javascript是单线程这一核心扔未改变。所以一切javascript版的“多线程”都是用单线程模拟出来的,一切javascript多线程都是纸老虎。
JavaScript为什么需要异步
  • 如果js中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞。 对于用户而言,阻塞就以为着“卡死”,这样就导致了很差的用户体验。比如在进行ajax请求的时候如果没有返回数据后面的代码就没办法执行。
  • JavaScript异步的执行机制其实就是事件循环(eventloop),理解了eventloop机制,就理解了js异步的执行机制。
javascript事件循环
  • 我们都知道JavaScript中的任务可以分为两种:
    • 同步任务
    • 异步任务
  • 当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。此处引入一张图片来辅助理解


    image
  • 图片中的内容可以用以下文字表述:
    • 首先判断JS是同步还是异步,同步就进入主线程运行,异步的进入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并执行
  • 相信通过上面的文字和代码,你已经对js的执行顺序有了初步了解。接下来我们来研究进阶话题:setTimeout
setTimeout
  • setTimeout是一个典型的异步操作,我们经常会用它来延迟执行一个任务。不知道大家在项目中会不会遇到这样的问题,明明定时延迟3秒,但是3秒时间过去了,任务还没执行,这是怎么回事呢?让我们先看看下面的代码:
setTimeout(() => {
   task()
},3000)
console.log('执行console');
  • 根据前面我们的结论,setTimeout是异步的,所以上面的代码应该先执行console.log这个同步任务,然后再执行setTimeout里面的task()
  • 现在我们修改一下代码
setTimeout(() => {
    task()
},3000)

wait(10000000)
  • 乍一看好像没有什么区别,但是如果拿到chrome里执行就会发现打印'执行setTimeout的时间远超3秒,这是为什么呢?
  • 现在我们来详细分析一下上面的代码
    • task()进入Event Table并注册,计时开始。
    • 执行wait函数,很慢,非常慢,计时仍在继续。
    • 3秒到了,计时事件setTimeout完成,但是wait也太慢了吧,还没执行完,只好等着。
    • wait终于执行完了,task()终于从Event Queue进入了主线程执行。
  • 分析结束,我们知道setTimeout这个函数,是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。
  • 我们经常还会遇到这样的代码setTimeout(fn,0),0秒后执行,但是实际上真的能做到0秒执行吗?
  • 答案是不会的,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。举例说明:
//代码1
console.log('先执行这里');
setTimeout(() => {
    console.log('执行啦')
},0);

# 代码1执行结果
//先执行这里
//执行啦
//代码2
console.log('先执行这里');
setTimeout(() => {
    console.log('执行啦')
},3000);


# 代码2执行结果
//先执行这里
3s后...
//执行啦
  • 关于setTimeout要补充的是,即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒哟。
setInterval
  • setTimeoutsetInterval差不多,只不过后者是循环的执行。对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。
  • 需要注意的一点是,对于setInterval(fn,ms)来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。所以一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。

Promise与process.nextTick(callback)

  • 除了广义的同步任务和异步任务,我们对任务有更精细的定义:
  • 宏任务:包含整个script代码块,setTimeout, setIntval
  • 微任务promise.then, process.nextTick(node.js环境)
  • 不同类型的任务会进入对应的event queue, 比如setTimesetIntval会进入相同(宏任务)的event queue, 而promiseprocess.nextTick会进入相同(微任务)的event queue。
  • 事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。是不是觉得有点绕?我们下面来通过代码进行分析。
setTimeout(function() {
    console.log('setTimeout');
})

new Promise(function(resolve) {
    console.log('promise');
}).then(function() {
    console.log('then');
})

console.log('console');
  • 这段script代码块是一个宏任务,进入主线程执行
  • 首先遇到了setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。
  • 接下来遇到了new Promise,立即执行,then函数分发到微任务Event Queue。
  • 然后又遇到console.log(),立即执行。
  • 至此,整体代码script作为第一个宏任务执行结束,接着看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。
  • ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
  • 结束。
  • 事件循环,宏任务,微任务的关系如图所示:


    image
  • 下面用代码来深入理解上面的机制:
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'),执行输出1
    • 再遇到setTimeout,分配到宏任务Event Queue中。并记为setTimeout1
    • 下面遇到process.nextTick,分配到微任务Event Queue中。记为process1
    • 接着遇到Promisenew Promise,执行输出7,then被分发到微任务Event Queue中。记为then1
    • 最后又遇到setTimeout,分配到宏任务Event Queue中。记为setTimeout2。第一轮宏任务执行完毕。
    • 现在开始执行微任务,微任务Event Queue中有process1then1。顺序执行,输出6,8。
    • 到此第一次事件循环执行完毕,输出1,7,6,8。
  • 第二轮的事件循环从宏任务的第一个Event Queue,setTimeout1开始执行。
    • 执行setTimeout1,首先输出2,
    • 遇到了process.nextTick,分配到微任务Event Queue中。记为process2
    • 遇到Promisenew Promise立即执行,输出4,then被分发到微任务Event Queue中。记为then2。第二轮宏任务setTimeout1执行完毕。
    • 现在开始执行微任务,微任务Event Queue中有process2then2,顺序执行,输出3,5。
    • 到此第二次事件循环执行完毕,输出2,4,3,5。
  • 第三轮的事件循环就从宏任务的第二个Event Queue,setTimeout2开始执行。
    • 执行setTimeout1,首先输出9,
    • 遇到了process.nextTick,分配到微任务Event Queue中。记为process3
    • 遇到Promisenew Promise立即执行,输出11,then被分发到微任务Event Queue中。记为then3。第二轮宏任务setTimeout2执行完毕。
    • 现在开始执行微任务,微任务Event Queue中有process3then3,顺序执行,输出10,12。
    • 至此整段代码执行完毕,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
      (请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)。
  • 下面再来一个更复杂的案例代码:
new Promise(function (resolve) { 
    console.log('1')
    resolve()
}).then(function () {    // then1
    console.log('3')
})
setTimeout(function () {   // setTimeout1
    console.log('4')
    setTimeout(function () {    // setTimeout4
        console.log('7')
        new Promise(function (resolve) {
            console.log('8')
            resolve()
        }).then(function () {     // then2
            console.log('10')
            setTimeout(function () {   // setTimeout6
                console.log('12')
            })
        })
        console.log('9')
    })
})
setTimeout(function () {   // setTimeout2
    console.log('5')
})
setTimeout(function () {    // setTimeout3
    console.log('6')
    setTimeout(function () {   // setTimeout5
        console.log('11')
    })
})
console.log('2') 
  • 第一轮事件循环:
    • 首先script代码快作为一个宏任务进入主线程执行,遇到Promisenew Promise立即执行,输出1,then被分发到微任务Event Queue中。记为then1
    • 然后遇到setTimeout,分配到宏任务Event Queue中。并记为setTimeout1
    • 再遇到setTimeout,分配到宏任务Event Queue中。并记为setTimeout2
    • 接下来还是setTimeout,分配到宏任务Event Queue中。并记为setTimeout3
    • 遇到console.log('2'),执行,输出2。第一轮宏任务执行完毕。
    • 现在开始执行微任务,微任务Event Queue中有then1。执行,输出3。
    • 到此第一轮事件循环结束,输出1,2,3。
  • 第二轮事件循环从宏任务的第一个Event Queue,setTimeout1开始执行。
    • 遇到console.log('4'),执行,输出4,
    • 遇到setTimeout,分配到宏任务Event Queue中。并记为setTimeout4
    • 此时微任务Event Queue里没有任务,第二轮事件循环结束,输出4。
  • 第三轮事件循环从宏任务的第二个Event Queue,setTimeout2开始执行。
    • 遇到console.log('5'),执行,输出5。
    • 此时微任务Event Queue里没有任务,第三轮事件循环结束,输出5。
  • 第四轮事件循环从宏任务的第三个Event Queue,setTimeout3开始执行。
    • 遇到console.log('6'),执行,输出6
    • 遇到setTimeout,分配到宏任务Event Queue中。并记为setTimeout5
    • 此时微任务Event Queue里没有任务,第四轮事件循环结束,输出6。
  • 第五轮事件循环从宏任务的第四个Event Queue,setTimeout4开始执行。
    • 遇到console.log('7'),执行,输出7
    • 遇到Promisenew Promise立即执行,输出8,then被分发到微任务Event Queue中。记为then2
    • 遇到console.log('9'),执行,输出9。第五轮宏任务执行完毕。
    • 现在开始执行微任务,微任务Event Queue中有then2
      • 遇到console.log('10'),执行,输出10,
      • 遇到setTimeout,分配到宏任务Event Queue中。并记为setTimeout6
    • 到此第五轮事件循环结束,输出7,8,9,10
  • 第六轮事件循环从宏任务的第五个Event Queue,setTimeout5开始执行。
    • 遇到console.log('11'),执行,输出11,
    • 此时微任务Event Queue里没有任务,第六轮事件循环结束,输出11。
  • 第七轮事件循环从宏任务的第六个Event Queue,setTimeout6开始执行。
    • 遇到console.log('12'),执行,输出12,
    • 此时微任务Event Queue里没有任务,第七轮事件循环结束,输出12。
  • 到此整段代码执行完毕,输出顺序为1,2,3,4,5,6,7,8,9,10,11,12。
写在最后:
  • 事件循环是js实现异步的一种方法,也是js的执行机制。
  • 执行和运行有很大的区别,javascript在不同的环境下,比如node,浏览器,Ringo等等,执行方式是不同的。而运行大多指javascript解析引擎,是统一的。
  • 最后的最后
    • javascript是一门单线程语言
    • Event Loop(事件循环)是JavaScript的执行机制
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342