[TOC] #### 1. 任务概念 --- 在 JavaScript 中,任务可以分为同步任务、异步任务,而异步任务又进一步细分为宏任务和微任务 + 同步任务:指那些在主线程上排队执行的任务,只有前一个任务执行完毕后,才能开始执行下一个任务 + 异步任务:异步任务不会进入主线程,而是进入 “任务队列” 中等待执行 JavaScript 语言本身是单线程,这意味着在任何时刻,JavaScript 引擎中只有一条主线程来处理所有的任务 #### 2. 同步任务 --- 同步任务只有前一个任务执行完毕后,才能开始执行下一个任务,特点是按照顺序执行,会阻塞后续代码的执行 ```javascript console.log(1); console.log(2); ``` #### 3. 异步任务 --- 异步任务是只有当主线程上的同步任务全部执行完毕后,才会从任务队列中取出异步任务进入主线程执行 ```javascript console.log(1); // 同步任务 setTimeout(() => console.log(3), 0) // 异步任务,等待同步任务执行完毕之后,才会执行 console.log(2); // 同步任务 ``` ##### 宏任务 宏任务是由宿主环境发起的异步任务,通常包含较长的等待时间或需要等待 `I/O` 操作完成 常见的宏任务:setTimeout / setInterval、ajax 请求、dom 事件 ##### 微任务 微任务通常用于处理 Promise 或其他需要尽快执行的操作 常见的微任务:Promise.then、async / await ```javascript setTimeout(() => console.log('定时器 - 宏任务'), 0) Promise.resolve().then(value => { console.log('Promise - 微任务'); }) console.log('123 - 同步任务') // 123 - 同步任务 // Promise - 微任务 // 定时器 - 宏任务 ``` #### 4. 定时器的任务编排 --- 在 JavaScript 中,`setTimeout` 函数的延时时间的最小值通常是 4 毫秒 这是因为在某些浏览器或操作系统中,为了优化性能和节能,定时器的实际执行可能会有一定的最小延迟 ```javascript // 即使设置了非常小的延时,浏览器也可能不会立即执行定时器回调函数,而是等待至少 4 毫秒后才执行 setTimeout(() => console.log('定时器'), 0) ``` 设置定时器延时 200 毫秒执行,那么它真的就会 200 毫秒后立即执行吗 ?通过以下代码可以发现并不是这样的 代码解析原因:因为定时器是异步任务,for 循环是同步任务,所以定时器要等 for 循环执行完之后才会执行定时器 定时器的任务编排原理:到达延时时间会先将其放到宏任务队列中,等待主线程处理完同步任务,再来处理异步任务 ```javascript console.log(new Date().getTime() / 1000); setTimeout(() => { console.log('定时器:' + new Date().getTime() / 1000); }, 200) console.log('同步任务') for (let i = 0; i < 20000; i++) { console.log(''); } ``` #### 5. Promise 微任务处理逻辑 --- Promise 是微任务中的典型代表,Promise 的构造函数代码是立即执行的,也就是说它是同步代码 ```javascript // 4. 定时器,放入宏任务队列 setTimeout(() => console.log('定时器')) new Promise(resolve => { // 1. 这里是 promise 的构造函数,它是同步代码 console.log('promise'); resolve() }).then(() => { // 3. 这里的代码会放到微任务队列 console.log('then'); }) // 2. 同步代码 console.log('hello'); // 由于任务执行顺序是:同步任务 > 微任务 > 宏任务,所以上面代码的输出顺序为 // promise // hello // then // 定时器 ``` 如果在定时器内部再放入 Promise 和 定时器,那么执行顺序是怎么样的 ? ```javascript setTimeout(() => { // 1. 同步代码 console.log('定时器') // 因为它是在定时器内部,所以它在宏任务执行之后才会放入任务队列,才会执行 new Promise(resolve => { // 2. 同步代码 console.log('timeout promise'); resolve() }).then(() => { // 3. 微任务 console.log('timeout then'); }) // 4. 宏任务 setTimeout(() => console.log('定时器内部的定时器')) }) new Promise(resolve => { console.log('promise'); resolve() }).then(() => { console.log('then'); }) console.log('hello'); // 其实很简单,在上一段代码的结果上,继续按照任务执行优先顺序执行 // promise // hello // then // 定时器 // timeout promise // timeout then // 定时器内部的定时器 ``` #### 6. DOM 渲染任务 --- 浏览器在解析页面时,也是按照顺序从上往下解析的,如下所示,先解析 `utils.js`,然后再渲染 `h1` 标签 也可以理解为先将解析 `utils.js` 的任务执行完,再执行渲染 `h1` 标签的任务 ```html <head> <script src="./utils.js"></script> </head> <body> <h1>JavaScript 的宏任务和微任务</h1> </body> ``` 如果执行 `utils.js` 的时间比较长,那么页面将迟迟不渲染,进入页面会有较长时间的空白内容 ```javascript for (let index = 0; index < 5000000; index++) { document.write(' ') } ``` 我们可以将 `utils.js` 的引入放到内容渲染后面,改变任务的执行顺序,进入页面就不会有长时间的空白了 ```html <body> <h1>JavaScript 的宏任务和微任务</h1> <script src="./utils.js"></script> </body> ``` #### 7. 任务共享内存 --- 任务共享内存是指多个任务可以访问同一块内存区域 多个定时器设置相同时间延时执行,它们并不会同时执行,而是到达设定的时间后将它们放入任务队列,依次执行任务 ```javascript let i = 0 setTimeout(() => { // 宏任务 console.log(++i); }, 1000) setTimeout(() => { // 宏任务 console.log(++i); }, 1000) ``` #### 8. 进度条实例体验任务轮询 --- 先写好一个进度条的样式 ```html <head> <style> .container { width: 200px; border-radius: 3px; border: 1px solid #ccc; } #progress { width: 0%; color: #fff; text-align: center; background-color: #376de1; } </style> </head> <body> <div class="container"> <div id="progress">0</div> </div> </body> ``` 定时器其实就是往任务队列中不断的放入任务,到达执行时间后,交由主线程依次执行任务 ```javascript function handle() { let i = 0; (function run() { if (++i <= 100) { progress.innerHTML = i progress.style.width = i + '%' setTimeout(run, 50) } })(); } handle() ``` #### 9. 任务拆分为多个子任务 --- 现有以下代码,前面的累加函数需要较长时间才能执行完成,阻塞了后面代码的运行,我们来看一下怎么解决这个问题 ```javascript let count = 0 let num = 987654321 function task() { for (let i = 0; i < num; i++) { count += num-- } console.log(count); } task() // 同步任务 console.log('hello'); // 同步任务,需要等待 task() 执行完后才能执行 ``` 思路是将大量的计算放入异步任务,让同步任务先执行,以免造成阻塞 ```javascript let count = 0 let num = 10000000 function task() { // 首次调用先累加一次 for (let i = 0; i <= num; i++) { if (num <= 0) break; count += num-- } if (num > 0) { // 后续累加,放入宏任务执行 setTimeout(task) } else { console.log(count) } } task() console.log('hello') ``` #### 10. Promise 微任务处理复杂逻辑 --- 将定时器放入 Promise 也可以处理复杂逻辑,不影响同步任务的执行,但下面用的还是宏任务(定时器) ```javascript let count = 0 let num = 987654321 // 由于 promise 里面的代码是同步执行的,在里面直接写循环还是会卡住,所以可以写个定时器 function task() { return new Promise(resolve => { let count = 0 setTimeout(() => { for (let i = 0; i < num; i++) { count += num-- } resolve(count) }) }); } async function getSum(num) { let res = await task(num) console.log(res); } getSum(num) console.log('hello'); ``` 因为 Promise 的 then 回调方法是微任务,那么我们可以将代码调整为: ```javascript let num = 987654321 async function getSum(num) { let res = await Promise.resolve().then(_ => { let count = 0 for (let i = 0; i < num; i++) { count += num-- } return count }) } getSum(num) console.log('hello'); ```