博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Promise面试题
阅读量:6979 次
发布时间:2019-06-27

本文共 10375 字,大约阅读时间需要 34 分钟。

Promise面试题1

有这样一道关于promise的面试题,描述如下:

页面上有一个输入框,两个按钮,A按钮和B按钮,点击A或者B分别会发送一个异步请求,请求完成后,结果会显示在输入框中。

题目要求,用户随机点击A和B多次,要求输入框显示结果时,按照用户点击的顺序显示,举例:

用户点击了一次A,然后点击一次B,又点击一次A,输入框显示结果的顺序为先显示A异步请求结果,再次显示B的请求结果,最后再次显示A的请求结果。

UI界面如图:

这个需求该如何用promise来实现呢?代码如下:

//dom元素            var a = document.querySelector("#a")            var b = document.querySelector("#b")            var i = document.querySelector("#ipt");            //全局变量p保存promie实例            var P = Promise.resolve();            a.onclick  = function(){                //将事件过程包装成一个promise并通过then链连接到                //全局的Promise实例上,并更新全局变量,这样其他点击                //就可以拿到最新的Promies执行链                P = P.then(function(){                    //then链里面的函数返回一个新的promise实例                    return new Promise(function(resolve,reject){                        setTimeout(function(){                            resolve()                            i.value = "a";                        },1000)                    })                })            }            b.onclick  = function(){                P = P.then(function(){                    return new Promise(function(resolve,reject){                        setTimeout(function(){                            resolve()                            console.log("b")                            i.value = "b"                        },2000)                    })                })            }复制代码

我们用定时器来模拟异步请求,仔细于阅读代码我们发现,在全局我们定义了一个全局P,P保存了一个promise的实例。

然后再观察点击事件的代码,用户每次点击按钮时,我们在事件中访问全局Promise实例,将异步操作包装到成新的Promise实例,然后通过全局Promise实例的then方法来连接这些行为。

连接的时候需要注意,then链的函数中必须将新的promise实例进行返回,不然就会执行顺序就不正确了。

需要注意的是,then链连接完成后,我们需要更新全局的P变量,只有这样,其它点击事件才能得到最新的Promise的执行链。

这样每次用户点击按钮就不需要关心回调执行时机了,因为promise的then链会按照其连接顺序依次执行。

这样就能保证用户的点击顺序和promise的执行顺序一致了。

Promise面试题2

按照要求:

实现 mergePromise 函数,把传进去的函数数组按顺序先后执行,并且把返回的数据先后放到数组 data 中。

代码如下:

const timeout = ms => new Promise((resolve, reject) => {    setTimeout(() => {        resolve();    }, ms);});const ajax1 = () => timeout(2000).then(() => {    console.log('1');    return 1;});const ajax2 = () => timeout(1000).then(() => {    console.log('2');    return 2;});const ajax3 = () => timeout(2000).then(() => {    console.log('3');    return 3;});const mergePromise = ajaxArray => {    // 在这里实现你的代码};mergePromise([ajax1, ajax2, ajax3]).then(data => {    console.log('done');    console.log(data); // data 为 [1, 2, 3]});// 要求分别输出// 1// 2// 3// done// [1, 2, 3]复制代码

分析:

timeout是一个函数,这个函数执行后返回一个promise实例。

ajax1 、ajax2、ajax3 都是函数,不过这些函数有一些特点,执行后都会会返回一个 新的promise实例。

按题目的要求我们只要顺序执行这三个函数就好了,然后把结果放到 data 中,但是这些函数里都是异步操作,想要按顺序执行,然后输出 1,2,3并没有那么简单,看个例子。

function A() {  setTimeout(function () {      console.log('a');  }, 3000);}function B() {  setTimeout(function () {      console.log('b');  }, 1000);}A();B();// b// a复制代码

例子中我们是按顺序执行的 A,B 但是输出的结果却是 b,a 对于这些异步函数来说,并不会按顺序执行完一个,再执行后一个。

这道题主要考察的是Promise 控制异步流程,我们要想办法,让这些函数,一个执行完之后,再执行下一个,代码如何实现呢?

// 保存数组中的函数执行后的结果var data = [];// Promise.resolve方法调用时不带参数,直接返回一个resolved状态的 Promise 对象。var sequence = Promise.resolve();ajaxArray.forEach(function (item) {    // 第一次的 then 方法用来执行数组中的每个函数,    // 第二次的 then 方法接受数组中的函数执行后返回的结果,    // 并把结果添加到 data 中,然后把 data 返回。    sequence = sequence.then(item).then(function (res) {        data.push(res);        return data;    });})// 遍历结束后,返回一个 Promise,也就是 sequence, 他的 [[PromiseValue]] 值就是 data,// 而 data(保存数组中的函数执行后的结果) 也会作为参数,传入下次调用的 then 方法中。return sequence;复制代码

大概思路如下:全局定义一个promise实例sequence,循环遍历函数数组,每次循环更新sequence,将要执行的函数item通过sequence的then方法进行串联,并且将执行结果推入data数组,最后将更新的data返回,这样保证后面sequence调用then方法,如何后面的函数需要使用data只需要将函数改为带参数的函数。

Promise面试题3

题目是这样的:

有 8 个图片资源的 url,已经存储在数组 urls 中(即urls = ['http://example.com/1.jpg', …., 'http://example.com/8.jpg']),而且已经有一个函数 function loadImg,输入一个 url 链接,返回一个 Promise,该 Promise 在图片下载完成的时候 resolve,下载失败则 reject。

但是我们要求,任意时刻,同时下载的链接数量不可以超过 3 个

请写一段代码实现这个需求,要求尽可能快速地将所有图片下载完成。

已有代码如下:

var urls = [    'https://www.kkkk1000.com/images/getImgData/getImgDatadata.jpg',     'https://www.kkkk1000.com/images/getImgData/gray.gif',     'https://www.kkkk1000.com/images/getImgData/Particle.gif',     'https://www.kkkk1000.com/images/getImgData/arithmetic.png',     'https://www.kkkk1000.com/images/getImgData/arithmetic2.gif',     'https://www.kkkk1000.com/images/getImgData/getImgDataError.jpg',     'https://www.kkkk1000.com/images/getImgData/arithmetic.gif',     'https://www.kkkk1000.com/images/wxQrCode2.png'];function loadImg(url) {    return new Promise((resolve, reject) => {        const img = new Image()        img.onload = function () {            console.log('一张图片加载完成');            resolve();        }        img.onerror = reject        img.src = url    })};复制代码

看到这个题目的时候,脑袋里瞬间想到了高效率排队买地铁票的情景,那个情景类似下图:

上图这样的排队和并发请求的场景基本类似,窗口只有三个,人超过三个之后,后面的人只能排队了。

首先想到的便是利用递归来做,就如这篇文章采取的措施一样,代码如下:

//省略代码var count = 0;//对加载图片的函数做处理,计数器叠加计数function bao(){    count++;    console.log("并发数:",count)    //条件判断,urls长度大于0继续,小于等于零说明图片加载完成    if(urls.length>0&&count<=3){    //shift从数组中取出连接        loadImg(urls.shift()).then(()=>{        //计数器递减            count--            //递归调用            }).then(bao)    }}function async1(){//循环开启三次    for(var i=0;i<3;i++){        bao();    }}async1()复制代码

以上是最常规的思路,我将加载图片的函数loadImg封装在bao函数内,根据条件判断,是否发送请求,请求完成后继续递归调用。

以上代码所有逻辑都写在了同一个函数中然后递归调用,可以优化一下,代码如下:

var count = 0;   //当前正在进行数// 封装请求的异步函数,增加计数器功能function request(){    count++;    loadImg(urls.shift()).then(()=>{            count--            }).then(diaodu)    }// 负责调度的函数function diaodu(){    if(urls.length>0&&count<=3){        request();    }}function async1(){    for(var i=0;i<3;i++){        request();    }}async1()复制代码

上面代码将一个递归函数拆分成两个,一个函数只负责计数和发送请求,另外一个负责调度。

这里的请求既然已经被封装成了Promise,那么我们用Promise和saync、await来完成一下,代码如下:

//省略代码// 计数器var count = 0;// 全局锁var lock = [];var l = urls.length;async function bao(){    if(count>=3){        //超过限制利用await和promise进行阻塞;        let _resolve;        await new Promise((resolve,reject)=>{            _resolve=resolve;            // resolve不执行,将其推入lock数组;            lock.push(_resolve);        });    }    if(urls.length>0){        console.log(count);        count++        await loadImg(urls.shift());        count--;        lock.length&&lock.shift()()    }}for (let i = 0; i < l; i++) {    bao();}复制代码

大致思路是,遍历执行urls.length长度的请求,但是当请求并发数大于限制时,超过的请求用await结合promise将其阻塞,并且将resolve填充到lock数组中,继续执行,并发过程中有图片加载完成后,从lock中推出一项resolve执行,lock相当于一个叫号机;

以上代码可以优化为:

//省略代码// 计数器var count = 0;// 全局锁var lock = [];var l = urls.length;// 阻塞函数function block(){    let _resolve;    return  new Promise((resolve,reject)=>{        _resolve=resolve;        // resolve不执行,将其推入lock数组;        lock.push(_resolve);    });}// 叫号机function next(){    lock.length&&lock.shift()()}async function bao(){    if(count>=3){        //超过限制利用await和promise进行阻塞;        await block();    }    if(urls.length>0){        console.log(count);        count++        await loadImg(urls.shift());        count--;        next()    }}for (let i = 0; i < l; i++) {    bao();}复制代码

最后一种方案,也是我十分喜欢的,思考好久才明白,大概思路如下:

用 Promise.race来实现,先并发请求3个图片资源,这样可以得到 3 个 Promise实例,组成一个数组promises ,然后不断的调用 Promise.race 来返回最快改变状态的 Promise,然后从数组(promises )中删掉这个 Promise 对象实例,再加入一个新的 Promise实例,直到全部的 url 被取完。

代码如下:

//省略代码function limitLoad(urls, handler, limit) {    // 对数组做一个拷贝    const sequence = [].concat(urls)    let promises = [];    //并发请求到最大数    promises = sequence.splice(0, limit).map((url, index) => {        // 这里返回的 index 是任务在 promises 的脚标,        //用于在 Promise.race 之后找到完成的任务脚标        return handler(url).then(() => {            return index        });    });    (async function loop() {        let p = Promise.race(promises);        for (let i = 0; i < sequence.length; i++) {            p = p.then((res) => {                promises[res] = handler(sequence[i]).then(() => {                    return res                });                return Promise.race(promises)            })        }    })()}limitLoad(urls, loadImg, 3)复制代码

第三种方案的巧妙之处,在于使用了Promise.race。并且在循环时用then链串起了执行顺序。

15 行代码实现并发控制(javascript)

做过爬虫的都知道,要控制爬虫的请求并发量,其实也就是控制其爬取频率,以免被封IP,还有的就是以此来控制爬虫应用运行内存,否则一下子处理N个请求,内存分分钟会爆。

python爬虫一般用多线程来控制并发,

然而如果是node.js爬虫,由于其单线程无阻塞性质以及事件循环机制,一般不用多线程来控制并发(当然node.js也可以实现多线程,此处非重点不再多讲),而是更加简便地直接在代码层级上实现并发。

为图方便,开发者在开发node爬虫一般会找一个并发控制的npm包,然而第三方的模块有时候也并不能完全满足我们的特殊需求,这时候我们可能就需要一个自己定制版的并发控制函数。

下面我们用15行代码实现一个并发控制的函数。

首先,一个基本的并发控制函数,基本要有以下3个参数:

  • list {Array} - 要迭代的数组
  • limit {number} - 控制的并发数量
  • asyncHandle {function} - 对list的每一个项的处理函数

设计

以下以爬虫为实例进行讲解

设计思路其实很简单,假如并发量控制是 5

1.首先,瞬发 5 个异步请求,我们就得到了并发的 5 个异步请求

// limit = 5    while(limit--) {        handleFunction(list)    }复制代码
  1. 然后,这 5 个异步请求中无论哪一个先执行完,都会继续执行下一个list
let recursion = (arr) => {        return asyncHandle(arr.shift())            .then(()=>{                // 迭代数组长度不为0, 递归执行自身if (arr.length!==0) return recursion(arr)                 // 迭代数组长度为0,结束 elsereturn'finish';            })    }复制代码
  1. list所有的项迭代完之后的回调
returnPromise.all(allHandle)复制代码

代码

上述步骤组合起来,就是

/** * @params list {Array} - 要迭代的数组 * @params limit {Number} - 并发数量控制数 * @params asyncHandle {Function} - 对`list`的每一个项的处理函数,参数为当前处理项,必须 return 一个Promise来确定是否继续进行迭代 * @return {Promise} - 返回一个 Promise 值来确认所有数据是否迭代完成 */ let mapLimit = (list, limit, asyncHandle) => {    let recursion = (arr) => {        return asyncHandle(arr.shift())            .then(()=>{                if (arr.length!==0) return recursion(arr)   // 数组还未迭代完,递归继续进行迭代                else return 'finish';            })    };        let listCopy = [].concat(list);    let asyncList = []; // 正在进行的所有并发异步操作    while(limit--) {        asyncList.push( recursion(listCopy) );     }    return Promise.all(asyncList);  // 所有并发异步操作都完成后,本次并发控制迭代完成}复制代码

测试demo

模拟一下异步的并发情况

var dataLists = [1,2,3,4,5,6,7,8,9,11,100,123];var count = 0;mapLimit(dataLists, 3, (curItem)=>{    return new Promise(resolve => {        count++        setTimeout(()=>{            console.log(curItem, '当前并发量:', count--)            resolve();        }, Math.random() * 5000)      });}).then(response => {    console.log('finish', response)})复制代码

结果如下:

手动抛出异常中断并发函数测试:

var dataLists = [1,2,3,4,5,6,7,8,9,11,100,123];var count = 0;mapLimit(dataLists, 3, (curItem)=>{    return new Promise((resolve, reject) => {        count++        setTimeout(()=>{            console.log(curItem, '当前并发量:', count--)            if(curItem > 4) reject('error happen')            resolve();        }, Math.random() * 5000)      });}).then(response => {    console.log('finish', response)})复制代码

并发控制情况下,迭代到5,6,7 手动抛出异常,停止后续迭代:

转载自

你可能感兴趣的文章
OSChina 周四乱弹 —— 曾经有只老鹰冲着我提需求
查看>>
LinearLayout增加divider分割线
查看>>
游戏角度分析产品
查看>>
CSS清除浮动
查看>>
测试发现equals和hashCode与书上描述的不一样
查看>>
vue 插件
查看>>
了解和入门注解的应用
查看>>
数字图像处理课设
查看>>
No.2 条件
查看>>
39个超实用jQuery实例应用特效
查看>>
CI报Disallowed Key Characters的解决
查看>>
关于手机已处理里重复单据的处理办法
查看>>
IIS 7.5 + FastCGI + PHP + Drupal 7 + Oracle
查看>>
我的友情链接
查看>>
英文版PDF不能显示中文PDF文件的解决方法
查看>>
linux 内核 出错-HP 方案
查看>>
Linux
查看>>
HashSet的使用
查看>>
WSFC 仲裁模型选择
查看>>
nginx安装 问题 1
查看>>