同步模式(Synchronous)

程序的执行顺序和代码的编写顺序是完全一致的,代码当中的任务依次执行,后一个任务要等待前一个任务结束才能开始执行。这种模式是很简单的,在单线程的情况下,大多数的任务都是同步模式执行

console.log("global begin");

function bar() {
  console.log("bar task");
}

function foo() {
  console.log("foo task");
  bar();
}

foo();

console.log("global finish");

这个简单的代码组从console以及call stack里分析执行顺序就是这样的:
console:

  1. 执行 console.log("global begin"),在控制台输出 "global begin"。
  2. 执行 foo() 函数,进入 foo 函数内部执行。
  3. 执行 console.log("foo task"),在控制台输出 "foo task"。
  4. 执行 bar() 函数,进入 bar 函数内部执行。
  5. 执行 console.log("bar task"),在控制台输出 "bar task"。
  6. bar 函数执行完毕,从调用栈中弹出。
  7. 回到 foo 函数继续执行,foo 函数执行完毕,从调用栈中弹出。
  8. 执行 console.log("global finish"),在控制台输出 "global finish"。
  9. 主程序执行完毕,程序结束。

call stack:

  1. 首先,在全局作用域中压入一条记录,记录当前正在执行的函数为 Node.js 创建的匿名函数。
  2. 执行 console.log("global begin"),将其记录压入调用栈中。
  3. console.log("global begin") 执行完毕,从调用栈中弹出。
  4. 执行 foo() 函数,将 foo 函数的记录压入调用栈中。
  5. foo 函数内部执行时,将 console.log("foo task") 的记录压入调用栈中。
  6. console.log("foo task") 执行完毕,从调用栈中弹出。
  7. 执行 bar() 函数,将 bar 函数的记录压入调用栈中。
  8. bar 函数内部执行时,将 console.log("bar task") 的记录压入调用栈中。
  9. console.log("bar task") 执行完毕,从调用栈中弹出。
  10. bar 函数执行完毕,从调用栈中弹出。
  11. 由于 bar 函数是在 foo 函数内部被调用的,所以回到 foo 函数继续执行。
  12. foo 函数执行完毕,从调用栈中弹出。
  13. 执行 console.log("global finish"),将其记录压入调用栈中。
  14. console.log("global finish") 执行完毕,从调用栈中弹出。
  15. 程序执行完毕,调用栈为空。

通俗的说,JS在执行引擎中维护了一个正在执行的工作表,会在里面记录正在做的事情,工作表的任务全部清空了之后,这一轮工作就结束了。
这种排队执行的机制存在很严重的问题,某一行的代码执行时间过长的话,后面的任务就会延迟,就是所谓的阻塞,所以就必须要用异步模式来解决程序里无法避免的耗时操作,比如大文件读写,或者ajax操作等等。

异步模式(Asynchronous)

  • 异步模式的api不会等待这个任务的结束才开始执行下一个任务
  • 对于耗时任务基本就是开启之后立即执行下一个任务
  • 后续的逻辑会用回调函数的方式来定义
  • 没有异步模式的话单线程语言JavaScript就无法同时处理大量的耗时任务
  • 对于开发者的难点就是代码的执行顺序不是太通俗易懂,代码的执行顺序混乱
console.log("global begin");

setTimeout(function timer1() {
  console.log("timer1 invoke");
}, 1800);

setTimeout(function timer2() {
  console.log("timer2 invoke");

  setTimeout(function inner() {
    console.log("inner invoke");
  }, 1000);
}, 1000);

console.log("global end");

以上的代码详细的描述一下的话,有四个需要注意的点:

  1. console控制台的输出
  2. call stack调用栈
  3. web APIs的队列
  4. Queue的事件队列,通过event loop询问

过程我已经用chatGPT做出了总结:

  1. 开启一个匿名函数作为主线程,将其记录压入调用栈中。
  2. 执行 console.log("global begin"),在控制台输出 "global begin",将其记录压入调用栈中。
  3. console.log("global begin") 执行完毕,从调用栈中弹出。
  4. 执行第一个 setTimeout 函数,将其记录压入 Web APIs 的定时器队列中。此时调用栈为空。
  5. 执行第二个 setTimeout 函数,将其记录压入 Web APIs 的定时器队列中。此时调用栈为空。
  6. 执行 console.log("global end"),在控制台输出 "global end",将其记录压入调用栈中。
  7. console.log("global end") 执行完毕,从调用栈中弹出。
  8. 在匿名函数中执行第三个 setTimeout 函数,将其记录压入 Web APIs 的定时器队列中。此时调用栈为空。
  9. 匿名函数执行完毕,从调用栈中弹出。
  10. 主程序执行完毕,等待计时器到期。
  11. 第二个计时器到期,将回调函数 timer2 压入任务队列中。
  12. 事件循环从任务队列中取出 timer2 函数并执行,输出 "timer2 invoke"。
  13. timer2 函数中再次执行一个 setTimeout 函数,将其记录压入 Web APIs 的定时器队列中。此时调用栈为空。
  14. 内部的计时器到期,将回调函数 inner 压入任务队列中。
  15. 事件循环从任务队列中取出 inner 函数并执行,输出 "inner invoke"。
  16. 第一个计时器到期,将回调函数 timer1 压入任务队列中。
  17. 事件循环从任务队列中取出 timer1 函数并执行,输出 "timer1 invoke"。
  18. 程序执行完毕,调用栈和任务队列都为空。

JavaScript异步调用逻辑图大概就是这样的:

JavaScript是单线程但是浏览器不是单线程的,所以有些JavaScript调用的内部api不是单线程的,内部会有单独的线程去倒计时或者其他操作,到了时机就会把回调放在消息队列里

运行环境提供的API是以同步或者异步模式的方式进行工作

最后修改:2024 年 03 月 21 日
收款不要了,给孩子补充点点赞数吧