事件循环机制

事件循环(evenloop)

事件循环机制是宿主环境提供的。
js中处理异步,增加了任务队列的概念(你不知道的js中卷把这个叫做事件循环队列)。是异步事件完成后才把回调函数放入队列中。不是触发异步事件就 放入队列。以Promise为例:Promise是一个micro-task,只有当Promise被决议了后才将then中的回调函数放入队列。ES6之前,JS引擎本身所做的只不过是在不断轮询任务队列,然后执行其中的任务。JS引擎根本不能做到自己主动把任务放到任务队列中。这一点在ES6中有所改变(主要因为 ES6 中 Promise 的引 入,因为这项技术要求对事件循环队列的调度运行能够直接进行精细控制)。
我认为,浏览器有一个主的js引擎,同步任务就在主线程上排队执行,异步的任务才进入事件循环队列。浏览器在合适的时候把异步队列的内容放到主线程去执行。
HTML5规范里有Event loops这一章节(读起来比较晦涩,只关注相关部分即可)。

  1. 每个浏览器环境,至多有一个event loop(2017年新版的HTML规范,浏览器包含2类事件循环:browsing contexts 和 web workers。)。
  2. 一个event loop可以有1个或多个task queue。
  3. 一个task queue是一列有序的task,用来做以下工作:Events task,Parsing task, Callbacks task, Using a resource task, Reacting to DOM manipulation task等。

每个task都有自己相关的document,比如一个task在某个element的上下文中进入队列,那么它的document就是这个element的document。

每个task定义时都有一个task source,从同一个task source来的task必须放到同一个task queue,从不同源来的则被添加到不同队列。

每个(task source对应的)task queue都保证自己队列的先进先出的执行顺序,但event loop的每个turn,是由浏览器决定从哪个task source挑选task。这允许浏览器为不同的task source设置不同的优先级,比如为用户交互设置更高优先级来使用户感觉流畅。

macro-task(task)和micro-task

一个事件循环可以有多个任务队列,包含macro-task queue和micro-task queue,这两个任务队列执行顺序如下,取1个macrotask queue中的task,执行之。
然后把所有microtask queue顺序执行完,再取macrotask queue中的下一个任务。

macrotasks: script(整体代码),setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promises, Object.observe, MutationObserver

当microtask队列为空时,event loop检查是否需要执行UI重渲染,如果需要则重渲染UI。这样就结束了当次循环,继续从头开始检查macrotask队列。


你不知道的js中提出:在 ES6 中,有一个新的概念建立在事件循环队列之上,叫作任务队列(job queue)其中每个任务叫job。因此,作者认为对于任务队列最好的理解方式就是,它是挂在事件循环队列的每个 tick(队列的每一个循环过程) 之后 的一个队列。在事件循环的每个 tick 中,可能出现的异步动作不会导致一个完整的新事件 添加到事件循环队列中,而会在当前 tick 的任务队列末尾添加一个项目(一个任务)。

事件循环队列类似于一个游乐园游戏:玩过了一个游戏之后,你需要重新到队尾排队才能 再玩一次。而任务队列类似于玩过了游戏之后,插队接着继续玩。

那microtasks不是只有Promise,为什么说知道ES6,JS才真正的出现异步?
因为除了Promise的其他几个都不属于JS范畴,



所以我认为micro-task在ES6规范中称为Job。micro-task queue 就是 job queue其次,macro-task代指task, macro-task queue 就是 事件循环队列。

setTimeout(function(){
  console.log(1)
},0)

new Promise(function(resolve, reject){
    console.log(2)
    resolve('resolve')
}).then(function(){
    console.log(3)
})

比如这段代码,依次输出 2 3 1。代码执行顺序是:

整体代码作为 macro-task ,输出 2 (Promise内部是同步的)
然后执行 mirco-task ,这里也就是 Promise 的resolve了,输出 3
再之后执行 macro-task ,这里就是 1 了
看下面这种情况

new Promise(function (resolve) {
        setTimeout(resolve,0)
    }).then(function() {
        console.log('then')
    });

虽然promise.then是microtask,setTimeout是macrotask,但是promise.then只有resolve或reject了才会触发(才会进入queue)。所以这里setTimeout 本身已经把 resolve 延迟到下个 event loop 执行了。就只能先执行setTimeout函数后执行promise.then了。
Event Loop、Tasks和Microtasks
Promise的队列与setTimeout的队列有何关联
Tasks, microtasks, queues and schedules
HTML系列:macrotask和microtask

async/await 执行顺序

例1
function  GenFunc () {
  new Promise(function(resolve) {
        resolve()
    }).then(function() {
        console.log('1')
    })
  console.log('2')
}
GenFunc()
// 执行结果
// 2
// 1
例2
async function  GenFunc () {
  new Promise(function(resolve) {
        resolve()
    }).then(function() {
        console.log('1')
    })
    await 'string';
    console.log('2')
}
GenFunc()
// 执行结果
// 1
// 2

为什么这两个例子的执行结果会不同??

正常情况下,await命令后面是一个 Promise 对象。如果不是,会被转成一个立即resolve的 Promise 对象。

async function f() {
  return await 123;
}

相当于 await Promise.resolve(123);
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

首先我们肯定知道Promise.then属于microtask。例1的结果是符合预期的,那我们来看下例2。因为加了一句await 'string'就改变了执行结果。为什么呢?
async/await 是generator的语法糖。

 function * GenFunc () {
  new Promise(function(resolve) {
        resolve()
    }).then(function() {
        console.log('1')
    })
    yield new Promise(resolve => resolve('string'))
    console.log('2')
}
co(GenFunc) // co库是geneator的自执行函数

之前实现co库的时候我们知道了yield如果跟的是promise。co
库的简易实现如下

function co (GenFunc) {
  var gen = GenFunc(); 
  next();
  function next() {
    var ret = gen.next();
    if(!ret.done) {
      ret.value.then(next) // 
    }
  }
}

我们看的会自动给promise加then函数then(next)。

所以我们这里的yield new Promise(resolve => resolve('string'))实际相当于
Promise.resolve('string').then(next)的语法糖,如果xxx本身是一个Promise,Promise.resolve(xxx)的then就是xxx的then,之前Promise的实现也分析过的。因为Promise.then是microtask,并且geneator函数遇到yield之后必须调用遍历器对象的next继续执行下去。这里 await 'string'; 暂停了geneator函数,并且把next函数放进了Promise.then中执行。所以就先执行了microtask队列后才出发next,GenFunc函数才能继续执行下去,继而执行 console.log('2')

(async function GenFunc() {
  new Promise(function constructPromise(resolve) { // 1. Promise构造器,同步执行constructPromise
    resolve()                                      // 2. resolve,将promiseCallback放入microtask队列位置1
  }).then(function promiseCallback() {
    console.log('1')                               // 6. microtask队列位置1
  })
  await 'string'                                   // 3. await,将next函数放入microtask队列位置2
  console.log('2')                                 // 7. next函数执行完后执行
})()                                               // 4. async call return,返回Promise<pending>
console.log('3')                                   // 5. 执行剩余的同步代码

vue的异步更新

Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会一次推入到队列中。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。
例如,当你设置 vm.someData = 'new value' ,该组件不会立即重新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个“tick”更新。
vue的异步更新

我的问题:
如果是异步更新的话为什么v-model的数据能实时的显示到页面上呢?难道是v-model不是异步的?
没有找到答案,但是我的理解为。v-model是个语法糖,主要是触发了input事件更改value的值。经过试验发现input的更新是异步的。因为我给input事件响应增加个同步延迟三秒。然后再输入。就会发现输入一个字符页面三秒后显示一个字符。输入两个字符页面就等待六秒后显示两个字符。那么为什么不加延迟函数就实时跟新呢?只能是浏览器刷新太快了(16ms)。我们肉眼难以辨认。input事件也是异步事件。输入一个字符,主线程空闲会立即执行异步队列的事件。我们这时候再输入只能再次添加到任务队列中。如果我们加了延迟函数。主线程上就会是延迟函数,还没执行完,又触发了input事件,主线程上又增加了延迟函数。所以输入n个字符,就会等待n*3秒。又更新的都是一个值,所以等待执行的异步队列只推入一个值,所以页面一下更新到最新值。

  change(e) {
      this.sleep(3000);
      this.value = e.value;
    },
dom事件也是异步事件
var button = document.getElement('#btn');
button.addEventListener('click', function(e) {
    console.log();
});

从事件的角度来看,上述代码表示:在按钮上添加了一个鼠标单击事件的事件监听器;当用户点击按钮时,鼠标单击事件触发,事件监听器函数被调用。
从异步过程的角度看,addEventListener函数就是异步过程的发起函数,事件监听器函数就是异步过程的回调函数。事件触发时,表示异步任务完成,会将事件监听器函数封装成一条消息放到消息队列中,等待主线程执行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容

  • 原文地址在我的博客, 转载注明来源 网上一搜事件循环, 很多文章标题的前面会加上 JavaScript, 但是我觉...
    莫凡_Tcg阅读 1,261评论 1 5
  • 静下心学了一波事件循环机制,好开心,我学会了,首先还是得感谢作者写的笔记特别详细 链接: http://www.c...
    Dianaou阅读 495评论 0 0
  • 在上一篇文章里面我大致介绍了JavaScript的事件循环机制,但是最后还留下了一段代码和几个问题。那就从这段代码...
    fangdown阅读 523评论 0 1
  • 浅薄概念 Javascript是单线程,执行任务时,分同步任务和异步任务,执行同步任务时放入栈中执行,执行异步任务...
    fangdown阅读 321评论 0 2
  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,656评论 0 5