摸鱼时间,手撸前端常见算法,持续更新...
感谢各位大佬的输出,参考文章详见:
- @全栈技匠:9k字 | Promise/async/Generator实现原理解析
- @写代码像蔡徐抻:各种源码实现,你想要的这里都有
- @小黄瓜没有刺:面试官直呼内行!如何实现一个比较完美的reduce函数?
1. 节流和防抖
1.1 什么是节流 / 防抖
防抖(
debounce)是指 事件在被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。节流(
throttle)是指 规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
1.2 手撕代码
1.2.1 防抖
function debounce(fun, delay) {
let timer;
return function() {
let that = this;
// 如果这个函数已经触发了
if(timer) {
clearTimeout(timer);
}
timer = setTimeout(function() {
// 透传 this 和 参数
fun.apply(that, arguments);
timer = null;
}, delay);
}
}
1.2.1 节流
function throttle(fun, delay) {
let timer;
return function() {
let that = this;
if(timer) {
return;
}
timer = setTimeout(function() {
timer = null;
fun.apply(that, arguments);
}, delay)
}
}
1.3 适用场景
防抖适用于:
- 搜索输入框:用户在不断输入值时,用防抖来节约请求资源;
- window 触发
resize时,不断的调整浏览器窗口大小会不断的触发这个时间,用防抖使其只能触发一次。节流适用于:
drag(拖动)事件或者scroll(滚动)事件,使用节流,使其触发,但是不那么频繁。
2. 实现 Promise
2.1 实现 Promise 的解析与整体代码
Promise的调用流程:
Promise的构造方法接收一个executor(),在new Promise()时就立刻执行这个executor回调;executor()内部的异步任务被放入宏 / 微任务队列,等待执行;then()被执行,收集成功 / 失败回调,放入成功 / 失败队列;executor()的异步任务被执行,触发resolve / reject,从成功 / 失败队列中取出回调依次执行。
Promise A+规范:
Promise本质是一个状态机,且状态只能是以下三种:Pending(等待态)、Fulfilled(执行态)、Rejected(拒绝态);状态的变更是单向的,只能从Pending-->Fulfilled或者Pending-->Rejected,状态的变更是不可逆的。then方法接收两个可选参数,分别对应状态改变时触发的回调函数。then方法返回一个promise。then方法可以被同一个promise调用多次。
// Promise/A+ 规范的三种状态
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MyPromise {
constructor(executor) {
this._status = PENDING;
this._resolveQueue = []; // then 收集的执行成功的回调队列
this._rejectQueue = [];
let _resolve = (val) => {
// 对应状态中的‘状态只能由pending到fulfilled或pending到rejected
if(this._status !== PENDING) return;
// 变更状态
this._status = FULFILLED;
while(this._resolveQueue.length) {
// 从回调队列里取出回调依次执行
const callback = this._resolveQueue.shift();
callback(val);
}
}
let _reject = (val) => {
if(this._status !== PENDING) return;
this._status = REJECTED;
while(this._rejectQueue.length) {
const callback = this._rejectQueue.shift();
callback(val);
}
}
// new Promise() 时立即执行executor,并传入resolve和reject
executor(_resolve, _reject);
}
// then 方法,接收一个成功的回调和一个失败的回调,并push进对应的队列
then(resolveFn, rejectFn) {
this._resolveQueue.push(resolveFn);
this._rejectQueue.push(rejectFn);
}
}
then的链式调用:
.then()需要返回一个Promise,这样才能找到then方法,所以需要把then方法包装成 Promise;
.then()的回调需要拿到上一个.then()的返回值;
.then()的回调需要顺序执行。需要等待当前Promise状态变更后,再执行下一个then收集的回调,因此需要对then的返回值进行讨论。
细节处理
根据规范,如果
then()接收的参数不是function,应该忽略。如果不忽略,会导致抛出异常,导致链式调用中断;处理状态为
resolve/reject的情况:then()的写法是对应状态为pending的情况。但是有时,resolve/reject在then()之前就被执行,这时如果还把then()回调push进resolve/reject队列中,回调将不会被执行。因此对于状态为fulfilled/rejected的情况,直接执行then回调;
- 兼容同步任务
- Promise 的执行顺序:
new Promise() --> then()收集回调 --> resolve/reject执行回调,是建立在executor是异步任务的基础上的,如果executor是同步任务,则执行顺序变成new Promise() --> resolve/reject 执行回调 --> then() 收集回调。- 为了兼容这种情况,使用
setTimeout来包裹resolve/reject执行回调,使其异步执行。
then(resolveFn, rejectFn) {
// 根据规范,如果 then 不是 function,则需要忽略,让链式调用继续往下执行
typeof resolveFn !== 'function' ? resolveFn = value => value : null;
typeof rejectFn !== 'function' ? rejectFn = reason => {
throw new Error(reason instanceof Error ? reason.message: reason);
} : null;
// return 一个新的 promise
return new MyPromise((resolve, reject) => {
// 将 resolveFn 重新包装后,再 push 进 resolve 执行队列,对获取的回调返回值进行讨论
const fulfilledFn = value => {
try {
let x = resolveFn(value);
// 讨论返回值,如果是 Promise,等待 Promise 状态变更,否则直接 resolve
// resolve 之后,就能被下一个 .then() 回调获取到返回值,从而实现链式调用
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
} catch(error) {
reject(error);
}
}
// 将后续 then 收集的依赖都 push 进当前 Promise 的成功回调队列中,以保证顺序调用
const rejectedFn = error => {
try {
let x = rejectFn(error);
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
} catch(error) {
reject(error);
}
}
switch(this._status) {
// 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行
case PENDING:
this._resolveQueue.push(fulfilledFn);
this._rejectQueue.push(rejectedFn);
break;
// 当状态已经变为resolve/reject时,直接执行then回调
case FULFILLED:
fulfilledFn(this._value);
break;
case REJECTED:
rejectedFn(this._value);
break;
}
})
}
完整版代码如下所示:
// Promise/A+ 规范的三种状态
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MyPromise {
constructor(executor) {
this._status = PENDING;
this._resolveQueue = []; // then 收集的执行成功的回调队列
this._rejectQueue = [];
let _resolve = (val) => {
// 把 resolve执行回调的操作封装成一个函数,放进 setTimeout 里,用来兼容 executor 是同步代码的情况
const run = () => {
// 对应状态中的状态只能由 pending 到 fulfilled 或 pending 到rejected
if(this._status !== PENDING) return;
// 变更状态
this._status = FULFILLED;
while(this._resolveQueue.length) {
// 从回调队列里取出回调依次执行
const callback = this._resolveQueue.shift();
callback(val);
}
}
setTimeout(run);
}
let _reject = (val) => {
if(this._status !== PENDING) return;
this._status = REJECTED;
while(this._rejectQueue.length) {
const callback = this._rejectQueue.shift();
callback(val);
}
}
// new Promise() 时立即执行 executor,并传入 resolve 和 reject
executor(_resolve, _reject);
}
// then 方法,接收一个成功的回调和一个失败的回调,并push进对应的队列
then(resolveFn, rejectFn) {
// 根据规范,如果 then 不是 function,则需要忽略,让链式调用继续往下执行
typeof resolveFn !== 'function' ? resolveFn = value => value : null;
typeof rejectFn !== 'function' ? rejectFn = reason => {
throw new Error(reason instanceof Error ? reason.message: reason);
} : null;
// return 一个新的 promise
return new MyPromise((resolve, reject) => {
// 将 resolveFn 重新包装后,再 push 进 resolve 执行队列,对获取的回调返回值进行讨论
const fulfilledFn = value => {
try {
let x = resolveFn(value);
// 讨论返回值,如果是 Promise,等待 Promise 状态变更,否则直接 resolve
// resolve 之后,就能被下一个 .then() 回调获取到返回值,从而实现链式调用
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
} catch(error) {
reject(error);
}
}
// 将后续 then 收集的依赖都 push 进当前 Promise 的成功回调队列中,以保证顺序调用
const rejectedFn = error => {
try {
let x = rejectFn(error);
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
} catch(error) {
reject(error);
}
}
switch(this._status) {
// 当状态为 pending 时,把 then 回调 push 进 resolve/reject 执行队列,等待执行
case PENDING:
this._resolveQueue.push(fulfilledFn);
this._rejectQueue.push(rejectedFn);
break;
// 当状态已经变为 resolve/reject 时,直接执行 then 回调
case FULFILLED:
fulfilledFn(this._value);
break;
case REJECTED:
rejectedFn(this._value);
break;
}
})
}
}
2.2 实现 Promise.all
Promise.all(iterable)方法返回一个Promise实例,此实例在iterable参数内所有的promise都“完成(resolve)”或参数中不包含promise时回调完成(resolve);如果参数中promise有一个失败(rejected),此实例回调失败(reject),失败原因是第一个失败promise的结果。
static all(promiseAll) {
let index = 0;
let result = [];
return new MyPromise((resolve, reject) => {
// Promise.resolve(p) 用于处理传入值不为 Promise 的情况
promiseArr.resolve(p).then(val => {
index++;
result[i] = val;
// 所有 then 执行后,resolve 结果
if(index === promiseAll.length) {
resolve(result);
}
}, err => {
//有一个 Promise 被 reject 时,MyPromise 的状态变为 reject
reject(err);
})
})
}
2.3 实现 Promise.race
Promise.race(iterable)方法返回一个promise,一旦迭代器中的某个promise被解决或者拒绝,返回的promise就会被解决或者拒绝。
static race(promiseArr) {
return new MyPromise((resolve, reject) => {
// 同时执行 promise,如果有一个 Promise 的状态变更,就更新 MyPromise 的状态
for(let p of promiseArr) {
MyPromise.resolve(p).then(val => {
resolve(val); // resolve 的是上面 new MyPromise 的
}, err => {
reject(err);
})
}
})
}
2.4 实现 Promise.resolve
static resolve(value) {
if(value instanceof MyPromise) return value;
return new MyPromise(resolve => resolve(value));
}
2.5 实现 Promise.reject
static reject(reason) {
return new MyPromise((resolve, reject) => reject(reason));
}
3. 数组相关
3.1 实现 reduce
MDN:reduce()方法对数组中的每个元素按序执行一个由用户提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总成单个返回值。
参数:
callbackFn:一个reducer函数,包含四个参数
previousValue:上一次调用callbackFn时的返回值。在第一次调用时,若指定了初始值initialValue,其值为initialValue,否则为数组索引为0的元素array[0];currentValue:数组中正在处理的元素。在第一次调用时,若指定了初始值initialValue,其值则为数组索引为 0 的元素array[0],否则为array[1];currentIndex:数组中正在处理的元素的索引。若指定了初始值initialValue,则起始索引号为 0,否则从索引 1 起始;可选。array:用于遍历的数组;可选。
initialValue:可选。作为第一次调用callback函数时参数previousValue的值。若指定了初始值initialValue,currentValue则使用数组的第一个元素;若未指定,则previousValue使用数组的第一个元素,currentValue使用数组的第二个元素。
function myReduce(data, iteratee, memo, context) {
// 重置 iteratee 函数的 this 指向
iteratee = bind(iteratee, context);
// 兼容对象,取出所有的 key 值
let _keys = !Array.isArray && Object.keys(data);
// 长度赋值
let len = (_key || data).length;
// 判断是否传递了第三个参数
let initial = arguments.length >= 3;
// 初始的遍历下标
let index = 0;
// 若用户没有传入第三个参数,则取第一项作为默认值
if(!initial) {
memo = data[_keys ? _keys[index] : index];
// 遍历从第二项开始
index += 1;
}
for(let i = 0;i < data.length; i++) {
let currentKey = _keys ? _keys[i] : i;
memo = iteratee(memo, data[currentKey], currentKey, data);
}
return memo;
}
// 绑定函数,使用 call 进行绑定
funtion bind(fn, context) {
// 返回一个匿名函数,执行时重置 this 指向
return function(memo, value, index, collection) {
return fn.call(context, memo, value, index, collection);
}
}
3.2 实现 Array.isArray
Array.IsArray = (arr) => {
return Object.prototype.toString.call(arg) === '[Object Array]'
}
3.3 实现数组扁平化(拍平、降维)
3.3.1 ES6 的 flat()
const arr = [1, [1, 2], [1, 2, 3]];
arr.flat(); // [1, 1, 2, 1, 2, 3]
3.3.2 序列化后正则
const arr = [1, [1, 2], [1, 2, 3]];
const str = `[${JSON.stringfy(arr).replace(/(\[|\])/g, '')}]`
JSON.parse(str); // [1, 1, 2, 1, 2, 3]
3.3.3 递归
const arr = [1, [1, 2], [1, 2, 3]];
function flat(arr) {
let result = [];
for(const item of arr) {
item instanceof Array ? result = result.concat(flat(item)) : result.push(item);
}
return result;
}
flat(arr); // [1, 1, 2, 1, 2, 3]
3.3.4 reduce() 递归
const arr = [1, [1, 2], [1, 2, 3]];
function flat(arr) {
return arr.reduce((preValue, curValue) => {
return preValue.concat(curValue instanceof Array ? flat(curValue) : curValue)
}, []);
}
flat(arr); // [1, 1, 2, 1, 2, 3]
3.3.5 迭代 + 展开运算符
const arr = [1, [1, 2], [1, 2, 3, [4, 4, 4]]];
while(arr.some(Array.isArray)) {
arr = [].concat(...arr);
}
flat(arr); // [1, 1, 2, 1, 2, 3]
3.4 实现数组去重
3.4.1 ES6 的 Set 去重
new Set 是 ES6 新推出的一种类型。和数组的区别是,Set 类型的数据中不能有重复的值。
适用于:数组中都是值类型的数据。
缺点:无法去重引用类型数据。
const arr = [1, 1, 2, 3, 3, 4, 5, 5];
const setData = Array.from(new Set(arr)); // [...new Set(arr)]
console.log(setData); // [1, 2, 3, 4, 5];
3.4.2 双重 for 循环 + splice
function unique(arr) {
for(lei i = 0, len = arr.length; i < len; i++) {
for(let j = i + 1; j < len; j++) {
if(arr[i] === arr[j]) {
arr.splice(j, 1);
j--;
len--;
}
}
}
return arr;
}
3.4.3 使用 includes 或 indexof + 新数组
function unique(arr) {
let uniqueArr = [];
for(let i = 0, len = arr.length; i < len; i++) {
if(uniqueArr.indexof(arr[i]) === -1) { // !uniqueArr.includes(arr[i])
uniqueArr.push(arr[i]);
}
}
return uniqueArr;
}
3.4.4 使用哈希表存储元素(ES6 map)
function unique(arr) {
let map = new Map();
let uniqueArr = new Array();
for(let i = 0, len = arr.length; i < len; i++) {
if(map.has(arr[i])) { // 如果有该 key 值
map.set(arr[i], true);
} else {
map.set(arr[i], false);
uniqueArr.push(arr[i]);
}
}
return uniqueArr;
}
3.4.5 filter
function unique(arr) {
return arr.filter(function (item, index, arr) {
//当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
//不是那么就证明是重复项,就舍弃
return arr.indexOf(item) === index;
})
}
3.4.6 reduce
function unique(arr){
let uniqueArr = arr.reduce((acc,cur) => {
if(!acc.includes(cur)){
acc.push(cur);
}
return acc;
},[])
return uniqueArr;
}