同步和异步的区别是什么?
- 异步基于 JS 是单线程语言,只能同时做一件事
- JS 和 DOM 渲染共用同一个线程,因为 JS 可以修改 DOM 结构
- 异步不会阻塞代码执行
- 同步会阻塞代码执行
手写Promise加载一张图片
javascript
function loadImg(src) {
return new Promise((resolve, reject) => {
const img = document.createElement('img')
// 图片加载成功的回调
img.onload = () => {
resolve(img);
}
// 图片加载失败的回调
img.onerror = () => {
const err = new Error(`图片加载失败 ${src}`);
reject(err);
}
img.src = src;
})
}
Promise演示
javascript
function loadImg(src) {
const p = new Promise(
(resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
const err = new Error(`图片加载失败 ${src}`)
reject(err)
}
img.src = src
}
)
return p
}
// const url = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
// loadImg(url).then(img => {
// console.log(img.width)
// return img
// }).then(img => {
// console.log(img.height)
// }).catch(ex => console.error(ex))
const url1 = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
const url2 = 'https://img3.mukewang.com/5a9fc8070001a82402060220-100-100.jpg'
loadImg(url1).then(img1 => {
console.log(img1.width)
return img1 // 普通对象
}).then(img1 => {
console.log(img1.height)
return loadImg(url2) // promise 实例
}).then(img2 => {
console.log(img2.width)
return img2
}).then(img2 => {
console.log(img2.height)
}).catch(ex => console.error(ex))
异步应用场景
- 网络请求,如
ajax
图片加载 - 定时任务,如
setTimeout
请描述event loop(事件循环/事件轮询)的机制,可画图
我们知道:
- JS是单线程运行的
- 异步要基于回调来实现
- event loop就是异步回调的实现原理
Event loop的执行过程
- 同步代码,一行一行放在 Call Stack 执行,执行完之后,会将其清空
- 遇到异步,会先"记录下",等待时机(定时、网络请求等)
- 时机到了,就移到 Callback Queue
- 如果 Call Stack 为空(即同步代码执行完)
- 执行当前微任务队列中的微任务(宏任务和微任务区别后面有讲到)
- 尝试DOM渲染(如果DOM结构改变)
- Event Loop开始工作
- 轮询查找 Callback Queue,如果有则移动到 Call Stack 执行
- 然后继续轮询查找(永动机一样)
什么是宏任务和微任务,两者有什么区别?
Promise有哪三种状态?如何变化?
三种状态
- pending 过程中
- resolved(fulfilled) 成功
- rejected 失败
- 状态变化不可逆,一旦改变一次,则无法再发生改变。
状态的表现
- pending状态,不会触发then和catch
- resolved状态,会触发后续的then回调函数
- rejected 状态,会触发后续的catch回调函数
then和catch改变状态
then正常返回resolved,里面有报错则返回rejected
javascript
const p1 = Promise.resolve().then(() => {
return 100
})
console.log('p1', p1); //resolved 会触发后续 then 回调
p1.then(() => {
console.log('123');
})
const p2 = Promise.resolve().then(() => {
throw new Error('then error')
})
console.log('p2', p2); //rejected 会触发后续 catch 回调
p2.then(() => {
console.log('456');
})
catch 正常返回resolved,里面有报错则返回rejected
javascript
const p3 = Promise.reject('my error').catch(err => {
console.log(err);
})
console.log('p3', p3); // resolved 注意!!!! 触发 then回调
p3.then(() => {
console.log(100);
})
const p4 = Promise.reject('my error').catch(err => {
throw new Error('catch error')
})
console.log('p4', p4); //rejected 触发catch回调
p4.then(() => {
console.log(200);
}).catch(() => {
console.log('some err');
})
如何改变?
-
执行resolve()由
pedding
改变为resolved
-
执行reject()由
pedding
改变为rejected
-
遇到错误抛出由
pedding
改变为rejected
-
Promise.then()
方法中- return非Promise对象,返回成功的Promise成功的值为return的值(
pendding
=>resolved
) - return一个Promise对象,则返回的Promise状态和return的Promise一样,值也一样。
- 抛出错误,返回失败的Promise,值为抛出错误内容(
pending
=>rejected
)
- return非Promise对象,返回成功的Promise成功的值为return的值(
-
Promise.catch()
方法实际上是语法糖,相当于Promise.then(空, err=>{})
,因此catch返回的Promise状态和then()一样 -
Promise.all()
方法中- 如果传入的Promise数组状态都为成功,则返回成功的Promise,值为所有Promise成功值组成的数组。
- 如果传入的Promise数组只要有一个失败,则返回失败的Promise,值为第一个状态变为失败的Promise的值。
-
Promise.race()
方法中,取决于传入Promise数组中第一个改变状态的Promise,- 这个Promise如果成功,则race()返回成功的Promise。
- 如果失败,则race()返回失败的Promise
- 成功或失败的值和这个改变状态的Promise相同。
async/await和Promise 的关系
- 执行async函数,返回的是Promise对象
- await 相当于Promise的then
- try ... catch可捕获异常,代替了Promise的catch
async/await
-
由异步回调的 callback hell(回调地狱) 引出了Promise
-
但Promise的then和catch的链式调用,依然是基于回调函数的
-
async/await
是同步语法,彻底消灭回调函数 -
async函数返回一个Promise对象,返回的规则同then()方法:
- return 非Promise值,返回成功的值,成功的值为return 的值
- return Promise值,返回的Promise状态由return的Promise决定,成功或失败的值相同
- 抛出错误,返回失败的Promise,值为抛出的错误
javascript
async function fn1() {
// return 100 //相当于 return Promise.resolve(100)
return Promise.resolve(200)
}
const res1 = await fn1() //执行async 函数,返回的是一个Promise对象
// console.log('res1', res1); //打印结果:res1 --> Promise {<fulfilled>: 100}
res1.then((data) => {
console.log('data', data); //打印结果: data 200
})
-
await返回值的情况根据其后面语句执行结果决定:
- 后面的语句执行结果如果是成功的Promise,await语句返回其成功的值(类似
Promise.then()
方法) - 后面的语句执行结果如果是失败的Promise,则await语句抛出错误,可以使用try...catch...捕获
- 后面的语句执行结果如果是非Promise值,则await语句直接返回该值
- 后面的语句执行结果如果是成功的Promise,await语句返回其成功的值(类似
-
async
函数调用时,是同步执行的 -
await
语句以下的所有代码,都可以看作是一个callback回调里的内容,即异步,微任务 -
async/await
只是一个语法糖,异步的本质还是回调函数 -
async/await
是消灭异步回调的终极武器 -
JS还是单线程,还得是有异步,还得基于event loop
javascript
//匿名函数
!(async function () {
const p1 = Promise.resolve(300)
const data = await p1 //await 相当于 Promise then
console.log('data1', data); //打印结果: data 300
})()
!(async function () {
const data = await 400 //相当于 await Promise.resolve(400)
console.log('data2', data); //打印结果: data 400
})()
!(async function () {
const p2 = Promise.reject('err') //rejected状态
try {
const data = await p2 //await 相当于 Promise then
console.log('data3', data); //打印结果: data 300
} catch (error) {
console.log(error); //try ...catch 相当于 Promise catch
}
})()
!(async function () {
const p4 = Promise.reject('err1') //rejected状态
const res = await p4 //await 相当于 Promise then,由于p4是rejected状态,所以不会执行这句代码,也就没有打印结果
console.log(res);
})()
场景题
做完之后,可以右滑,查看注释的答案。
1. setTimeout
javascript
console.log(1);
setTimeout(() => {
console.log(2);
}, 1000);
console.log(3);
setTimeout(() => {
console.log(4);
}, 0);
console.log(5);
// 1 3 5 4 2
2. Promise (1)
javascript
Promise.resolve().then(() => {
console.log(1);
}).catch(() => {
console.log(2);
}).then(() => {
console.log(3);
})
// 1 3
3. Promise (2)
javascript
Promise.resolve().then(() => { // then正常返回resolved,里面有报错则返回rejected
console.log(1);
throw new Error('erro1')
}).catch(() => { // catch 正常返回resolved,里面有报错则返回rejected
console.log(2);
}).then(() => {
console.log(3);
})
// 1 2 3
4. Promise (3)
javascript
Promise.resolve().then(() => {
console.log(1);
throw new Error('erro1')
}).catch(() => {
console.log(2);
}).catch(() => {
console.log(3);
})
// 1 2
5. 宏任务和微任务
javascript
console.log(100)
setTimeout(() => {
console.log(200);
});
Promise.resolve().then(() => {
console.log(300);
})
console.log(400);
// 100 400 300 200
6. async/await (1)
javascript
(async function() {
console.log('start');
const a = await 100;
console.log('a:', a);
const b = await Promise.resolve(200);
console.log('b:', b);
const c = await Promise.reject(300);
console.log('c', c);
console.log('end');
})()
// start
// a: 100
// b: 200
// 报错
7. async/await (2)
javascript
async function async1 () {
console.log('async1 start')
await async2() //undefined
//await 的后面,都可以看做是callback 里的内容,即异步
//类似。event loop,setTimeout(cb1)
//setTimeout(function(){ console.log('async1 end')})
//Promise.resolve().then(()=>{ console.log('async1 end'}) //微任务/宏任务
console.log('async1 end')
}
async function async2 () {
console.log('async2')
}
console.log('script start')
async1()
console.log('script end')
//同步代码已经执行完,开始执行event loop
// script start
// async1 start
// async2
// script end
// async1 end
8.async/await (3)
javascript
async function async1() {
console.log('async1 start')
await async2()
//下面三行是异步回调callback 的内容
console.log('async1 end')
await async3()
//下面一行是异步回调callback 的内容
console.log('async1 end 2')
}
async function async2() {
console.log('async2')
}
async function async3() {
console.log('async3')
}
console.log('script start')
async1()
console.log('script end')
//同步代码已经执行完,开始执行event loop
// script start
// async1 start
// async2
// script end
// async1 end
// async3
// async1 end 2
9. 综合题
javascript
async function async1 () {
console.log('async1 start');
await async2();
//await 后面的都做完回调内容----微任务
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {//宏任务
console.log('setTimeout');
}, 0);
async1();
//初始化Promise时,传入的函数会立刻被执行
new Promise (function (resolve) {
console.log('promise1');
resolve()
}).then(function() {//微任务
console.log('promise2');
})
console.log('script end');
//同步代码执行完毕(相当于event loop--call stack被清空)
//执行微任务
//尝试触发DOM渲染
//触发event loop,执行宏任务
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
for ... of
- for... in(以及forEach for)是常规的同步遍历
- for ... of 常用于异步的遍历
javascript
function muti(num) {
return new Promise(resolve => {
setTimeout(() => {
resolve(num * num)
}, 1000)
})
}
const nums = [1, 2, 3]
// 使用 forEach ,是 1s 之后打印出所有结果,即 3 个值是一起被计算出来的
// nums.forEach(async (i) => {
// const res = await muti(i)
// console.log(res);
// })
// 使用 for...of ,可以让计算挨个串行执行
!(async function () {
for (let i of nums) {
const res = await muti(i)
console.log(res);
}
})()
5. 什么是宏任务,什么是微任务?
异步任务分为两种,一种宏任务,一种微任务,分别位于两个任务队列,微任务的执行实际比宏任务要早。至于为什么,先了解以下event loop 和 DOM 渲染的关系。
宏任务有哪些?微任务有哪些?
微任务的执行实际比宏任务要早。
宏任务:
- setTimeout、
- setInterval、
- Ajax、
- DOM事件、
- setImmediate(Node.js 环境)、
- I/O 操作、
- UI 渲染
微任务:
- Promise 回调函数(
.then()
、.catch()
、.finally()
), - async/await、
- process.nextTick(Node.js 环境)
javascript
console.log(100)
//宏任务
setTimeout(() => {
console.log(200)
})
//微任务
Promise.resolve().then(() => {
console.log(300)
})
console.log(400)
// 100 400 300 200
event loop 和 DOM 渲染 的关系
- 每次 Call Stack 清空(即每次轮询结束),即同步任务执行完成
- 都是 DOM 重新渲染的机会,都会先尝试DOM渲染,如果DOM结构有改变则重新渲染
- 然后再去触发下一次 Event Loop
宏任务和微任务的区别,执行时机与DOM渲染的关系
- 宏任务:DOM 渲染后触发,如
setTimeout
- 微任务:DOM 渲染前触发,如
Promise
- 根本区别:为任务是 ES6 语法规定的,宏任务是由浏览器规定的
可以通过alert
阻断代码执行,来验证微任务、宏任务与DOM渲染的关系:
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DOM渲染示例</title>
</head>
<body>
<div id="container"></div>
<script>
// 创建三个<p>元素并将它们添加到#container中
const p1 = document.createElement("p");
p1.textContent = "一段文字";
const p2 = document.createElement("p");
p2.textContent = "一段文字";
const p3 = document.createElement("p");
p3.textContent = "一段文字";
const container = document.getElementById("container");
container.appendChild(p1);
container.appendChild(p2);
container.appendChild(p3);
console.log("length", container.children.length);
alert("本次 call stack 结束,DOM 结构已更新,但尚未触发渲染");
// 到此,即本次 call stack 结束后(同步任务都执行完了),浏览器会自动触发渲染,不用代码干预
// 另外,按照 event loop 触发 DOM 渲染时机,setTimeout 时 alert ,就能看到 DOM 渲染后的结果了
setTimeout(function () {
alert(
"setTimeout 是在下一次 Call Stack ,就能看到 DOM 渲染出来的结果了"
);
});
</script>
</body>
</html>
xml
<div id="container"></div>
<script>
const div = document.getElementById('container');
div.innerHTML = `<p>一段文字</p>
<p>一段文字</p>
<p>一段文字</p>`
Promise.resolve().then(() => {
console.log(111);
alert('执行微任务,此时可以看到页面没有发生渲染');
})
setTimeout(() => {
console.log(222);
alert('执行宏任务,此时可以看到页面DOM结构已经重新渲染');
}, 0);
</script>
从event loop解释,为何微任务执行更早
执行Promise时,会等待时机进入微任务队列,但不会经过Web APIs,因为Promise时ES6规范,不是W3C规范
- 微任务是ES66语法规定的
- 宏任务是浏览器规定的
综上
执行顺序:
- 1、Call Stack清空
- 执行当前的微任务
- 尝试DOM渲染
- 触发Event Loop