我们都只到在js代码中很多动作都是异步执行的。但是如果回调嵌套层数太多就会造成代码层次加深,维护难度增加,这也就是我们说的回调地狱。为了解决这种问题我们有和多种解决办法,目前最常用的就是Promise
在说异步之前我们首先要了解一下js执行栈(call stack)!
一个数据结构,用于存放各种函数的执行环境,每一个函数执行之前他的相关信息会加入到执行栈。函数调用之前创建执行环境(执行上期下文),然后加入到执行栈。函数调用之后,销毁执行环节(出栈)
整个js引擎只有一个执行栈,js、引擎永远执行的是执行栈最顶部的函数。
定义函数的时候会将函数定义到一个特定的环境中,我们把这个环境叫做全局执行环节,所以js代码执行的时候回首先初始化一个全局上下文,全局上下文就会加入到执行栈中。调用任何函数的时候都会为这个函数创建一个上下文。执行完成之后销毁上下文。
js代码的执行线程
- js代码的执行线程只有一个。我们经常说的js是单线程。
- 单线程的优点是极大的避免了执行时候的冲突。
- 缺点当执行需求变大的时候,执行效率会变低。
- 浏览器宿主环境的中包含的线程
- js引擎线程:负责执行栈最顶端的代码,专门执行js代码。
- GUI线程:负责渲染页面,负责html和css的渲染,
补充:js引擎线程和GUI线程是需要相互等待的。当js线程执行的时候GUI线程等待,当GUI线程执行的时候js线程需要等待。 - 事件监听线程:负责监听各种事件
- 当事件被点击之后,web api浏览器宿主中的事件监听线程将点击事件放置到事件队列中,如果这个时候执行栈中没有动作在执行。则直接开始执行点击事件,否则等待前面的事件执行完毕之后执行这个点击事件。
- 这里说下问什么web api直接到执行栈中:因为当执行栈中的js正在执行的时候并不希望执行的进程被打断。所以就需要一个排队的过程,就像我们排队吃饭!你插队别人也不同意啊!
- 记时线程:负责执行setTimeout(()=>{},1000) 这类函数。
- 网络线程:负责网络通信,ajax请求。
这五个线程监听执行动作。线程发现执行函数有处理程序,他会将处理程序加入到事件队列的内存中。当js引擎发现执行栈中没有任何内容之后,会将事件队列中的第一个函数加入到事件队列中。
js引擎对事件队列的取出方式,以及与宿主环境的配合称之为事件循环。
异步函数
某些函数不会立即执行。需要等到某个时机到达之后才会执行。这样的函数称之为异步函数,比如事件处理函数。异步函数的执行机制会被宿主环境控制。
异步函数一定在事件队列中,并且要等到执行栈清空之后才会执行事件队列中的函数,同步函数直接在执行函数中执行,不会到事件队列中。
事件队列在不同的宿主环境中有所差异,大部分宿主环境会将事件队列进行细分,在浏览器中事件队列分为两种:
- 宏任务队列:macroTask,计时器结束的回调,http回调等大多数异步函数进入宏任务队列。
- 微任务队列(优先执行队列):mutationObserver,promise产生的回调进入微任务队列。
- mutationObserver:这个属性不经常用,用来监听某个DOM对象的变化(属性变化,子元素发生变化)
const observer = new mutationObserver(()=>{
//当监听的元素发生变化的时候,触发函数
})
observer.observer(ul,{
attributes:true //监听属性的变化
childList:true //监听子元素变化
subtree:true //监听后代元素发生变化。
})
//取消监听
observer.disconnect()
当执行栈清空时,js引擎首先会将微任务中的所有任务依次执行结束,如果没有微任务则执行宏任务
Promise
Promise是将“生产者代码”和“消费者代码”连接的一个特殊的JavaScript对象。
生产者代码
Promise构造语法:
let promise = new Promise(function(resolve,reject){
//executor(生产者代码) --当promise被创建的时候,会自动调用,包含生产者的代码,
})
promise对象内部属性
state:最初是"padding"
result:一个任意值,初始值是undefined
当executor完成任务时应该调用下列某一个方法
executor:接受两个参数resolve,reject,这两个函数来自于JavaScript引擎,我们不需要创建,并且executor会在他们准备好的时候调用
resolve(value) -调用这个方法的时候代表任务已经完成
将state设置为"fulfilled"
sets result to value
reject(error) -当调用这个方法的时候说明有error
将state设置为"reject"
将result设置为error
Promise结果应该是resolve或reject的状态都被称为settled,而不是pading状态的Promise
消费者:then和catch
.then和.catch执行的时候他会首先进入内部队列,JavaScript引擎从队列中提取处理器,并在代码完成时执行,
Promise链
- Promise的设计理念
- 将result传入.then的处理程序链中: promise.then运行之后返回了一个promise对象(这是能链式调用的关键),当控制函数返回一个值,这个值会作为当前promise的result,然后再调用下一个.then…
- 返回promise
- 正常情况下.then处理程序返回的值会立即传入下一个处理程序,but有一个地方需要注意:如果返回的是一个promise的时候,南无指导这个promise结束之前,下一步执行都一直被暂停,结束之后才会传递下去。promise 是允许建立异步动作链
- Promise错误处理
- 当Promise被reject,控制权就移交给了链中最近的rejection处理程序。
- 捕获错误最简单的方法就是在promise链的末尾加上.catch()。
- 隐式try…catch()
- Promise执行executor和promise处理程序的时候,有一个隐式的try…catch(),如果发生异常,它就会被捕获并且视为rejection。
- 处理程序:如果我们在then找那个throw error的时候,catch也会捕获。
- 代码中的书写错误和编程错误同样会导致catch捕获异常
- 重新抛出异常
- 在常规的try…catch(),可以分析错误,当处理不了的时候还能再次抛出错误。当我们使用.then().catch(),如果在catch中throw error此时控制移交到下一个最近的错误处理程序。如果我们处理错误,并正常完成。那么他将回到最近的成功的.then()处理程序。下例:
new Promise((resolve,reject)=>{
throw new Error('error');
}).catch(()=>{
alert('is error')
}).then(()=>{
alert('next...')
})
5. 未处理的rejections
1. 如果错误出现没有程序去处理他,错误就会卡住,当一个错误没有被处理的话,程序就可能会挂掉。
2. JavaScript引擎跟踪此类错误,会造成一个全局错误,然后在控制台看到。
3. 如果catch没有捕获这类错误。unhandledrejection处理程序就会触发并且获取这些错误信息的event对象。
4. 补充:unhandledrejection 继承自PromiseRejectionEvent,而 PromiseRejectionEvent 又继承自 Event。因此unhandledrejection 含有 PromiseRejectionEvent 和 Event 的属性和方法。
Promise API
- Promise.resolve
let promise = Promise.resolve(value);
等价:let promise = new Promise(resolve =>resolve(value))
当只有一个value的时候,就会使用这种方法。
- Promise.reject
let promise = Promise.reject(error) //创建一个带有error的rejected
let promise = new promise((resolve,reject)=>reject(error))
- ***Promise.al
如果我们想要并行执行多个promise,并等待所有的promise准备就绪。这个时候可以使用Promise.all
let promise = Promise.all([...promises..]) //需要一个promise的数组作为其参数并返回一个新的promise。
当所有给定的promise都被处理,并以数组的形式呈现其结果,新的promise也就被resolve。
当promise数组中,即使数组的第一个promise需要很长时间resolve,但它仍然是结果数组中的第一个。
**在promise数组中,如果有任意的promise是reject,promise.all返回promise就会立即reject这个错误。
**如果出现错误,其他的promise就会被忽略。
没有什么方法能够取消Promise.all。因为promise中没有取消的概念。
通常情况下,Promise.all()接受可迭代的promise集合,但是这些对象中的任意一个不是promise,他将会被直接包装进Promise.resolve。
- Promise.allSettled
Promise.allSettled等待所有的promise都被处理,即使其中有一个reject,任然会等待其他的promise
这种情况我们能获取每个promise的处理结果。
Polyfill
Promise.allSettled不支持的时候,使用polyfill。
- Promise.race
- Promise.race接收一个可迭代的promise集合,但是他只等待第一个完成或者error,而不会等待所有的都完成。
Promisification
作用:做一个简单的转换,一个接受回调的函数转换为一个返回promise的函数。
微任务队列和宏任务队列
微任务队列(JavaScript标准规定一个内部队列PromiseJobs–微任务队列)
- 只有在引擎中没有其他任务运行的时候才会启动微任务队列:当一个promise准备就绪时,他的.then/catch/finally处理程序就会被放入队列中。当JavaScript引擎执行栈中执行完成当前的代码。才会从队列中获取他并且执行他。
- 如果与一个promise链带有多个.then/.catch/.finally,那么它们中每一个都是异步执行的,也就是说,它们首先会排入一个微任务队列中,只有当前代码执行完毕并且之前排好队的处理程序都完成时才会被执行。
- 如果想要顺序输出,我们可以将输出程序按照顺序放入.then处理程序中。
- 未处理的promise:这里未处理的rejection指的是microitask队列结束时未处理的promise错误。
- 宏任务队列和微任务队列的区别
-
当执行栈清空时,js引擎首先会将微任务中的所有任务依次执行结束,如果没有微任务则执行宏任务
-
如果在链式调用的末尾加上.catch,错误就会被处理
如果链式调用的末尾没有加上.catch,则会触发下面事件
window.addEventLister(‘unhandledrejection’,event=>alert(event.reason)) //捕获错误event。 -
还有一种延迟处理的情况,
unhandledrejection在microtask队列完后才会被生成,引擎会检查promise,如果其中任何一个出现rejection状态,unhandledrejection
就会被触发。同样延迟执行的.catch,只不过实在unhandledrejection处理程序之后执行。
-
Async/await
-
async
- 作用:确保返回值是一个promise,同时也会包装非promise的值(函数总是返回一个promise,即使这个函数实际上会发挥一个非promise的值,在函数定义前加上async关键字会指示JavaScript引擎自动将返回值包装在一个已决议的promise内)。
- 允许内部使用await。
-
await
- 作用:让JavaScript引擎等到promise执行完后后并且返回结果之后再执行后面的代码。
- 这样做的优点是:不会浪费cpu资源,js引擎可以同时处理过个任务。
-
注意哦
- 在async可以单独使用,但是await不能单独使用,await使用的场景必须是在这个函数定义前有async。
- await不能在顶层代码运行。使用的时候将其包裹在一个函数中。
- await允许接收thenable对象(具有then方法的对象),这种第三方对象是兼容promise,如果这些对象支持.then就可以使用await。具体的执行过程:接收非promise对象(提供了.then方法)之后,调用.then方法,同时将resolve和reject作为参数传入,然后await等到着两个方法中的某一个被调用,再处理得到结果。
-
Error handling
- 如果一个primise正常resolve,await返回的就是其结果,如果promise–>rejected,就会抛出错误,类似于throw。
- 通常情况下,primise被拒绝前通常会等待一段时间。所以await会等待,然后抛出一个错误。捕获这个错误可以使用try…catch 或者在返回promise对象链式调用catch。