前端造轮子之面试手写代码

403 阅读2分钟

1.手写 Promise .all & .race

MDN Promise

promise.jpg

class xnPromise {
  constractor(executor) {
    this.status = 'pending'
    this.value = undefined
    this.reason = undefined
    this.onResolvedCallbacks = []
    this.onRejectedCallbacks = []
    
    const resolve = value => {
      if(this.status === 'pending') {
       this.status = 'fulfilled'
       this.value = value
       this.onResolvedCalbacks.forEach(fn => fn())
      }
    }
    
    const reject = reason => {
      if(this.status === 'pending') {
        this.status = 'rejected'
        this.reason = reason
        this.onRejectedCallbacks.forEach(fn => fn())
      }
    }
    
    try {
      executor(resolve, reject)
    } catch(err) {
      reject(err)
    }
  }
  
  then(onFulfilled, onRejected) {
    if(this.status === 'fulfilled') {
      onFulfilled(this.value)
    } else if(this.status === 'rejected') {
      onRejected(this.reason)
    } else if(this.status === 'pending') {
      this.onResolvedCallbacks.push(() => {
        onFulfilled(this.value)
      })
      this.onRejectedCallbacks.push(() => {
        onRejected(this.reason)
      })
    }
  }
  
  static all(promiseArr) {
   return new xnPromise((resolve, reject) => {
    let result = []
    promiseArr.forEach((promise, index) => {
      promise.then(value => {
       result[index] = value
       if(result.length === promiseArr.length) {
        resolve(result)
       }
      }, reject)
    })
   })
  }
  
  static race(promiseArr) {
   return new xnPromise((resolve, reject) => {
     promiseArr.forEach(promise => {
      promise.then(value => {
       resolve(value)
      }, reject)
     })
   })
  }
}

module.exports = xnPromise

1.1 jest 测试: xnPromise.all() .race()

jest-promise.spec.js

展开 jest 测试代码
// file: xnPromise.spec.js
const xnPromise = require('./xnPromise.js')
it('基本功能:', (done) => {
  let xnpromise
  xnpromise = new xnPromise((resolve, reject) => {
    setTimeout(() => {
      resolve('success')
    }, 213)
  })
  xnpromise.then(data => {
    expect(data).toBe('success')
  })
  xnpromise = new xnPromise((resolve, reject) => {
    setTimeout(() => {
     reject('failure')
    }, 213)
  })
  xnpromise.then(data => {
    console.log('resolve data: ', data)
  }, err => {
    expect(err).toBe('failure')
    done()
  })
})
// test: .all()
test('xnPromise.all()', done => {
  const promise1 = new xnPromise((resolve, reject) => {
    setTimeout(() => {
      resolve('promise1 success')
    }, 213)
  })
  const promise2 = new xnPromise((resolve, reject) => {
    setTimeout(() => {
     resolve('promise2 success')
    }, 213)
  })
  xnPromise.all([promise1, promise2]).then(result => {
    expect(result).toEqual(['promise1 success', 'promise2 success'])
    done()
  })
})
// test: .race()
test('xnPromise.race()', done => {
  const promise1 = new xnPromise((resolve, reject) => {
    setTimeout(() => {
      resolve('promise1 success')
    }, 213)
  })
  const promise2 = new xnPromise((resolve, reject) => {
    setTimeout(() => {
      resolve('promise2 success')
    }, 50)
  })
  // promise2 时间短 成功后 .race() 立马返回 'promise2 success'
  xnPromise.race([promise1, promise2]).then(result => {
    expect(result).toBe('promise2 success')
    done()
  })
})

2. 节流throttle & 防抖debounce

2.1 节流 throttle

throttle.jpg

function throttle(fn, delay) {
  let last = 0
  return (...args) => {
    const now = +Date.now()
    // 两次事件执行时间间隔大于 delay,则可调用 fn
    if(now - last > delay) {
      last = now
      fn.apply(this, args)
    }
  }
}

jest throttle

jest-throttle.spec.js.jpg

2.2 防抖 debounce

function debounce(fn, delay) {
  let timer
  return (...args) => {
    if(timer) {
      clearTimeout(timer)
    }
    // 只能有定时器调用 fn
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

jest debounce

jest-03-debounce.spec.js

3. 手写发布订阅 EventEmitter

监听 DOM 事件, 其实就是发布订阅模式, 订阅 DOM事件,当用户执行相应的操作时发布事件,也可以手动取消对事件的订阅

const el = document.querySelector('el')
el.addEventListener('click', () => {console.log('click1'))
// 可以订阅多个相同事件
el.addEventListener('click', () => {console.log('click2'))
// 可以取消订阅, 这里必须要使用与订阅时相同的函数
el.removeEventListener('click', handler)

vue中使用自定义事件也会用到 发布订阅模式

// 父组件
<my-component v-on:my-event="doSomething"></my-component>
// MyCoponent 组件
methods: {
  fn() {
    this.$emit('my-event', data)
  }
}

首先就是 架子

class EventEmitter {
  on() {} // 订阅事件
  emit() {} // 触发事件
  off() {} // 移除事件 => removeListener
  once() {} // 执行一次
}

完整版

class EventEmitter {
   private events = {} // 声明全局变量存储订阅的事件及其对应的执行函数
   // 订阅事件方法
   on(eventName, callback) {
     this.events[eventName] = this.events[eventName] || []
     this.events[eventName].push(callback)
   }
   // 触发事件方法
   emit(eventName, data) {
     // if(!this.events[eventName]) return
     // this.events[eventName].forEach(cb => cb(data))
     (this.events[eventName] || []).forEach(cb => cb(data))
   }
   // 移除订阅事件
   removeListener(eventName, callback) {
     if(this.events[eventName]){
       this.events[eventName] = this.events[eventName].filter(cb => cb != callback)
     }
   }
   // 只执行一次订阅的事件,然后移除
   once(eventName, callback) {
     let fn = () => {
       callback() // fn 函数中调用原有的 callback
       this.removeListener(eventName, fn) // 删除 fn, 再执行的时候之后执行一次
     }
     this.on(eventName, fn)
   }
}