不使用setTimeout和setInterval在页面中实现setInterval和setTimeout效果

·  阅读 3038

前言

昨天面试一家公司,面试官问我,如何在不使用setTimeout和setInterval在页面中实现setInterval和setTimeout效果,我:????。

后来我仔细想了一下,思路就是获取时间戳,然后用递归判断实现。那么思路是这样,下面就代码实现一下吧。

setTimeout的实现

初步实现

function setTimeout_(dalay) {
  // 第一次的时间戳
  const timestampFirst = Date.now()
  // 返回一个promise对象
  return new Promise(reslove => {
    // 操作
    function handle() {
      // 每一次的时间戳
      const timestamp = Date.now()
      // 当时间戳减去后大于延迟时间
      if ((timestamp - timestampFirst) >= dalay) {
        // 成功回调
        reslove()
      } else {
        // 递归
        handle()
      }
    }
    // 初次调用
    handle()
  })
}

setTimeout_(10).then(() => {
  alert(10)
})
复制代码

上面的代码看似没有毛病,但是运行后发现,setTimeout_()里面的值设置小一点没有问题(比如2、3),但是一旦超过,就会造成堆栈溢出,乃至报错。

解决堆栈溢出方法

下面隆重介绍一个人,蹦床函数(trampoline)

蹦床函数(trampoline)就是将 递归执行 转为 循环执行
执行的都是同样的步骤,只是反复执行,就好像在蹦床,跳上去,掉下来,在跳上去…

  • 蹦床函数的实现:
function trampoline(f){
  while(f && f instanceof Function && falg){
   f = f()
  }
  return f
}
复制代码

它接受一个函数f作为参数。只要f执行后返回一个函数,就继续执行。注意,这里是返回一个函数,然后执行该函数,而不是函数里面调用函数,这样就避免了递归执行,从而就消除了调用栈过大的问题

最终实现

// 定时器
function setTimeout_(dalay) {
  // 第一次的时间戳
  const timestampFirst = Date.now()
  // 返回一个 Promise 对象
  return new Promise(reslove => {
    // 具体操作
    function handle() {
      // 每一次的时间戳
      const timestamp = Date.now()
      // 当时间戳减去后大于延迟时间
      if ((timestamp - timestampFirst) >= dalay) {
       // 成功回调
        reslove()
      } else {
       // 不满足条件继续调用
        return handle
      }
    }
    // 调用蹦床函数、将递归变为循环
    trampoline(handle)()
  })
}

// 蹦床函数
function trampoline(f){
  while(f && f instanceof Function){
    f = f()
  }
  return f
}

setTimeout_(1000).then(res => {
  alert(1000)
})
复制代码

以上的代码,就能实现效果了

思路:定义一个函数,参数为延迟时间,调用时记录一个第一次时间戳,然后里面返回一个Promise对象,再里面有一个闭包,是执行递归操作的函数,这个函数里面做的事就是记录每一次的时间戳,然后减去第一次的时间戳,得出的就是间隔时间,跟规定的间隔时间作比较,如果大于的话,就调用Promise成功回调。再下面就是将递归转为循环,防止堆栈溢出。最后调用

setInterval的实现

这个跟setTimeout差不多,区别就是这个需要每隔一段时间执行代码,并且需要手动清除

// 如果 falg 为 false就不会继续执行循环操作
let falg = true
// 蹦床函数技术,利用循环
function trampoline(f){
  while(f && f instanceof Function && falg){
    f = f()
  }
  return f
}

// 计时器
function setInterval_(f, dalay) {
  // 第一次的时间戳
  let timestampFirst = Date.now()
  // 操作
  function handle() {
    // 每一次的时间戳
    const timestamp = Date.now()
    if ((timestamp - timestampFirst) >= dalay) {
      // 间隔时间到了就重置第一次时间戳
      timestampFirst = Date.now()
      // 调用函数
      f()
    }
    return handle
  }
  trampoline(handle)()
}

let count = 0
// 调用
setInterval_(function() {
  count ++
  if (count === 3) {
    falg = false
  }
  console.log(count)
}, 1000)
复制代码

上面这个代码我定义的是在控制台输入1、2、3,然后关闭

思路:同样是判断时间戳,但是跟setTimeout不一样的是每次执行里面的函数需要重置时间,达到每次执行的效果。并且在蹦床函数里面的while增加一个判断,用来控制计时器的停止。

总结:这种东西了解一下,以后当个吹牛逼资本就可以了,毕竟这性能嘛.....

图.png

使用requestAnimationFrame实现

requestAnimationFrame基本介绍

之前我是不知道这个东西的,后来经过评论区大佬指点,我查了一下,发现这个东西确实可以完美解决这个需求,比递归要好太多,为了防止有的兄弟像我一样不了解,就简单介绍一下。

实现动画效果的方法比较多,Javascript 中可以通过定时器 setTimeout 来实现,css3 可以使用 transition 和 animation 来实现,html5 中的 canvas 也可以实现。除此之外,html5 还提供一个专门用于请求动画的API,那就是 requestAnimationFrame,顾名思义就是请求动画帧。

优势:由系统决定回调函数的执行时机。60Hz的刷新频率,那么每次刷新的间隔中会执行一次回调函数,不会引起丢帧,不会卡顿。

用requestAnimationFrame实现setTimeout

function setTimeout_(callback, dalay) {
  // 第一次的时间戳
  const timestampFirst = Date.now()
  // 操作
  function handel() {
    // 每一次的时间戳
    const timestamp = Date.now()
    // 当时间戳减去后大于延迟时间
    if ((timestamp - timestampFirst) >= dalay) {
      callback()
    } else {
      requestAnimationFrame(handel)
    }
  }
  // 初次调用
  requestAnimationFrame(handel)
}

// 使用
setTimeout_(function() {
  console.log(2000)
}, 2000)
复制代码

上面的代码没啥好说的,就是延续之前的思路,把递归换成requestAnimationFrame

用requestAnimationFrame实现setInterval

class setInterval_ {
  constructor(dalay) {
    // 延迟时间
    this.dalay = dalay || 1000
    // requestAnimationFrame 的ID
    this.id = undefined
    // 控制是否结束的状态
    this.isEnd = false
  }

  // 行动
  action(callback) {
    // 第一次的时间戳
    let timestampFirst = Date.now()
    // 具体操作
    const handel = () => {
      // 每一次的时间戳
      const timestamp = Date.now()
      // 当时间戳减去后大于延迟时间
      if ((timestamp - timestampFirst) >= this.dalay) {
        // 执行回调
        callback()
        // 每次把时间初始化
        timestampFirst = Date.now()
      }
      // 如果状态为 true 不会往下执行
      if (this.isEnd) return
      // 每次调用
      this.id = requestAnimationFrame(handel)
    }
    // 初次调用
    this.id = requestAnimationFrame(handel)
  }

  // 清除
  clearInterval_() {
    // 调用清除requestAnimationFrame的方法
    cancelAnimationFrame(this.id)
    // 改状态
    this.isEnd = true
  }
}

// 使用
const s = new setInterval_(1000)
let count = 0
s.action(() => {
  count ++
  console.log(count)
  if (count >= 3) s.clearInterval_()
})
复制代码

这个稍微比上面的复杂一点,因为要考虑清除的问题,所以我决定写到类里面,实现思路也跟之前递归的差不多,就是加了个清除cancelAnimationFrame的方法

分类:
前端
收藏成功!
已添加到「」, 点击更改