超简洁版手写代码 这次一定记得住(一)

449 阅读5分钟

这是我参与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)
    })
  })
}

我们先来定义几个别名方便我们之后的叙述

  • promise1new Promise()返回的 promise
  • promise2Promise.prototype.then 返回的 promise
  • user 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注册的回调函数,其实进入了promise2cbs 回调数组里,又因为我们刚刚知道,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 的实现,如 mapreducefiltercall等,敬请期待

参考文章

最简实现 Promise,支持异步链式调用(20 行)