事件队列机制

入门事例

setTimeout改变执行顺序

setTimeout(()=>{
    console.log('2');
},0);
console.log('1');
// 输出1 2
  • setTimeout会把其内部代码放到下一个事件循环任务队列的队列头部执行
  • 意味着事件循环每次执行一个任务,任务之间顺序隔离
  • 注意:setTimeout为0在js中实际是4ms,在操作dom时是16ms,而requestAnimationFrame的渲染效率则更高,专门为动画设计,但该执行顺序问题与4ms间隔无关

for循环的常见问题

for (var i = 0; i < 5; i++) {
 setTimeout(function() {
  console.log(new Date, i);
 }, 1000);
}
console.log(new Date, i);
  • 这里常见的误解是输出为1 2 3 4 5,造成该误解的原因是不了解js的异步机制
  • 另一种误解是:预解析阶段for循环相当于全部执行完,i为5,function待执行,效果如下:
var i = 5;
setTimeout(function() { console.log(new Date, i);}, 1000);
setTimeout(function() { console.log(new Date, i);}, 1000);
setTimeout(function() { console.log(new Date, i);}, 1000);
setTimeout(function() { console.log(new Date, i);}, 1000);
setTimeout(function() { console.log(new Date, i);}, 1000);
 console.log(new Date,i);

预解析只是提升变量作用域,不包括代码执行,这个问题用“预解析”理解为上述形式歪打正着地解释了这个问题

  • 实际上该问题也是用事件循环和任务队列来解释的

事件循环概念

简介

  • 由于js是单线程的,必须有一种机制来处理异步代码以防止主线程阻塞、提高效率,js提供的方案是事件循环(Event Loop)
  • 任务队列实际上是所有任务的重新划分,规定同步任务和异步任务(包括事件回调、用户交互、UI 渲染和网络处理等行为)按什么顺序执行
  • 每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列
  • 事件循环分两种运行环境,一种是常见的浏览器环境,以下介绍此种;一种是H5新的线程使用机制web worker标准,更适用于耗CPU的计算

工作原理

微任务与宏任务

  • 不存在宏任务macrotask,根据规范,实际叫task,按task划分每个tick的执行代码
  • 微任务microtask指的是同一次tick内同一个宏任务中的小异步操作,如promise
  • (macro)task主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)
  • microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)
  • 微任务在宏任务之后紧随宏任务之后,理解上可以认为在宏任务“之中”

工作流程

  1. 主线程运行的时候,产生堆(heap)和栈(stack),堆中放的是变量常量,如for循环结束后的i的值,栈中的代码调用各种外部API
  2. 这些外部API在"任务队列"中加入各种事件(click,load,done)
  3. 只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。
  4. 主线程重复执行123步

Promise题目

题目一

new Promise(resolve => {
    resolve(1);

    Promise.resolve().then(() => {
        // t2
        console.log(2)
    });
    console.log(4)
}).then(t => {
    // t1
    console.log(t)
});
console.log(3);
  1. 同步脚本执行:new构造函数使得先输出4,构造函数中微任务队列内容靠后
  2. 继续执行同步脚本,执行栈中输出 3
  3. 微任务队列内容按先后顺序执行 2 1
  4. Promise.resolve 方法允许调用时不带参数,直接返回一个resolved 状态的 Promise 对象
  5. 立即 resolved 的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。

题目二

setTimeout(function() {
  console.log('setTimeout1');
    Promise.resolve().then(function() {
      console.log('promise1');
    }).then(function() {
      console.log('promise2');
    })
}, 0);
setTimeout(function() {
  console.log('setTimeout2');
    Promise.resolve().then(function() {
      console.log('promise3');
    }).then(function() {
      console.log('promise4');
    })
}, 0);
/* 
    setTimeout1
    promise1
    promise2
    setTimeout2
    promise3
    promise4
*/
  1. 宏任务中注册的微任务会在宏任务中执行
  2. 微任务可以认为是宏任务中的普通script代码

题目三

const first = () => (new Promise((resolve,reject)=>{
    console.log(3);

    let p = new Promise((resolve,reject)=>{
        console.log(7);
        setTimeout(()=>{
            console.log(5);
            resolve(6);
        },0)
        resolve(1);
    });

    resolve(2);

    p.then((arg)=>{
        console.log(arg);
    });
}));

first().then((arg)=>{
    console.log(arg);
});
console.log(4);
// 3 7 4 1 2 5
  1. 先执行script同步代码 3 7 4
  2. 再执行微任务队列中的代码 1 2
  3. 最后执行下一个任务队列或者事件循环中的setTimeout 5

题目四

async function timeout() {
    return 'hello world'
}
timeout();
console.log('虽然在后面,但是我先执行');

这个例子用来说明,async函数return的值会被包装到promise的resolve参数中输出,如果async中不存在return则async的promise返回值为undefined;如果async内出错则reject返回promise对象

题目五

async function async1() {
console.log("async1 start");
await  async2();
 console.log("async1 end");
}
async  function async2() {
 console.log( 'async2');
}
console.log("script start");
setTimeout(function () {
 console.log("settimeout");
},0);
async1();
new Promise(function (resolve) {
 console.log("promise1");
 resolve();
}).then(function () {
 console.log("promise2");
});
console.log('script end'); 
/* 
script start  // 整个代码作为script读入,同步代码script start先输出
async1 start  // 异步函数构造函数内部代码输出
async2   // await构造函数代码输出 async2
promise1   // await让出线程,new Promise构造函数内代码输出
script end  // 同步代码script end输出
promise2 //退出async后promise返回输出promise2
async1 end   // 外部代码执行完后回到await处拿到promise值并向后执行输出async1 end
settimeout  //下一个队列首部setTimeout输出
*/

有关说明:

  1. 流程
  2. await是一个让出线程的标志,即遇到await时,主线程执行await后的构造函数(同步部分)并跳出当前async函数执行当前调用栈的其他代码
  3. 后面代码执行完了之后又会跳回到async函数中等待await的结果
  4. 其他代码中promise微任务理解为与普通同步代码“同级”,promise执行完后才会执行await返回后的代码
  5. 注意点
  6. Promise是一个立即执行函数,但它他的成功(或失败:reject)的回调函数resolve却是一个异步执行的回调。当执行到resolve()时,这个任务会被放入到回调队列中,等待调用栈有空闲时事件循环再来取走它
  7. await和async可以看做优先级低于setTimeout宏任务的任务,async/await优先级高于promise
  8. await后面表达式的结果,如果返回值为非promise则继续执行async函数后面的代码,否则将返回的promise放入promise队列
  9. await返回 Promise对象的处理结果。如果等待的不是 Promise 对象,则返回该值本身。