题目:js是如何实现异步编程的?
解答
如何实现异步io?
1.回调函数callback
- 优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等待,会拖延整个程序的执行)
- 缺点:回调地狱,不能用
try catch
捕获错误,不能return
2.Promise
- 优点:解决了回调地狱的问题
- 缺点:无法取消
Promise
,错误需要通过回调函数来捕获
3.async/await
- 优点:代码清晰,不像
Promise
写一堆then
链,处理了回调地狱的问题 - 缺点:
await
将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用await
会导致性能降低
async/await
是怎么来的?/ async/await
如何通过同步的方式实现异步?
async/await
是参照generator
封装的一套异步处理方案,可以理解为generator
的语法糖,而generator
又依赖于迭代器Iterator
,Iterator
的思想又源于单向链表
1.单向链表
-
链表的优点
- 无需预先分配内存
- 插入/删除节点不影响其他节点,效率高
-
单向链表
是链表中最简单的一种,包含两个域:信息域与指针域,这个链接指向列表中的下一个节点,而最后一个节点则指向一个空值。
- 特点:
- 节点的链接方向是单向的
- 相对于数组来说,单链表的随机访问速度较慢,但单链表删除/添加数据的效率很高
- 特点:
2.Iterator
- Iterator迭代器 的遍历过程类似于单向链表
3.Generator
- Generator:生成器对象时生成器函数返回的,它符合可迭代协议和迭代器协议,既是迭代器也是可迭代对象,可以调用 next 方法,但它不是函数,更不是构造函数
本质:暂停
它会让程序执行到指定位置先暂停 (yield),然后再启动(next),再暂停(yield),再启动(next),而这个暂停就很容易让它和异步操作产生联系
-
处理异步时
- 开始异步处理(网络请求、IO操作)
- 然后暂停一下
- 处理完,再该干嘛干嘛
-
js是单线程的,异步还是异步,
callback
还是callback
,不会因为generator
而有任何改变
4.async/await
- async/await是 generator 的语法糖,就是一个自执行的 generate函数。利用 generator函数的特性把异步的代码写成同步的形式
async/await
和promise
的用法?
async/await
的用法
async/await
是函数定义的关键字await
用于等待promise对象
的返回结果,且不能单独使用必须放在async函数
中- 利用
async
定义的函数会返回一个promise
对象 async
函数的返回值就是promise
状态为resolved
的返回值
promise
promise
是一个构造函数,有all
reject
resolve
这几个方法,原型上有then
catch
等方法
特点
- 对象的状态不受外界影响
promise对象
代表一个异步操作,有三种状态pending
(进行中)fulfilled
(已成功)rejected
(已失败)
- 一旦状态改变就不会再变,任何时候都可以得到这个结果
promise对象
的状态改变,只有两种可能- 从
pending
变为fulfilled
- 从
pending
变为rejected
- 从
promise
的用法
- 首先new一个promise
let p = new Promise(function(resolve, reject) {
// do sth async
setTimeout(function() {
console.log('执行完成promise')
resolve('要返回的数据')
}, 2000)
})
// 执行完成promise
- new一个promise对象,不需要调用就执行了
- 使用promise的时候一般是包在一个函数中,在需要的时候去运行这个函数
const clickFunc = () => {
console.log('点击方法被调用')
return new Promise(function(resolve, reject) {
// do sth async
setTimeout(function() {
console.log('执行完成promise')
resolve('要返回的数据')
}, 2000)
})
}
-
为什么要放在函数里面
- 函数return出promise对象,执行这个函数我们可以得到一个promise对象。接下来就可以用promise对象上的then catch方法了
-
resolve是什么
- promise只是能够简化层层回调的写法,实质上,promise的精髓是 状态,用维护状态、传递状态的方式使得回调函数能够及时调用
-
reject的用法
- then方法可以接受两个参数,第一个对应resolved的回调,第二个对应reject的回调,并且能在回调函数中拿到成功的数据和失败的原因
function promiseClick(){
return new Promise(function(resolve, reject){
setTimeout(function(){
var num = Math.ceil(Math.random()*20); //生成1-10的随机数
console.log('随机数生成的值:',num)
if(num<=10){
resolve(num);
}
else{
reject('数字太于10了即将执行失败回调');
}
}, 2000);
})
}
promiseClick().then(
function(data){
console.log('resolved成功回调');
console.log('成功回调接受的值:',data);
},
function(reason){
console.log('rejected失败回调');
console.log('失败执行回调抛出失败原因:',reason);
}
);
-
catch的用法
- 作用一:用来捕获异常,与
then
方法中接受的第二参数rejected
的回调一样 - 作用二:再执行
resolved
的回调时,如果抛出异常,并不会报错卡死js,而是会进入catch
方法中
- 作用一:用来捕获异常,与
-
all的用法
- 该方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后并且执行结果都是成功的时候才执行回调
function promiseClick1(){
return new Promise(function(resolve, reject){
setTimeout(function(){
var num = Math.ceil(Math.random()*20); //生成1-10的随机数
console.log('随机数生成的值:',num)
if(num<=10){
resolve(num);
}
else{
reject('数字太于10了即将执行失败回调');
}
}, 2000);
})
}
function promiseClick2(){
return new Promise(function(resolve, reject){
setTimeout(function(){
var num = Math.ceil(Math.random()*20); //生成1-10的随机数
console.log('随机数生成的值:',num)
if(num<=10){
resolve(num);
}
else{
reject('数字太于10了即将执行失败回调');
}
}, 2000);
})
}
function promiseClick3(){
return new Promise(function(resolve, reject){
setTimeout(function(){
var num = Math.ceil(Math.random()*20); //生成1-10的随机数
console.log('随机数生成的值:',num)
if(num<=10){
resolve(num);
}
else{
reject('数字太于10了即将执行失败回调');
}
}, 2000);
})
}
Promise
.all([promiseClick3(), promiseClick2(), promiseClick1()])
.then(function(results){
console.log(results);
});
- race的用法
- 谁先执行完成就先执行回调
- 先执行完的不管是进行了
race
的成功回调还是失败回调,其余的将不会再进入race
的任何回调
function promiseClick1(){
return new Promise(function(resolve, reject){
setTimeout(function(){
var num = Math.ceil(Math.random()*20); //生成1-10的随机数
console.log('随机数生成的值:',num)
if(num<=10){
resolve(num);
}
else{
reject('数字太于10了即将执行失败回调');
}
}, 2000);
})
}
function promiseClick2(){
return new Promise(function(resolve, reject){
setTimeout(function(){
var num = Math.ceil(Math.random()*20); //生成1-10的随机数
console.log('随机数生成的值:',num)
if(num<=10){
resolve(num);
}
else{
reject('数字太于10了即将执行失败回调');
}
}, 2000);
})?
}
function promiseClick3(){
return new Promise(function(resolve, reject){
setTimeout(function(){
var num = Math.ceil(Math.random()*20); //生成1-10的随机数
console.log('随机数生成的值:',num)
if(num<=10){
resolve(num);
}
else{
reject('数字太于10了即将执行失败回调');
}
}, 2000);
})
}
Promise
.race([promiseClick3(), promiseClick2(), promiseClick1()])
.then(function(results){
console.log(results);
},function(reason){
console.log('失败',reason);
});
async/await
和promise
用法的区别?举例两三个?
promise
是ES6
,async/await
是ES7
async await
是基于Promise
实现的,可以说是改良版的Promise
,它不能用于普通的回调函数
async/await
相对于promise
来讲,写法更加简洁Promise
的出现解决了传统callback函数
导致的“地域回调”问题,但它的语法导致了它向纵向发展行成了一个回调链,遇到复杂的业务场景,这样的语法显然也是不美观的。- 而
async await
代码看起来会简洁些,使得异步代码看起来像同步代码,await
的本质是可以提供等同于”同步效果“的等待异步返回能力的语法糖,只有这一句代码执行完,才会执行下一句
reject
状态:promise
错误可以通过catch
来捕捉,建议尾部捕获错误,async/await
既可以用.then
又可以用try-catch
捕捉
async/await
和promise
性能的区别?
async/await
优点
- 它做到了真正的串行的同步写法,代码阅读相对容易
- 对于条件语句和其他流程语句比较友好,可以直接写到判断条件里面
- 处理复杂流程时,在代码清晰度方面有优势
async/await
缺点
- 无法处理
promise
返回的reject
对象,要借助try...catch...
await
只能串行,做不到并行await
做不到并行,不代表async
不能并行,只要await
不在同一个async
函数里就可以并行
try...catch...
内部的变量无法传递给下一个try...catch...
async/await
无法简单实现Promise
的各种原生方法,比如.race()
之类
多个await
和多个promise
执行的区别在哪里?
相关
js执行机制 -- 异步
-
js是一种单线程语言。
-
js的异步是通过回调函数实现的,即通过任务队列,在主线程执行完当前的任务栈,主线程空闲后轮询任务队列,并将任务队列中的任务(回调函数)取出来执行
EventLoop
- 同步任务进入主线程,异步任务进入Event Table并注册函数
- 当指定事件完成时,Event Table会将这个函数移入Event Queue
- 不同类型的任务会进入对应的Event Queue
- 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数进入主线程执行
- 重复上述过程,即Event Loop(事件循环)
promise.nextTick
nextTick
的由来
- 由于vue的
数据驱动视图更新
是异步的,即修改数据的当下,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。
nextTick
的触发时机
- 在同一事件循环中的数据变化后,DOM完成更新,立即执行
nextTick(callback)
内的回调。
应用场景
- 需要在视图更新之后,基于新的视图进行操作。
简单总结事件循环
- 同步代码执行 -> 查找异步队列,推入执行栈,执行
callback1[事件循环1]
->查找异步队列,推入执行栈,执行callback2[事件循环2]
... - 即每个异步callback,最终都会形成自己独立的一个事件循环。
结合nextTick
的由来,可以推出每个事件循环中,nextTick
触发的时机
- 同一事件循环中的代码执行完毕 -> DOM 更新 -> nextTick callback触发
使用promise实现串行
var createPromise = function(time) {
return (resolve, reject)=>
new Promise((resolve, reject)=>{
setTimeout(()=>{
console.log('timein'+time)
resolve();
}, time*1000)
})
}
function serpromise(arr) {
arr.reduce((pre, next, index, carr)=>{
return pre.then(next)
}, Promise.resolve())
}
var arr=[createPromise(2), createPromise(1), createPromise(3), createPromise(4), createPromise(5)];
serpromise(arr)
node与浏览器eventLoop的区别
- 浏览器和node环境下,
microtask
任务队列的执行时机不同- node端,
microtask
在事件循环的各个阶段之间执行 - 浏览器端,
microtask
在事件循环的macrotask
执行完之后执行
- node端,