同步模式(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:
- 执行
console.log("global begin")
,在控制台输出 "global begin"。 - 执行
foo()
函数,进入foo
函数内部执行。 - 执行
console.log("foo task")
,在控制台输出 "foo task"。 - 执行
bar()
函数,进入bar
函数内部执行。 - 执行
console.log("bar task")
,在控制台输出 "bar task"。 bar
函数执行完毕,从调用栈中弹出。- 回到
foo
函数继续执行,foo
函数执行完毕,从调用栈中弹出。 - 执行
console.log("global finish")
,在控制台输出 "global finish"。 - 主程序执行完毕,程序结束。
call stack:
- 首先,在全局作用域中压入一条记录,记录当前正在执行的函数为 Node.js 创建的匿名函数。
- 执行
console.log("global begin")
,将其记录压入调用栈中。 console.log("global begin")
执行完毕,从调用栈中弹出。- 执行
foo()
函数,将foo
函数的记录压入调用栈中。 - 在
foo
函数内部执行时,将console.log("foo task")
的记录压入调用栈中。 console.log("foo task")
执行完毕,从调用栈中弹出。- 执行
bar()
函数,将bar
函数的记录压入调用栈中。 - 在
bar
函数内部执行时,将console.log("bar task")
的记录压入调用栈中。 console.log("bar task")
执行完毕,从调用栈中弹出。bar
函数执行完毕,从调用栈中弹出。- 由于
bar
函数是在foo
函数内部被调用的,所以回到foo
函数继续执行。 foo
函数执行完毕,从调用栈中弹出。- 执行
console.log("global finish")
,将其记录压入调用栈中。 console.log("global finish")
执行完毕,从调用栈中弹出。- 程序执行完毕,调用栈为空。
通俗的说,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");
以上的代码详细的描述一下的话,有四个需要注意的点:
- console控制台的输出
- call stack调用栈
- web APIs的队列
- Queue的事件队列,通过event loop询问
过程我已经用chatGPT做出了总结:
- 开启一个匿名函数作为主线程,将其记录压入调用栈中。
- 执行
console.log("global begin")
,在控制台输出 "global begin",将其记录压入调用栈中。 console.log("global begin")
执行完毕,从调用栈中弹出。- 执行第一个
setTimeout
函数,将其记录压入 Web APIs 的定时器队列中。此时调用栈为空。 - 执行第二个
setTimeout
函数,将其记录压入 Web APIs 的定时器队列中。此时调用栈为空。 - 执行
console.log("global end")
,在控制台输出 "global end",将其记录压入调用栈中。 console.log("global end")
执行完毕,从调用栈中弹出。- 在匿名函数中执行第三个
setTimeout
函数,将其记录压入 Web APIs 的定时器队列中。此时调用栈为空。 - 匿名函数执行完毕,从调用栈中弹出。
- 主程序执行完毕,等待计时器到期。
- 第二个计时器到期,将回调函数
timer2
压入任务队列中。 - 事件循环从任务队列中取出
timer2
函数并执行,输出 "timer2 invoke"。 - 在
timer2
函数中再次执行一个setTimeout
函数,将其记录压入 Web APIs 的定时器队列中。此时调用栈为空。 - 内部的计时器到期,将回调函数
inner
压入任务队列中。 - 事件循环从任务队列中取出
inner
函数并执行,输出 "inner invoke"。 - 第一个计时器到期,将回调函数
timer1
压入任务队列中。 - 事件循环从任务队列中取出
timer1
函数并执行,输出 "timer1 invoke"。 - 程序执行完毕,调用栈和任务队列都为空。
JavaScript异步调用逻辑图大概就是这样的:
JavaScript是单线程但是浏览器不是单线程的,所以有些JavaScript调用的内部api不是单线程的,内部会有单独的线程去倒计时或者其他操作,到了时机就会把回调放在消息队列里
运行环境提供的API是以同步或者异步模式的方式进行工作