InterviewMap —— Javascript (四)

一、函数防抖

限制事件的频繁触发,我们的解决办法是防抖、节流。

JS奇淫巧技:防抖函数与节流函数

应用场景

实际工作中,我们经常性的会通过监听某些事件完成对应的需求,比如:

  • 通过监听 scroll 事件,检测滚动位置,根据滚动位置显示返回顶部按钮
  • 通过监听 resize 事件,对某些自适应页面调整DOM的渲染(通过CSS实现的自适应不再此范围内)
  • 通过监听 keyup 事件,监听文字输入并调用接口进行模糊匹配
  • ...

我们先来看一下scroll事件的触发频率

window.onscroll = function () {
            let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;

            console.log(scrollTop);
        }
复制代码

从效果上,我们可以看到,在页面滚动的时候,会在短时间内触发多次绑定事件。

我们知道DOM操作是很耗费性能的,如果在监听中,做了一些DOM操作,那无疑会给浏览器造成大量性能损失。

1、认识函数防抖

函数防抖: 多次触发事件之后,处理函数不会执行多次,而是在连续事件触发结束之后执行一次

原理: 对处理函数进行延时操作,若设定的延时到来之前,再次触发事件,则清除上一次的延时操作定时器,重新定时。

我们改进一下scroll事件:

let timer;
        window.onscroll = function () {
            if(timer) {
                clearTimeout(timer);
            }
            timer = setTimeout(function () {
                let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;

                console.log(scrollTop);
            }, 200);
        }
复制代码

实现效果为: 连续滚动时候不会触发,知道停止才会触发。

防抖函数的封装使用

/**
 * 防抖  普通
 * 事件连续触发之后在执行
 * @param methods 要执行的方法函数
 * @param delay 延迟时间
 * @returns {Function}
 */
function debounce(methods, delay) {
    let timer;
    return function () {
        let self = this;
        let args = arguments;
        timer && clearTimeout(timer);
        timer = setTimeout(function() {
            methods.apply(self, args);
        }, delay);
    }
}

window.onscroll = debounce1(function (event) {
    let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
    console.log(this);
    console.log(event);
    console.log(scrollTop);
}, 200, true);
复制代码

这里使用了闭包。当debounce函数执行完毕之后,由于使用闭包,timer还能在返回的匿名函数中被调用。

连续触发的过程中,不断的清除上一个定时器,又不断创建新的定时器,知道最后触发结束,最后一个定时器执行。

适当改进一下上面的需求,如果我们想开始时触发一次,事件触发结束之后再执行一次,实现如下:

/**
 * 防抖  高级(一)
 * 开始时触发一次,事件触发结束之后再执行一次
 * @param methods
 * @param delay
 * @param immediate  是否要立即触发一次再执行防抖,true / false
 * @returns {Function}
 */
function debounce1(func, wait, immediate) {


    var timer;

    return function () {

        var context = this;
        var args = arguments;

        timer && clearTimeout(timer);
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timer;
            timer = setTimeout(function(){
                timer = null;
                if (!timer) func.apply(context, args)
            }, wait)

            if (callNow) func.apply(context, args)
        }
        else {
            timer = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
}
复制代码

二、 函数节流

在事件连续触发的时间里,我们让回调函数,每隔delay时间调用一次。

scroll的栗子:

var startTime = Date.now();
    var time = 500; // 间隔时间
    var timer = null;
    window.onscroll = function throttle(event) {
        var newTime = Date.now();

        if(newTime - startTime > time) {
            let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
            console.log('滚动条位置:' + scrollTop);
            startTime = Date.now();
        } else {
            timer && clearTimeout(timer);

            timer = setTimeout(function () {
                throttle()
            }, 50)
        }
    }
复制代码

节流函数封装:

/**
     * 节流: 在事件连续触发的时间里,我们让回调函数,每隔delay时间调用一次
     * @param method
     * @param delay
     * @returns {loop}
     */
    function throttle(method, delay) {
        // 这些变量因为是自由变量,可以被下次调用loop的时候使用
        let timer,
            startTime,
            args = arguments;

        return function loop() {
            var self = this;
            if(!startTime) {
                startTime = Date.now();
            }

            timer && clearTimeout(timer);

            if(Date.now() - startTime > delay) {
                method.apply(self, args);
                startTime = Date.now();
            } else {
                timer = setTimeout(function () {
                    loop.apply(self, args);
                }, delay)
            }


        }
    }

    window.onscroll = throttle(function () {
        let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
        console.log('滚动条位置:' + scrollTop);
    }, 500);
复制代码

三、深浅拷贝

深入理解 Javascript 之 JS的深浅拷贝

谈谈 js 深浅拷贝 那点事(一)

谈谈 js 深浅拷贝 那点事(二)

四、Promise

我们先来一个栗子:

// a.json
{
  "next": "b.json",
  "msg": "this is a" 
}
复制代码
// b.json
{
  "next": "c.json",
  "msg": "this is b" 
}
复制代码
// c.json
{
  "next": null,
  "msg": "this is c" 
}
复制代码

首先我们实现读取文件的栗子;

// app.js
const path = require('path');
const fs   = require('fs');

let filename = path.resolve(__dirname, 'files', 'a.json');
/**
 * 读取
 */
fs.readFile(filename, (err, data) => {
  if(err) {
    console.error(err);
    return ;
  }

  if(data) { 
    console.log(JSON.stringify(data.toString()));
    // { next: 'b.json', msg: 'this is a' }
  }
})
复制代码

我们封装一下,使用callback方式

function getFileContent(filename, callback) {
  let fullFilename = path.resolve(__dirname, 'files', filename);

  fs.readFile(fullFilename, (err, data) => {
    if(err) {
      console.error(err);
      return ;
    }
  
    if(data) { 
      callback(
        JSON.parse(data.toString())
      )
    }
  })
}

// 测试

getFileContent('a.json', (data) => {
  getFileContent(data.next, (data) => {
    getFileContent(data.next, (data) => {
      console.log(data);
    })
  })
})

// { next: null, msg: 'this is c' }
复制代码

上面就是臭名昭著额回调地狱。

回调地狱带来的负面作用有以下几点:

代码臃肿。
可读性差。
耦合度过高,可维护性差。
代码复用性差。
容易滋生 bug。
只能在回调里处理异常。
复制代码

使用Promise


function getContent(filename) {
  return new Promise((resolve, reject) => {
    let fullFilename = path.resolve(__dirname, 'files', filename);

    fs.readFile(fullFilename, (err, data) => {
      if (err) {
        reject(err);
        return;
      }

      if (data) {
        resolve(
          JSON.parse(data.toString())
        )
      }
    })
  })
}

getContent('a.json').then((data) => {
  console.log(data);
  return getContent(data.next)
}).then((data) => {
  console.log(data);
  return getContent(data.next);
}).then((data) => {
  console.log(data)
})
复制代码

1、认识Promise

Promise 是异步编程的一种解决方案,比传统的异步解决方案【回调函数】【事件】更合理、更强大。现已被 ES6 纳入进规范中。

2、常用API

Promise.resolve(value)

Promise.resolve(value)将现有对象转成Promise对象。

它的参数有四种情况:

(1)参数是一个Promise实例

如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。

(2)参数是一个thenable对象

thenable对象指的是具有then方法的对象,比如下面这个对象。

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};
复制代码

Promise.resolve方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法。

let thenable = {
    then: function(resolve, reject) {
        resolve('40');
    }
}

var p = Promise.resolve(thenable);

p.then((val) => {
    console.log(val)
})
// 40
复制代码

(3)参数不是具有then方法的对象,或根本就不是对象

如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved。

const p = Promise.resolve(300);
p.then((v) => {console.log(v)})
复制代码

上面代码生成一个新的 Promise 对象的实例p。由于字符串Hello不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是resolved,所以回调函数会立即执行。

(4)不带有任何参数

不带任何参数会返回一个状态为resolve的Promise对象。会直接调用then方法。

const p = Promise.resolve();

p.then(function () {
  // ...
});
复制代码

需要注意的是,立即resolvePromise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。

setTimeout(function () {
  console.log('three');
}, 0);

Promise.resolve().then(function () {
  console.log('two');
});

console.log('one');

// one
// two
// three
复制代码

上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log('one')则是立即执行,因此最先输出。

Promise.reject()

Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。

const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s) {
  console.log(s)
});
// 出错了
复制代码

此时的函数回调会立即执行。注意,reject的参数会原封不动的变成理由,变成后续方法的参数。

const thenable = {
  then(resolve, reject) {
    reject('出错了');
  }
};

Promise.reject(thenable)
.catch(e => {
  console.log(e === thenable)
})

// true
复制代码

得到的结果并不是‘出错了’,而是thenable对象。

Promise.prototype.then

Promise 实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。

如果then方法返回的是一个新的Promise实例。因此可以采用链式写法,即then方法后面再调用另一个then方法。

getJSON("/post/1.json").then(function(post) {
  return getJSON(post.commentURL);
}).then(function funcA(comments) {
  console.log("resolved: ", comments);
}, function funcB(err){
  console.log("rejected: ", err);
});
复制代码

Promise.prototype.catch()

实例方法,捕获异常,函数形式:fn(err){}, err 是 catch 注册 之前的回调抛出的异常信息。

// 写法一
const promise = new Promise(function(resolve, reject) {
  try {
    throw new Error('test');
  } catch(e) {
    reject(e);
  }
});
promise.catch(function(error) {
  console.log(error);
});

// 写法二
const promise = new Promise(function(resolve, reject) {
  reject(new Error('test'));
});
promise.catch(function(error) {
  console.log(error);
});
复制代码

上面方法等价。

Promise.prototype.finally()

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
复制代码

上面代码中,不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。

Promise.all()

Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);

上面代码中,Promise.all方法接受一个数组作为参数p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise实例,再进一步处理。(Promise.all方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。)

p的状态由p1、p2、p3决定,分成两种情况。

(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
复制代码

Promise.race()

Promise.race方法同样是将多个Promise 实例,包装成一个新的Promise 实例。

const p = Promise.race([p1, p2, p3]); 上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

Promise.race方法的参数与Promise.all方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。

3、Promise 与事件循环

Promise初始化之后,首先会执行内部的同步代码,注册then回调。然后继续往下执行promise同级的同步代码,当这些代码执行完后,去事件循环中去查询可用的promise回调,如果有,那么执行then回调中的同步代码,如果没有找到,那么就在下一次事件循环中找。

4、Promise的升级

ES6 出现了 generator 以及 async/await 语法,使异步处理更加接近同步代码写法,可读性更好,同时异常捕获和同步代码的书写趋于一致。需要记住的是,async/await也是基于 Promise 实现的,所以,我们仍然有必要深入理解 Promise 的用法。

5、Promise常问

常见 Promise 面试题 我们看一些 Promise 的常见面试问法,由浅至深。

1、了解 Promise 吗?
2、Promise 解决的痛点是什么?
3、Promise 解决的痛点还有其他方法可以解决吗?如果有,请列举。
4、Promise 如何使用?
5、Promise 常用的方法有哪些?它们的作用是什么?
6、Promise 在事件循环中的执行过程是怎样的?
7、Promise 的业界实现都有哪些?
8、能不能手写一个 Promise 的 polyfill。
复制代码

Promise 必知必会(十道题)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值