‘异步’ 这个概念若是放到十年前的08,09年的时候,你们会以为: 哇~ 这是一个新鲜的概念,不用再把全部Web页面同步处理了,节省了服务资源的同时也提高了用户体验。也正是从那个时候开始,咱们开始关注先后端分离这个概念。
通过10年的努力,咱们如今很高兴的看到,前端已经快速的成长为一门有着独立发展方向的技术。这一切也就是从异步这个关键的点开始的,所以可见 异步对于前端来讲意味着什么?大概就是意味着基石和根本吧。javascript这篇文章将不局限于上述的Http异步网络请求这个独立的场景,将细数一下前端发展过程当中对于异步这个概念是如何逐步落实的。html
可能咱们的思惟固定化了,毕竟在漫长的JS脚本语言发展的过程当中,回调函数就曾经是异步编程的标准解决方案,直到如今WebAPI和NodeJs中还保留着大量的APi使用回调函数做为异步结束的处理。为了解决回调函数这个解决方案在开发体验上的弱势。ES6支持了Promise这样使用同步编程的方式来开发异步程序前端
在以前咱们开发回调函数的时候,没有人知道哪一个函数先执行,哪一个随之执行,除非咱们把要逐次进行的函数进行嵌套,让程序依照回调的层级从深层到浅层的执行。java
咱们可能常常会据说这样的一句话Promise 是一个表现为状态机的异步容器。怎么理解这句话呢:node
咱们在看完 # ES6-Promise 的文档介绍的时候,都会跃跃欲试的使用其提供的API方法来进行开发和升级,并大呼过瘾。在兴奋之余,咱们也一块儿来盘点一下那些年咱们使用Promise。es6
咱们以前尝试着理解了--状态机这个概念。也清晰的知道了这个Promise 的一个重要特性。web
new Promise((resolve, reject) => {
resolve('程序执行成功');
setTimeout(() => reject('程序执行失败'), 5000);
}).then(console.log, console.log);
复制代码
咱们会想到第二个问题:
Promise 执行函数中,在resolve或者reject触发以后的代码会不会执行呢?ajax
new Promise((resolve, reject) => {
resolve('程序执行成功');
console.log('后面的程序会不会执行呢');
setTimeout(() => reject('程序执行失败'), 5000);
}).then(console.log, console.log);
复制代码
OUTPUT:chrome
后面的程序会不会执行呢
程序执行成功
复制代码
在任何程序中,代码的执行顺序都是很重要的。既然说Promise是一个异步容器,容器中或外的代码是同步的仍是异步的,甄别他们的执行顺序也是须要有明确认识的。编程
console.log('A');
new Promise((resolve, reject) => {
console.log('B');
resolve('C');
console.log('D');
}).then(console.log, console.error);
console.log('E');
复制代码
首先要区分一下,哪些代码是同步的,那些代码是异步执行的。
OUTPUT:
A B D E C
复制代码
思考: 在Promise构造函数的参数函数中,代码是同步执行的。若是在函数体中存在异步方法,好比 setTimeout()
执行顺序会发生什么变化?
【 这部份内容会在浏览器异步机制部分提到 】
Promise 做为一个异步容器,他存在的意义就是为了改变Promise的状态。那么在状态已经触发以后的代码就变得没有意义了。若是你已经判定 resolve 或 reject 后的代码无心义。可使用 return resolve() / return reject()
避免发生没必要要的错误。
new Promise((resolve, reject) => {
return resolve('程序执行成功');
console.log('后面的程序会不会执行呢');
setTimeout(() => reject('程序执行失败'), 5000);
}).then(console.log, console.log);
复制代码
做为一个状态机,咱们只须要关注Promise的状态变化便可。状态变化才会触发异步执行。峰回路转,状况变幻无穷。仍是要关注他状态机的本质。
说到Promise的API,就到了你们比较熟悉的内容了。在ES6发展以前社区就有对Promise的社区实现,凡是被大规模承认的实现官方也很快就会给出支持,这也是JavaScript语言得以不断进步的缘由。
从Api的使用来看,then有两个接受参数分别对应着的是 Promise 构造函数的参数函数的成功结果和失败结果,也就是状态机将状态变为了 成功 或者是 失败。
其实对于Promise.prototype.then 这个api很容易理解,总结来看:
.then
能够用于链式调用,也能够不用。.then
的本质是建立了一个新的隐形的Promise,所以能够继续链式调用。.then
的参数函数(回调函数)在触发以前,Promise的状态已经发生了变化。.then
只有在Promise的参数函数中,有错误发生的时候才会有reject。const p1= new Promise((resolve, reject) => resolve('hello'));
const p2 = p1.then(value => {
console.log(value); // hello
return value;
});
const p3 = p2.then(console.log); // hello
复制代码
从上边的总结来看,.then
是在Promise原型上的Promise.prototype.then
。要想让p2和p3的then可以成功执行,必须保证前面调用then的那个对象是一个Promise。
...
const p2 = p1.then(value => {
// 替换这里
return Promise.resolve(value);
});
const p3 = p2.then(console.log); // hello
复制代码
Promise.resolve()
和在 .then
里面直接用return返回能够获得一样的结果。.then
函数(方法)的执行结果是一个新的 Promise。.then
若是要是返回空值,至关于 Promise.resolve()
;来继续看.then
的最后一个Part,咱们知道.then
有两个回调函数,第一个是在成功时候触发的,第二个是在失败时候触发的。
const p1 = new Promise((resolve, reject) => resolve('hello'));
const p2 = p1.then(value => {
// return abcd; // ReferenceError: x is not defined
return Promise.reject('手动错误');
// VM258:4 Uncaught (in promise) 手动错误
});
const p3 = promiseB.then(console.log, console.error);
复制代码
以上的DEMO是触发第二个回调函数的两种方法:程序错误OR手动抛错(逻辑错误)。这两种的侧重点可能不由相同,所以能够区别来使用。
这个Api从某些角度来看是.then
方法的一个小变种或者说是语法糖。怎么来理解这个呢,在then的回调函数中,已经有对于err的处理,只不过在链式调用的过程当中,若是每一步都进行err的处理会严重的阻塞咱们的开发的流畅性。所以也就诞生了 .catch
来捕获异常。
除了.catch
的Api以外,有如下常见总结:
.catch
会捕获整个Promise链路上的异常。.catch
捕获的异常包括 程序错误 && 手动抛错(逻辑错误) 。Promise
会将全部的内部错误内部处理,不会影响外部的逻辑。详细来讲:
// 捕获异常
new Promise((resolve, reject) => {
console.log(x);
resolve('hello Mr.ZA');
}).then(res => {
console.log(y);
}).catch(err => {
console.log('err', err);
})
setTimeout(() => { console.log('log: 后续程序') }, 1000);
// err VM8513:7 ReferenceError: x is not defined
// at <anonymous>:2:14
// at new Promise (<anonymous>)
// at <anonymous>:1:1
// log: 后续程序
new Promise((resolve, reject) => {
resolve(1);
console.log(x); // 区别在这里
}).then(res => {
console.log(y);
}).catch(err => {
console.log(err);
})
ReferenceError: y is not defined
at <anonymous>:6:14
复制代码
.catch
捕获的异常不是全部的异常,而是捕获第一个影响状态变化的异常。new Promise((resolve, reject) => {
console.log(x);
resolve(1);
}).catch(err => {
if(err) { console.log('异常捕获')};
return 'continue progress';
}).then(res => {
console.log('res: ', res);
})
// 异常捕获
// res: continue progress
复制代码
.catch
的位置不必定是在最后面,它和其余的api同样都会返回一个新的Promise为链式调用提供服务。写在最后面符合咱们对开发流程的认知。finally
在英语上来说是最终的意思,放在Promise的Api中,它会被咱们理解为无论状态如何变化,都会发生的事情。
对于.finally
,有如下常见总结:
.finally
是Promise状态机状态变化的兜底方案,也是不管如何都能执行的。.finally
这个Api的回调函数没有参数。.finally
不必定放在链式调用的最后面,若是他在链式调用的中间,他前面的resolve或者reject传出的值会跳过finally传入下面的链式调用中。// 伪代码,模拟发送请求处理loading的问题。
new Promise((resolve, reject) => {
this.loading = true;
$.ajax(url, data, function(res) {
resolve('res');
})
}).finally(() => {
this.loading = false;
return '尝试更改';
}).then(value => {
// handle value
console.log(value), // res
}).catch(
// handle error
error => console.error(error),
);
复制代码
.finally
会更关注于状态的变化过程而不是状态变化带来的影响。.finally
若是在其中的回调中尝试更改以前的流转的值的时候,不能得到成功,可是若是有抛错产生,会被错误处理程序依次捕获。接下来关注一下这个Api的兼容性问题,我想之因此你尚未使用这种方法来减小重复的工做,颇有多是由于这个Api出世的时间比较晚。
据 MDN 的资料显示,这个API是ES2018引入TC39规范的也就是 ES9。
下面来看看这个新Api的兼容性问题有如下关注点,根据本身状况食用吧。
new Promise()
来建立实例。Promise.resolve = new Promise((resolve,reject)=>resolve('xx'));
Promise.reject = new Promise((resolve, reject)=>reject('xx'));
复制代码
这两个Api应该是Promise里面比较难理解的Api了,可是在使用上他们其实很简单。咱们仍是要追求一下实现的原理,这样咱们在使用Api的时候也不会那么迷惑何时应该有什么样的结果。这两个Api也常常会放到一块作一些对比。
对于他们来说,有如下常见总结:
.all()
和.race()
不是Promise的原型方法,所以在使用他们的时候不用new Promise()
.all()
和.race()
都接收一个数组为参数,返回的也是一个数组。若是参数数组中的值不是一个Promise实例,那么会被转换成直接返回。.all()
中若是有一个Promise执行出错,将中止执行返回错误。所有成功以后才返回值数组。.race()
中若是第一个Promise完成了就直接返回,不等待其他执行完毕。对于这些官方的Api及其用法,有人曾提出一个结论,使用Promise.all
能够并发的执行异步动做,获得性能的提高。那么其中的原理是什么呢?为何单线程的JavaScript会有异步性能提高呢?咱们来看下其中的缘由。
若是你有关于【微任务和宏任务】的理解,下面的内容会更加容易理解。
Promise.all([
new Promise((resolve)=>{
setTimeout(() => {
console.log('-', 0)
resolve(0);
}, 1000)
}),
new Promise((resolve) => {
setTimeout(()=>{
console.log('-',1)
resolve(1);
}, 2000)
})
]).then(res=>console.log(res))
复制代码
执行的过程:
pending
状态的Promise实例放入栈中,并记录下他们的顺序编号。.then
是会建立一个新的Promise执行,所以在执行数组中实例的时候会建立新的 Promise(新的微任务)。下面来看一下Promise.all 的源码实现。
Promise.all = function (arr) {
// ... Step0: 返回新的Promise
return new Promise(function (resolve, reject) {
var args = Array.prototype.slice.call(arr);
if (args.length === 0) return resolve([]);
var remaining = args.length;
function res(i, val) {
//...
};
// Step 1. 对数组进行同步循环
for (var i = 0; i < args.length; i++) {
// Step 2. 执行这些个Promise实例。
res(i, args[i]);
}
});
};
复制代码
res
这个方法。
function res(i, val) {
// Step 3: 确认要执行的那个Promise实例
if (val && (typeof val === 'object' || typeof val === 'function')) {
// Step 4: 建立.then 也就是一个新的Promise微任务
var then = val.then;
if (typeof then === 'function') {
then.call(
val,
function (val) {
res(i, val);
},
reject
);
return;
}
}
args[i] = val;
if (--remaining === 0) {
resolve(args);
}
// race
// for (; i < len; i++) {
// promises[i].then(resolver,rejecter);
// }
}
复制代码
结果也就大概简化成了:
setTimeout(() => {
console.log(1)
}, 1000)
setTimeout(() => {
console.log(2)
}, 1500)
复制代码
复制代码
结论:了解宏任务和微任务能够有效缓解焦虑。
也许咱们在讲JavaScript的时候,都会去说Js是一个单线程的拥有异步特性的事件驱动的解释型脚本语言。虽然它是单线程的,可是在保证流畅性和性能优化方便拥有各类各样的异步任务和主线程进行通讯,异步能够说是Js的一大难点和重点。不少时候初学者们都在为这个异步任务什么时候执行而感到迷茫。在了解异步机制以前,咱们仍是须要在一下基础的概念或者理论上达成一个有效共识,这样会很大程度上帮助咱们。
如今咱们就从Chrome浏览器的主要进程入手,了解一下咱们经常使用的工具是如何切分这些线程和进程的。这里有一些关于进程、线程、多线程相关总结。
从通俗易懂的角度来理解:
进程
像是一个工厂,进程拥有独立的资源 -- 独立的内存空间。进程
之间相互独立,没有更大型的内存空间包裹。进程
(工厂)之间想要通讯,能够借助第三方进程 -- 管理进程。进程
内的多个线程,共享进程
的资源 -- 共享内存空间。用偏官方的话术来表示一下:
进程
是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)根据上面的知识,咱们很容易就能发现浏览器做为不少程序的集合体,它的设计必定是一个多进程的,若是他只有一个进程那么体验会差到爆炸。咱们也常常会听到这样的一个说法,说Google Chrome浏览器是一个性能怪兽,每每打开它内存就会飙升。那么一个浏览器中,有哪些进程呢:
从Chrome的任务管理器中,能够清晰的看到那些正在运行在咱们浏览器上的进程。
扩展一下:关于Chrome有好多种内存管理的机制,这也是Chrome强大的地方,能够在浏览器里面输入 chrome://flags
进行设置。
Process-per-site-instance
。每打开一个网站,而后从这个网站链开的一系列网站都属于一个进程。这也是Chrome的默认进程模型。Process-per-site
。同域名范畴的网站属于一个进程。Process-per-tab
。每个页面都是一个独立的进程。这就是外界盛传的进程模型。Single Process
。传统浏览器的单进程模型。对于整个浏览器来说,咱们上文说到了浏览器有自身的浏览器进程。可是这个进程对于每一个标签页中显示的网页内容来说,帮助不大。它只负责一个调度管理的做用。真正在浏览器大部分窗口内工做的仍是Renderer
进程。所以咱们把 Renderer进程称之为浏览器内核。
来了解一下Renderer进程包含哪些线程:
他们在Renderer进程下,各司其职。关于他们的详细工做,估计又是一篇系列长文。待我写完以后,会补充一个连接到这里。
从这些共识中,咱们能够理解以前的那句对JavaScript的描述了把。JavaScrit是一个单线程( JS引擎是单线程的 )的拥有异步特性( 拥有独特的异步线程 )的事件驱动( 事件也是一个单独的线程处理 )的解释型脚本语言。
在这一章中,咱们不先不关心浏览器渲染进程中的其余线程,也不关心具体的JS代码上下文,做用域等细节问题。把注意力集中在JS引擎上,从宏观上观察一下浏览器内核的一些特性。这将在很长一段时间内有助于咱们梳理咱们所写代码执行流程,避免意外的发生。
在继续深刻研究以前,咱们先来回忆一些知识点,避免有疏漏对下面的内容难以理解:
先来看段简单的代码理解一下调用关系。
console.log('1');
function a() {
console.log('2');
b();
console.log('3');
}
function b() {
console.log('4');
}
a();
// output: 1 2 4 3
复制代码
相信你已经很快就获得了答案,由于这段代码中是纯同步执行的,也没有事件,IO等异步方法。因此咱们知道他的调用数序,可是程序是如何知道调用数序的呢?或者说程序执行的时候有什么很牛的办法么?
你可能怀疑这样的一个事情发生,就像刚刚学习这门技术时候的我同样认为程序会不会作下面的事情呢?
程序设计的时候可能没有那么的粗暴,由于这样会致使一系列的问题好比函数做用域如何处理呢?那它可能有它做为程序来说的办法来实现这种调用 -- 执行栈(调用栈)
在上面的程序执行的时候,调用栈的工做顺序为:
注:
再来看下这个不通常的程序:
function hello() {
hello();
}
hello();
复制代码
这个程序的独特之处在于,它一直在像执行栈中插入 hello()
这个栈桢,没一会咱们的执行栈就会溢出,(内存溢出)。这个时候浏览器就会假死掉,报出溢出的错误。
咱们在书写代码的时候,其实运用的 大部分是 JS这门高级语言封装的各类API,剩下的一部分Api不是JS引擎封装的,而是跟JS这么门语言处于的环境有关系的。好比在浏览器中咱们直接使用的Navigator
就是浏览器环境决定的,在Node.js中就不能用,同理 Node.js中的 process
浏览器也是不能用的。
JS引擎是一个单线程的设计,可是在Web应用中少不了发送网络请求的场景,JS引擎不能彻底静止的等待网络请求结束在进行下面的工做,所以咱们有理由怀疑网络请求有本身的单独的线程来处理,不和主线程抢资源。
const url = 'https://xxx.com/api/getName';
fetch(url).then(res => res.json()).then(console.log);
复制代码
首先须要知道的是,定时器线程也是脱离JS引擎的独立线程,为何会给他这种特殊的待遇呢?道理我想很好理解:
setTimeout(function(){
console.log('1');
}, 0);
console.log('2');
复制代码
由于是异步线程执行的,那么结果应该是 2 1
。
const $btn = document.getElementById('btn');
$btn.addEventListener('click', console.log);
const timeoutId = setTimeout(() => {
for (let i = 0; i < 10000; i++) {
console.log('hello');
}
clearTimeout(timeoutId);
}, 5000);
复制代码
看上面代码的执行过程:在5s以后开启一个事件循环,使JS引擎处于阻塞状态,讲道理的话若是事件的触发不在单独线程上解决,那么在这5s以后JS处理循环的时候,事件都不会被感知和触发(由于JS引擎阻塞了,你的入栈不会执行)。
可是事实结果确实: 在循环的开始的时候,你点击按钮也会获得响应,只不过这个响应会在循环执行完成以后发生,可是已经说明了事件被触发了。至于为何在以后执行,咱们看下一章事件循环的时候会说起。
如今咱们知道了,无论是JS引擎实现的仍是浏览器等运行环境实现的一些Api,他们拥有特权 -- 专门处理本身事务的线程。这解决了不少问题,那么如今新问题的关键出现了,独立的线程是如何和JS引擎通讯的呢。搞懂了这个问题,那么JS的异步运行的机制也就清晰了。
这应该就是咱们这个章节的主角 -- 大名鼎鼎的Task Queue。咱们先看一张图找找Task Queue的位置。
对于任务队列来讲,上面所列就是通用规则,就是在不断进步的过程当中总会对这些规则进行不断修正。正因如此,在ES6的Promise
和HTML5的MutationObserver
出现以后,任务队列就变得复杂了,主要体如今:将任务队列中的任务按照等级从新肯定顺序,等待Event—Loop的调用。咱们接下来的任务就是对这个顺序进行研究。
事件循环,就是咱们常常说的那个 Event-Loop,想必你们应该都会对它有所耳闻。事件循环是任务队列和JS主引擎之间的桥梁。EventLoop触发也是有时机的,它被设计出来的目的也就是为了保证JS引擎线程的安全和稳定的。全部只有等到JS引擎空闲的时候才会经过EventLoop来取这时候在任务队列中排队等待的任务。
由于宏任务和微任务既设计任务队列又跟EventLoop有关系,又是异步中很关键的一个概念,因此单独来谈谈关于宏任务和微任务的问题。本章将从HTML规范 - Event Loop入手。来看看EventLoop是怎么区分宏任务和微任务的。
首先,若是咱们按照任务队列章节的内容来进行理解,队列作为一个数据结构应该是先进先出的结构,若是任务是一样存在于一个队列里的,那应该按照顺序执行。来看一个例子:
setTimeout(() => {
console.log(1)
},0)
Promise.resolve().then(() => {
console.log(2)
})
console.log(3)
复制代码
输出: 3 2 1
确定是大多数人都知道的结局,那么这就直接和咱们对于任务队列的理解是相悖的。也就是说 任务队列 有点不同。带着这个问题,咱们去翻翻规范。
为了协调事件、用户交互、脚本、渲染、网络等,用户代理必须使用这一小节描述的事件循环。
从上面的例子中,咱们产生了一个问题,并怀疑队列中任务仍然是有优先级的。可是按照这个思路继续想的话很容易产生矛盾的地方:
若是从性能的角度考虑,应该会设计成两个独立的列表,分别存听任务。咱们仍是去看规范中,怎么定义EventLoop,对于规范来说,着实是很是详细的,总结来看有如下重要的不容错过的点:
果真从规范中,咱们了解到EventLoop能够对应多个队列。流程也就变成了这样。
对于 Task Queue 和 Microtask Queue 常有这样的总结:
Promise
、process.nextTick
、MutaionObserver
因此通过理论的验证咱们的出这样的
同步任务 -> Micro -> Task -> Mic1, Mic2, ... -> Task -> Mic1, Mic2
能够根据上面的结论来看一个DEMO
console.log(1)
// Part A
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
})
// Part B
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
// Part C
setTimeout(() => {
console.log(9)
new Promise(resolve => {
console.log(11)
resolve()
}).then(() => {
console.log(12)
})
})
复制代码
首先:
1, 7
。 (Promise 的参数函数是同步的)PartA
和PartC
的两个setTimeout
放入Task列表中,把PartB
中的 .then
产生的新的Promise放入到 Micro中。8
Part A
。2,4
, 把 .then 放入 Mico 而后清空它 ,输出 5
。Part C
。9, 11
, 把 .then 放入 Mico 而后清空它 ,输出 12
。1, 7, 8, 2, 4, 5, 9, 11, 12
在明白了Task和MicroTask的顺序以后,基本上在浏览器中就不会有应用上的问题。
注意:若是你不想在Node中使用的话这部分能够绕行避免发生混淆。接下来咱们在Node环境下运行代码看看有没有什么'异常'发生。
console.log(1)
// Part A
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(3)
resolve()
}).then(() => {
console.log(4)
})
})
// Part B
setTimeout(() => {
console.log(5)
new Promise(resolve => {
console.log(6)
resolve()
}).then(() => {
console.log(7)
})
})
复制代码
根据咱们以前的经验,会很快得出结果。
1,2,3,4,5,6,7
。1,2,3,5,6,4,7
。详细的说明这个问题,咱们能够先提出这样的一个怀疑 :
在 Node.js 中,setTimeout 和 Promise 用了一样的方法实现。经过咱们以前的经验来说,可能Node 用了和以前ES6-Promsie出现以前的方案同样,使用了setTimeout进行伪实现,也就是说Node的Promise不是微任务。
带着这个疑问我翻看了Node的源码,源码(V12.3.1)在下方的连接里,这里直接来看得出这个结论,结论可能跟咱们想的不太同样,又差不太多:
这确实出乎咱们的意料,咱们用这个结论去跑一个示例,来看看能不能解释的通:
console.log(1)
// Part A
setTimeout(() => {
console.log(2)
process.nextTick(() => {
console.log(3)
})
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
})
// Part B
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
// Part C
process.nextTick(() => {
console.log(6)
})
// Part D
setTimeout(() => {
console.log(9)
process.nextTick(() => {
console.log(10)
})
new Promise(resolve => {
console.log(11)
resolve()
}).then(() => {
console.log(12)
})
})
复制代码
来分析一下Node执行的步骤:
1,7
。6
。8
。2, 4
9, 11
3, 10, 5, 2
总结输出: 1 7 6 8 2 4 9 11 3 10 5 2
这应该就是 node 与 Chrome 浏览器中, 异步机制的不一样之处把。,对于有一些研究文章说不会存在稳定的输出结果,致使timer执行的不稳定,我以为多是Node版本的问题,12中的结果是会稳定输出的,多是数据结构进行了升级,这个部分仍是有待详细研究。
new Promise((resolve) => {
console.log(1);
setTimeout(() => {
console.log(2);
resolve();
}, 1000)
}).then(() => {
console.log(3)
setTimeout(() => {
console.log(4)
}, 1000)
}).then(()=>{
console.log(5);
setTimeout(() => {
console.log(6)
}, 1000)
})
复制代码
这个代码是不会获得理想输出的。输出结果为: 1 -> 2 3 5 -> 4 6
new Promise((resolve) => {
console.log(1);
setTimeout(() => {
console.log(2);
resolve();
}, 1000)
}).then(() => {
console.log(3)
return new Promise((resolve)=>{
setTimeout(() => {
console.log(4)
resolve()
}, 1000)
})
}).then(()=>{
console.log(5);
new Promise(()=>{
setTimeout(() => {
console.log(6)
}, 1000)
})
})
复制代码