前端手写秘籍

237 阅读9分钟

前端手写秘籍

翻开秘籍,直接看目录:

  • 手写Promise

  • 手写防抖与节流

  • BFS与DFS

  • 排序题

    • 快速排序

    • 插入排序



手写Promise

上一篇文章(Promise与async/await)中,已经详细介绍了Promise的用法,这里稍微回顾下:

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('resolve');
    });
});
p1.then((res) => {
    console.log('p1 res:' + res);
    console.log(p1);
});

const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('resolve');
    });
}).then((res) => {
    console.log('p2 res:' + res);
    console.log(p2);
});

const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('resolve');
        reject('reject');
    });
}).then((res) => {
    console.log('p3 res:' + res);
    console.log(p3);
});

const p4 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('resolve');
        reject('reject');
    });
}).then((res) => {
    console.log('p4 res:' + res);
    console.log(p4);
}).catch((err) => {
    console.log('p4 err:' + err);
    console.log(p4);
});

setTimeout模拟的是异步请求,因为setTimeout里调用了resolve,此时的Promise状态为resolved,请求成功之后执行then里面的内容。

接下来开始手写Promise,先定义一个class。从上面的例子可以看到,new Promise的参数是一个fn,并且Promise会马上执行fn,那手写Promiseclass可以这样写:

class Promise {
    constructor (fn){
        // ...
        fn();
    }
    
}

接下来看到fn有两个参数,也把他加上,因为fn是传进来的,不能保证fn的准确性,为了防止发生错误我们给它加一个try/catch,并且这里使用bind的原因是解决classthis指向问题:

class Promise {
    constructor (fn){
        // ...
        try {
            fn(this.resolve.bind(this), this.reject.bind(this));
        } catch {
            console.log('Promise内部报异常');
        }
    }

    resolve(result) {
        // ...
    }
    reject(reason) {
        // ...
    }
}

然后开始处理resolvereject,我们回到Promise的基础用法,发现resolvereject是主动调用的,并且他们都能接收参数,我们一步一步来:

class Promise {
    state = 'pending';

    constructor (fn){
        // ...
        try {
            fn(this.resolve.bind(this), this.reject.bind(this));
        } catch {
            console.log('Promise内部报异常');
        }
    }

    resolve() {
        // ...
        if (this.state === 'pending') {
            this.state = 'fulfilled';
        }
    }
    reject() {
        // ...
        if (this.state === 'pending') {
            this.state = 'rejected';
        }
    }
}

现在能调resolvereject了,接下来得再加一个Result来存回调:

class Promise {
    state = 'pending';
    result = '';

    constructor (fn){
        // ...
        try {
            fn(this.resolve.bind(this), this.reject.bind(this));
        } catch {
            console.log('Promise内部报异常');
        }
    }

    resolve(result) {
        // ...
        if (this.state === 'pending') {
            this.state = 'fulfilled';
            this.result = result;
        }
    }
    reject(reason) {
        // ...
        if (this.state === 'pending') {
            this.state = 'rejected';
            this.result = reason;
        }
    }
}

到这一步,我们Promise的状态已经解决了,接下来看看如何执行then的回调:

class Promise {
    state = 'pending';
    result = '';

    resolveCallbackList = [];
    rejectCallbackList = [];

    constructor (fn){
        // ...
        try {
            fn(this.resolve.bind(this), this.reject.bind(this));
        } catch (err){
            console.log(err);
            console.log('Promise内部报异常');
            this.reject('Promise内部报异常');
        }
    }

    resolve(result) {
        // ...
        if (this.state === 'pending') {
            this.state = 'fulfilled';
            this.result = result;
            this.resolveCallbackList.forEach(resolveFn =>{
                resolveFn(this.result)
            });
        }
    }
    reject(reason) {
        // ...
        if (this.state === 'pending') {
            this.state = 'rejected';
            this.result = reason;
            this.rejectCallbackList.forEach(rejectFn =>{
                rejectFn(this.result)
            });
        }
    }

    then(resolveFn, rejectFn){
        if (this.state === 'pending'){

        }
        if (this.state === 'fulfilled'){
            return new Promise((resolve, reject) => {
                return resolveFn(this.result);
            });
        }
        if (this.state === 'rejected'){
            return new Promise((resolve, reject) => {
                return rejectFn(this.result);
            });
        }
    }
}

上面代码的then判断了Promise的三种状态,我们先实现fulfilledrejected,这样就已经能满足Promise内没有异步代码的需求了,下面我们尝试运行一下:

const myPromise = new Promise((resolve, reject) => {
    console.log('Promise');
    resolve('new Promise resolved');
    // setTimeout(() => {
    //     resolve('resolve');
    // },1000);
}).then((res) => {
    console.log('Promise:' + res);
});

企业微信截图_16799033061899.png

没有问题,接下来把setTimeout打开,这时候大家应该都能猜到,我们要开始写this.state === 'pending'里面的内容了。

class Promise {
    state = 'pending';
    result = '';

    resolveCallbackList = [];
    rejectCallbackList = [];

    constructor (fn){
        // ...
        try {
            fn(this.resolve.bind(this), this.reject.bind(this));
        } catch (err){
            console.log(err);
            console.log('Promise内部报异常');
            this.reject('Promise内部报异常');
        }
    }

    resolve(result) {
        // ...
        if (this.state === 'pending') {
            this.state = 'fulfilled';
            this.result = result;
            this.resolveCallbackList.forEach(resolveFn =>{
                resolveFn(this.result)
            });
        }
    }
    reject(reason) {
        // ...
        if (this.state === 'pending') {
            this.state = 'rejected';
            this.result = reason;
            this.rejectCallbackList.forEach(rejectFn =>{
                rejectFn(this.result)
            });
        }
    }

    then(resolveFn, rejectFn){
        if (this.state === 'pending'){
            return new Promise((resolve, reject) => {
                this.resolveCallbackList.push(() => {
                    this.result = resolveFn(this.result);
                    this.resolve(this.result);
                });
                this.rejectCallbackList.push(() => {
                    this.result = rejectFn(this.result);
                    this.reject(this.result);
                });
            });
        }
        if (this.state === 'fulfilled'){
            return new Promise((resolve, reject) => {
                this.result = resolveFn(this.result);
                this.resolve(this.result);
            });
        }
        if (this.state === 'rejected'){
            return new Promise((resolve, reject) => {
                this.result = rejectFn(this.result);
                this.reject(this.result);
            });
        }
    }
}

可以看到pending主要做的内容就是将resolveFnrejectFn放进resolveCallbackListrejectCallbackList里。

const myPromise = new Promise((resolve, reject) => {
    console.log('Promise');
    // resolve('new Promise resolved');
    setTimeout(() => {
        resolve('resolve');
    },1000);
}).then((res) => {
    console.log('Promise:' + res);
});

企业微信截图_1679904082826.png

最后再实现catch,因为我们已经实现了then,所以可以直接拿then实现就可以了:

catch(rejectFn){
    return this.then(()=>{}, rejectFn);
}

再运行测试下代码:

const myPromise = new Promise((resolve, reject) => {
    console.log('Promise');
    // reject('new Promise rejected');
    setTimeout(() => {
        reject('reject');
    },1000);
}).catch((err) => {
    console.log('Promise:' + err);
});

企业微信截图_16799050446401.png

至此,Promise的一个简单版就手写完了,虽然里面还有可改进的地方,但是如果是面试问到的话,能回答以上内容已经足够了。

下面再贴一下完整的代码:

class Promise {
    state = 'pending';
    result = '';

    resolveCallbackList = [];
    rejectCallbackList = [];

    constructor (fn){
        // ...
        try {
            fn(this.resolve.bind(this), this.reject.bind(this));
        } catch (err){
            console.log(err);
            console.log('Promise内部报异常');
            this.reject('Promise内部报异常');
        }
    }

    resolve(result) {
        // ...
        if (this.state === 'pending') {
            this.state = 'fulfilled';
            this.result = result;
            this.resolveCallbackList.forEach(resolveFn =>{
                resolveFn(this.result)
            });
        }
    }
    reject(reason) {
        // ...
        if (this.state === 'pending') {
            this.state = 'rejected';
            this.result = reason;
            this.rejectCallbackList.forEach(rejectFn =>{
                rejectFn(this.result)
            });
        }
    }

    then(resolveFn, rejectFn){
        if (this.state === 'pending'){
            return new Promise((resolve, reject) => {
                this.resolveCallbackList.push(() => {
                    this.result = resolveFn(this.result);
                    this.resolve(this.result);
                });
                this.rejectCallbackList.push(() => {
                    this.result = rejectFn(this.result);
                    this.reject(this.result);
                });
            });
        }
        if (this.state === 'fulfilled'){
            return new Promise((resolve, reject) => {
                this.result = resolveFn(this.result);
                this.resolve(this.result);
            });
        }
        if (this.state === 'rejected'){
            return new Promise((resolve, reject) => {
                this.result = rejectFn(this.result);
                this.reject(this.result);
            });
        }
    }

    catch(rejectFn){
        return this.then(()=>{}, rejectFn);
    }
}


手写防抖与节流

debounce 防抖与节流是经典手写题的代表,下面来看看怎么封装debounce:

function debounce(fn, wait = 1000) {
    let timer = null;

    return function() {
        let context = this,
            args = arguments;

        if (timer) {
            clearTimeout(timer);
            timer = null;
        }
        timer = setTimeout(() => {
            fn.apply(context, args);
            timer = null;
        }, wait);
    };
}

let myDebounce = debounce(() => {
    console.log('debounce');
}, 500);

myDebounce();
myDebounce();
myDebounce();
myDebounce();
myDebounce();
myDebounce();  //debounce

代码解读:首先封装了debounce,然后执行,返回一个新函数给了myDebounce,接下来模拟连续触发6次myDebounce,结果是到了第6次之后,等待了500毫秒输出了debounce,符合预期。

再看看里面的细节,let timer = null;debounce中,在return外面,很明显timer是在闭包中的,可以在debounce外面访问到。

注意这里的if (timer) {},代表如果已经有一个计时器,则把计时器清空,重新创建一个计时器。

接下来debounce执行,return一个函数,并赋值给myDebounce

之后高频执行myDebounce,待其静止后,触发fn回调。



throttle 节流是在一段时间内,控制执行的次数,达到节流的效果。

function throttle(func, interval) {
    var timer = null;
    return function() {
        let context = this,
            args = arguments;

        if (!timer) {
            timer = setTimeout(function() {
                func.apply(context, args);
                timer = null;
            }, interval);
        }
    }
}
let myThrottle = throttle(() => {
    console.log('throttle');
}, 500);

myThrottle();  //throttle
myThrottle();
myThrottle();
myThrottle();
myThrottle();
myThrottle();  //throttle
myThrottle();
myThrottle();
// 这里输出的throttle只是方便演示,不是实际执行效果

代码解读:首先封装了throttle,然后执行,返回一个新函数给了myThrottle,接下来模拟连续触发6次myThrottle,每隔500毫秒输出throttle,也是符合预期。

再看看里面的细节,同样的let timer = null;也是在闭包中的。

之后高频执行throttle,每隔500毫秒触发fn回调。



BFS与DFS

BFS

BFS(Breadth First Search)--广度优先遍历

从一点出发,查出它的邻接节点放入队列并标记,然后从队列中弹出第一个节点,寻找它的邻接未被访问的节点放入队列,直至所有已被访问的节点的邻接点都被访问过;若图中还有未被访问的点,则另选一个未被访问的点出发,执行相同的操作,直至图中所有节点都被访问。

步骤:

  • 创建一个队列,并将开始节点放入队列中

  • 若队列非空,则从队列中取出第一个节点,并检测它是否为目标节点

    • 若是目标节点,则结束搜寻,并返回结果
    • 若不是,则将它所有没有被检测过的字节点都加入队列中
  • 若队列为空,表示图中并没有目标节点,则结束遍历

DFS

DFS(Depth First Search)--深度优先遍历

DFS沿着树的深度遍历树的节点,尽可能深地搜索树的分支。当节点v的所有边都已被探寻过,将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已探寻源节点到其他所有节点为止,如果还有未被发现的节点,则选择其中一个未被发现的节点为源节点并重复以上操作,直到所有节点都被探寻完成。深度DFS属于盲目搜索,无法保证搜索到的路径为最短路径,也不是在搜索特定的路径,而是通过搜索来查看图中有哪些路径可以选择

步骤:

  • 访问顶点v
  • 依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问
  • 若此时途中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到所有顶点均被访问过为止


它们是遍历图当中所有节点点的两种方式,我们看看需要遍历的数据结构,如下图:

接下来使用JavaScript分别写下BFS与DFS,

企业微信截图_16798323274702.png

将它装成对象:

var dir = {
    value: 1,
    children: [
        {
            value: 2,
            children: [
                {
                    value: 5,
                    children: null,
                },
                {
                    value: 6,
                    children: null,
                },
            ],
        },
        {
            value: 3,
            children: [
                {
                    value: 7,
                    children: null,
                },
            ],
        },
        {
            value: 4,
            children: [
                {
                    value: 8,
                    children: null,
                },
                {
                    value: 9,
                    children: null,
                },
                {
                    value: 10,
                    children: [
                        {
                            value: 11,
                            children: null,
                        },
                        {
                            value: 12,
                            children: null,
                        },
                        {
                            value: 13,
                            children: null,
                        },
                    ],
                },
            ],
        },
    ],
};

接下来分别用BFSDFS遍历以上数据:

// BFS实现(队列)
function BFS(obj){
    const queue = [];
    queue.unshift(obj);

    while (queue.length > 0){
        const curObj = queue.pop();

        // 输出value
        console.log(curObj.value);

        const children = curObj.children;
        if (children){
            children.forEach(element => {
                queue.unshift(element);
            });
        }
    }
}
BFS(dir);
//  1
//  2
//  3
//  4
//  5
//  6
//  7
//  8
//  9
//  10
//  11
//  12
//  13

企业微信截图_1679835525460.png



// DFS实现(递归)
function DFS(obj){
    // 输出value
    console.log(obj.value);

    let children = obj.children;
    if (children){
        children.forEach(element => {
            // 递归
            DFS(element);
        });
    }
}

DFS(dir);
//  1
//  2
//  5
//  6
//  3
//  7
//  4
//  8
//  9
//  10
//  11
//  12
//  13

企业微信截图_16798336414132.png

注意看,用递归调用,会有爆栈的风险,这里不展开了,以后有机会单独再出一篇。



排序题

快速排序

  • 时间复杂度:平均 O(nlogN)、最好 O(nlogN)、最坏 O(n²)

  • 空间复杂度:O(nlogN)

  • In-place 内排序

  • 不稳定

快排,我们让区间末尾作为锚点,小于锚点的值在左边,维护一个i来标记其窗口右侧,或者我们可以认为, 最终i是第一个大于锚点的元素,之后我们将锚点i所在位置数据交换,那么 此时i左侧是小于锚点的右侧是大于锚点的,整体看就相对有序了,此时以 锚点为标准,分成两个区间,这两个区间重复上述操作,区间就越来越小,直至完全有序。

快速.gif

var sortArray = function(nums) {
  quickSort(nums, 0, nums.length - 1);
  return nums;
};
function quickSort(nums, start, end) {
  if (start >= end) {
    return;
  }
  const mid = partition(nums, start, end);
  quickSort(nums, start, mid - 1);
  quickSort(nums, mid + 1, end);
}
function partition(nums, start, end) {
    const pivot = nums[start];
    let left = start + 1;
    let right = end;
    while (left < right) {
        while (left < right && nums[left] <= pivot) {
            left++;
        }
        while (left < right && nums[right] >= pivot) {
            right--;
        }
        if (left < right) {
            [nums[left], nums[right]] = [nums[right], nums[left]];
            left++;
            right--;
        }
    }
    if (left === right && nums[right] > pivot) {
        right--;
    }
    if (right !== start) {
        [nums[start], nums[right]] = [nums[right], nums[start]];
    }
    return right;
}

插入排序

  • 时间复杂度:平均 O(n²)、最好 O(n)、最坏 O(n²)

  • 空间复杂度:O(1)

  • In-place 内排序

  • 稳定

将数组分为已排序区未排序区, 将未排序区间的元素拿到,然后在已排序区间寻找插队的位置即可。插入排序就像打扑克时整理手牌,抽出一张未排序的牌将它插在已经整理过的牌组中,通过扩大有序牌组,最终使得整副牌都有序。

插入.gif

var sortArray = function(nums) {
    const n = nums.length;
    for (let i = 1; i < n; ++i) {
        let j = i - 1;
        const tmp = nums[i];
        while (j >= 0 && tmp < nums[j]) {
            nums[j + 1] = nums[j];
            --j;
        }
        nums[j + 1] = tmp;
    }
    return nums;
};


最后,让我们一起加油吧!

gg.jpg

参考资料:

MDN Promise
Promises/A+