JS 异步(下)

异步(下)

 

在异步(上)中,只是简单介绍了一些概念,在本章中,会带着详细学习异步
 

5.1 面试题

1.请描述 event loop (事件循环 / 事件轮询)的机制,可画图
2.什么是宏任务和微任务,两者有什么区别?
3.Promise 有哪三种状态,如何变化?
4.promise 中 then 和 catch 的连接

// 第一题
Promise.resolve().then(() => {
  console.log(100);
}).catch(() => {
  console.log(200);
}).then(() => {
  console.log(300);
});
// 第二题
Promise.resolve().then(() => {
  console.log(400);
  throw new Error('error1')
}).catch(() => {
  console.log(500);
}).then(() => {
  console.log(600);
});
// 第三题
Promise.resolve().then(() => {
  console.log(700);
  throw new Error('error2')
}).catch(() => {
  console.log(800);
}).catch(() => {
  console.log(900);
});

附加题:将上面三个 promise 放在一起执行,给出输出结果

5.async 和 await 语法题

async function fn() {
  return 100;
};
async function excute() {
  const res1 = fn();
  const res2 = await fn();
}
excute();	// res1, res2 的内容
async function excute() {
  console.log('start');
  const res1 = await 100;
  console.log('res1:', res1);
  const res2 = await Promise.resolve(200)
  console.log('res2:', res2);
  const res3 = await Promise.reject(300)
  console.log('res3:', res3);
  console.log('end');
}
excute();// 打印内容

6.promise 和 setTimeout 的顺序

console.log(100);
setTimeout(() => {
  console.log(200);
}, 0);
Promise.resolve().then(() => {
  console.log(300);
});
console.log(400);

7.promise 和 async / await 的顺序问题

async function asyncFn1() {
  console.log('asyncFn1 excute');
  await asyncFn2();
  console.log('asyncFn1 end');
}

async function asyncFn2() {
  console.log('asyncFn2 excute');
}

console.log('script start');

setTimeout(() => {
  console.log('setTimeout');
});

asyncFn1();

new Promise ((resolve) => {
  console.log('promise excute');
  resolve();
}).then(() => {
  console.log('then excute');
});

console.log('script end');

8.手写实现 promise
 

5.2 event loop(事件循环 / 事件轮询)

JS 是单线程运行的,遇到等待(网络请求,定时任务等)不能卡住,需要异步(通过 回调 callback 函数形式 )的方式来执行 JS 。
异步是基于回调实现实现的,而 event loop 就是异步回调的实现原理
 
1.JS 如何执行(执行顺序)
从前到后,一行一行执行
如果某一行执行报错,会停止下面代码的执行
先执行完同步,再执行异步
 
2.简单描述 JS 整个执行流程

以下面一段代码为例:

console.log('start');
setTimeout(() => {
  console.log('setTimeout');
}, 1000);
console.log('end');

分析:
event loop 执行流程图
执行顺序:

  1. 执行 console.log(‘start’) 时,会放入到执行栈( call stack )中,然后立即执行,这时候浏览器控制台( browser console )输出 start 。
  2. 执行setTimeout 时,为了不阻碍程序继续执行,会直接把 setTimeout 放进 web APIs,注意,这时候已经开始计时,等到计时结束,会将 setTimeout 中的执行函数放进回调队列( callback queue )中等待,注意,这个时候也不会执行。
  3. 执行 console.log(‘end’) 时,会放入到执行栈( call stack )中,然后立即执行,这时候浏览器控制台( browser console )输出 end 。
  4. 这时候同步代码已经执行完成,这时候通过 event loop 轮询,发现 回调队列( callback queue )中还有任务在等待,于是放入 执行栈( call stack )中立即执行。这时候浏览器控制台( browser console )输出 setTimeout 。

上面四步是这段代码的顺序,可以看出来,遇到异步并不会停止代码执行,而是按一定规则继续执行同步代码,同步代码执行完后,会通过 event loop 轮询查询是否有异步任务等待执行,而 event loop 正是异步回调实现的关键所在。

注:上面只是简单示例,并非完整的js执行过程
 
3.DOM 事件 和 event loop 的关系

DOM 事件也使用回调,基于 event loop

console.log('start');
setTimeout(() => {
  console.log('setTimeout');
}, 1000);
document.getElementById('divClick').addEventListener('click', function () {
  console.log('click div');
})
console.log('end');

可以看到,我们在上面代码的基础上添加了一个监听事件,整体执行与上面的一致,需要注意的是,当执行到 addEventListener 时,会保存下来这个事件(已执行,可能暂存web APIs),相当于一个 loading 状态,用户点击时会立即放进回调队列( callback queue )等待执行。这就和 event loop 实现异步的道理一样。

注意:虽然 DOM 事基于 event loop 去使用回调,但DOM事件不属于异步
 

5.3 Promise 进阶

1.promise 有哪三种状态
pending: 初始状态,不是成功或失败状态。
fulfilled: 意味着操作成功完成。
rejected: 意味着操作失败。
 
2.状态的表现和变化
 
1.pending 状态,不会触发 then 和 catch,PromiseState 值也是 pending

const pro = new Promise((resolve, reject) => {
   // ...,没有调用 resolve 或 reject
   // 这里 resolve 和 reject 都是函数
})
console.log(pro)
/**
  * Promise {<pending>}
  *  __proto__: Promise
  * [[PromiseState]]: "pending"//当前 promise 的状态
  * [[PromiseResult]]: undefined//传入resolve 或 reject 中的参数,不传为 undefined
 */ 

 
2.调用resolve函数,状态会从 Pending 变为 Fulfilled, 触发后续的 then 回调函数

const pro = new Promise((resolve, reject) => {
   resolve('resolve string');
})
// Promise {<fulfilled>: resolve string}
// 其中 PromiseState:fulfilled,PromiseResult:resolve string
// 触发紧邻的then函数

 
3. 调用resolve函数,状态会从 Pending 变为 Rejected, 会触发后续的 catch 回调函数

const pro = new Promise((resolve, reject) => {
   reject('reject string');
})
// Promise {<rejected>: "reject"}
// 其中 PromiseState:rejected,PromiseResult:reject string
// 触发 catch

 
4.状态凝固

一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。

const pro = new Promise((resolve, reject) => {
   reject('reject');
   resolve('resolve');
})
// 先执行 reject, 状态改变为 rejected,触发catch,
// 那么即使后面再执行 resolve,状态不会改变,也不会再触发 then了
// 因为只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。

 
5.then 和 catch 对状态的影响

1.then 正常返回状态 fulfilled,里面有报错则返回 rejected(如果 then后面还想接 then,并需要使用上一个 then 的数据,则可以在上一个 then 中使用 return 返回即可)

2.cathc 正常返回状态也是 fulfilled(注意这里),里面有报错则返回 rejected

const pro = new Promise((resolve, reject) => {
  reject('reject string');
})
pro.then((res) => {
  console.log(res);	// 不触发
}).catch((err) => {
  console.log(err); // 触发,输出 reject string
  return err;	// 无报错,正常返回状态也是 fulfilled,因此能触发下一个then
}).then((res) => {
  console.log(res);	// 触发,输出 reject string
})

注意:报错可以用 throw new Error 来触发
 

5.4 async / await

以前存在 callback hell 这种情况

后来出现了 promise then catch 链式调用,从嵌套执行转变到顺序执行,但是本质上也是基于回调函数

当出现 async / await 这种同步语法后 ,彻底消灭了回调函数(注意:是从语法层面上消灭,它只是语法糖,本质仍然是回调)

需要明确的一点是,由于 JS 是单线程,是不可能消灭异步和回调的。但是可以从语法上去改变,让其看起来是一个同步执行
 

1.async / await 和 promise 的关系

async / await 和 promise 并不冲突,两者相辅相成
 

直接执行 async 函数,返回的是 promise 对象

async function asyncFn() {
  console.log('asyncFn');
}
async function executeFn() {
  const res = asyncFn();
  console.log(res); // Promise {<fulfilled>: undefined}
}
executeFn();

 
await 相当于 promise 的then(不会走catch), 返回的直接就是then的参数,处理的是成功的情况

async function asyncFn() {
  console.log('asyncFn');
  return 'return asyncFn'
}
async function executeFn() {
  const res = await asyncFn();
  console.log(res); // 输出: return asyncFn
}
executeFn();

 
由于不会走 catch 可以用 try…catch 捕获异常,代替了 promise 的catch,处理的是失败的情况

async function asyncFn() {
  console.log('asyncFn');
  throw new Error('error asyncFn'); 
}
async function executeFn() {
  try {
    const res = await asyncFn();
    console.log(res); // 未触发
  } catch(error) {
    console.log(error); // 触发
  }
}
executeFn();

 
关于 try catch:

能被 try catch 捕捉到的异常,必须是在报错的时候,线程执行已经进入 try catch 代码块,且处在 try catch 里面,这个时候才能被捕捉到。

5.5 异步的本质,async / await 是语法糖

上面已经提到过:

当出现 async / await 这种同步语法后 ,彻底消灭了回调函数(注意:是从语法层面上消灭,它只是语法糖,本质仍然是回调)

需要明确的一点是,由于 JS 是单线程,是不可能消灭异步和回调的。但是可以从语法上去改变,让其看起来是一个同步执行

在 await 后面执行的代码,都可以看作是 await 后的异步代码,需要等待同步执行完后才能执行

执行示例:

async function asyncFn1() {
  console.log('asyncFn1 start'); // 2
  await asyncFn2();
  console.log('asyncFn1 wait'); // 5
  await asyncFn3();
  console.log('asyncFn1 end'); // 7
}

async function asyncFn2() {
  console.log('asyncFn2');  // 3
}

async function asyncFn3() {
  console.log('asyncFn3'); // 6
}

console.log('script start'); // 1
asyncFn1();
console.log('script end'); // 4

执行顺序已经由注释标记

分析:

  1. 首先执行第 17 行 ,输出 script start ,随后进入 asyncFn1 函数内;
  2. 执行第 2 行,输出 asyncFn1 start ,因为 async 会立马执行,因此进入 asyncFn2 函数;
  3. 执行第 10 行,输出 asyncFn2 ,退出 asyncFn2 函数;
  4. 如开头所说,在 await 后面执行的代码,都可以看作是 await 后的异步代码,需要等待同步执行完后才能执行,因此第 4 到 6 行都被认作是异步,需要等待同步代码首先执行,因此跳出 asyncFn1 函数;
  5. 执行 第 19 行,输出 script end,至此同步代码全部执行完毕;
  6. 通过 event loop 轮询,发现还有异步代码未执行;
  7. 执行第 4 行,输出 asyncFn1 wait,随后执行第 5 行,进入 asyncFn3 函数;
  8. 执行第 14 行,输出 asyncFn3,退出 asyncFn3 函数;
  9. 第 6 行本身是作为 第 5 行 await 的异步,等待同步代码;
  10. 后面已经无同步代码,因此执行第 6 行,输出 asyncFn1 end;

以上过程仅仅作为分析参考,如果已经完全理解了 async / await ,这一段分析就不需要看了。
 

5.6 for … of

forEach 常规的同步遍历
for … of 常用于异步的遍历,除此之外,for … in ,for,也支持异步

示例:

function asyncForFn(num) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(num);
    }, 1000)
  });
};

const arr = [1, 2, 3, 4, 5]

// 同步,结果会一下全部打印出来
// async function excute() {
//   arr.forEach(async (item) => {
//     const res = await asyncForFn(item);
//     console.log(res);
//   })
// }

// 异步,结果会隔一秒打印,直至打印完成
// async function excute() {
//   for (let item in arr) {
//     const res = await asyncForFn(arr[item]);
//     console.log(res);
//   }
// }

// 异步,结果会隔一秒打印,直至打印完成
// async function excute() {
//   for (let item of arr) {
//     const res = await asyncForFn(item);
//     console.log(res);
//   }
// }

// 异步,结果会隔一秒打印,直至打印完成
async function excute() {
  for (let i = 0; i < arr.length; i += 1) {
    const res = await asyncForFn(arr[i]);
    console.log(res);
  }
}

excute();

 

注1:for…in 和 for…of的区别

1.for…in 支持遍历对象;但for…of只能遍历数组
2.for…in 会将数组原型上自定义的属性和方法也遍历出来;而for…of 不会,因此不建议使用 for…in 来遍历数组(但是如下面示例,可以使用 hasOwnProperty 来进行筛选,就只会选出当前数组的下标)

const arr = [1,2,3];
Array.prototype.arr1 = function(){
    console.log('consoleLog string');
}
Array.prototype.arr2 = 'stringOrNumber';
for(let index in arr){
    // if (arr.hasOwnProperty(index)) {
    //   console.log(index, arr[index]);
    // }
    console.log(index, arr[index]);
}
// 可以看除用 for in遍历数组,会将数组原型下自定义的的属性和方法也遍历出来
// 打印结果
// 0 1
// 1 2
// 2 3
// arr1 ƒ (){
//     console.log('consoleLog string');
// }
// arr2 stringOrNumber

注2:使用 for … in 遍历对象

Object.prototype.c = function() {};
const obj = {
  a: 1,
  b: 2,
}
for (let key in obj) {
  // if (obj.hasOwnProperty(key)) {
  //   console.log(key, obj[key]);
  // }
  console.log(key, obj[key]);
}
// 输出 a 1, b 2, c ƒ () {}

可以看出,使用for … in 遍历对象,会将对象原型上自定义的属性和方法也遍历出来,当然,可以像代码中注释的那样,使用 hasOwnProperty 来筛选属于本对象的 key 。
 

5.7 宏任务 和 微任务

宏任务是由宿主(node,浏览器)发起的,而微任务由JavaScript自身发起。
 
1.示例题

console.log('start');
setTimeout(() => {
  console.log('setTimeout');
});
Promise.resolve().then(() => {
  console.log('promise');
});
console.log('end');

输出顺序:start, end, promise,setTimeout
为什么会这样输出,接着往下看
 
2.宏任务和微任务有哪些
宏任务:setTimeout,setInterval,ajax,DOM事件(不是异步,但依赖于event loop)
微任务:promise,async / await
 
3. 宏任务和微任务执行时机

结论:微任务执行时机要比宏任务早
 
4.event loop 和 DOM 渲染的关系
event loop 和 DOM 渲染的关系图
再来看这张图:

之前讲到,当 call stack 空闲(同步代码执行完),会触发 event loop 来查询是否有异步代码在等待执行。

实际上,在每次 call stack 空闲之后,触发 event loop 之前,都是 DOM 重新渲染的机会,DOM 结构如有改变,则会先进行 DOM 渲染( JS 和 DOM渲染共用一个线程),再进行 event loop 。
 
5.宏任务和微任务执行时机的区别

微任务:DOM 渲染前触发,如 promise
宏任务:DOM 渲染触发后,如setTimeout
 

结合event loop 和 DOM 渲染的关系,可以得到完整的执行顺序
结合event loop 和 DOM 渲染的关系,可以得到完整的执行顺序
再来看这道题:

console.log('start');
setTimeout(() => {
  console.log('setTimeout');
});
Promise.resolve().then(() => {
  console.log('promise');
});
console.log('end');

分析:

  1. 执行第一行,输出 start;

  2. 遇到 setTimeout ,放进 web APIs,开始计时,计时完成放进 callback queue 中等待执行;

  3. 遇到 promise ,放进 micro queue 中,等待执行;

  4. 执行完同步代码,首先从 micro queue 轮询是否有微任务,执行第 6 行,输出 promise;

  5. 微任务执行完,从 callback queue 轮询是否有宏任务,执行第 3 行,输出 setTimeout;
    至此,程序执行结束

为了避免篇幅过长,面试题的解答部分会放在下一篇 JS 异步(下)答案分析中!

欢迎大家点赞,收藏,关注!!!

  • 8
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值