同步编程:同步编程是一种请求响应模型,调用一个方法,等待其响应返回,也就是一个线程获得了一个任务,然后去执行这个任务,当这个任务执行完毕后,才能执行接下来的另外一个任务。
举例:我今天要干三件事情,搞卫生、洗衣服、做饭,我必须要先搞完卫生后才能洗衣服,洗完衣服后才能做饭,这就是同步任务,也就是顺序交付的工作1234,必须按照1234的顺序完成(这样一个上午过去了还不知道啥时候能吃上饭,狗带)
异步编程:异步编程不需要等待响应返回,可以继续执行其他任务,随后将响应结果存入消息队列(任务队列),等待主线程同步代码执行完毕后,才会去查找任务队列并执行,注意是轮询任务队列,如果有新任务,则继续执行新任务,遵循先进先出原则。
举例:我今天要干三件事情,搞卫生、洗衣服、做饭,我(主线程)可以买一个洗衣机,一个电饭煲(两个异步任务),然后我去搞卫生(执行同步任务),等洗衣机和电饭煲执行完毕后叮一声通知我已经干完活了(存入任务队列),等我搞完卫生以后(等待主线程代码执行完毕后,轮询任务队列)再去晾衣服或者吃饭,可以先吃饭,也可以先晾衣服,谁先完成我就先干哪件事(先进先出原则)。
任务调度流程:
任务分为同步任务、异步任务(异步任务分为宏任务和微任务),同步任务的优先级最高,其次是微任务,最后是宏任务,宏任务与微任务创建时会进入宏任务队列和微任务队列,当主线程的同步任务执行完毕后才会去任务队列中读取新任务(不管主线程的同步任务需要执行多长时间,任务队列中的异步任务都需要等待),所以尽量避免同步任务中出现大量耗时的计算。
宏任务(MacroTask):Script整体代码、setTimeout、setInterval、setImmediate(浏览器暂不支持,只有IE10支持)、I/O、UI Rendering
微任务(MicroTask):Process.nextTick(node独有)、Promise、Object.observe(废弃)、MutationObserver
// 1、
setTimeout(() => { // 创建宏任务并存入宏任务队列
console.log('哈哈')
}, 2000)
setTimeout(() => { // 创建宏任务并存入宏任务队列
console.log('嚯嚯')
}, 1000)
// 主线程的同步代码
for (let i = 0; i < 10000; i++) {
console.log('')
}
-------------------------------------------------------------
// 2、
setTimeout(() => { // 创建宏任务并存入宏任务队列
console.log('哈哈');
}, 0)
Promise.resolve('嚯嚯').then(val => { // 创建微任务并存入微任务队列
console.log(val);
})
for (let i = 0; i < 10000; i++) {
console.log('');
}
解析1:宏任务必须等到主线程的同步代码跑完了才会去任务队列读取结果,注意不是等延时时间过了才会执行,主线程代码跑的同时宏任务已经在执行了,所以这里等到for循环完了会直接输出定时器中的结果,虽然后面的定时器晚于前面的定时器创建,但由于它先执行完成所以先把执行结果通知给任务队列,因此后面的定时器先输出结果。
解析2:结果同上,虽然定时器的延时时间为0,按照我们的执行流程,微任务优先级比宏任务高,因此先输出微任务的结果,再输出宏任务的结果。
EventLoop?
执行栈(JS stack)在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去检查微任务(microTask)队列是否为空,
如果为空的话,就执行(macroTask)宏任务,否则一次性执行完所有的微任务。
每次单个宏任务执行完毕后,检查微任务队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务后,设置微任务队列为null,然后再执行宏任务,如此循环。
看一个案例:
console.log('script start'); //同步 日志
setTimeout(function() { //异步 宏任务
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() { // 微任务1
console.log('promise1');
}).then(function() { // 微任务2
console.log('promise2');
});
console.log('script end'); // 同步 日志
/**
* 第一次执行:
* 执行同步代码、将宏任务和微任务划分到各自队列中。
*
* Tasks:run script、 setTimeout callback
* Microtasks:Promise then
* JS stack: script
* Log: script start、script end。
*/
/**
* 第二次执行:
* 执行宏任务后,检测到微任务队列中不为空,执行Promise1,执行完Promise1后,
* 调用Promise2.then,放入到微任务队列中,再执行Promise2.then
*
* Tasks:run script、 setTimeout callback
* Microtasks:Promise2 then
* JS stack: Promise2 callback
* Log: script start、script end、promise1、promise2
*/
/**
* 第三次执行:
* 当微任务队列中为空时,执行宏任务,再执行setTimeout callback,最后打印日志
*
* Tasks:setTimeout callback
* Microtasks:null
* JS stack: setTimeout callback
* Log: script start、script end、promise1、promise2、setTimeout
*/
/**
* 第四次执行:
* 清空宏任务队列和JS执行栈(stack)。
*
* Tasks:setTimeout callback
* Microtasks:null
* JS stack: null
* Log: script start、script end、promise1、promise2、setTimeout
*/
/**
* 控制台输出如下:
*
* script start
* script end
* promise1
* promise2
* setTimeout
*/
① 消息队列:消息队列(message queue),也叫任务队列(task queue):存储待处理消 息及对应的回调函数或事件处理程序; 执行栈(execution context stack),也可以叫执行上下文栈:JavaScript 执行 栈,顾名思义,是由执行上下文组成,当函数调用时,创建并插入一个执 行上下文,通常称为执行栈帧(frame),存储着函数参数和局部变量, 当该函数执行结束时,弹出该执行栈帧; 注:关于全局代码,由于所有的代码都是在全局上下文执行,所以执行栈 顶总是全局上下文就很容易理解,直到所有代码执行完毕,全局上下文退 出执行栈,栈清空了;也即是全局上下文是第一个入栈,最后一个出栈。
② 任务:分析事件循环流程前,先阐述两个概念,有助于理解事件循环:同步任务 和异步任务。 任务很好理解,JavaScript 代码执行就是在完成任务,所谓任务就是一个 函数或一个代码块,通常以功能或目的划分,比如完成一次加法计算,完 成一次 ajax 请求;很自然的就分为同步任务和异步任务。同步任务是连续 的,阻塞的;而异步任务则是不连续,非阻塞的,包含异步事件及其回调, 当我们谈及执行异步任务时,通常指执行其回调函数。
③ 事件循环流程:
- 宿主环境为 JavaScript 创建线程时,会创建堆(heap)和栈(stack),堆内存储 JavaScript 对象,栈内存储执行上下文
- 栈内执行上下文的同步任务按序执行,执行完即退栈,而当异步任务执行 时,该异步任务进入等待状态(不入栈),同时通知线程:当触发该事件 时(或该异步操作响应返回时),需向消息队列插入一个事件消息
- 当事件触发或响应返回时,线程向消息队列插入该事件消息(包含事件及 回调)
- 当栈内同步任务执行完毕后,线程从消息队列取出一个事件消息,其对应 异步任务(函数)入栈,执行回调函数,如果未绑定回调,这个消息会被 丢弃,执行完任务后退栈
- 当线程空闲(即执行栈清空)时继续拉取消息队列下一轮消息(next tick, 事件循环流转一次称为一次 tick)
④ 常见的异步操作:
- setTimeout (setInterval)
- AJAX
- Promise
- Generator
参考博客:https://blog.csdn.net/weixin_40851188/article/details/90648666
思考题:下面的代码 i 输出几
let i = 0;
setTimeout(() => {
console.log(++i);
}, 1000);
setTimeout(() => {
console.log(++i);
}, 1000);输出结果是2,虽然两个定时器的时间是一样的,但他们并不是同时执行而是会产生两个宏任务存入任务队列,当主线程执行完同步代码let i = 0之后,会从任务队列中把第一个宏任务的结果拿过来执行,执行完后继续下一个宏任务。
任务拆分成多个子任务案例:
问题来源:由于js是单线程语言,我们的同步代码在执行过程中,如果由于前面的代码计算非常耗时,会导致后面的同步代码长时间不执行
// 举例:
// 我们希望前面的大数值计算不影响后面的代码执行
// 但这里显然是要等到计算完成才会输出刘德华
function fn() {
for (let i = 0; i < num; i++) {
count += num--;
}
console.log(count);
}
let num = 987654321;
let count = 0;
fn();
console.log('刘德华'); // 等待上面代码计算完成再输出'刘德华'
解决方式:
// 1、利用宏任务处理复杂业务
function fn(num) {
for (let i = 0; i < num; i++) {
if(num <= 0) break;
count += num--;
}
if(num > 0) {
setTimeout(fn); // 将每一个计算都存入任务队列
}else {
console.log(count); // 最终得到的计算结果
}
}
let count = 0;
fn(987654321);
console.log('刘德华'); // 先输出'刘德华',然后等待任务队列计算完成
-----------------------------------------------------------------------
function fn(num) {
return new Promise(resolve => {
// 由于promise构造函数中的代码是同步执行
// 因此我们套一层定时器
setTimeout(() => {
let count = 0;
for (let i = 0; i < num; i++) {
count += num--;
}
resolve(count);
})
})
}
async function f(num) {
// 等待计算完成输出计算结果
let res = await fn(num);
console.log(res);
}
f(987654321);
console.log('刘德华'); // 先输出'刘德华',然后等待任务队列计算完成
// 2、利用微任务处理复杂业务
async function fn(num) {
let res = await Promise.resolve().then(_ => {
let count = 0;
for (let i = 0; i < num; i++) {
count += num--;
}
return count;
})
console.log(res);
}
fn(987654321);
console.log('刘德华');

京公网安备 11010502036488号