这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
面试的时候常常会要求我们手写代码,理解这些手写代码的实现也有利于增加我们的技术沉淀,今天我们就来一起看看这些常见的手写代码实现吧
Promise
重要程度:⭐⭐⭐⭐⭐
手写频率:⭐⭐
Promise
是一个非常重要的知识点,我们必须掌握,但是需要我们手写 Promise 的情况其实并不多,更多的是考我们对 Promise 的理解
下面 promise 的实现出处是 ssh 大佬的最简实现 Promise,支持异步链式调用(20 行),我进行了一些删改
function Promise(exec) {
this.cbs = []
const resolve = value => {
setTimeout(() => {
this.data = value
this.cbs.forEach(cb => cb())
})
}
exec(resolve)
}
Promise.prototype.then = function (onResolved) {
return new Promise(resolve => {
this.cbs.push(() => {
const res = onResolved(this.data)
res instanceof Promise ? res.then(resolve) : resolve(res)
})
})
}
我们来分析一下原理
首先是构造函数
function Promise(exec) {
this.cbs = []
const resolve = value => {
setTimeout(() => {
this.data = value
this.cbs.forEach(cb => cb())
})
}
exec(resolve)
}
首先用cbs
来保存 Promise resolve 时的回调函数集合
这里是为了解决对一个 promise 调用多次then
方法的情况,如下
const promise = new Promise()
promise.then()
然后是resolve
函数,这里使用setTimeout
是为了模拟then
中的函数异步执行,当然setTimeout
属于 task ,Promise.then()
属于 micro task ,所以实际表现存在一些不同,但我们的重点不是这个,忽略掉这一问题
然后在setTimeout
回调中,依次执行cbs
中的函数
最后,执行用户函数exec
, 并传入resolve
然后来分析then
的实现,这一块是重中之中,链式调用的关键,坐好了,准备起飞 ✈️!
Promise.prototype.then = function (onResolved) {
return new Promise(resolve => {
this.cbs.push(() => {
const res = onResolved(this.data)
res instanceof Promise ? res.then(resolve) : resolve(res)
})
})
}
我们先来定义几个别名方便我们之后的叙述
promise1
:new Promise()
返回的 promisepromise2
:Promise.prototype.then
返回的 promiseuser promsie
:在用户调用then
方法的时候,用户手动构造了一个 promise 并且返回的 promise,即res
(可能是 promise)
好了,现在正式开始分析,注意then
中的this
指向promise1
首先我们要明确一点,then
方法是 Promise 实例上的方法,所以我们为了能够链式,才需要在then
方法中返回一个新的 promise 实例
在 promise2
中,promise2
的传入的函数执行了this.cbs.push()
,将一个函数 push 进了 cbs
中,等待后续执行
我们来重点看看 push 的这个函数,这个函数在 promise1
被 resolve 了以后才会执行
;() => {
const res = onResolved(this.data)
res instanceof Promise ? res.then(resolve) : resolve(res)
}
onResolved
就对应then
传入的函数,如果用户自己返回了一个user promise
,那么在这个user promise
里,用户会自己选择合适的时机去 resolve promise2
即这一段逻辑
res instanceof Promise ? res.then(resolve)
将 promise2
的 resolve 交给了 user promise
结合下面这个例子来看:
new Promise(resolve => {
setTimeout(() => {
// resolve1
resolve(1)
}, 500)
})
// then1
.then(res => {
console.log(res)
// user promise
return new Promise(resolve => {
setTimeout(() => {
// resolve2
resolve(2)
}, 500)
})
})
// then2
.then(console.log)
then1
这一整块其实返回的是 promise2
,那么 then2
其实本质上是 promise2.then(console.log)
,
也就是说 then2
注册的回调函数,其实进入了promise2
的 cbs
回调数组里,又因为我们刚刚知道,resolve2
调用了之后,user promise
会被 resolve,进而触发 promise2
被 resolve,进而 promise2
里的 cbs
数组被依次触发。
这样就实现了用户自己写的 resolve2
执行完毕后,then2
里的逻辑才会继续执行,也就是异步链式调用。
深拷贝
重要程度:⭐⭐⭐
手写频率:⭐⭐⭐
开发的时候我们常常会用一些简单的方法去深拷贝对象,比如
const cloneObj = { ...rawObj }
但是在面试的时候,面试官肯定不会满意这样的答案,而且这种方式确实有一定的问题,比如无法复制嵌套的对象情况的情况
const nestedObj = {
name: 'nested',
}
const rawObj = {
name: 'raw',
nestedObj,
}
const cloneObj = { ...rawObj }
cloneObj.nestedObj.name = 'clone'
console.log(cloneObj, nestedObj)
// { name: 'raw', nestedObj: { name: 'clone' } } { name: 'clone' }
我们需要写一个能勾解决上述问题的深拷贝
代码如下
function deepClone(target, map = new Map()) {
if (target instanceof Object) {
let cloneTarget = target instanceof Array ? [] : {}
if (map.get(target)) {
return map.get(target)
}
map.set(target, cloneTarget)
for (const key in target) {
cloneTarget[key] = deepClone(target[key], map)
}
return cloneTarget
} else {
return target
}
}
首先是判断传入的target
是不是一个对象,这里用instanceof
判断而不是``typeof,可以避免将
null`识别为对象的问题
如果不是对象的话,直接返回,我们不需要对它进行处理,否则进入下面的流程
我们将 target
细分为数组和对象,cloneTarget
初始为一个空数组或空对象
然后我们用到了 map
来判断这个 target 是否已经存在于我们的 map 中了,如果存在,直接返回 map 中的 对象,否则将 target 加入 map
这样做是为了解决循环引用的问题,也可以用 weakMap
,性能更好
const rawObj = {
name: 'raw',
}
rawObj.nestedObj = rawObj
const cloneObj = deepClone(rawObj)
console.log(cloneObj)
// { name: 'raw', nestedObj: [Circular *1] }
没有 map 的处理的话,就会报错RangeError: Maximum call stack size exceeded
接下来,遍历 target 的每个 key 对它们递归调用 deepClone
最后返回 cloneTarget
测试一下
const nestedObj = {
name: 'nested',
}
const rawObj = {
name: 'raw',
nestedObj,
}
const cloneObj = deepClone(rawObj)
cloneObj.nestedObj.name = 'clone'
console.log(cloneObj, nestedObj)
// { name: 'raw', nestedObj: { name: 'clone' } } { name: 'nested' }
防抖节流
重要程度:⭐⭐⭐
手写频率:⭐⭐⭐⭐
防抖和节流都是常用且常常背考察手写的代码,防抖和节流的解析如下
防抖:多次执行函数后,只执行最后一次函数,常用于点击按钮发送请求,获取搜索结果等
节流:多次执行函数,每隔一段事件执行一次函数,常用于通过监听滚轮事件实现的懒加载等
来看看它们的实现
防抖
function debounce(fn, delay) {
let timer = null
return (...args) => {
clearTimeout(timer)
timer = setTimeout(fn, delay, ...args)
}
}
节流
function throttle(fn, delay) {
let canRun = true
return (...args) => {
if (!canRun) return
canRun = false
setTimeout(() => {
fn(...args)
canRun = true
}, delay)
}
}
这两个函数只要理解了需要实现的目标,代码就很好理解啦,所以这里不再赘述
这篇文章对你有帮助的话,请给我点一个小小的赞吧,我们下一期再见
下一期我会介绍各种 api 的实现,如 map
、reduce
、filter
、call
等,敬请期待