前端场景应用类面试题,赶紧收藏系列(一)

403 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第22天,点击查看活动详情

  • 循环打印红黄绿
  • 实现每隔一秒打印1,2,3,4
  • 小孩报数问题
  • 用Promise实现图片的异步加载
  • 实现发布-订阅者模式

1. 循环打印红黄绿

下面来看一道比较典型的问题,通过这个问题来对比几种异步编程方法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯?

三个亮灯函数:

 function red() {
     console.log('red');
 }
 function green() {
     console.log('green');
 }
 function yellow() {
     console.log('yellow');
 }

这道题复杂的地方在于需要“交替重复”亮灯,而不是“亮完一次”就结束了。

(1)用 callback 实现

 const task = (timer, light, callback) => {
     setTimeout(() => {
         if (light === 'red') {
             red()
         }
         else if (light === 'green') {
             green()
         }
         else if (light === 'yellow') {
             yellow()
         }
         callback()
     }, timer)
 }
 task(3000, 'red', () => {
     task(2000, 'green', () => {
         task(1000, 'yellow', Function.prototype)
     })
 })

这里存在一个 bug:代码只是完成了一次流程,执行后红黄绿灯分别只亮一次。该如何让它交替重复进行呢?

上面提到过递归,可以递归亮灯的一个周期:

 const step = () => {
     task(3000, 'red', () => {
         task(2000, 'green', () => {
             task(1000, 'yellow', step)
         })
     })
 }
 step()

注意看黄灯亮的回调里又再次调用了 step 方法 以完成循环亮灯。

(2)用 promise 实现

 const task = (timer, light) => 
     new Promise((resolve, reject) => {
         setTimeout(() => {
             if (light === 'red') {
                 red()
             }
             else if (light === 'green') {
                 green()
             }
             else if (light === 'yellow') {
                 yellow()
             }
             resolve()
         }, timer)
     })
 const step = () => {
     task(3000, 'red')
         .then(() => task(2000, 'green'))
         .then(() => task(2100, 'yellow'))
         .then(step)
 }
 step()

这里将回调移除,在一次亮灯结束后,resolve 当前 promise,并依然使用递归进行。

(3)用 async/await 实现

 const taskRunner =  async () => {
     await task(3000, 'red')
     await task(2000, 'green')
     await task(2100, 'yellow')
     taskRunner()
 }
 taskRunner()

2. 实现每隔一秒打印 1,2,3,4

 // 使用闭包实现
 for (var i = 0; i < 5; i++) {
   (function(i) {
     setTimeout(function() {
       console.log(i);
     }, i * 1000);
   })(i);
 }
 // 使用 let 块级作用域
 for (let i = 0; i < 5; i++) {
   setTimeout(function() {
     console.log(i);
   }, i * 1000);
 }

3. 小孩报数问题

有30个小孩儿,编号从1-30,围成一圈依此报数,1、2、3 数到 3 的小孩儿退出这个圈, 然后下一个小孩 重新报数 1、2、3,问最后剩下的那个小孩儿的编号是多少?

 function childNum(num, count){
     let allplayer = [];    
     for(let i = 0; i < num; i++){
         allplayer[i] = i + 1;
     }
     
     let exitCount = 0;    // 离开人数
     let counter = 0;      // 记录报数
     let curIndex = 0;     // 当前下标
     
     while(exitCount < num - 1){
         if(allplayer[curIndex] !== 0) counter++;    
         
         if(counter == count){
             allplayer[curIndex] = 0;                 
             counter = 0;
             exitCount++;  
         }
         curIndex++;
         if(curIndex == num){
             curIndex = 0               
         };           
     }    
     for(i = 0; i < num; i++){
         if(allplayer[i] !== 0){
             return allplayer[i]
         }      
     }
 }
 childNum(30, 3)

4. 用Promise实现图片的异步加载

异步加载:也称为图片的预加载。利用js代码提前加载图片,用户需要时可以直接从本地缓存获取。

当用户在手机上打开了一个带有含大量图片的的页面,这个时候页面加载很有可能变的嘎嘎卡!因此,实现图片的异步加载可以提高用户的体验。

 let imageAsync=(url)=>{
             return new Promise((resolve,reject)=>{
                 let img = new Image();
                 img.src = url;
                 img.οnlοad=()=>{
                     console.log(`图片请求成功,此处进行通用操作`);
                     resolve(image);
                 }
                 img.οnerrοr=(err)=>{
                     console.log(`失败,此处进行失败的通用操作`);
                     reject(err);
                 }
             })
         }
         
 imageAsync("url").then(()=>{
     console.log("加载成功");
 }).catch((error)=>{
     console.log("加载失败");
 })

5. 实现发布-订阅模式

什么是发布-订阅模式?

此模式分为发布者和订阅者两个概念,发布者收集订阅者的需求,然后在某个时刻告知订阅者

举个例子,小古一直想买手机,但是心仪的手机价格太高,于是在购物平台上面点击“降价通知”,此为订阅降价的信息。当手机降价时,平台就会通知到个人收藏的宝贝降价啦,此为发布者模式。

 class EventCenter{
   // 1. 定义事件容器,用来装事件数组
     let handlers = {}
 ​
   // 2. 添加事件方法,参数:事件名 事件方法
   addEventListener(type, handler) {
     // 创建新数组容器
     if (!this.handlers[type]) {
       this.handlers[type] = []
     }
     // 存入事件
     this.handlers[type].push(handler)
   }
 ​
   // 3. 触发事件,参数:事件名 事件参数
   dispatchEvent(type, params) {
     // 若没有注册该事件则抛出错误
     if (!this.handlers[type]) {
       return new Error('该事件未注册')
     }
     // 触发事件
     this.handlers[type].forEach(handler => {
       handler(...params)
     })
   }
 ​
   // 4. 事件移除,参数:事件名 要删除事件,若无第二个参数则删除该事件的订阅和发布
   removeEventListener(type, handler) {
     if (!this.handlers[type]) {
       return new Error('事件无效')
     }
     if (!handler) {
       // 移除事件
       delete this.handlers[type]
     } else {
       const index = this.handlers[type].findIndex(el => el === handler)
       if (index === -1) {
         return new Error('无该绑定事件')
       }
       // 移除事件
       this.handlers[type].splice(index, 1)
       if (this.handlers[type].length === 0) {
         delete this.handlers[type]
       }
     }
   }
 }