事件循环(evenloop)
事件循环机制是宿主环境提供的。
js中处理异步,增加了任务队列的概念(你不知道的js中卷把这个叫做事件循环队列)。是异步事件完成后才把回调函数放入队列中。不是触发异步事件就 放入队列。以Promise为例:Promise是一个micro-task,只有当Promise被决议了后才将then中的回调函数放入队列。ES6之前,JS引擎本身所做的只不过是在不断轮询任务队列,然后执行其中的任务。JS引擎根本不能做到自己主动把任务放到任务队列中。这一点在ES6中有所改变(主要因为 ES6 中 Promise 的引 入,因为这项技术要求对事件循环队列的调度运行能够直接进行精细控制)。
我认为,浏览器有一个主的js引擎,同步任务就在主线程上排队执行,异步的任务才进入事件循环队列。浏览器在合适的时候把异步队列的内容放到主线程去执行。
HTML5规范里有Event loops这一章节(读起来比较晦涩,只关注相关部分即可)。
- 每个浏览器环境,至多有一个event loop(2017年新版的HTML规范,浏览器包含2类事件循环:browsing contexts 和 web workers。)。
- 一个event loop可以有1个或多个task queue。
- 一个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
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函数就是异步过程的发起函数,事件监听器函数就是异步过程的回调函数。事件触发时,表示异步任务完成,会将事件监听器函数封装成一条消息放到消息队列中,等待主线程执行。