
前言
最近瞄了下前端大厂的面试题 真是感触颇多 思考到原来我们平常用的很多习以为常的方法 都没有仔细深入原理去分析 所以导致很多方法只是停留在使用的表层 比如 promise 实现 vue 框架 数据驱动核心思路 各种指令实现 第三方库封装好的工具函数 js 防抖节流...
所以决定来一个手写源码系列(简易版) 好好学习优秀源码的思路
注意:本文所有的手写源码实现都是基于 es6 的 不想用原生去实现原因如下: 一方面是网上太多原生实现的方案了 另一方面是我们要面向未来编程 多使用 es6 的特性更加贴合实际工作
带来的问题: es6 有一部分语法糖 比如展开符... 箭头函数 class 等等 这些就属于语言基础了 所以有兴趣的也可以去探究下 es6 转正 es5 的实现方案
1. 手写 promise
先思考?
1.promise 特点是什么? => 异步回调解决方案
2.为什么能在事件回调之后再去调用?=>事件注册机制(只有先注册事件 当事件执行完了之后再去触发)
3.怎么知道事件执行完了?=>需要状态表示
那么开始实现吧
//这里使用es6 class实现
class Mypromise {
constructor(fn) {
// 表示状态
this.state = "pending";
// 表示then注册的成功函数
this.successFun = [];
// 表示then注册的失败函数
this.failFun = [];
let resolve = val => {
// 保持状态改变不可变
if (this.state !== "pending") return;
// 成功触发时机 改变状态 同时执行在then注册的回调事件
this.state = "success";
// 为了保证then事件先注册(主要是考虑在promise里面写同步代码) promise规范 这里为模拟异步
setTimeout(() => {
// 执行当前事件里面所有的注册函数
this.successFun.forEach(item => item.call(this, val));
});
};
let reject = err => {
if (this.state !== "pending") return;
// 失败触发时机 改变状态 同时执行在then注册的回调事件
this.state = "fail";
// 为了保证then事件先注册(主要是考虑在promise里面写同步代码) promise规范 这里模拟异步
setTimeout(() => {
this.failFun.forEach(item => item.call(this, err));
});
};
// 调用函数
try {
fn(resolve, reject);
} catch (error) {
reject(error);
}
}
// 实例方法 then
then(resolveCallback, rejectCallback) {
// 判断回调是否是函数
resolveCallback =
typeof resolveCallback !== "function" ? v => v : resolveCallback;
rejectCallback =
typeof rejectCallback !== "function"
? err => {
throw err;
}
: rejectCallback;
// 为了保持链式调用 继续返回promise
return new Mypromise((resolve, reject) => {
// 将回调注册到successFun事件集合里面去
this.successFun.push(val => {
try {
// 执行回调函数
let x = resolveCallback(val);
//(最难的一点)
// 如果回调函数结果是普通值 那么就resolve出去给下一个then链式调用 如果是一个promise对象(代表又是一个异步) 那么调用x的then方法 将resolve和reject传进去 等到x resolve的时候(状态完成)就会自动执行传入的resolve 这样就形成了链式调用
x instanceof Mypromise ? x.then(resolve, reject) : resolve(x);
} catch (error) {
reject(error);
}
});
this.failFun.push(val => {
try {
// 执行回调函数
let x = rejectCallback(val);
x instanceof Mypromise ? x.then(resolve, reject) : reject(x);
} catch (error) {
reject(error);
}
});
});
}
//静态方法
static all(promiseArr) {
let result = [];
return new Mypromise((resolve, reject) => {
for (let i = 0; i < promiseArr.length; i++) {
promiseArr[i].then(
res => {
result.push(res);
if (i === promiseArr.length - 1) {
resolve(result);
}
},
err => {
reject(err);
}
);
}
});
}
//静态方法
static race(promiseArr) {
return new Mypromise((resolve, reject) => {
for (let i = 0; i < promiseArr.length; i++) {
promiseArr[i].then(
res => {
resolve(res);
},
err => {
reject(err);
}
);
}
});
}
}
// 使用
let promise1 = new Mypromise((resolve, reject) => {
setTimeout(() => {
resolve(123);
}, 2000);
});
let promise2 = new Mypromise((resolve, reject) => {
setTimeout(() => {
resolve(1234);
}, 1000);
});
// Mypromise.all([promise1,promise2]).then(res=>{
// console.log(res);
// })
// Mypromise.race([promise1, promise2]).then(res => {
// console.log(res);
// });
promise1
.then(
res => {
console.log(res);
return new Mypromise((resolve, reject) => {
setTimeout(() => {
resolve("success");
}, 1000);
});
},
err => {
console.log(err);
}
)
.then(
res => {
console.log(res);
},
err => {
console.log(err);
}
);
扩展:如何取消 promise
先思考?
怎么才能取消已经发起的异步呢
Promise.race()方法可以用来竞争 Promise 谁的状态先变更就返回谁
那么可以借助这个 自己构造一个 Promise 来实现
function wrap(pro) {
let obj = {};
// 构造一个新的promise用来竞争
let p1 = new Promise((resolve, reject) => {
obj.resolve = resolve;
obj.reject = reject;
});
obj.promise = Promise.race([p1, pro]);
return obj;
}
let testPro = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(123);
}, 1000);
});
let wrapPro = wrap(testPro);
wrapPro.promise.then(res => {
console.log(res);
});
wrapPro.resolve("被拦截了");
2. 手写防抖节流
先思考?
1.防抖和节流区别=> 防抖是 N 秒内函数只会被执行一次,如果 N 秒内再次被触发,则重新计算延迟时间(举个极端的例子 如果 window 滚动事件添加了防抖 2s 执行一次 如果你不停地滚动 永远不停下 那这个回调函数就永远无法执行)
节流是规定一个单位时间,在这个单位时间内最多只能触发一次函数执行(还是滚动事件 如果你一直不停地滚动 那么 2 秒就会执行一次回调)
2.防抖怎么保证 事件延迟执行 并且在规定时间内再次触发需要清除 这个很容易就想到了 setTimeout
3.节流怎么保证 在单位时间内触发了一次就不再生效了 可以用一个 flag 标志来控制
那么开始实现吧(防抖节流属于性能优化的一点 更多性能优化扩展请点击 性能优化)
// 防抖
function debounce(fn, delay) {
//默认300毫秒
let timer;
return function() {
var args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args); // 改变this指向为调用debounce所指的对象
}, delay);
};
}
window.addEventListener(
"scroll",
debance(() => {
console.log(111);
}, 1000)
);
// 节流
//方法一:设置一个标志
function throttle(fn, delay) {
let flag = true;
return () => {
if (!flag) return;
flag = false;
timer = setTimeout(() => {
fn();
flag = true;
}, delay);
};
}
//方法二:使用时间戳
function throttle(fn, delay) {
let startTime = new Date();
return () => {
let endTime = new Date();
if (endTime - startTime >= delay) {
fn();
startTime = endTime;
} else {
return;
}
};
}
window.addEventListener(
"scroll",
throttle(() => {
console.log(111);
}, 1000)
);
3. 手写 EventEmitter(发布订阅模式--简单版)
先思考?
1.什么是发布订阅模式=> 发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知
2.怎么实现一对多=> 既然一对多 肯定有一个事件调度中心用来调度事件 用户可以注册事件到事件中心 发布者可以发布事件到调度中心 用户也可以取消订阅或者只订阅一次
那么开始实现吧
// 手写发布订阅模式 EventEmitter
class EventEmitter {
constructor() {
this.events = {};
}
// 实现订阅
on(type, callBack) {
if (!this.events) this.events = Object.create(null);
if (!this.events[type]) {
this.events[type] = [callBack];
} else {
this.events[type].push(callBack);
}
}
// 删除订阅
off(type, callBack) {
if (!this.events[type]) return;
this.events[type] = this.events[type].filter(item => {
return item !== callBack;
});
}
// 只执行一次订阅事件
once(type, callBack) {
function fn() {
callBack();
this.off(type, fn);
}
this.on(type, fn);
}
// 触发事件
emit(type, ...rest) {
this.events[type] && this.events[type].forEach(fn => fn.apply(this, rest));
}
}
// 使用如下
const event = new EventEmitter();
const handle = (...rest) => {
console.log(rest);
};
event.on("click", handle);
event.emit("click", 1, 2, 3, 4);
event.off("click", handle);
event.emit("click", 1, 2);
event.once("dbClick", () => {
console.log(123456);
});
event.emit("dbClick");
event.emit("dbClick");
4. 手写 call、apply、bind
先思考?
- call 用法=>第一个参数 可以改变调用函数的 this 指向 第二个以及之后的参数为传入的函数的参数
let obj = {
a: 1
};
function fn(name, age) {
console.log(this.a);
console.log(name);
console.log(age);
}
fn.call(obj, "我是 lihua", "18");
- 怎么改变 this 指向呢=>对象的方法调用 那么方法内部的 this 就指向这个对象
let obj = {
a: 1,
fn(name, age) {
console.log(this.a);
console.log(name);
console.log(age);
}
};
obj.fn("我是lihua", "18");
- 怎么获取传入的不定参数呢 =>...args 剩余参数获取方法(rest)
那么开始实现吧
Function.prototype.myCall = function(context, ...args) {
if (!context || context === null) {
context = window;
}
// 创造唯一的key值 作为我们构造的context内部方法名
let fn = Symbol();
context[fn] = this;
// 执行函数并返回结果
return context[fn](...args);
};
// apply原理一致 只是第二个参数是传入的数组
Function.prototype.myApply = function(context, args) {
if (!context || context === null) {
context = window;
}
// 创造唯一的key值 作为我们构造的context内部方法名
let fn = Symbol();
context[fn] = this;
// 执行函数并返回结果
return context[fn](...args);
};
// apply原理一致 只是返回值是一个函数
Function.prototype.myBind = function(context, ...args) {
if (!context || context === null) {
context = window;
}
// 创造唯一的key值 作为我们构造的context内部方法名
let fn = Symbol();
context[fn] = this;
// 返回函数
return () => {
context[fn](...args);
};
};
// 测试一下
let obj = {
a: 1
};
function fn(name, age) {
console.log(this.a);
console.log(name);
console.log(age);
}
fn.myCall(obj, "我是lihua", "18");
fn.myApply(obj, ["我是lihua", "18"]);
let newFn = fn.myBind(obj, "我是lihua", "18");
newFn();
5. 手写 new 操作符
先思考?
- new 用法=>从构造函数创造一个实例对象 构造函数的 this 指向为创造的实例函数 并且可以使用构造函数原型属性和方法
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function() {
console.log(this.age);
};
let p1 = new Person("lihua", 18);
console.log(p1.name);
p1.say();
-
怎么实现 this 指向改变 =>call apply
-
怎么实现构造函数原型属性和方法的使用 =>原型链 原型继承
那么开始实现吧(对原型链深入理解 建议看看 原型链)
function myNew(fn, ...args) {
// 1.创造一个实例对象
let obj = {};
// 2.生成的实例对象继承构造函数原型
// 方法一 粗暴的改变指向 完成继承
obj.__proto__ = fn.prototype;
// 方法二 利用Object.create实现
// obj=Object.create(fn.prototype)
// 3.改变构造函数this指向为实例对象
let result = fn.call(obj, ...args);
// 4. 如果构造函数执行的结果返回的是一个对象,那么返回这个对象
if ((result && typeof result === "object") || typeof result === "function") {
return result;
}
return obj;
}
// 测试一下
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function() {
console.log(this.age);
};
let p1 = myNew(Person, "lihua", 18);
console.log(p1.name);
console.log(p1);
p1.say();
6. 手写 instanceof
先思考?
- instanceof 原理=>右侧对象的原型对象(prototype )是否在左侧对象的原型链上面
- 怎么遍历左侧对象全部的原型链是关键点=>while(true) 一直遍历 知道原型链的尽头 null 都没有相等就说明不存在 返回 false
那么开始实现吧
function myInstanceof(left, right) {
let leftProp = left.__proto__;
let rightProp = right.prototype;
// 一直会执行循环 直到函数return
while (true) {
// 遍历到了原型链最顶层
if (leftProp === null) {
return false;
}
if (leftProp === rightProp) {
return true;
} else {
// 遍历赋值__proto__做对比
leftProp = leftProp.__proto__;
}
}
}
// 测试一下
let a = [];
console.log(myInstanceof(a, Array));