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)

④ 常见的异步操作:

  1. setTimeout (setInterval)
  2. AJAX
  3. Promise
  4. 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('刘德华');

图片说明

全部评论

相关推荐

不愿透露姓名的神秘牛友
10-05 10:13
已编辑
HHHHaos:让这些老登来现在秋招一下,简历都过不去
点赞 评论 收藏
分享
去B座二楼砸水泥地:不过也可以理解,这种应该没参加过秋招
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务