1. 同步和异步的区别?
基于 JS 是单线程语言
- 异步不会阻塞代码执行
- 同步会阻塞代码执行
1.1 什么是同步、异步?
同步: 按代码顺序执行
异步: 简单来说, 不按照代码顺序执行, 就是异步
1.2 为什么会有异步?
异步是为了解决, JS 单线程阻塞问题的
1.3 如何 异步 解决 JS 单线程阻塞问题?
通过 事件循环 来解决, 事件循环的执行流程, 同步任务会进入主线程执行, 而异步任务会进入任务队列, 等到主线程任务执行完, 任务队列的任务就会放入主线程执行, 如此循环反复
1.4 JS 如何实现异步?
异步在于创建宏任务和微任务, 通过事件循环机制实现异步机制
宏任务
- 定时器 setTimeout、setInterval
- 事件监听 (发布订阅 postMessage)
- 回调函数
- I/O
微任务
- Promise
- async/await
标准回答 (按异步编程进化史来说)
所有异步任务都是在同步任务执行结束之后,从任务队列中依次取出执行。
回调函数是异步操作最基本的方法,比如 AJAX
回调,回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。此外它不能使用 try catch
捕获错误
return Promise
包装了一个异步调用并生成一个 Promise
实例,当异步调用返回的时候根据调用的结果分别调用实例化时传入的resolve
和 reject
方法,then
接收到对应的数据,做出相应的处理。Promise
不仅能够捕获错误,而且也很好地解决了回调地狱的问题,缺点是无法取消 Promise
,错误需要通过回调函数捕获。
Generator
(迭代器) 函数是 ES6
提供的一种异步编程解决方案,Generator
函数是一个状态机,封装了多个内部状态,可暂停函数, yield
可暂停,next
方法可启动,每次返回的是yield
后的表达式结果。优点是异步语义清晰,缺点是手动迭代Generator
函数很麻烦,实现逻辑有点绕
async/await
是基于Promise
实现的,async/await
使得异步代码看起来像同步代码,所以优点是,使用方法清晰明了,缺点是await
将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await
会导致性能上的降低,代码没有依赖性的话,完全可以使用 Promise.all
的方式。
加分回答 JS
异步编程进化史:callback -> promise -> generator/yield -> async/await
。 async/await
函数对 Generator
函数的改进
体现在以下三点: - 内置执行器。 Generator
函数的执行必须靠执行器,而 async
函数自带执行器。
也就是说,async
函数的执行,与普通函数一模一样,只要一行。 更广的适用性。
yield
命令后面只能是 Thunk
函数或 Promise
对象,而 async
函数的 await
命令后面,可以跟 Promise
对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作), 更好的语义。
async
和 await
,比起 星号 和 yield
,语义更清楚了
async
表示函数里有异步操作,await
表示紧跟在后面的表达式需要等待结果。 目前使用很广泛的就是 promise
和 async/await
2. 手写用 Promise 加载一张图片
// 加载函数 ...
function loading (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
}
)
}
const url1 = 'https://img.com/img1.jpg'
const url2 = 'https://img.com/img2.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))
3. 前端使用异步的场景有哪些?
场景
- 网络请求
- 定时任务
4. 读代码
// setTimeout 笔试题
console.log(1)
setTimeout(function() {
console.log(2)
}, 1000)
console.log(3)
setTimeout(function() {
console.log(4)
}, 0)
console.log(5)
// 执行结果 1 3 5 4 2
下面就是相关知识点
知识点:
单线程和异步
应用场景
callback hell (回调地狱) 和 Promise
(一)
单线程和异步
- JS 是单线程语言, 只能同时做一件事儿
- 浏览器和 nodejs 已支持 JS 启动进程, 如 Web Worker
- JS 和 DOM 渲染共同一个线程, 因为 JS 可修改 DOM 结构
- 遇到等待 (网络请求, 定时任务) 不能卡住, 所以需要异步, 以回调 callback 函数形式
// 异步
console.log(100)
setTimeout(function () {
console.log(200)
}, 1000)
console.log(300)
// 输出结果 100、300、200
// 同步
console.log(100)
aleart(200)
console.log(300)
// 输出结果 100、200、300
(二)
异步的应用场景
- 网络请求, 如 ajax 图片加载
- 定时任务, 如 setTimeout
网络请求
// 网络请求 ajax
console.log('start')
$.get('./data1.json', function (data1) {
console.log(data1)
})
console.log('end')
// 执行结果 start end data1
// 图片懒加载
console.log('start')
let img = document.createElement('img')
img.onload = function() {
console.log('loaded')
}
img.src = '/xxx.png'
console.log('end')
// 执行结果 start end loaded
定时任务
// setTimeout
console.log(100)
setTimeout(function() {
console.log(200)
}, 1000)
console.log(300)
// 执行结果 100 300 200
// setInterval
console.log(100)
setInterval(function() {
console.log(200)
}, 1000)
console.log(300)
// 执行结果 100 300 200 200 ...
(三)
callback hell (回调地狱)
// 获取第一份数据
$.get(url1, (data1) => {
console.log(data1)
// 获取第二份数据
$.get(url2, (data2) => {
console.log(data2)
// 获取第三份数据
$.get(url3, (data3) => {
console.log(data3)
// 还可能获取更多的数据
})
})
})
解决回调地狱的方案就是, Promise
// Promise 定义
function getData(url) {
return new Promise((resolve, reject) => {
$.ajax({
url,
success(data) {
resolve(data)
},
error(err) {
reject(err)
}
})
})
}
// Promise 使用
const url1 = '/data1.json'
const url2 = '/data2.json'
const url3 = '/data3.json'
getData(url1).then( data1 => {
console.log(data1)
return getData(url2)
}).then( data2 => {
console.log(data2)
return getData(url3)
}).then(data3 => {
console.log(data3)
}).catch(err => console.error(err))
let approvalProcess = (name. time) => {
return new Promise((resolve, reject) => {
setTimeout(() => { // setTimeout 模拟异步
let approvalRes = Math.random() >= 0.2 // 随机数模拟异步成功操作成功或失败
if (approvalRes) {
resolve(name+'已审核通过')
} else {
reject('审核不通过')
}
}, time)
})
}
let boss = approvalProcess('老板', 1000)
let manager = approvalProcess('经理', 2000)
boss.then(res => {
console.log(res)
return manager
}).then(res => {
console.log(res)
return '老板和经理都通过了'
}).then(result => {
console.log(result)
}).catch(err => {
console.log(err)
})
[扩展] - promise 经典面试题
5. Promise
Promise 的状态?
Promise 有三种状态:pengding、fulfilled、rejected
Promise 的状态一经改变,便不可以修改
var pro = new Promise( (resolve, reject) => {
reject()
resolve()
})
pro.then( () => { console.log('resolve1') }).
catch( () => {console.log('catch1') }) // reject1
Promise 链式调用
Promise 的链式调用,有三个 Promise.prototype.then()
、Promise.prototype.catch()
和 Promise.prototype.finally()
Promise.prototype.then()
then 方法可以接收两个回调函数作为参数,第一个参数resolve()
返回的数据,第二个参数reject()
返回的数据当然了,异常也会被第二个参数接收
.finally()
一定会执行,但是它没有回调参数
.then()
可有多个,.catch()
也可以有多个,但是 .then()
或者 .catch()
必须返回一个 Promise 才可以这样做
数据的接收顺序
- then -> catch -> finally
var pro = new Promise( (resolve, reject) => {
reject()
resolve()
})
pro.then(
() => { console.log('resolve1') },
() => { console.log('reject1') }).
catch( () => {console.log('catch1') }
) // reject1
只有
.then()
的第二个参数传,reject()
返回的数据 或者是 异常才会进到.catch()
中[注意] Promise 抛出异常是不会,直接中断的,会进入
.then()
的第二个参数,没有.then()
的第二个参数才会 进入.catch()
中
Promise 如果接收错误
- catch
- then 的第二个回调函数参数
Promise 的一些方法
Promise.resolve() 返回 成功状态
Promise.reject() 返回 失败状态
Promise.finally() 不管什么状态都执行
Promise.then() 成功回调
Promise.catch() 错误回调
Promise.all() 一个
reject()
, 整个结束执行 (获取全部都成功,再返回)Promise.allSettled() 全部状态变更,才执行结束
Promise.any() 一个
resolve()
,整个再返回 (获取全部都失败,再返回)Promise.race() 那个状态先改变,那个先返回
await
后面可以跟 Promise 对象、非 Promise 值以及另一个await
表达式。
await
后面也可以跟非 Promise 值,如基本数据类型(number
、string
、boolean
等)、对象、数组等。在这种情况下,await
会将该值直接返回,就好像该值被包装在一个已经解决的 Promise 中。
5.1 两个异步请求如何合并?
使用 Promise
//定义两个http请求方法
const getList1 = ()=>{
return new Promise((res,rej) =>{
//省去get方法获取过程
.then((json) => resolve(json))
})
}
const getList2 = ()=>{
return new Promise((res,rej) =>{
//省去get方法获取过程
.then((json) => resolve(json))
})
}
Promise.all([getList1(),getList2()]).then(value => {
//第一个请求的数据
const x = value[0];
//第二个请求的数据
const y = value[1];
//合并操作
for(const i of x){
for(const k of y){
//Todo
}
}
})
5.2 Promise有哪几种状态,各个状态之间是如何进行转换的?
三种状态: pending
、fulfilled
、rejected
(未决定,履行,拒绝)
1.初始化,状态:pending
2.当调用resolve(成功),状态:pengding=>fulfilled
3.当调用reject(失败),状态:pending=>rejected
5.3 Promise 解决哪些问题?
回调地狱
const request = url => {
return new Promise((resolve,reject) => {
$.get(url,params => {
resolve(params)
})
})
}
request(url).then(params1 => {
return request(params1.url)
}).then(params2 => {
return request(params2.url)
}).then(params3 => {
console.log(params3)
}).catch(err => throw new Error(err))
5.4 Promise.all、Promise.any、Promise.race、Promise.allsettled
Promise.all
场景: 多个 Promise 请求, 如果只有一个出错的话, 那么整个就会抛出异常, 不会继续执行
// 模拟异步操作
const request = (delay, flag = true) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (flag) {
resolve(`成功了${delay}`)
} else {
reject(`失败了${delay}`)
}
}, delay)
})
}
const fun = async (promises) => {
Promise.all(promises)
.then(res => {
console.log('res', res)
})
.catch(error => {
console.log('error', error)
})
}
fun([request(1000), request(500)])
// res [ '成功了1000', '成功了500' ]
fun([request(1000), request(500, false)])
// error 失败了500
如果其中一个错误, 让成功的也能输出出来
const fun = async (promises) => {
Promise.all(
promises.map(promise => {
console.log(promise.catch(err => err))
return promise.catch(err => err)
})
).then(res => {
console.log('res', res)
})
}
fun([request(1000), request(500, false)])
// res [ '成功了1000', '失败了500' ]
使用 ES2020 (ES11) 的新语法 Promise.allSettled
, 就能捕获正常和错误的返回
const fun = async (promises) => {
Promise.allSettled(promises)
.then(res => {
console.log('res', res)
})
}
fun([request(1000), request(500, false)])
// res [
// { status: 'fulfilled', value: '成功了1000' },
// { status: 'rejected', reason: '失败了500' }
// ]
6. async await
await 通常是添加一个 promise 函数嘛
那它可以添加一个普通函数吗,能正确执行吗?
可以添加一个普通函数
那可以添加一个值吗?
可以的,直接返回那个值
为什么 await 后面可以普通函数,或者值?
因为await
后面跟的是一个 Promise
对象,如果不是,则会包裹一层 Promise.resolve()
语法规则
async
是function
的一个前缀,只有async
函数中才能使用await
语法async
函数是一个Promise
对象,有无resolve
取决于有无在函数中return
值await
后面跟的是一个Promise
对象,如果不是,则会包裹一层Promise.resolve()
async await 原理
async/await
是由 generator函数(迭代器)
来实现的
async await 如何捕获异常
try catch
async function fetchData() {
try {
const result = await fetch('...')
} catch (err) {
console.log(err)
}
}
fetchData()
await
的catch
await
返回一个 Promise
对象,Promise
对象有 then、catch
,我们可以在 catch
中捕获错误
fetchData().then().catch(err => console.log('发生错误:', err))
6.1 async/await 解决了什么问题?
解决了 异步问题, 可以 异步转同步
// 使用async/await获取成功的结果
// 定义一个异步函数,3秒后才能获取到值(类似操作数据库)
function getSomeThing(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('获取成功')
},3000)
})
}
async function test(){
let a = await getSomeThing()
console.log(a)
}
test() // 3秒后输出:获取成功
7. 事件循环
什么是事件循环?
js 是单线程, 而为了解决这个问题, 引入了异步, 同步任务会进入主线程执行, 而异步任务会进入任务队列, 等到主线程任务执行完, 任务队列的任务就会放入主线程执行, 如此循环反复就是事件循环。
事件循环中的异步任务?有哪些能举例吗?
异步任务可以分为微任务、宏任务
宏任务:定时器、请求、事件监听 (发布订阅 postMessage)、I/O
微任务:promise、async/await
宏任务与微任务那个先执行?
在同一次循环中,微任务先执行,宏任务后执行
网上一些面试题,有些执行结果是 宏任务先给出结果,但其内部的微任务仍然会在该宏任务完成之前被优先执行
Vue的
$nextTick
方法是微任务还是宏任务?
$nextTick
在Vue中的实现通常利用Promise
的then
方法来创建一个微任务
Vue
中watch
与computer
哪个是同步、哪个是异步?computer
是同步,watch
是异步watch
与computer
哪个先执行?需要根据实际情况决定,正常情况下是
computer
先执行如果
watch
设置了immediate: true
,watch
先于computer
执行