一道美团的promise面试题,引发的思考

题目

原题已不记得,根据记忆复原如下:

const list = [1,2,3];
const YB = async(v) => {
    return new Promise(resolve => {
        setTimeout(resolve, 1000, v**2)
    })
};

list.forEach(async (v) => {
    let res = await YB(v);
    console.log(res);
});
复制代码

以下是笔者自己的思考:
首先res的值肯定是[1,2,3]三个值的平方1,4,9。因为知道await会把后面的同步代码包装在一个Promise.then()里异步执行,所以回答说一秒后打印出1,再隔一秒打印出4,再隔一秒打印出9。

面试的时候,心里也没有底。下来后自己写demo试运行。发现并不是这个样子。会打印出1,4,9。不过是等一秒钟后,打印出1,然后4和9紧接着就被打印出来,并不会等待一秒。

分析

笔者一开始对这个结果并不是很明白,好在经过一定分析与实践,总算是搞明白了。

串行执行

抛开题目,我们先写一个1,4,9分别间隔1秒打印的例子

const list = [1,2,3];
const YB = async(v) => {
    return new Promise(resolve => {
        console.log(v, 'clog');
        setTimeout(resolve, 1000, v**2)
    })
};

async function LIST() {
    for(let i = 1; i < 4; i++){
        let res = await YB(i);
        console.log(res);
    }
}

LIST();
复制代码

打印的结果如下

1 "clog"   //不用等待
1          //1秒后打印出
2 "clog"   //1秒后打印出
4          //2秒后打印出
2 "clog"   //2秒后打印出
9          //3秒后打印出
复制代码

这个例子并不难理解,按照高级程序设计上的说法,属于期约的串行执行,简单理解就是第一个期约执行完毕,再去执行下一个期约。(期约就是我们常说的Promise)。

并行执行

既然有串行执行,那么按照常识,应该会有并行执行,也确实有,高程上称为平行执行,理解为多个期约同时执行。

现在把上边的例子改成平行执行

const list = [1,2,3];
const YB = async(v) => {
    return new Promise(resolve => {
        console.log(v, 'clog');
        setTimeout(resolve, 1000, v**2)
    })
};

const promises = list.map(YB);

async function LIST() {
    for(const p of promises) {
        let res = await p;
        console.log(res);
    }
}

LIST();
复制代码

打印的结果如下

1 "clog"   //不用等待
2 "clog"   //不用等待
3 "clog"   //不用等待
1          //1秒后打印出
4          //1秒后打印出
9          //1秒后打印出
复制代码

两种执行的区别

对比这两种方式,我们可以看出区别。

  • 想要并行执行期约,那么Promise实例是需要有new操作的。什么时候会有Promisenew操作呢,示例中可以看出,是在YB函数中完成的。
  • 串行的例子中,YB函数是在await后边出现的,所以上一个期约执行(resolve)完,才会new下一个期约并执行,在进入new Promse的同步代码中,把setTimeout添加进事件队列。在串行中,3个Promise的new操作并不是同时进行的,所以setTimeout进入事件队列也不是同时的,开始计时的时刻也不一样。
  • 并行的例子中,通过list.map(YB)完成了Promse的实例化,并把这些Promise存入了LIST数组里,在这个时候,Promse里的setTimeout已经放入了事件队列,开始计时了。因为list.map(YB)里并没有await进行阻塞,所以3个Promise的实例化和3个setTimeout的添加进事件队列,可以认为是同时的(虽然实际上会有逻辑上的先后顺序),计时也是同时开始的。

forEach和for循环的区别

根据上边的并行执行的例子,我们可以看出,并行的执行效果和题目的执行效果基本一致。所以我们可以猜测题目中的代码,也属于并行执行的一种实现。

在对这个思考进行验证前,我们先看下forEach和for循环的区别。

for循环的特点

  • 通过break;终止整个循环;
  • 通过continue;跳过本次循环,继续执行后边的循环;
  • 直接在for循环里写return;会报错,因为return不能脱离函数作用域,return是对函数执行结果的返回;

forEach的特点

  • 不能执行break;continue;,因为这两个是对块作用域(不仅仅是for产生的块作用域,switch case也有类似的操作);
  • return是对最直接一级的函数执行结果进行返回,forEach的参数就是一个函数,所以return的行为,会类似for循环里的continue;;forEach一定会迭代完整个可迭代对象(含有迭代器的对象),不可中断迭代过程。
  • forEach对于数组中的empty值,是不会进行处理的,不会进入对应的循环体,所以循环次数不会因为empty值而增加。而for循环,一般是根据数组的length值来确定循环次数,所以会进入empty值对应的循环。这一点在后边可以明确

一般forEach不可中止迭代,元素有n个,一定会循环n次。但可以通过try catch实现中止,如

try{
    [1,2,3,4,6].forEach((v, i) => {
        if(i === 2){
            throw new Error('跳出')
        }
        console.log(v)
    });
}catch(e){console.log(e)}
复制代码

数组的empty值

有以下方法可以产生empty值:

const a = Array(5);//(5)[empty × 5]
const b = [1,,3,,,];//(5)[1, empty, 3, empty × 2]
复制代码

在控制台的打印值分别如下

image.png

image.png

这两个数组的length属性都为5,但empty值对应的位置是没有相应的数字属性的。如果访问a[0]会得到undefined

Array(5)[undefined,undefined,undefined,undefined,undefined]是不一样的,后者的控制台打印是

image.png

他们的属性数量是不一样的。

forEach的手写实现

这是自己的用for循环实现的forEach

Array.prototype.myForEach = function(fn) {
    const len = this.length;
    const tempArr = Object.keys(this);//可枚举的常规属性集合
    for(let i =0;i<len;i++) {
        if(tempArr.includes(i+'')){
            //不是empty的值,才执行到
            fn(this[i], i, this)
        }
    }
}
复制代码

其中Object.keys(this)获取数组的可枚举常规属性,includes方法检查对应的索引值,是否在这个集合里。关于对象属性遍历的方法,可参考遍历对象方法总结及思考

另外也可以通过Object.entriesfor of得到更简单的实现

Array.prototype.myForEach = function(fn) {
    const tempArr = Object.entries(this);
    for(const v of tempArr){
        fn(v[1], v[0], this)
    }
}
复制代码

因为Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组,而empty是不存在的,所以不会被包含在内。这样的实现和上边的实现区别在于,上边的会执行规定的this.length次的循环,而Object.entries()得到的值只包含可枚举的项,这个长度会小于等于this.length。

分解结果

从前边的手写forEach可以看出,forEach的参数(一个匿名函数),题目中是如下函数

async (v) => {
    let res = await YB(v);
    console.log(res);
}
复制代码

这个函数中异步代码的执行完毕与否,不会影响for循环的执行,如下,fn即是要执行的函数

for(let i =0;i<len;i++) {
    if(tempArr.includes(i+'')){
        //不是empty的值,才执行到
        fn(this[i], i, this)
    }
}
复制代码

这个循环过程都是在同步代码中执行的,虽然fn中包含异步执行的代码,但这个循环的结束和fn的中的异步代码执行完毕并没有关系,只要fn中的同步代码执行完毕即可(因为同步代码都在主线程里执行)。这个for循环执行的时间很短,可以认为是同时完成。new Promise和setTimeout的进入事件队列,都是在同步代码中完成的,和for循环一样都是在js的主线程中顺序执行。

到这里,我们最开始遇到的题目,便可看出,也是一种并行执行的实现,结果也就容易理解了。

笔者之所以没有答对,还是因为基础知识没复习好。并行执行和串行执行,在高程里都有详细的解答。

面试中不懂的问题记录

美团面试中其他的问题如下,
自己认为回答的好的问题:

  • webpack打包对文件进行hash值处理的原因
  • 介绍下浏览器缓存

其他的记得不是很清楚了。

最后列举自己回答的不好的几个问题,也是后面学习要逐个弄明白的知识点,如果读者感兴趣,也可以帮忙解答下

  • flex: 1;的真实值是啥?
  • vue3对diff算法的优化有哪些?
  • vue中key的作用是啥,有什么弊端吗?
  • 手写一个柯里化函数实现累加。(自己只说了思路,面试时不想去写)
  • 如何对页面性能进行分析,首字节时间?
  • vue-router路由切换的原理,onhashchange做了什么?
  • app和h5交互中,app如何知道h5最内的变化?
  • 对微前端的理解?
  • webpack5的了解
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享