前端手写秘籍
翻开秘籍,直接看目录:
-
手写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,那手写Promise的class可以这样写:
class Promise {
constructor (fn){
// ...
fn();
}
}
接下来看到fn有两个参数,也把他加上,因为fn是传进来的,不能保证fn的准确性,为了防止发生错误我们给它加一个try/catch,并且这里使用bind的原因是解决class的this指向问题:
class Promise {
constructor (fn){
// ...
try {
fn(this.resolve.bind(this), this.reject.bind(this));
} catch {
console.log('Promise内部报异常');
}
}
resolve(result) {
// ...
}
reject(reason) {
// ...
}
}
然后开始处理resolve和reject,我们回到Promise的基础用法,发现resolve和reject是主动调用的,并且他们都能接收参数,我们一步一步来:
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';
}
}
}
现在能调resolve和reject了,接下来得再加一个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的三种状态,我们先实现fulfilled与rejected,这样就已经能满足Promise内没有异步代码的需求了,下面我们尝试运行一下:
const myPromise = new Promise((resolve, reject) => {
console.log('Promise');
resolve('new Promise resolved');
// setTimeout(() => {
// resolve('resolve');
// },1000);
}).then((res) => {
console.log('Promise:' + res);
});
没有问题,接下来把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主要做的内容就是将resolveFn和rejectFn放进resolveCallbackList和rejectCallbackList里。
const myPromise = new Promise((resolve, reject) => {
console.log('Promise');
// resolve('new Promise resolved');
setTimeout(() => {
resolve('resolve');
},1000);
}).then((res) => {
console.log('Promise:' + res);
});
最后再实现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);
});
至此,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,
将它装成对象:
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,
},
],
},
],
},
],
};
接下来分别用BFS与DFS遍历以上数据:
// 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
// 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
注意看,用递归调用,会有爆栈的风险,这里不展开了,以后有机会单独再出一篇。
排序题
快速排序
-
时间复杂度:平均 O(nlogN)、最好 O(nlogN)、最坏 O(n²)
-
空间复杂度:O(nlogN)
-
In-place 内排序
-
不稳定
快排,我们让区间末尾作为锚点,小于锚点的值在左边,维护一个i来标记其窗口右侧,或者我们可以认为, 最终i是第一个大于锚点的元素,之后我们将锚点和i所在位置数据交换,那么 此时i的左侧是小于锚点的,右侧是大于锚点的,整体看就相对有序了,此时以 锚点为标准,分成两个区间,这两个区间重复上述操作,区间就越来越小,直至完全有序。
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 内排序
-
稳定
将数组分为已排序区和未排序区, 将未排序区间的元素拿到,然后在已排序区间寻找插队的位置即可。插入排序就像打扑克时整理手牌,抽出一张未排序的牌将它插在已经整理过的牌组中,通过扩大有序牌组,最终使得整副牌都有序。
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;
};
最后,让我们一起加油吧!
参考资料: