一、函数防抖
限制事件的频繁触发,我们的解决办法是防抖、节流。
应用场景
实际工作中,我们经常性的会通过监听某些事件完成对应的需求,比如:
- 通过监听 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);
复制代码
三、深浅拷贝
四、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 () {
// ...
});
复制代码
需要注意的是,立即resolve
的 Promise
对象,是在本轮“事件循环”(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。
复制代码