概念
数据结构
堆(Heap)、栈(Stack)、队列(Queue)是存储数据的数据结构或存储机制。
它们对数据的操作顺序
堆:进出随意。
栈:先进后出/后出先进的压入式存储。
队列:先进先出。
后进先出 LIFO 译为 Last In First Out
也可以称为 先进后出 FILO 译为 First In Last Out
先进先出 FIFO 译为 First In Last Out
数据存储区域
JS数据存储在内存中,分为两个数据结构类型:栈内存和堆内存。
栈内存:存储的是标示符(变量)和基本数据类型。也就是 String Number null undefined Boolean Symbol和引用数据的指针(对象的指针)。
堆内存:存放引用数据类型的代码块。也就是栈中指针指向的对象的值。
JS根据垃圾回收机制对内存进行回收。
任务/消息队列(任务存储区域)
JS是单线程的,同步执行的,为避免代码阻塞,会将阻塞性的任务使用异步方式执行。
异步任务如setTimeout HTTP请求 Promise等。
同步任务在主线程执行。异步任务通过API在其他线程执行,执行完将回调函数放入一个任务队列。
任务队列存放的数据结构类型是 队列(Queue) 。
JS的单线程
执行上下文
JS代码运行前会创建执行上下文(执行环境),JS有三种执行上下文:
- 全局执行上下文
- 函数执行上下文
- eval执行上下文
执行栈(调用栈)
执行栈是js代码执行(方法调用)时候开辟的内存空间。
变量声明操作不会占用这个空间。
JS任务开始处理,将代码逐行压入执行栈,执行完一行就移出一行。
如果有嵌套执行上下文,就依次压入,上下文执行完同样移出(先进后出)。
任务处理完后(执行栈清空),再压入下一个任务的代码。
执行栈的数据结构是栈,所以采用先进后出的方式进行 执行和移出。
示例
console.log(1);
function foo() {console.log('foo');bar();
}
function bar() {console.log('bar');
}
foo();
console.log(4);
执行顺序:
- JS引擎将全部代码加载,在执行栈中压入一个匿名的调用
anonymous
console.log(1)
压入执行栈console.log(1)
执行完,移出执行栈foo()
压入执行栈- foo函数中有执行上下文,运行这个上下文,将
console.log('foo')
压入 console.log('foo')
执行完移出bar()
压入- bar函数中有执行上下文,
console.log('bar')
压入 console.log('bar')
移出bar()
移出foo()
移出anonymouse
移出,执行栈清空,等待下个任务。
任务
我理解的JS任务包括同步任务和异步产生的微任务、宏任务。
同步任务
同步运行的代码。
宏任务
# | 浏览器 | Node |
---|
I/O | ✅ | ✅ |
setTimeout | ✅ | ✅ |
setInterval | ✅ | ✅ |
setImmediate | ❌ | ✅ |
requestAnimationFrame | ✅ | ❌ |
微任务
# | 浏览器 | Node |
---|
process.nextTick | ❌ | ✅ |
MutationObserver | ✅ | ❌ |
Promise.then catch finally | ✅ | ✅ |
async/await本质上属于对Promise的封装,所以await相当于promise.then。
示例
console.log(1);
const p = new Promise((resolve,reject)=>{console.log(2);resolve();
});
p.then(()=>{console.log(3)
});
setTimeout(()=>{console.log(4)
},0)
同步任务A:打印1;创建一个promise实例化对象p,打印2。
微任务B:执行promise.then回调,打印3。
宏任务C:执行setTimeout回调。
任务执行顺序
JS执行从同步任务开始,执行完查询微任务,微任务全部执行完,执行下一个宏任务。
一个宏任务中包含同步任务,也可能包含微任务。
同样从同步任务开始,执行完查询微任务,微任务全部执行完,执行下一个宏任务。
…依此处理
所以上例是按照A>B>C顺序执行的。
- 微任务和宏任务都会创建一个队列。
- 微任务先执行,宏任务后执行。
- 微任务全部拉入执行栈,宏任务一次拉一个。
事件循环EventLoop
JS拥有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的任务。
并发:同一时间段执行多个任务,但同一时间点只能执行一个任务。如吃饭、喝水,同一时间点只能干一件事情
并行:同一时间点可以执行多个任务。如烧水、玩手机可以同时进行
- 执行代码:运行 执行栈 中的代码。
- 收集和处理事件:收集任务,判断任务执行顺序。
- 执行队列中的任务:执行栈清空时,查询任务队列是否有任务,有则将下一个任务取出压入执行栈去执行。这个顺序依据 队列Queue 的先进先出。
总结
执行栈相当于JS引擎正在执行的工作表。
队列相当于待办任务列表。
执行栈中工作执行完成,触发事件循环,从任务列表中取下一个任务执行。
当执行栈和任务队列中都没有工作要处理。JS执行完毕。
运行时的概念
可视化描述
栈内存 Stack
函数调用形成了一个由若干 帧(Frame) 组成的 栈(Stack)。
function foo(b) {let a = 10;return a + b + 11;
}function bar(x) {let y = 3;return foo(x * y);
}console.log(bar(7));
当调用 bar 时,第一个帧包含了 bar 的参数和局部变量,压入栈中。 当 bar 调用 foo 时,第二个帧被创建并被压入栈中,放在第一个帧之上,帧中包含 foo 的参数和局部变量。
开始执行后,当 foo 执行完毕然后返回时,第二个帧就被弹出栈(剩下 bar 函数的调用帧 )。当 bar 也执行完毕然后返回时,第一个帧也被弹出,栈就被清空了。
队列 Queue
一个 Javascript 运行时包含了一个待处理任务的 队列(Queue) 。每一个 任务(Task) 都关联着一个用以处理这个任务的回调函数。
在 事件循环 期间的某个时刻,运行时会从最先进入队列的任务开始处理。被处理的任务会被移出队列,并调用与之关联的函数。
正如前面所提到的,调用一个函数总是会为其创造一个新的 栈帧。
函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个任务(如果还有的话)。
事件循环 Event Loop
任务队列一直在同步的等待任务执行完成,一个任务被完整的执行完(栈帧执行完,栈为空),才会继续执行下一个任务。
这个实现方式,称为 事件循环 。
在浏览器里,每当一个事件发生并且有一个事件监听器绑定在该事件上,一个任务就会被添加到任务队列。
如果没有事件监听器,这个事件将会丢失。
setTimeout 描述运行时的过程
函数 setTimeout(tastFunc, delay) 接受两个参数:待加入队列的任务(一个函数) 和 一个时间值(可选,默认为 0)。这个时间值代表了从执行setTimeout开始, 任务 被实际 加入到队列 的 延迟时间 。
------------------个人理解 start--------------
这里的延迟时间,需要区分是针对 添加消息 还是 处理任务。
个人理解为 添加任务&#xff0c;所以未使用参考文档中的 <最小延迟时间>作为表述
个人理解参考文档中的 最小延迟时间 &#xff0c;指的是setTimeout一经执行&#xff0c;就将任务添加到队列中&#xff0c;然后保证前面的任务执行完毕 和 延迟时间到达 两个条件后&#xff0c;再处理。
这样理解&#xff0c;等于当执行两条setTimeout时&#xff0c;任务会按照执行setTimeout顺序插入到队列中&#xff0c;根据任务队列先进先出原则无论它们的延迟时间如何&#xff0c;后面的总要等到前面的任务执行完才会执行。
可是实际确不是如此&#xff1a;
setTimeout(()&#61;>{console.log(1)}, 1000);
setTimeout(()&#61;>{console.log(2)}, 0);
参考文档中的 最小 &#xff0c;可能指的是执行setTimeout之前的代码会影响 添加任务 的时间。
所以当按照下面表述时&#xff0c;使用 最小 应该是合理的&#xff1a;
时间值代表了&#xff0c;运行脚本时&#xff0c;脚本中的这条 setTimeout 将任务添加到队列的 最小延迟时间。
------------------个人理解 end--------------
任务tastFunc 在到达 延迟时间delay 后&#xff0c;被加入到 队列Queue&#xff0c;然后等待 执行栈 里的 帧 执行完毕&#xff0c;接着等待 当前队列中前面的任务 处理完毕&#xff0c;最终才轮到自己。
function foo() {console.log(&#39;foo function&#39;)
}
function inside() {console.log(&#39;inside function&#39;)
}
function outside() {console.log(&#39;outside function&#39;)
}
function bar() {setTimeout(inside, 0);
}
setTimeout(outside, 0);
bar()
foo();
执行顺序&#xff08;个人理解&#xff09;&#xff1a;
- 任务队列添加3个任务&#xff1a;setTimeout(outside,0)、bar()、foo()并按顺序处理。
- 处理任务1&#xff1a;setTimeout 添加 任务outside 到队列中。
- 处理任务2&#xff1a;bar 执行 setTimeout 添加 任务inside 到队列中。
- 处理任务3&#xff1a;foo 打印。
- 处理任务4&#xff1a;outside 打印。
- 处理任务5&#xff1a;inside 打印。
变更一下&#xff1a;
function foo() {console.log(&#39;foo function&#39;)
}
function inside() {console.log(&#39;inside function&#39;)
}
function outside() {console.log(&#39;outside function&#39;)
}
function bar() {setTimeout(inside, 0);
}
setTimeout(outside, 100);
bar()
foo();
执行顺序&#xff08;个人理解&#xff09;&#xff1a;
- 队列添加3个任务&#xff1a;setTimeout(outside,100)、bar()、foo()并按顺序处理。
- 处理任务1&#xff1a;setTimeout 告诉系统100毫秒后&#xff0c;添加 任务outside 到队列中。
- 处理任务2&#xff1a;bar 执行 setTimeout 添加 任务inside 到队列中。
- 处理任务3&#xff1a;foo 打印。
- 处理任务4&#xff1a;inside 打印。
- 100毫秒等待结束&#xff0c;向队列添加 任务outside。
- 处理任务5&#xff1a;outside 打印。
再变更一下&#xff1a;
function foo() {console.log(&#39;foo function&#39;)
}
function inside() {console.log(&#39;inside function&#39;)
}
function outside() {console.log(&#39;outside function&#39;)
}
function bar() {setTimeout(inside, 0);
}
setTimeout(outside, 1);
bar()
foo();
执行顺序&#xff08;个人理解&#xff09;&#xff1a;
- 队列添加3个消息&#xff1a;setTimeout(outside,100)、bar()、foo()并按顺序处理。
- 处理任务1&#xff1a;setTimeout 告诉系统1毫秒后&#xff0c;添加 任务outside 到队列中。
- 1毫秒等待结束&#xff0c;向队列添加 任务outside。
- 处理任务2&#xff1a;bar 执行 setTimeout 添加 任务inside 到队列中。
- 处理任务3&#xff1a;foo 打印。
- 处理任务4&#xff1a;outside 打印。
- 处理任务5&#xff1a;inside 打印。
setTimeout中的延迟时间参数delay&#xff0c;定义了在向队列添加任务的延迟时间。
任务处理是在执行栈中以帧为单位&#xff0c;同步依次执行至完成。
在不同的设备中&#xff0c;1帧所定义的时间不一样&#xff0c;大致有 1/24 1/30 1/40 1/60 秒等值。
所以上面示例定义的1毫秒&#xff0c;抢在bar函数执行setTimeout之前&#xff0c;执行了操作。
多个运行时(Runtimes)通信
一个 web worker 或者一个跨域的 iframe 都有自己的执行栈、堆栈和任务队列。两个不同的 运行时(Runtimes) 只能通过 postMessage 方法进行通信。如果另一个运行时侦听 message 事件&#xff0c;则此方法会向该运行时添加消息&#xff08;任务&#xff09;。
JS具体执行步骤
主线程执行同步代码块&#xff0c;遇到定时器、Promise等异步任务时&#xff0c;会创建事件队列&#xff0c;把它们丢到队列里去&#xff0c;等主线程执行完成后&#xff0c;再回去执行队列中的task。
JS执行主要包括 同步任务和异步任务&#xff0c;这个同步任务会进入主线程中&#xff0c;最后放入执行栈中执行。
异步任务分为微任务和宏任务&#xff0c;分别创建一个队列放入队列中&#xff08;而不是栈中&#xff09;。
主线程任务执行完&#xff0c;会把微任务全部放入执行栈中执行。
微任务执行完再取一个宏任务放入执行栈执行&#xff0c;执行完后再取一个&#xff0c;直到执行完所有宏任务。
理解不足
很多文章描述事件循环时&#xff0c;只会描述执行栈、堆、队列3个区域。
执行栈和栈内存似乎是同一区域的不同工作。
参考&#xff1a;
并发模型与事件循环
微任务、宏任务与Event-Loop
浅析JS堆、栈、执行栈和EventLoop
究竟什么是异步编程&#xff1f;(关键词讲的很清楚)