来聊一道前端面试题吧

8,221 阅读6分钟

前言

金三银四,技术论坛上诸如:阿里、头条、腾讯….面经层出不穷,朋友圈很多小伙伴都在找工作也遇到了各种各样的麻烦。本文希望那些在准备面试的过程中蕉绿的童鞋别僵化了自己的思维,以自己曾经遇见到一道面试题为引,用自己对待问题的想法行文,天马行空,从僵硬的知识点中跳脱出来一起思考,内容简单易懂。评论区有很多的同学留下了许多很棒的思路,大家不要错过哟,欢迎大家一起继续交流学习。

const fucArr = [
	next => {
		setTimeout(() => {
			console.log(1);
			next()
		}, 300)
	},
	next => {
		setTimeout(() => {
			console.log(2);
			next()
		}, 200)
	},
	next => {
		setTimeout(() => {
			console.log(3);
			next()
		}, 100)
	}
]

var run = arr=>{

  

}
// 实现一个run方法,使得run(fucArr)能顺序输出1、2、3.

题目简析

我们观察 fucArr 每一个子项都具有如下结构:

  1. 接收一个方法 next
  2. 有一个计时器,计时器回调方法体内对应着相应的输出
  3. 输出结束调用 next 方法。

他们的差异就是:计时器时间逐个减少。

直接循环调用 3 个方法肯定是不可取的。为了能按序输出,函数的执行过程应该是上一个函数 console 之后, 再执行下一个函数,而接收的这个 next参数就是执行下一个方法的关键。因为是头到尾依次调用,我们就把fucArr 称之为一个队列。

思路一、

我们假象自己是个编译器,然后把执行的过程进行单步拆解。

  1. fucArr 是做先执行等待队列第一个,等待中的函数队列为原函数队列的slice(1);
  2. 等待next执行后,然后又执行等待函数队列的第一个函数,等待中的函数队列为原函数队列的slice(1);

听着是不是很像一个递归的过程,没错,那我们先用递归来实现一下

var run = arr => {
	// 递归语句千万条,找到出口第一条,那咱们判断递归出口的条件就是等待队列为空
	if (arr.length === 0) return;
	// 好的,一句话执行过程写完了
	arr[0](() => run(arr.slice(1)));
}
run(fucArr)

// 1 2 3;

思路二、

现在我们从递归的思路中跳脱出来,换种思路继续思考.....

上一个函数执行到某个时机触发了下一个函数的执行。

也就是说上一个函数 trigger,下一个函数才开始执行。

根据描述 trigger 实际上做的就是触发等待队列的第一个函数的执行,因此我们可以如下定义。

var run = arr => {
	const trigger = () => {
		if (arr.length === 0) return;
		arr.shift()();
	}
}

那么 trigger 何时进行调用呢?很显然, 上一个函数式通过next 去触发下一个函数调用,因此 trigger 应该就是函数接收的next,我们为了方便参数绑定需要重构一下咱们的等待队列函数。当然不要忘了,首次执行要手动trigger一下喔。

var run = arr => {
	const trigger = () => {
		if (arr.length === 0) return;
		arr.shift()();
	}
	arr = arr.map(val => {
		return () => val(trigger);
	})
	trigger();
}

其实做参数绑定还有一种更优雅一点的方式,bind,所以大家注意咯,bind不单单能绑定this喔。

我们可以稍微改动一下:

var run = arr => {
	const trigger = () => {
		if (arr.length === 0) return;
		arr.shift()();
	}
	arr = arr.map(val => {
		return val.bind(null, trigger);
	})
	trigger();
}

都9102年了,既然是前端面试那肯定少不了Promise 的对吧,那我们可不可以掺入一些Promise的元素在里面呢?答案是必然的。

根据Promise的特性,当本身状态改变,去触发then里的方法(这里不要深究这句话,意思了解就好)。是resolve 作为本身状态改动的方法。那状态改变是去做什么事呢?好的,没错trigger。那何时状态改变呢?上一个函数next调用的时候。

var run = arr => {
	const trigger = () => {
		if (arr.length === 0) return;
		arr.shift()();
	}
	arr = arr.map(val => {
		return () => new Promise(resolve => {
			val(resolve)
		}).then(trigger);
	})
	trigger();
}

redux的思路、

现在继续清空上面的思路,不要被干扰。

首先给 applymiddleware(以下简称amw)一个简单的定义,amw是接收若干个函数作为参数,最终会返回一个函数,这个函数调用,会按照顺序,依次执行前面作为参数传入的函数。为了不把问题复杂化,请接收我的误导引导,不要怀疑。

以下是作为参数传入的函数要求的结构以下称a结构:

store=>next=>action=>{
	// dosomething...
	next()
}

a结构在第一次调用时,会返回一个方法,第二次调用时返回第二个方法,我们先来看源码的一个操作过程。

const chain = middlewares.map(middleware => middleware(middlewareAPI))

首先是一层循环调用,使得函数体变为b结构:

next=>action=>{
	// dosomething...
	next()
}

这样做是为了以闭包的形式在 dosomething 中能够使用到 middlewareApi

根据b结构我们可以稍稍改变下原题 :

const fucArr = [
	next => action => {
		setTimeout(() => {
			console.log(action++);
			next(action)
		}, 300)
	},
	next => action => {
		setTimeout(() => {
			console.log(action++);
			next(action)
		}, 200)
	},
	next => action => {
		setTimeout(() => {
			console.log(action++);
			next(action)
		}, 100)
	}
]

var run = arr=>{

  

}
// 实现一个run方法,run方法接收fucArr为参数;返回一个函数,这个函数接收一个参数1,最终,依次输出1、2、3
// run(fucArr)(1) => 1 2 3

变题相对于多了一个参数传递的过程,实际上我们需要顺序执行的其实是结构c:

action=>{
    // dosomething...
	next()
}

这些关键还是要如何构建每个函数接收的参数next

我们做如下假设,当fucArr只有一个函数时 返回的就应该是:

fucArr[0](()=>{}) // 为了避免报错,next应为一个空函数
// 即:
action => {
    setTimeout(() => {
        console.log(action++);
        //(()=>{}) 这玩意儿就是接收的next
        (()=>{})(action)
    }, 300)
}

fucArr有两个函数时返回:

fucArr[0](fucArr[1](()=>{}))
// 即:
action => {
    setTimeout(() => {
        console.log(action++);        
		fucArr[1](()=>{})(action)
    }, 300)
}

当有三个函数的时返回:

fucArr[0](fucArr[1](fucArr[2](()=>{}))

仔细观察返回函数的结构发现,所有的函数都是接受上一个函数调用后的返回值(以下称模式1),最后一个函数接收的是一个空函数。我们尝试构建模式1:

// 首先初始想法模型是这样的
// 但是由于咱们是程序执行,不能像上面咱们描述问题的时候,继续往next里塞函数。
// 而在遍历到 next 的下一个函数的时当前是无法明确next应该是什么,因此我们需要将模式改变一下。
pre(next());
// 当遍历到next下一个节点时,把当前函数作为arg传入进来
arg=>pre(next(arg))

pre + next + 遍历,这三个关键词没错,就是reduce。因此:

var reduceResult = fucArr.reduce((pre,next)=> (...arg)=>pre(next(...arg)));
// 我们发现这个返回的还是一个 arg=>pre(next(arg)) 这样模式的函数,接收的参数任然是一个函数。
// 于是乎真的需要返回的函数其实是 
return reduceResult(()=>{});

所以最终形态是

var run = arr=>{
	var reduceResult = arr.reduce((pre,next)=> (...arg)=>pre(next(...arg)));
	return reduceResult(()=>{});
}
run(fucArr)(1);
// 1 2 3

总结

其实还可以聊下expresskoa中间件compose的思路,但是没有必要(汪汪大笑.gif)。本文主旨也不是灌输这个题目的解法,只是希望大家将来在面试和工作中遇到问题尝试着用自己构建的知识体系去解决积极面对,最后祝小伙伴们找工作顺利。