事件循环
JavaScript
是一门单线程的编程语言,所以没有并发并行等特性。
为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞,(事件循环)
Event Loop
的方案应用而生。
JavaScript
处理任务是在等待任务、执行任务 、休眠等待新任务中不断循环中,也称这种机制为事件循环。
主线程中的任务执行完后,才执行任务队列中的任务
有新任务到来时会将其放入队列,采取先进先执行的策略执行队列中的任务
比如多个
setTimeout
同时到时间了,就要依次执行
任务包括
script
(整体代码)、
setTimeout
、
setInterval
、DOM渲染、DOM事件、
Promise
、
XMLHTTPREQUEST
等
任务详解
任务分类
任务大致分为以下三种:
主线程任务
应放入宏队列中的任务
应放入微队列中的任务
放入宏队列中的任务 | ||
---|---|---|
# | 浏览器 | Node |
setTimeout | √ | √ |
setInterval | √ | √ |
setImmediate | x | √ |
requestAnimationFrame | √ | x |
放入微队列中的任务 | ||
---|---|---|
# | 浏览器 | Node |
process.nextTick | x | √ |
MutationObserver | √ | x |
Promise.then catch finally | √ | √ |
执行顺序
根据任务的不同,执行顺序也有所不同:
1.主线程任务
2.微队列任务
3.宏队列任务
<script>\"use strict\";new Promise(resolve => {console.log(\"主线程任务执行 1...\")resolve();}).then(_ => {console.log(\"微队列任务执行 7...\");});console.log(\"主线程任务执行 2...\");setTimeout(() => {console.log(\"宏队列任务执行 9...\");}, 1);console.log(\"主线程任务执行 3...\");new Promise(resolve => {console.log(\"主线程任务执行 4...\")resolve();}).then(_ => {console.log(\"微队列任务执行 8...\");});console.log(\"主线程任务执行 5...\");console.log(\"主线程任务执行 6...\");/*主线程任务执行 1...主线程任务执行 2...主线程任务执行 3...主线程任务执行 4...主线程任务执行 5...主线程任务执行 6...微队列任务执行 7...微队列任务执行 8...宏队列任务执行 9...*/</script>
作用体现
使用
Promise
能让代码变得更易阅读,方便后期维护。
特别是在回调函数嵌套上,更应该使用
Promise
来书写代码。
嵌套问题
以下示例将展示通过
Js
来使得
<div>
标签形态在不同时刻发生变化。
代码逻辑虽然清晰但是定时器回调函数嵌套太过复杂,阅读体验较差。
<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Document</title><style>div {width: 100px;height: 100px;background-color: red;transition: 1s;}button {margin-top: 20px;}</style></head><body><div></div><button>点我</button></body><script>\"use strict\";document.querySelector(\"button\").addEventListener(\"click\", () => {let div = document.querySelector(\"div\");div.style.backgroundColor = \"blue\";setTimeout(() => {div.style.width = \"50px\";setTimeout(() => {div.style.transform = \"translate(100px)\";setTimeout(() => {div.style.width = \"100px\";div.style.backgroundColor = \"red\";setTimeout(() => {div.style.backgroundColor = \"yellow\";},1000);}, 1000);}, 1000);}, 1000);});</script></html>
尝试解决
使用
Promise
来解决该问题。
这里看不懂没关系,下面会慢慢进行剖析,只是感受一下是不是嵌套没那么严重了看起来好看多了。
<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Document</title><style>div {width: 100px;height: 100px;background-color: red;transition: 1s;}button {margin-top: 20px;}</style></head><body><div></div><button>点我</button></body><script>\"use strict\";function chain(callback, time=1000) {return new Promise(function (resolve, reject) {setTimeout(() => {let res = callback();resolve(res);}, time);});}document.querySelector(\"button\").addEventListener(\"click\", () => {new Promise(function (resolve, reject) {let div = document.querySelector(\"div\");div.style.backgroundColor = \"blue\";resolve(div);}).then(div => {return chain(() => {div.style.width = \"50px\";return div;});}).then(div => {return chain(() => {div.style.transform = \"translate(100px)\";return div;});}).then(div => {return chain(() => {div.style.width = \"100px\";div.style.backgroundColor = \"red\";return div;})}).then(div => {return chain(() => {div.style.backgroundColor = \"yellow\";return div;})})});</script>
Promise
JavaScript
中存在很多异步操作,
Promise
将异步操作队列化,按照期望的顺序执行,返回符合预期的结果。
可以通过链式调用多个
Promise
达到我们的目的,如同上面示例一样会让代码可读性大幅度提升。
声明状态
每一个
Promise
对象都接收一个函数,该函数需要提供两个参数,分别是
resolve
以及
reject
,代表当前函数中的任务成功与失败,这是属于线程任务的,所以会优先执行。
此外,每一个
Promise
对象都具有三种状态,分别是
pending
,
fulfilled
,
rejected
。
当一个
Promise
对象状态改变过后,将不能再次改变。
pending
指初始等待状态,初始化
promise
时的状态
resolve
指已经解决,将
promise
状态设置为
fulfilled
reject
指拒绝处理或未解决,将
promise
状态设置为
rejected
当没有使用
resolve
或
reject
更改状态时,状态为
pending
<script>\"use strict\";let p1 = new Promise(function (resolve, reject) { });console.log(p1); // Promise{<pending>}</script>
使用
resolve
修改状态后,状态为
fulfilled
<script>\"use strict\";let p1 = new Promise(function (resolve, reject) {resolve(\"已解决\");});console.log(p1); // Promise{<fulfilled>: \"已解决\"}</script>
使用
reject
修改状态后,状态为
rejected
<script>\"use strict\";let p1 = new Promise(function (resolve, reject) {reject(\"未解决\");});console.log(p1); // Promise{<rejected>: \"未解决\"}</script>
then
在一个
Promise
对象状态为
resolve
或
reject
时,可以紧跟
then
方法,该方法可接收两个个函数对象,用于处理
Promise
对象
reject
或
resolve
传递过来的值。
<script>\"use strict\";new Promise(function (resolve, reject) {reject(\"未解决\");}).then(success => {console.log(\"resolve:\", success);},error => {console.log(\"reject:\", error); // resolve: 未解决});</script>
catch
每个
then
都可以指定第二个函数用于处理上一个
Promise
失败的情况,如果每个
then
都进行这样设置会显得很麻烦,所以我们只需要使用
catch
即可。
catch
可以捕获之前所有
promise
的错误,所以建议将
catch
放在最后。
建议使用
catch
处理错误
将
catch
放在最后面用于统一处理前面发生的错误
错误是冒泡操作的,下面没有任何一个
then
定义第二个函数,将一直冒泡到
catch
处理错误
<script>\"use strict\";new Promise((resolve, reject) => {reject(\"失败\");}).then(success => {console.log(\"成功\");}).then(success => {console.log(\"成功\");}).catch(error => {console.log(error); // 失败})</script>
catch
也可捕捉到
throw
自动触发的异常。
<script>\"use strict\";new Promise((resolve, reject) => {resolve(\"成功\");}).then(success => {throw new Error(\"失败\");}).catch(error=>{console.log(error); // Error: 失败})</script>
finally
无论状态是
fulfilled
或
rejected
都会执行此动作,
finally
与状态无关。
<script>\"use strict\";new Promise((resolve, reject) => {reject(\"失败\");}).then(success => {console.log(\"成功\");}).catch(error => {console.log(error); // 失败}).finally(() => {console.log(\"都会执行\"); // 都会执行})</script>
链式调用
使用
Promise
进行链式调用,可以规避掉嵌套问题。
基本概念
其实每一个
then
都是一个新的
Promise
,默认返回为
fulfilled
状态。
<script>\"use strict\";let p1 = new Promise(function (resolve, reject) {resolve(\"已解决\");})let p2 = p1.then(success => {console.log(success);}, error => {console.log(error);});setTimeout(() => {console.log(p2); // 宏任务队列中的任务最后执行 Promise{<fulfilled>: undefined}},3000)</script>
此时就会产生一种链式关系,每一个
then
都是一个新的
Promise
对象,而每个
then
的作用又都是处理上个
Promise
对象的状态。
要想使用链式调用,一定要搞明白每一个
then
的返回值。
返回了一个值,那么
then
返回的
Promise
将会成为接受状态,并且将返回的值作为接受状态的回调函数的参数值。
没有返回任何值,那么
then
返回的
Promise
将会成为接受状态,并且该接受状态的回调函数的参数值为
undefined
。
抛出一个错误,那么
then
返回的
Promise
将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值。
返回一个已经是接受状态的
Promise
,那么
then
返回的
Promise
也会成为接受状态,并且将那个
Promise
的接受状态的回调函数的参数值作为该被返回的
Promise
的接受状态回调函数的参数值。
返回一个已经是拒绝状态的
Promise
,那么
then
返回的
Promise
也会成为拒绝状态,并且将那个
Promise
的拒绝状态的回调函数的参数值作为该被返回的
Promise
的拒绝状态回调函数的参数值。
返回一个未定状态(
pending
)的
Promise
,那么
then
返回 Promise 的状态也是未定的,并且它的终态与那个 Promise 的终态相同;同时,它变为终态时调用的回调函数参数与那个 Promise 变为终态时的回调函数的参数是相同的。
无返回
上一个
then
无返回值时该
then
创建的
Promise
对象为
fulfilled
状态。
下一个
then
会立即执行,接收值为
undefined
。
<script>\"use strict\";new Promise((resolve, reject) => {resolve(\"成功\");}).then(success => {console.log(\"无返回1\"); // 上一个Promise状态是fulfilled 立刻执行}).then(success => {console.log(\"无返回2\"); // 上一个Promise状态是fulfilled 立刻执行})</script>
返回值
上一个
then
有返回值时该
then
创建的
Promise
对象为
fulfilled
状态。
下一个
then
会立即执行,接收值为上一个
then
的返回值。
<script>\"use strict\";new Promise((resolve, reject) => {resolve(\"成功\");}).then(success => {return \"v1\" // 上一个Promise状态是fulfilled 立刻执行}).then(success => {console.log(success); // v1 上一个Promise状态是fulfilled 立刻执行})</script>
返回Promise
上一个
then
有返回值且该返回值是一个
Promise
对象的话下一个
then
会等待该
Promise
对象状态改变后再进行执行,接收值根据被返回的
Promise
对象的任务处理状态来决定。
<script>\"use strict\";new Promise((resolve, reject) => {resolve(\"成功\");}).then(success => {return new Promise((resolve, reject) => {// resolve(\"成功\");})}).then(success => {console.log(success); // 上一个Promise状态是pending 不执行,等待状态变化})</script>
嵌套解决
我们可以利用在一个
then
中返回
Promise
下面的
then
会等待状态的特性,对定时器回调函数嵌套进行优化。
<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Document</title><style>div {width: 100px;height: 100px;background-color: red;transition: 1s;}button {margin-top: 20px;}</style></head><body><div></div><button>点我</button></body><script>\"use strict\";document.querySelector(\"button\").addEventListener(\"click\", () => {new Promise(function (resolve, reject) {let div = document.querySelector(\"div\");div.style.backgroundColor = \"blue\";resolve(div);}).then(div => {return new Promise(function (resolve, reject) {setTimeout(() => {div.style.width = \"50px\";resolve(div);}, 1000);})}).then(div => {return new Promise(function (resolve, reject) {setTimeout(() => {div.style.transform = \"translate(100px)\";resolve(div);}, 1000);})}).then(div => {return new Promise(function (resolve, reject) {setTimeout(() => {div.style.width = \"100px\";div.style.backgroundColor = \"red\";resolve(div);}, 1000);})}).then(div => {return new Promise(function (resolve, reject) {setTimeout(() => {div.style.backgroundColor = \"yellow\";resolve(div);}, 1000);})})});</script>
代码优化
继续对上面的代码做优化。
<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Document</title><style>div {width: 100px;height: 100px;background-color: red;transition: 1s;}button {margin-top: 20px;}</style></head><body><div></div><button>点我</button></body><script>\"use strict\";function chain(callback, time=1000) {return new Promise(function (resolve, reject) {setTimeout(() => {let res = callback();resolve(res);}, time);});}document.querySelector(\"button\").addEventListener(\"click\", () => {new Promise(function (resolve, reject) {let div = document.querySelector(\"div\");div.style.backgroundColor = \"blue\";resolve(div);}).then(div => {return chain(() => {div.style.width = \"50px\";return div;});}).then(div => {return chain(() => {div.style.transform = \"translate(100px)\";return div;});}).then(div => {return chain(() => {div.style.width = \"100px\";div.style.backgroundColor = \"red\";return div;})}).then(div => {return chain(() => {div.style.width = \"100px\";div.style.backgroundColor = \"yellow\";return div;})})});</script>
扩展接口
resolve
使用
Promise.resolve()
方法可以快速的返回一个状态是
fulfilled
的
Promise
对象。
<script>\"use strict\";Promise.resolve(\"成功\").then(success=>console.log(success)); // 成功</script>
reject
使用
Promise.reject()
方法可以快速的返回一个状态是
rejected
的
Promise
对象。
<script>\"use strict\";Promise.reject(\"失败\").then(null,error=>console.log(error)); // 失败// 使用null来对成功的处理进行占位</script>
all
使用
Promise.all()
方法可以同时执行多个并行异步操作,比如页面加载时同进获取课程列表与推荐课程。
任何一个
Promise
执行失败就会调用
catch
方法
适用于一次发送多个异步操作
参数必须是可迭代类型,如
Array/Set
成功后返回
Promise
结果的有序数组
以下示例将展示同时提交两个异步操作,只有当全部成功时才会执行
Promise.all()
其下的
then
<script>\"use strict\";const p1 = new Promise((resolve, reject) => {setTimeout(() => {resolve(\"成功\");}, 3000);});const p2 = new Promise((resolve, reject) => {setTimeout(() => {resolve(\"成功\");}, 3000);});Promise.all([p1, p2]).then(success => {console.log(success); // (2)[\"成功\", \"成功\"]}).catch(error => {console.log(error); // 任何一个失败都会执行这里});</script>
allSettled
allSettled
用于处理多个
Promise
,只关注执行完成,不关注是否全部执行成功,
allSettled
状态只会是
fulfilled
。
<script>\"use strict\";const p1 = new Promise((resolve, reject) => {setTimeout(() => {resolve(\"成功\");}, 1000);});const p2 = new Promise((resolve, reject) => {setTimeout(() => {resolve(\"成功\");}, 3000);});Promise.allSettled([p1, p2]).then(success => {console.log(success);})/*[{status: \"fulfilled\", value: \"成功\"}, {status: \"fulfilled\", value: \"成功\"}]*/</script>
race
使用
Promise.race()
处理容错异步,和
race
单词一样哪个
Promise
快用哪个,哪个先返回用哪个。
其实这在某些资源引用上比较常用,可以添加多个资源地址进行请求,谁先快就用谁的。
以最快返回的
Promise
为准
如果最快返加的状态为
rejected
那整个
Promise
为
rejected
执行
cache
如果参数不是
Promise
,内部将自动转为
Promise
下面示例中成功1比较快,就用成功1的。
<script>\"use strict\";const p1 = new Promise((resolve, reject) => {setTimeout(() => {resolve(\"成功1\");}, 1000);});const p2 = new Promise((resolve, reject) => {setTimeout(() => {resolve(\"成功2\");}, 3000);});Promise.race([p1, p2]).then(success => {console.log(success); // 成功1})</script>
async/await
使用
async/await
是
Promise
的语法糖,可以让编写
Promise
更清晰易懂,也是推荐编写
Promise
的方式。
async/await
本质还是
Promise
,只是更简洁的语法糖书写
async
在某一个函数前加上
async
,该函数会返回一个
Promise
对象。
我们可以依照标准
Promise
来操纵该对象。
<script>\"use strict\";async function get() {return \"请求成功...\";}get().then(success => {console.log(success); // 请求成功...})</script>
await
使用
await
关键词后会等待
Promise
完。
await
后面一般是
Promise
,如果不是直接返回
await
必须放在
async
定义的函数中使用
await
用于替代
then
使编码更优雅
<script>\"use strict\";async function get() {const ajax = new Promise((resolve, reject) => {setTimeout(()=>{resolve(\"返回的结果\");},3000);});let result = await ajax;console.log(result); // 返回的结果}get();</script>
一般
await
后面是外部其它的
Promise
对象
<script>\"use strict\";async function getName() {return new Promise((resolve, reject) => {resolve(\"姓名数据...\");});}async function getGrades() {return new Promise((resolve, reject) => {resolve(\"成绩数据...\");});}async function run() {let nameSet = await getName();let gradesSet = await getGrades();console.log(nameSet);console.log(gradesSet);}run();</script>
异常处理
Promise
状态为
rejected
其实我们就可以将它归为出现异常了。
当一个
await
发生异常时,其他的
await
不会进行执行。
<script>\"use strict\";async function getName() {return new Promise((resolve, reject) => {reject(\"姓名数据获取失败...\");})}async function getGrades() {return new Promise((resolve, reject) => {resolve(\"成绩数据...\");});}async function run() {let nameSet = await getName(); // Uncaught (in promise) 姓名数据获取失败...let gradesSet = await getGrades(); // 不执行}run();</script>
如果在
async
中不确定会不会抛出异常,我们可以在接收时使用
catch
进行处理。
<script>\"use strict\";async function getName() {return new Promise((resolve, reject) => {reject(\"姓名数据获取失败...\");})}async function run() {let nameSet = await getName().catch(error => console.log(error));}run();</script>
更推荐写成下面这种形式
<script>\"use strict\";async function getName() {return new Promise((resolve, reject) => {reject(\"姓名数据获取失败...\");}).catch(error => console.log(error));}async function run() {let nameSet = await getName();}run();</script>
也可使用
try...catch
进行处理。
<script>\"use strict\";async function getName() {return new Promise((resolve, reject) => {reject(\"姓名数据获取失败...\");});}async function run() {try {let nameSet = await getName();} catch (e) {console.log(e); // 姓名数据获取失败...}}run();</script>