JavaScript同步编程?异步编程?
同步编程:同步编程是一种请求响应模型,调用一个方法,等待其响应返回,也就是一个线程获得了一个任务,然后去执行这个任务,当这个任务执行完毕后,才能执行接下来的另外一个任务。
举例:我今天要干三件事情,搞卫生、洗衣服、做饭,我必须要先搞完卫生后才能洗衣服,洗完衣服后才能做饭,这就是同步任务,也就是顺序交付的工作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('刘德华');