ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# 一、栈(stack) > 学习编程的时候,经常会看到stack这个词,它的中文名字叫做"栈"。 理解这个概念,对于理解程序的运行至关重要。容易混淆的是,这个词其实有三种含义,适用于不同的场合,必须加以区分。 <br> <br> ## 含义一:数据结构 stack的第一种含义是一组数据的存放方式,特点为LIFO,即**后进先出**(Last in, first out)。 <br> ![](https://box.kancloud.cn/0bfd496a679a94344736cc29fb3681e8_385x260.png) <br> 在这种数据结构中,数据像积木那样一层层堆起来,后面加入的数据就放在最上层。使用的时候,最上层的数据第一个被用掉,这就叫做"后进先出"。 ``` // 用数组对 栈结构 进行模拟 var stack = []; // push就相当于放羽毛球进去,专业术语叫压栈 stack.push('1号羽毛球'); stack.push('2号羽毛球'); stack.push('3号羽毛球'); // pop就相当于拿一个羽毛球出来, 专业术语叫弹栈 stack.pop(); ``` <br> ## 含义二:代码运行方式 stack的第二种含义是"调用栈"(call stack),表示函数或代码像堆积木一样存放,以实现层层调用。 <br> ![](https://box.kancloud.cn/db72d02c411342045393de826dd5875f_562x303.png) 1. 运行console.log(1),这个时候浏览器就会把console.log(1)丢进调用栈 <br> ![](https://box.kancloud.cn/ab313f90b2f472b07a5d14d0c28278ac_857x376.png) 2. 因为调用栈有东西,所以js解析器就会去执行,console.log(1)执行完毕,就会弹出调用栈,浏览器控制台就会打印1 <br> <br> 剩下的代码如此类推,见下图 <br> ![](https://box.kancloud.cn/dd344f564d69467357bf3946ea3fb9be_932x387.png) ![](https://box.kancloud.cn/209a446976c9f2047e6447b506745669_856x376.png) ![](https://box.kancloud.cn/9c5b207c626aa1537a4838df66357288_893x370.png) ![](https://box.kancloud.cn/4a0c25429d8467b8779c3fdc940965d5_859x384.png) 调用像积木一样堆起来,就叫做"调用栈"。程序运行的时候,总是先完成最上层的调用,然后将它的值返回到下一层调用,直至完成整个调用栈,返回最后的结果。 <br> <br> ## 含义三:存储数据的区域 stack的第三种含义是存放数据的一种内存区域。程序运行的时候,需要内存空间存放数据。一般来说,系统会划分出两种不同的内存空间:一种叫做stack(栈),另一种叫做heap(堆)。栈是存基本数据类型,堆存引用数据类型。 <br> <br> 我们来看看下面的代码: ``` var a = 20; var b = 'abc'; var c = true; var d = { m: 20 }; var e = d; e.m = 30; console.log(d.m) ??? ``` <br> <br> ![](https://box.kancloud.cn/aa987499988edaf05b86cf051f20cff7_722x303.png) 1. js解析器会去找var,然后把声明的变量都赋值undefined并且存到栈内存 <br> <br> ![](https://box.kancloud.cn/4dd90c627a6503c38fac54d1449c4967_728x317.png) 2. 然后第二次读取代码的时候会执行声明赋值,基本数据类型就存储在栈内存,对象就存储到堆内存,栈里面的d通过地址访问堆里面关联对象 <br> <br> ![](https://box.kancloud.cn/1220e76405bdaf477bea0f8e0af78cb7_736x337.png) <br> <br> # 二、任务队列 JS是单线程,一次只能执行一个任务,所以所有任务都需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。 如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。 其实主线程完全可以不管Ajax请求,挂起处于等待中的任务,先运行排在后面的任务。等到请求返回了结果,再回过头,把挂起的任务继续执行下去。 于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。 <br> 具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。) (1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。 (2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。 (3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。 (4)主线程不断重复上面的第三步。 <br> 下图就是主线程和任务队列的示意图。 ![](https://box.kancloud.cn/7b324ea98928a6097a95deb458e4e05b_581x420.png) 只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。 <br> 下面我们看一个例子: ``` console.log(1); setTimeout(function (){ console.log(2); }, 0); console.log(3); ``` 下面是这段代码的执行流程图: <br> ![](https://box.kancloud.cn/ce1b57f2d3e1f9cb12fb8e7bee140d16_1288x585.png) <br> ![](https://box.kancloud.cn/8ea1af7f9a6a1d2719a0d9c7767276bc_1275x593.png) <br> ![](https://box.kancloud.cn/071e808f743bec8fcbaa69408724394b_1312x574.png) 延时器是外部API,异步的,所以放Web APIs里面执行,不阻塞主线程的调用栈 <br> ![](https://box.kancloud.cn/aa3407ac9ee49165079d17799b1f4d04_1274x577.png) 因为调用栈为空,这个时候```console.log(3)```进主线程的调用栈 <br> ![](https://box.kancloud.cn/cc7e6732d7fb2f3d76263b53c6edc832_1282x563.png) <br> ![](https://box.kancloud.cn/5b203e33463d98fd51d64729e0e2d5c2_1286x577.png) 定时器执行完毕,回调函数进任务队列 <br> ![](https://box.kancloud.cn/5e8173916ed272f5ba53ed2e28871c92_1290x576.png) 事件循环会不断的检测调用栈以及队列是否有东西,这个时候它发现调用栈为空,队列又有任务,所以回调函数进主线程的调用栈 <br> ![](https://box.kancloud.cn/13550c9943beb5b4ed51271c126c3276_1344x570.png) 回调函数1执行后就执行```console.log(2)```,```console.log(2)```进调用栈 <br> ![](https://box.kancloud.cn/102aa1ded5888aeeeca2cf257ddc9c79_1346x553.png) <br> ![](https://box.kancloud.cn/5de37b21ed9904b3266862c45c75c7c5_1320x556.png) <br> > 主线程调用栈从"任务队列"中读取事件并执行,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。 <br> <br> <br> ``` for (var i = 1; i <= 3; i++) { setTimeout(function () { console.log(i); }, 0); } ``` <br> ![](https://box.kancloud.cn/6efa07d55bd9a0ecaebe8b73bcd008bb_1392x562.png) <br> ``` for (var i = 1; i <= 3; i++) { if(i == 1){ setTimeout(function () { console.log(i); }, 100); } if(i == 2){ setTimeout(function () { console.log(i); }, 50); } if(i == 3){ setTimeout(function () { console.log(i); }, 150); } } ``` ![](https://box.kancloud.cn/c8a841d07267426a04ab4fbe26a20851_1379x546.png) ## 宏任务与微任务 宏任务有:settimeout、setInterval 微任务有:Promise 事件环检测队列,如果有微任务,那么微任务一定比宏任务先执行,如果微任务队列清空了,才开始执行宏任务 <br> ### 经典例题1 ``` setTimeout(function () { console.log(1); Promise.resolve().then(() => { console.log(3); }); }, 0); Promise.resolve().then(() => { console.log(2); setTimeout(function () { console.log(4); }, 0); }); ``` 定时器1的回调1进宏任务队列,promise2的回调2进微任务队列,因为微任务比宏任务快,所以先打印2,然后执行定时器4,回调4排在回调1后面,然后回调2执行完毕,微任务清空完毕,宏任务的回调1开始打印1,然后回调3进微任务队列,有微任务就执行微任务打印3,最后打印4。 答案:2 1 3 4 <br> ### 经典例题2 ``` setTimeout(function () { console.log(1); Promise.resolve().then(() => { console.log(2); }); }, 0); Promise.resolve().then(() => { console.log(3); Promise.resolve().then(() => { console.log(4); Promise.resolve().then(() => { console.log(5); }); }); setTimeout(function () { console.log(6); }, 0); }); ``` <br> ### 经典例题3 ``` setTimeout(function () { console.log(1); Promise.resolve().then(() => { console.log(2); }); }, 0); ### 经典例题3 Promise.resolve().then(() => { console.log(3); Promise.resolve().then(() => { console.log(4); setTimeout(function () { console.log(5); }, 0); }); Promise.resolve().then(() => { console.log(6); }); }); ``` <br> ### 拓展 ``` Promise.resolve().then(() => { console.log(1); }); // vue的nextTick是最快的微任务 this.$nextTick(() => { console.log(2); }); ``` <br> ### 经典例题4 ``` setTimeout(function () { console.log(1); }, 0); this.$axios.post('/users/signin', { account:'admin', password:'admin' }).then(res => { console.log(2); }); ```