原文: https://segmentfault.com/a/1190000018527530
异步相关背景介绍
浏览器内核
首先聊一下浏览器, 一直对浏览器的结构比较好奇,查了很多资料总结就有下面一点相关总结; 其中也借鉴其他人的一些东西.
浏览器是多进程的,有主进程, GPU加速进程,渲染进程(内核)等, 一般新开一个tab页面就是新启动一个进程, CPU就会给他分配资源; 但其中有一个核心进程==>渲染进程(浏览器内核), 是我们前端人员需要特别关注的,它包括了多个线程…GUI渲染线程
负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),
GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
JS引擎线程
也称为JS内核,负责处理Javascript脚本程序.(例如V8引擎)JS引擎线程负责解析Javascript脚本,运行代码。
JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
不过H5中新增了Web Worker, 实现了多线程: js会新开线程来处理一些其他任务,但不会影响DOM结构...
创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)
JS引擎线程与worker线程间通过特定的方式通信
(postMessage API,需要通过序列化对象来与线程交互特定的数据)
事件触发线程
这个线程是归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求,
页面滚动等),会将对应任务添加到事件线程中
当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
注意,
由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
定时触发器线程
即setInterval与setTimeout所在线程
浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确);
因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
当然setTimeout中的延时参数也不一定准确
异步HTTP请求线程
在XMLHttpRequest在连接后是通过浏览器新开一个网络线程去请求
将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,
将这个回调再放入事件队列中。再由JavaScript引擎执行。
那么关于浏览器方面的背景知识就介绍到这里啦, 想要深入去了解,可以去查相关资料…
事件队列和循环
大家都知道JavaScript引擎是单线程的工作模式, 即同一时间只能跑一段代码,还要按顺序自上而下执行; 但是碰到I/O操作, 定时器, 事件监听函数等这些耗时操作; JS引擎不会等待它们有结果了才去之下它们后面的代码, 而是会将它们扔进任务(事件)队列中, 等待同步代码执行栈空了之后, 再去任务队列将任务一个个取出来执行任务所对应的回调函数, 执行完毕后会一直等待新的任务到来; 如此循环…
几个类型的回调
同步回调函数
- 我们可以利用了函数的执行栈顺序,函数作为参数放到另一个函数中调用, 谁在后面调用谁就先被放在函数执行栈栈顶
- 我们可以利用了函数的执行栈顺序,函数作为参数放到另一个函数中调用, 谁在后面调用谁就先被放在函数执行栈栈顶
- 异步回调函数
- 事先在外面定义好一个callback; 将回调函数作为某个函数的参数, 利用函数的作用域将函数中异步任务得到的结果存在回调函数的形参中, 然后在函数体末尾调用…
- 事先在外面定义好一个callback; 将回调函数作为某个函数的参数, 利用函数的作用域将函数中异步任务得到的结果存在回调函数的形参中, 然后在函数体末尾调用…
定时器
setTimeout的作用是在间隔一定的时间后,将回调函数插入任务队列中,等栈中的同步任务都执行完毕后,再执行, 当然这个时间不一定准确…Promise是用来干什么的?
看阮老师的ES6出门上说Promise是JS异步编程的一种解决方案. 举个例子, Ajax的回调问题, 如果下一个ajax请求要用到上一个Ajax请求中的结果, 那么往往就会导致多个回调嵌套的问题, 那么Promise就可以解决这种代码上的嵌套问题, 是我们的代码变得更优美, 更利于维护; 我暂时先对Promise的理解就是: 处理异步任务, 保存异步结果状态, 异步代码同步化…
Promise是什么?
Promise 它就是一个对象,相当于一个容器, 里面存的就是一个异步操作的结果; 我们可以是从中获取异步操作结果的相关信息。
Promise对象代表一个未完成、但预计将来会完成的操作。
它有以下三种状态:
pending:初始值,不是fulfilled,也不是rejected
fulfilled:代表操作成功
rejected:代表操作失败
Promise有两种状态改变的方式,既可以从pending转变为fulfilled,也可以从pending转变为rejected。一旦状态改变,就「凝固」了,会一直保持这个状态,不会再发生变化。当状态发生变化,promise.then绑定的函数就会被调用。
注意:Promise一旦新建就会「立即执行」(它属于microtask),无法取消。这也是它的缺点之一。Promise的创建和使用?
1.创建promise对象
//1.使用new Promise(func)的形式
//2.快捷语法: Promise.resolve(func) || Promise.reject(func)
// 参数1: 一般是一个处理异步任务的函数
// 返回值: 一个promise实例对象
Promise.resolve('foo')
// 等价于, 不过参数类型不一样执行的操作也会有所不同
new Promise(resolve => resolve('foo'))
2.在函数func中 放异步处理代码
// 传入两个参数: 回调函数resolve, reject分别去保存异步处理的结果
// 成功: 使用resolve(结果)
// 失败: 使用reject(原因)
3.调用实例的then(func1) 或者 catch(err)
首先then方法是异步执行, 对上面的异步结果进行处理的函数
参数: 传回调函数, 一个两个都行, 前者是成功状态的回调,后者是失败的回调
Promise常用的场景?
promise一般的使用套路就是:
1.先定义一个函数, 函数内部使用new Promise()的方式来返回一个promise对象, resolve用来保存 异步处理成功的结果
reject用来保存 异常处理的结果
2.然后函数调用,传参
3.链式语法点出then方法, then中的回调用来处理异步结果
4.有错误就点出catch方法, 也可以用then(null, function() {})代替catch
5.then的回调中也可return一个值, 会被包装成一个新的promise, 因此可以继续调用then方法
应用场景: 在ajax中使用, 解决异步嵌套问题
function ajax(url) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
// 请求类型, 地址, 异步
xhr.open('get', url, true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
try {
// 处理响应内容, 将内容丢到成功状态的回调
resolve(JSON.parse(xhr.responseText))
} catch (e) {
// 捕获错误, 丢到失败状态的回调
reject(e)
}
}
}
});
}
// 调用 封装的ajax函数
let url = 'http://127.0.0.1:3000/xxoo'; // 自己本地开的一个服务
ajax(url)
.then(res => console.log(res)) // 输出 {code: 0, msg: 'hello cors'}
.catch(err => console.log(err))
其他场景 ```javascript // 实现串行任务管道; 即当前任务的输出可以作为下一个任务的输入,形成一条数据管道; // 比如: 比如从url1获取参数userId,拿到后再从url2获取第三方openId,最后再从url3货取orderList, 然后把结果展示给用户,类似的逻辑都是任务管道:
new Promise(function(resolve, reject) { resolve(1); }) .then(function(res) { return new Promise(function(resolve, reject) { resolve(res + 1); }); }) .then(function(res) { return new Promise(function(resolve, reject) { resolve(res + 1); }); }) .then(function(res) { console.log(res); // 3 });
<a name="item-1-6"></a>
## promise的好处
- 在异步执行的流程中,使用Promise可以把 执行代码 和 处理结果 的代码清晰地分离,这样我们便可以 把执行代码 和 结果处理 分成不同的模块来写,易于维护
- 减少异步回调的嵌套, 比如ajax回调, 我们可以依次调用then方法即可, 还可以控制回调的顺序
- 多个异步任务是为了容错去访问用同一资源时, 可以使用Promise.race([promise实例...])
- 多个异步任务并行执行时,比如ajax访问两个接口, 可以用Promise.all([promise实例...])
<a name="item-1-7"></a>
## Promise使用的注意事项
1. Promise构造函数内的同步代码立即执行
1. 回调函数参数resolve异步执行, 将结果作为参数传给then方法中的回调函数
1. resolve只有第一次执行有效,状态不能二次改变
1. then和catch如果有return, 返回的是一个全新的promise对象, 可以链式调用
1. Promise构造函数只会执行一次, promise实例会保存resolve的状态,<br />以后这个实例每次调用then都是返回一个这个状态, 若链式调用then,下一个则会打印undefined, res没有值...
1. then中返回任意一个非 promise 的值都会被包裹成 promise 对象
1. .then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环
1. .then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透。
1. .then 可以接收两个参数,第一个是处理成功的函数,第二个是处理错误的函数。
- .catch 是 .then 第二个参数的简便写法,但是它们用法上有一点需要注意:
- .then 的第二个处理错误的函数捕获不了第一个处理成功的函数抛出的错误,
- 而后续的 .catch 可以捕获之前的错误。
<a name="axECu"></a>
## promise方法
- promise是一种异步编程的解决方案,其实是一个构造函数,自己身上有all、reject、resolve这几个方法,原型上有then、catch等方法。
- 注意!我只是new了一个对象,并没有调用它,我们传进去的函数就已经执行了
```javascript
let p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
console.log('执行完成Promise');
resolve('要返回的数据可以任何数据例如接口返回数据');
}, 2000);
});
这是需要注意的一个细节。所以我们用Promise的时候一般是包在一个函数中,在需要的时候去运行这个函数,如:
<div onClick={promiseClick}>开始异步请求</div>
const promiseClick =()=>{
console.log('点击方法被调用')
let p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
console.log('执行完成Promise');
resolve('要返回的数据可以任何数据例如接口返回数据');
}, 2000);
});
return p
}
- 当放在函数里面的时候只有调用的时候才会被执行
- 为什么要放在函数里面
- 我们包装好的函数最后,会return出Promise对象,也就是说,执行这个函数我们得到了一个Promise对象。
then的用法
- 接下来就可以用Promise对象上有then、catch方法了,这就是Promise的强大之处了,看下面的代码:先是方法被调用执行了promise,最后执行了promise的then方法,then方法是一个函数接受一个参数是接受resolve返回的数据
- then里面的函数就跟我们平时的回调函数一个意思,,能够在promiseClick这个异步任务执行完成之后被执行。
- 这就是Promise的作用了,简单来讲,就是能把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。
promiseClick().then(function(data){
console.log(data);
//后面可以用传过来的数据做些其他操作
//......
})
多层回调
- 你可能会觉得在这个和写一个回调函数没有什么区别;那么,如果有多层回调该怎么办?
- 如果callback也是一个异步操作,而且执行完后也需要有相应的回调函数,该怎么办呢?
- 总不能再定义一个callback2,然后给callback传进去吧。
- 而Promise的优势在于,可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作。
所以:精髓在于:Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。所以使用Promise的正确场景是这样的:
promiseClick()
.then(function(data){
console.log(data);
return runAsync2();
})
.then(function(data){
console.log(data);
return runAsync3();
})
.then(function(data){
console.log(data);
});
catch的用法
- resolve是对promise成功时候的回调,它把promise的状态修改为fullfiled,那么,reject就是失败的时候的回调,他把promise的状态修改为rejected,这样我们在then中就能捕捉到,然后执行“失败”情况的回调
- 与Promise对象方法then方法并行的一个方法就是catch,与try catch类似,catch就是用来捕获异常的,也就是和then方法中接受的第二参数rejected的回调是一样的。
all的用法
- 与then同级的另一个方法,all方法,该方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后并且执行结果都是成功的时候才执行回调。
- Promise.all来执行,all接收一个数组参数,这组参数为需要执行异步操作的所有方法,里面的值最终都算返回Promise对象。这样,三个异步操作的并行执行的,等到它们都执行完后才会进到then里面。
- 那么,三个异步操作返回的数据哪里去了呢?都在then里面,all会把所有异步操作的结果放进一个数组中传给then,然后再执行then方法的成功回调将结果接收。
```javascript
function promiseClick1(){
let p = new Promise(function(resolve, reject){
setTimeout(function(){
}, 2000); }) return p } function promiseClick2(){ let p = new Promise(function(resolve, reject){ setTimeout(function(){var num = Math.ceil(Math.random()*20); //生成1-10的随机数
console.log('随机数生成的值:',num)
if(num<=10){
resolve(num);
}
else{
reject('数字太于10了即将执行失败回调');
}
}, 2000); }) return p } function promiseClick3(){ let p = new Promise(function(resolve, reject){ setTimeout(function(){var num = Math.ceil(Math.random()*20); //生成1-10的随机数
console.log('随机数生成的值:',num)
if(num<=10){
resolve(num);
}
else{
reject('数字太于10了即将执行失败回调');
}
}, 2000); }) return p }var num = Math.ceil(Math.random()*20); //生成1-10的随机数
console.log('随机数生成的值:',num)
if(num<=10){
resolve(num);
}
else{
reject('数字太于10了即将执行失败回调');
}
Promise .all([promiseClick3(), promiseClick2(), promiseClick1()]) .then(function(results){ console.log(results); });
<br />
<a name="BjkbG"></a>
### race的用法
- all是等所有的异步操作都执行成功再执行then方法,Promse.all在处理多个异步处理时非常有用,比如说一个页面上需要等两个或多个ajax的数据回来以后才正常显示,在此之前只显示loading图标。
```javascript
let p1 = new Promise((resolve, reject) => {
resolve('成功了')
})
let p2 = new Promise((resolve, reject) => {
resolve('success')
})
let p3 = Promise.reject('失败')
Promise.all([p1, p2]).then((result) => {
console.log(result) //['成功了', 'success']
}).catch((error) => {
console.log(error)
})
Promise.all([p1,p3,p2]).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error) // 失败了,打出 '失败'
})
- 那么race方法就是相反的,谁先执行完成就先执行回调。
- Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。 ```javascript let p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve(‘success’) },1000) })
let p2 = new Promise((resolve, reject) => { setTimeout(() => { reject(‘failed’) }, 500) })
Promise.race([p1, p2]).then((result) => { console.log(result) }).catch((error) => { console.log(error) // 打开的是 ‘failed’, 500 < 1000 }) ```