您现在的位置是:首页 >技术教程 >【前端面经】JS-异步解决方案网站首页技术教程
【前端面经】JS-异步解决方案
同步和异步
众所周知, JavaScript
是一门单线程的语言, 单线程就意味着同一时间只能执行一个任务, 当前任务执行结束, 才会执行下一个任务. 这种模式的好处就是执行环境比较单纯, 但坏处也很明显, 一旦有某个任务卡住了, 就会导致整个程序阻塞. 为了解决这个问题, JS将任务的执行模式分成了同步(Synchronous)
和异步(Asynchronous)
两种.
更详细的同步异步的执行模式可以参考事件循环
下面我们就来讲讲JS异步编程的几种解决方案.
1.回调函数
回调函数是异步编程的最基本的方法, 它通常作为一个参数传递给另一个函数, 并且在父函数执行完成后执行回调函数
例如:
function logB(){
console.log("b");
}
function parent(a, callback) {
console.log(a);
typeof callback === 'function' ? callback() : return
}
parent('a', logB)
在上面的例子中, 在parent
函数中, 如果callback参数传入的是一个函数, 那么在输出形参a
之后就会执行callback
函数, 上述例子中logB就是作为回调函数传入parent中并执行
在实际开发过程中, 回调函数的使用场景还是很多的, 例如常见的数组遍历方法forEach
, 其第一个参数就是回调函数
let arr = [1, 2, 3]
arr.forEach((item) => {
console.log(item);
})
回调函数的优点是简单易懂, 但其实一旦嵌套起来很不利于代码的维护和可读性. 而且各个部分之间高度耦合, 流程会比较混乱. 最经典就是回调地狱(callback hell)
.
在实际开发过程中, 经常能遇到一些业务需要先发送一个请求获取数据后, 再根据这个请求的返回结果再发送第二个请求, 第三个请求可能又需要第二个请求的返回结果作为参数, 如此层层嵌套就称之为回调地狱
, 这样的代码就很难维护.
ajax('url1', (res1) => {
console.log(res1)
ajax('url2+res1', (res2) => {
console.log(res2)
ajax('url3 + res2', (res) => {
console.log(res3)
...
})
})
})
优点: 简单易懂
缺点: 多层回调函数嵌套时容易混乱, 不利于代码维护和可读性, 容易写出回调地狱
事件监听
这种方式, 异步任务的执行不取决于代码的执行顺序, 而取决于某个事件是否发生.
element.addEventListener('click', ()=> {
console.log('click')
})
上面这个例子的含义是, 当element
被点击时, 就会输出click
- 优点: 比较容易理解, 并且可以绑定多个事件, 每个事件可以指定多个回调函数, 可以比较好的
去耦合
, 有利于实现模块化. - 缺点: 整个程序变成了事件驱动型, 运行流程很不清晰, 比较难看出主流程, 代码可读性低
Promise
Promise
是一种处理异步代码(并且不会陷入回调地狱)的方式
Promise
代表一个异步操作, 其有三种状态pending(进行中)
、fulfilled(已完成)
、rejected(已失败)
, 一个Promise
对象必然处于这三种状态之一
pending
: 初始状态, 既没有完成, 也没有被拒绝fulfilled
: 操作成功完成rejected
: 操作失败
当Promise
被调用后, 它会以pending
开始, 代码会继续往下执行, 而Promise
仍处于pending
状态, 直到执行完成为止, 最终会以fulfilled
或rejected
结束, 并对应的传给回调函数–then
或者catch
.
优点: 不仅可以解决回调地狱问题, 而且能够捕获错误并进行处理
缺点: 无法取消Promise
Generator
Generator
函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator
最大的特点就是可以控制函数的执行。
语法上,首先可以把它理解成,Generator
函数是一个状态机,封装了多个内部状态。
Generator
函数除了状态机,还是一个遍历器对象生成函数。
可暂停函数, yield可暂停,next方法可启动,每次返回的是yield后的表达式结果。
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
async/await
本质上async和await是Promise的语法糖, 它可以使得异步代码看起来像同步代码一样
更加详细的Promise可以参考: Promise
总结
- JS 异步编程进化史:
callback
→promise
→generator
→async/await
async/await
函数的实现,就是将Generator
函数和自动执行器,包装在一个函数里。async/await
可以说是异步终极解决方案了。
(1) async/await
函数相对于Promise
, 优势体现在:
- 处理 then 的调用链,能够更清晰准确的写出代码
- 并且也能优雅地解决回调地狱问题。
当然async/await
函数也存在一些缺点: - 因为
await
将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了await
会导致性能上的降低,代码没有依赖性的话,完全可以使用Promise.all
的方式。
(2) async/await
函数对 Generator 函数的改进,体现在以下三点:
-
内置执行器。
Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
-
更广的适用性。
co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
-
更好的语义。
async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。