关于前端:深入理解js事件循环机制

36次阅读

共计 6247 个字符,预计需要花费 16 分钟才能阅读完成。

同步工作和异步工作(微工作和宏工作)

转自掘金,已获作者批准

JavaScript 是一门单线程语言

分为同步工作和异步工作

同步工作 是指在主线程上排队执行的工作,只有前一个工作执行结束,能力继续执行下一个工作。

异步工作 指的是,不进入主线程、而进入 ” 工作队列 ” 的工作;只有等主线程工作全副执行结束,” 工作队列 ” 的工作才会进入主线程执行。

异步工作 分为宏工作和微工作

new promise()、console.log()属于同步工作

宏工作(macrotask) 微工作(microtask)
谁发动的 宿主(Node、浏览器) JS 引擎
具体事件 1. script (能够了解为外层同步代码) 2. setTimeout/setInterval 3. UI rendering/UI 事件 4. postMessage,MessageChannel 5. setImmediate,I/O(Node.js) 1. Promise 2. MutaionObserver 3. Object.observe(已废除;Proxy 对象代替)4. process.nextTick(Node.js)
谁先运行 后运行 先运行
会触发新一轮 Tick 吗 不会

执行过程: 同步工作 —> 微工作 —> 宏工作

  1. 先执行所有同步工作,碰到异步工作放到工作队列中
  2. 同步工作执行结束,开始执行以后所有的异步工作
  3. 先执行工作队列外面所有的微工作
  4. 而后执行一个宏工作
  5. 而后再执行所有的微工作
  6. 再执行一个宏工作,再执行所有的微工作·······顺次类推到执行完结。

3- 6 的这个循环称为事件循环 Event Loop

事件循环是 JavaScript 实现异步的一种办法,也是 JavaScript 的执行机制

async/await(重点)

(集体注解:async/await 底层仍然是 Promise,所以是微工作,只是 await 比拟非凡)

async

当咱们在函数前应用 async 的时候,使得该函数返回的是一个 Promise 对象

async function test() {return 1   // async 的函数会在这里帮咱们隐士应用 Promise.resolve(1)
}
// 等价于上面的代码
function test() {return new Promise(function(resolve, reject) {resolve(1)
   })
}
// 可见 async 只是一个语法糖,只是帮忙咱们返回一个 Promise 而已

await

await 示意期待,是右侧「表达式」的后果,这个表达式的计算结果能够是 Promise 对象的值或者一个函数的值(换句话说,就是没有非凡限定)。并且只能在带有 async 的外部应用

应用 await 时,会从右往左执行,当遇到 await 时,★★★★★会阻塞函数外部处于它前面的代码,去执行该函数内部的同步代码,当内部同步代码执行结束,再回到该函数外部执行残余的代码★★★★★, 并且当 await 执行结束之后,会先解决微工作队列的代码

示例

//1
console.log('1');    
//2
setTimeout(function() {console.log('2');
    process.nextTick(function() {console.log('3');
    })
    new Promise(function(resolve) {console.log('4');
        resolve();}).then(function() {console.log('5')
    })
})
//3
process.nextTick(function() {console.log('6');
})
//4
new Promise(function(resolve) {console.log('7');
    resolve();}).then(function() {console.log('8')
})
//5
setTimeout(function() {console.log('9');
    process.nextTick(function() {console.log('10');
    })
    new Promise(function(resolve) {console.log('11');
        resolve();}).then(function() {console.log('12')
    })
})

// 先执行 1 输入 1
// 执行到 2,把 setTimeout 放入异步的工作队列中(宏工作)// 执行到 3,把 process.nextTick 放入异步工作队列中(微工作)// 执行到 4,下面提到 promise 外面是同步工作,所以输入 7,再将 then 放入异步工作队列中(微工作)// 执行到 5,同 2
// 下面的同步工作全副实现,开始进行异步工作
// 先执行微工作,发现外面有两个微工作,别离是 3,4 压入的,所以输入 6 8
// 再执行一个宏工作,也就是第一个 setTimeout
// 先输入 2,把 process.nextTick 放入微工作中,再如上 promise 先输入 4,再将 then 放入微工作中
// 再执行所以微工作输入输入 3 5
// 同样的,再执行一个宏工作 setTImeout2,输入 9 11 在执行微工作输入 10 12
// 所以最好的程序为:1 7 6 8 2 4 3 5 9 11 10 12
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')

// 首先执行同步代码,console.log('script start')
// 遇到 setTimeout, 会被推入宏工作队列
// 执行 async1(), 它也是同步的,只是返回值是 Promise,在外部首先执行 console.log( 'async1 start')
// 而后执行 async2(), 而后会打印 console.log( 'async2')
// 从右到左会执行, 当遇到 await 的时候,阻塞前面的代码,去内部执行同步代码
// 进入 new Promise, 打印 console.log('promise1')
// 将.then 放入事件循环的微工作队列
// 继续执行,打印 console.log('script end')
// 内部同步代码执行结束,接着回到 async1()外部, 继续执行 await async2() 前面的代码,执行 console.log('async1 end'),所以打印出 async1 end。(集体了解:async/await 实质上也是 Promise,也是属于微工作的,所以当遇到 await 的时候,await 前面的代码被阻塞了,应该也是被放到微工作队列了,当同步代码执行结束之后,而后去执行微工作队列的代码,执行微工作队列的代码的时候,也是依照被压入微工作队列的程序执行的)// 执行微工作队列的代码, 打印 console.log('promise2')
// 进入第二次事件循环,执行宏工作队列, 打印 console.log('setTimeout')
/** * 执行后果为:* script start * async1 start * async2 * promise1 * script end * async1 end * promise2 * setTimeout */
console.log(1);
async function fn(){console.log(2)
    new Promise((resolve)=>{resolve();
    }).then(()=>{console.log("XXX")
    })
    await console.log(3)
    console.log(4)
}
fn();
new Promise((resolve)=>{console.log(6)
    resolve();}).then(()=>{console.log(7)
})
console.log(8)

// 执行后果为:1 2 3 6 8 XXX 4 7
/* 后面的 1 2 3 6 8 不再解析,重点是前面的 XXX 4 7,由此可见 await console.log(3) 之后的代码 console.log(4) 是被放入到微工作队列了,代码 console.log("XXX") 也是被压入微工作队列了,console.log("XXX")  是在 console.log(4) 之前,所以当同步工作执行结束之后,执行微工作队列代码的时候,优先打印进去的是 XXX,而后才是 4。*/
console.log(1);
async function fn(){console.log(2)
    await console.log(3)
    await console.log(4)
    await console.log("await 之后的:",11)
    await console.log("await 之后的:",22)
    await console.log("await 之后的:",33)
    await console.log("await 之后的:",44)
}
setTimeout(()=>{console.log(5)
},0)
fn();
new Promise((resolve)=>{console.log(6)
    resolve();}).then(()=>{console.log(7)
})
console.log(8)

/** * 执行后果为:* 1 * 2 * 3 * 6 * 8 * 4 * 7 * await 之后的:11 * await 之后的:22 * await 之后的:33 * await 之后的:44 * 5 */
/* 由此可见,代码执行的时候,只有碰见 await,都会执行完以后的 await 之后,把 await 前面的代码放到微工作队列外面。然而定时器外面的 5 是最初打印进去的,可见当一直碰见 await,把 await 之后的代码一直的放到微工作队列外面的时候,代码执行程序是会把微工作队列执行结束,才会去执行宏工作队列外面的代码。*/
Promise.resolve().then(() => {console.log(0);
  return Promise.resolve(4)   // 顺延 2 位  如果是 return 4 则打印 0、1、4、2、3、5、6、7
}).then(res => console.log(res))

Promise.resolve().then(() => {console.log(1);
}).then(() => {console.log(2);
}).then(() => {console.log(3);
}).then(() => {console.log(5);
}).then(() => {console.log(6);
}).then(() => {console.log(7);
})
/* 此题次要留神的是原生的 Promise 的 then 办法中,如果返回的是一个一般值,则返回的值会被立刻调用并赋值给 resolve 函数,如果返回的是一个 thenable,则 then 办法将会被放入到微队列中执行,如果返回的是一个 Promise.resolve,则会再加一次微工作队列。即微工作后移,Promise.resolve 自身是执行 then 办法,而 then 办法自身是在微工作队列中执行,同时 return Promise.resolve 时是将 resolve 调用的返回值 作为下级 then 中 resolve 的参数传递,调用外层 then 办法时自身是在微队列外面,所以函数的执行程序是要在微队列中下移两次。*/

依据 w3c 的最新解释

  • 每个工作都有一个工作类型 , 同一个类型的工作必须在一个队列也就是一共有多个队列 , 不同类型的工作能够分属不同的队列, 在一个次事件循环中, 浏览器能够依据理论状况从不同的队列中区出工作执行
  • 浏览器必须筹备好一个微队列 , 微队列中的工作优先所有其余工作执行他外面的货色 所有都要给我等 连绘制工作 都要等 就是最高优先级了

随着浏览器的复杂度急剧晋升 W3C 不再应用宏队列的说法

在目前 chrome 的实现中 至多蕴含了上面的队列

  • 延时队列 : 用于寄存计时器达到后的回调工作 , 优先级中
  • 交互列队 : 用于寄存用户操作后产生的事件处理工作 , 优先级高
  • 微队列 : 用户寄存须要最快执行的工作 优先级最高

增加工作到微队列的次要形式次要是应用 Promise、MutationObserver

例如:// 立刻把一个函数增加到微队列
Promise.resolve().then(函数)

工作有优先级吗?

  • 工作没有优先级,在音讯队列中先进先出
  • 音讯队列是有优先级的
// 立即把一个函数增加到微队列 最高执行
promise.resolve().then(函数)

setTimeOut(()=>{ // 第三步执行延时队列中的工作
  console.log(1);
},0)

promise.resolve().then(()=>{ // 第二步执行微队列中的工作
  console.log(2);
})

console.log(3); // 第一步先执行全局 js

// 3 2 1

面试题

1、如何了解 JS 的异步?

JS 是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。

而渲染主线程承当着诸多的工作,渲染页面、执行 JS 都在其中运行。

如果应用同步的形式,就极有可能导致主线程产生阻塞,从而导致音讯队列中的很多其余工作无奈失去执行。这样一来,一方面会导致忙碌的主线程白白的耗费工夫,另一方面导致页面无奈及时更新,给用户造成卡死景象。

所以浏览器采纳异步的形式来防止。具体做法是当某些工作产生时,比方计时器、网络、事件监听,主线程将工作交给其余线程去解决,本身立刻结束任务的执行,转而执行后续代码。当其余线程实现时,将当时传递的回调函数包装成工作,退出到音讯队列的开端排队,期待主线程调度执行。

在这种异步模式下,浏览器永不阻塞,从而最大限度的保障了单线程的晦涩运行。

2、论述一下 js 的事件循环

事件循环又叫做音讯循环,是浏览器渲染主线程的工作形式。

在 Chrome 的源码中,它开启一个不会完结的 for 循环,每次循环从音讯队列中取出第一个工作执行,而其余线程只须要在适合的时候将工作退出到队列开端即可。

过来把音讯队列简略分为宏队列和微队列,这种说法目前已无奈满足简单的浏览器环境,取而代之的是一种更加灵便多变的解决形式。

依据 W3C 官网的解释,每个工作有不同的类型,同类型的工作必须在同一个队列,不同的工作能够属于不同的队列。不同工作队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的工作。但浏览器必须有一个微队列,微队列的工作肯定具备最高的优先级,必须优先调度执行。

3、JS 中的计时器能做到准确计时吗?为什么?

不行,因为:

  1. 计算机硬件没有原子钟,无奈做到准确计时
  2. 操作系统的计时函数自身就有大量偏差,因为 JS 的计时器最终调用的是操作系统的函数,也就携带了这些偏差
  3. 依照 W3C 的规范,浏览器实现计时器时,如果嵌套层级超过 5 层,则会带有 4 毫秒的起码工夫,这样在计时工夫少于 4 毫秒时又带来了偏差
  4. 受事件循环的影响,计时器的回调函数只能在主线程闲暇时运行,因而又带来了偏差

正文完
 0