面试 | JS 异步编程经典面试题

原文地址

掘金

欢迎 GitHub star

大家好,我是林一一,异步编程在 JS 中是无法避免的,也是面试必问的。本文使用通俗易懂的语言解析异步编程中的原理,开始阅读吧😁

思维导图

未命名文件.png

一、定时器

定时器:设定一个定时器,到了设定时间,浏览器会把对应的方法执行。每一个定时器执行后都会有一个编号返回,每个定时器编号不一样。

1. 设定定时器

  • setTimeout([function], [interval])

function 都是在到达设定时间后才执行。且执行一次

let count = 1
let timer = setTimeout(function(){
    count++
    console.log(count)  // 2
}, 1000)
console.log(timer)  // 1
  • setInterval([function], [interval])

在设定时间内执行,不主动停止的情况下一直执行。

let count = 1
let timer = setInterval(function(){
    count++
    console.log(count)  // 2
}, 1000)
console.log(timer)  // 1
/*
*   2
*   3
*   4
*   ...
*/

2. 清除定时器

clearTimeout/clearInterval 两者都可以清除上面的两种定时器。

  • 如何清除定时器?

只需要定时器的返回值编号清除即可。

let count = 1
let timer = setInterval(function(){
    count++
    console.log(count)
    // count == 3 ? clearTimeout(timer) : null
    count == 3 ? clearInterval(timer) : null
}, 1000)

二、异步编程的原理

先来看一个小例子

let a = 0
setTimeout(() =>{
    console.log('a', ++a)
}, 0)
console.log(a)
/* 输出
*   0
*   1
*/

上面的例子中,setTimeout 是异步的,浏览器会将异步的代码加入到任务队列中,等到同步的代码执行完成后才执行异步的代码

1. 同步

JS 是单线程的,代码至上而下执行时遇到同步的代码需要先执行完才可以进行下一步任务。比如循环等

2. 异步

所有需要等待的任务都是异步的。遇到异步代码时,不需要等待而是直接异步任务放入任务队列,等到后面的任务完成后,才会返回来执行没有完成异步的代码。比如事件绑定,所有定时器,ajax 的异步处理,部分回调函数,浏览器的渲染过程等等。

let a = 0
setTimeout(() =>{
    console.log('a', ++a)
}, 0)
console.log(a)
while(true){
}

上面的代码死循环了,即使定时器的时间到了也不会执行。因为同步的代码没有执行完一步就不会执行。

三、promise

1.基本概念

Promise 只是一个管理异步编程的类,本身是同步。Promise 有三个状态 pending/fulfilled/rejected,三个状态只有两个状态出现要么成功要么失败。new Promise()时必须要传入回调函数 executor,否则报错。其中回调函数中有两个参数 resolve, reject,这两个参数可不写。

  • pending :初始化状态,开始执行异步的任务,只要执行 new,new Promise(()=>{}),promise 的状态就会变成 pending
  • fulfilled:成功状态,执行 resolve()
  • rejected:失败状态, 执行 rejected()
    先看一个小栗子。
new Promise(()=> {
    setTimeout(()=> {
        console.log(1)
    }, 0)
    console.log(2)
}).then()
console.log(3)
/* 输出
*   2
*   3
*   1
*/

创建一个新的 Promise 的实例也就是 new 这个过程中会把 Promise 中 的函数先执行(不清楚 new 创建实例的过程中发生了什么可以看看这篇 面试 | 你不得不懂得 JS 原型和原型链)。函数体内有异步操作的仍会加入任务队列,等到同步执行完成后才执行异步任务,比如函数体内的 setTimeout 函数。所以输出的结果就是 2,3,1

再来看一个小栗子

const promise = new Promise((resolve, reject) => {
  resolve('success1')
  reject('error')
  resolve('success2')
})

promise.then((res) => {
    console.log('then: ', res)
}).catch((err) => {
    console.log('catch: ', err)
}
// then: success1

promise 的状态只能改变一次,最后的状态不是 fulfilled 就是 rejected。也就是说同一个 promise 中的resolve()/ reject()只能执行一个且一次。

2. promise 是怎么管理异步的

promise 参数的回调函数体内接收两个参数 resolve/ reject,这两个参数可以作为两个回调函数。

  1. resolve():是异步操作执行成功后执行,promise 的状态变成了 fulfilled,可以提供返回值,在 .then()中第一个参数接收
  2. reject():异步操作执行失败后执行,promise 的状态变成了 rejected,可以提供返回值,在 .then() 中第二个参数接收
    resolve()reject() 中只能传递一个参数。promise 的状态发生改变后不会再变化。
    resolve() 和 reject() 是异步操作,执行这两个方法时,会先执行 resolve/reject 下面的同步代码,等到主任务为空时,再去调用 resolve/reject 把存放的方法执行。
    Promise 对象上有私有属性Promise.resolve()/ Promise.reject()等。
    举一个没什么意义的小栗子
new Promise((resolve, reject)=> {
    setTimeout(()=> {
        console.log('林')
        resolve('ok')
        // reject('fail')
        console.log('一一')
    }, 0)
}).then( res => {
    console.log('status:', res)
}, res => {
    console.log('status:', res)
})
// 林 
// 一一 
// ok

上面的栗子直接看出 resolve/reject 是异步的。

3. promise.then(onfulfilled, onrejected) / promise.catch()

  • promise.then(onfulfilled, onrejected)
  1. promise.then() 方法中有两个参数,分别对应着 promise 的两种不同的状态,fulfilled, rejected。对应的状态执行对应的方法。
  2. promise.then() 中的参数是函数,如果传递的不是函数就会造成值穿透,也就是resolve()/reject() 的返回值会.then()中接收。
  3. promise.then() 能够链式的调用,能够链式调用的原因不是.then() 方法中有return this,而是每一个 .then() 方法中都会返回一个新的Promise 实例。
  • promise.catch()
  1. promise.catch()promise.then() 第二个参数的简便写法,也就是用来捕获 reject() 执行后的 rejected状态。
  2. .catch()也可以实现链式调用,原因和.then() 方法一样都是返回了一个新的 promise

.then()/.catch() 中的返回值都不能是 promise自己本身的实例,因为会造成死循环

热身题

1.举一个小栗子

Promise.resolve(1)
    .then((res) => {
        console.log(res)
        return 2
    }).catch(err => {
        console.log(err)
    }).then( res => {
        console.log(res)
    })
// 1 2

最终输出 1 2,

2. 举一个值穿透的小栗子

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log)

// 等价于
// Promise.resolve(1)
//   .then(console.log)

3. then()/.catch() 内不能返回自己本身的promise 实例,举一个栗子

let pro = Promise.resolve()
  .then(() => {
    console.log('promise', pro)
    return pro
  })
// Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>

创建的promise 实例是pro,返回值就不能是pro, 否则会造成死循环。

5. Promise.all([promise1, promise2...])

Promise.all() 中需要等待参数中所有 promise 的状态都成功才执行回调的.then(),如果有一个是失败的那么就执行 .catch()。接收的参数是一个包含 promise 实例的数组,.all() 这个方法的返回值也是一个新的promise 实例。

  • 全部执行成功回调 .then() 接收到的就是一个数组
  • 如果有执行失败的 promise 状态,回调 .catch 中就会捕获到执行失败的 promisepromise.all() 执行结束。
var p1 = Promise.resolve(1)
var p2 = Promise.resolve(2)
var p3 = Promise.resolve(3)
let pro =  Promise.all([p1, p2, p3])
  .then(res => {
      console.log(res)  //  [1, 2, 3]
  })
  .catch( err => {
      console.log(err)
  })
console.log(pro)    // Promise {<pending>}

全部执行成功,那么.then() 获取到的值就是resolve() 的返回值数组。

6. Promise.race()

.race() 的作用也是接收一组异步任务,然后并行执行异步任务,只保留取一个最快执行完成的异步操作的结果,其他的方法仍在执行,不过执行结果会被抛弃。

  • 接收的是一组数组,只获取一个最快执行完成 resolve()/rejected() 的返回值,返回值不是一个数组
var p1 = new Promise(function(resolve, reject) {
    setTimeout(reject, 500, "one");
});
var p2 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 100, "two");
});

Promise.race([p1, p2]).then(function(value) {
  console.log(value); // "two"
});

两个都完成,但 p2 更快

7. Promise.finally()

.finally 方法也是返回一个 Promise,他在 Promise 结束的时候,无论结果为 resolved 还是 rejected,都会执行里面的回调函数。

let promise = new Promise((resolve, reject) => {
    reject('error')
}).then( res => {
    console.log('then', res)
    return res
}).catch( err => {
    console.log('catch', err)   // `catch error`
     return err
}).finally( () => {
    console.log('finally')  //'finally'
})

热身题

热身题1

const promise = new Promise((resolve, reject) => {
  console.log(1)
  resolve()
  console.log(2)
})
promise.then(() => {
  console.log(3)
})
console.log(4)
// 1, 2, 4, 3

因为 resolve() 是异步的,promise.then也是异步的,在没有获取到 resolve()fulfilled 状态时.then() 不会执行。

热身题2

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('once')
    resolve('success')
  }, 1000)
})

const start = Date.now()
promise.then((res) => {
  console.log(res, Date.now() - start)
})
promise.then((res) => {
  console.log(res, Date.now() - start)
})
/* 输出
* once
* success 1002
* success 1002
*/

promise.then() 即使有多个,但是resolve()/reject()后的调用时同时执行的。所以同时输出了 success 1002

三、async 和 await

先来看一个栗子

function fn(){
    return new Promise((resolve, reject) => {
        setTimeout( () => {
            Math.random() < 0.5 ? resolve('resolve 001') : reject('reject 002')
        }, 0)
    })
}

async function get() {
    let res = await fn()
    console.log(res)
    console.log(1212)
}
get()

1. async

  1. asyncawait 是 ES7 中增加来对 promise 操作的方法,是 ES7 系列提供的语法糖,await 不能单独使用一定要结合 async 来使用。
  2. async 会返回一个 promise 对象,async 函数调用不会造成代码的阻塞

2. await

  1. await 是用来 等待获取 一个 promiseresolve/reject 的执行结果,像上面的 let res = await fn() 是先把 fn()执行后,来获取 resolve/reject 返回的结果,不过 await 后面也可以不跟着一个 promise,但是这样写就没有意义了。
  2. await 或 await fn() 这个操作不是同步的,而是异步的。await 下面的代码不会执行,而是移入到任务队列等待区,等到主栈中的其他任务完成且 fn() 中的 promise 将结果返回,await 下面的代码才可以重新回到主栈中执行。await 可以使 promise 的操作更加像同步的代码。
  3. 如果 await 等到的获取结果是 reject() 返回的,那么 await 下面的代码,就不会再执行,因为已经报错了。
    举一个小栗子,说明 async/await 是语法糖
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
// 上面的代码等价于 ==>
async function async1() {
    console.log('async1 start');
    Promise.resolve(async2()).then(() => {
        console.log('async1 end')
    })
}

思考

热身1,await 是同步吗?,求输出的结果

console.log(1)
function fn(){
    return new Promise((resolve, reject) => {
        console.log(5)
        resolve('resolve 001')
        console.log(6)
    })
}

async function get() {
    console.log(2)
    let res = await fn()
    console.log(res)
    console.log(3)
}
get()
console.log(4)
//1, 2, 5, 6, 4, resolve 001, 3

await 是异步的同时会让出线程,fn() 执行后线程开始让出,那么 await 的后面的代码不会立即执行会先到主栈的等待区,主栈中 console.log(4) 执行后,再回来执行 await 处的代码。

2. 热身2,求输出结果

console.log(1)
async function get() {
    console.log(2)
    let res = await 200
    console.log(res)
    console.log(3)
}
get()
console.log(4)
// 1, 2, 4, 200, 3

根据上面一题 await 异步代码的原因,可以容易的分析出答案。同时说明 await 后面也可以跟着一个非 promise 的实例。

四、经典面试题

1. promise 的优缺点/为什么使用 promise?

  • 优点:promise 可以解决回调地狱,promise 大大增强了嵌套函数的可读性和可维护性,
  • 缺点:无法取消 Promise,错误需要通过回调函数来捕获;如果不设置回调函数,Promise 内部抛出的错误,不会反映到外部;当处于 pending(等待)状态时,无法得知目前进展到哪一个阶段,是刚刚开始还是即将完成

2. setTimeout、Promise、Async/Await 的区别

  • setTimeout: setTimeout 的回调函数放到宏任务队列里,等到执行栈清空以后执行
  • Promise: Promise 本身是同步的立即执行函数,当在 executor 中执行 resolve或者 reject 的时候,此时是异步操作,会先执行 then/catch 等,当主栈完成时,才会去调用 resolve/reject 方法中存放的方法。
  • async: async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。

3. 实现一个 sleep 函数,比如 sleep(1000) 意味着等待 1000 毫秒。

function sleep1(time) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve();
        }, time);
    })
}
sleep1(1000).then(() => console.log("sleep1"));

4. Promise 构造函数是同步执行还是异步执行,那么 then 方法呢

Promise 是同步的,执行 new promise(callback) 时回调函数callback 就会被立即执行,then() 方法是异步的。

const promise = new Promise((resolve, reject) => {
    console.log(1)
    resolve()
    console.log(2)
})
promise.then(() => {
    console.log(3)
})
console.log(4)

1243,promise 构造函数是同步执行的,then 方法是异步执行的

5. 介绍下 Promise.all 使用、原理实现及错误处理

const p = Promise.all([p1, p2, p3]);

Promise.all 方法接受一个数组作为参数,p1、p2、p3 都是 Promise 实例,如果不是,就会先调用下面讲到的 Promise resolve 方法,将参数转为 Promise 实例,再进一步处理。(Promise.all 方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。)

5. 介绍下 Promise.all, Promise.race() 的区别

看上面的介绍

Promise 的各种 api 实现会在下一篇文章中实现。给个期待吧😂

五、参考

Promise 必知必会(十道题)

BAT前端经典面试问题:史上最最最详细的手写Promise教程

MDN async/await

六、结束

感谢阅读到这里,如果着篇文章能对你有一点启发或帮助的话欢迎 github star, 我是林一一,下次见。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • 夜莺2517阅读 127,695评论 1 9
  • 版本:ios 1.2.1 亮点: 1.app角标可以实时更新天气温度或选择空气质量,建议处女座就不要选了,不然老想...
    我就是沉沉阅读 6,863评论 1 6
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,503评论 28 53
  • 兔子虽然是枚小硕 但学校的硕士四人寝不够 就被分到了博士楼里 两人一间 在学校的最西边 靠山 兔子的室友身体不好 ...
    待业的兔子阅读 2,570评论 2 9