十一、手写防抖节流和深拷贝和事件总线工具函数

2022/8/12 ES6防抖节流深拷贝事件总线

# 1、防抖debounce函数的深入学习

# 1.1.认识防抖和节流函数

  • 防抖和节流的概念其实最早并不是出现在软件工程中,防抖是出现在电子元件中,节流出现在流体流动中
    • 而JavaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理。
    • 而对于某些频繁的事件处理会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生;
  • 防抖和节流函数目前已经是前端实际开发中两个非常重要的函数,也是面试经常被问到的面试题。
  • 但是很多前端开发者面对这两个功能,有点摸不着头脑:
    • 某些开发者根本无法区分防抖和节流有什么区别(面试经常会被问到);
    • 某些开发者可以区分,但是不知道如何应用;
    • 某些开发者会通过一些第三方库(Underscore的官网 (opens new window))来使用,但是不知道内部原理,更不会编写;
  • 接下来我们会一起来学习防抖和节流函数:
    • 我们不仅仅要区分清楚防抖和节流两者的区别,也要明白在实际工作中哪些场景会用到;
    • 并且我会带着大家一点点来编写一个自己的防抖和节流的函数,不仅理解原理,也学会自己来编写;

# 1.2.认识防抖debounce函数

  • 我们用一副图来理解一下它的过程:
    • 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;
    • 当事件密集触发时,函数的触发会被频繁的推迟;
    • 只有等待了一段时间也没有事件触发,才会真正的执行响应函数;

vGPU1S.png

  • 防抖的应用场景很多:
    • ➢输入框中频繁的输入内容,搜索或者提交信息;
    • ➢频繁的点击按钮,触发某个事件;
    • ➢监听浏览器滚动事件,完成某些特定操作;
    • ➢用户缩放浏览器的resize事件;

# 1.3.防抖函数的案例

  • 我们都遇到过这样的场景,在某个搜索框中输入自己想要搜索的内容:
  • 比如想要搜索一个MacBook:
    • 当我输入m时,为了更好的用户体验,通常会出现对应的联想内容,这些联想内容通常是保存在服务器的,所以需要一次网络请求;
    • 当继续输入ma时,再次发送网络请求;
    • 那么macbook一共需要发送7次网络请求;
    • 这大大损耗我们整个系统的性能,无论是前端的事件处理,还是对于服务器的压力;

vGEXTJ.png

  • 但是我们需要这么多次的网络请求吗?
    • 不需要,正确的做法应该是在合适的情况下再发送网络请求;
    • 比如如果用户快速的输入一个macbook,那么只是发送一次网络请求;
    • 比如如果用户是输入一个m想了一会儿,这个时候m确实应该发送一次网络请求;
    • 也就是我们应该监听用户在某个时间,比如500ms内,没有再次触发时间时,再发送网络请求;
  • 这就是防抖的操作:只有在某个时间内,没有再次触发某个函数时,才真正的调用这个函数;

# 1.4.生活中的例子:防抖

  • 比如说有一天我上完课,我说大家有什么问题来问我,我会等待五分钟的时间。
  • 如果在五分钟的时间内,没有同学问我问题,那么我就下课了;
    • 在此期间,a同学过来问问题,并且帮他解答,解答完后,我会再次等待五分钟的时间看有没有其他同学问问题;
    • 如果我等待超过了5分钟,就点击了下课(才真正执行这个时间);

# 1.5.案例准备

  • 我们通过一个搜索框来延迟防抖函数的实现过程:监听input的输入,通过打印模拟网络请求
  • 测试发现快速输入一个macbook共发送了7次请求,显然我们需要对它进行防抖操作:
  <label for="shop">
    商品输入:<input type="text" id="shop">
  </label>
  <script>
    let index = 1
    const input = document.querySelector('input')
    input.oninput = function() {
      console.log(`发送了${index++}次网络请求`,this.value)
    }
  </script>
1
2
3
4
5
6
7
8
9
10

vGVPOO.png

  • 下面我们用一个Underscore库,使用库中已经封装好的防抖函数来解决这个问题;

# 1.6.Underscore库的介绍

  • 事实上我们可以通过一些第三方库来实现防抖操作:
    • lodash 库
    • underscore 库
  • 这里使用underscore
    • 我们可以理解成lodash是underscore的升级版,它更重量级,功能也更多;
    • 但是目前我看到underscore还在维护,lodash已经很久没有更新了;
  • Underscore的官网 (opens new window)
  • Underscore的安装有很多种方式:
    • 下载Underscore,本地引入;
    • 通过CDN直接引入;
    • 通过包管理工具(npm)管理安装;
  • 这里我们直接通过CDN:https://cdn.jsdelivr.net/npm/underscore@1.13.4/underscore-umd-min.js 库中防抖函数功能具体用法 (opens new window)
  • 用Underscore的_.debounce实现防抖
<label for="shop">
  商品输入:<input type="text" id="shop">
    </label>
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.4/underscore-umd-min.js"></script>
<script>
  let index = 1
  const input = document.querySelector('input')

  //_.debounce(function, wait, [immediate])
  input.oninput =_.debounce( function() {
    console.log(`发送了${index++}次网络请求`,this.value)
  }, 1000, true)

  const debounce = _.debounce(function() {
    return 'kobe'
  },1000)
  console.log(debounce())//undefined
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • 下面我们自己来试着手写一下这个防抖的功能函数吧

# 2、自定义实现防抖函数

# 2.1.我们按照如下思路来实现:

# 2.2.防抖基本功能实现:可以实现防抖效果

<label for="shop">
  商品输入:<input type="text" id="shop">
    </label>
<script>
    let index = 1

function ykDebounce (fn,delay) {
  let timer = null
  const _debounce = () => {
    if(timer) clearTimeout(timer)
    timer = setTimeout(()=> {
      fn()
      timer = null
    },delay)
  }
  return _debounce
}

  const input = document.querySelector('input')
  input.oninput = ykDebounce(function() {
    console.log(`发送了${index++}次网络请求`,this.value)
  },1000)
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 2.3.优化一:优化参数和this指向

  • ->实现到这里你就知道防抖的核心代码了
<label for="shop">
  商品输入:<input type="text" id="shop">
</label>
<script>
  let index = 1

  function ykDebounce (fn,delay) {
    let timer = null
    const _debounce = function(...args) {
      if(timer) clearTimeout(timer)
      timer = setTimeout(()=> {
        fn.apply(this,args)
        timer = null
      },delay)
    }
    return _debounce
  }

  const input = document.querySelector('input')
  input.oninput = ykDebounce(function() {
    console.log(`发送了${index++}次网络请求`,this.value)
  },1000)

  const _debounce = ykDebounce(function(name,age){
    console.log({name:name,age:age})
  },3000)
  _debounce('lyk',99)
  _debounce('lyk',99)
  _debounce('lyk',99)
  _debounce('lyk',99)
  _debounce('lyk',199)

</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 2.4.优化二:优化取消操作(增加取消功能cancel)

<label for="shop">
  商品输入:<input type="text" id="shop">
</label>
<button>取消</button>
<script>
  let index = 1

  function ykDebounce(fn, delay) {
    let timer = null
    const _debounce = function (...args) {
      if (timer) clearTimeout(timer)
      timer = setTimeout(() => {
        fn.apply(this, args)
        timer = null
      }, delay)
    }
    //给防抖函数绑定一个取消函数
    _debounce.cancel = function () {
      if (timer) clearTimeout(timer)
      timer = null
    }
    return _debounce
  }

  const input = document.querySelector('input')
  const btnEl = document.querySelector('button')

  const debounce = ykDebounce(function () {
    console.log(`发送了${index++}次网络请求`, this.value)
  },2000)

  input.oninput = debounce
  btnEl.onclick = debounce.cancel
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 2.5.优化三:优化立即执行效果(第一次立即执行immediate)

<label for="shop">
  商品输入:<input type="text" id="shop">
    </label>
<button>取消</button>
<script>
  let index = 1

  function ykDebounce(fn, delay, immediate = false) {
    let timer = null
    let immediateFlag = true
    const _debounce = function (...args) {
      if (timer) clearTimeout(timer)
      console.log('触发了事件')
      if (!immediate) {
        timer = setTimeout(() => {
          fn.apply(this, args)
          timer = null
        }, delay)
      }

      if (immediate && immediateFlag) {
        fn.apply(this, args)
        immediateFlag = false
      }
      if (immediate) {
        timer = setTimeout(() => {
          console.log('时间到了------------')
          timer = null
          immediateFlag = true
        }, delay)
      }
    }
    //给防抖函数绑定一个取消函数
    _debounce.cancel = function () {
      if (timer) clearTimeout(timer)
      timer = null
    }
    return _debounce
  }

  const input = document.querySelector('input')
  const btnEl = document.querySelector('button')

  const debounce = ykDebounce(function () {
    console.log(`发送了${index++}次网络请求`, this.value)
  }, 600, true)

  input.oninput = debounce
  btnEl.onclick = debounce.cancel
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# 2.6.优化四:优化返回值(ES5回调函数方案)

<label for="shop">
  商品输入:<input type="text" id="shop">
    </label>
<button>取消</button>
<script>
  let index = 1

  function ykDebounce(fn, delay, callBack, immediate = false) {
    let timer = null
    let immediateFlag = true
    const _debounce = function (...args) {
      if (timer) clearTimeout(timer)
      console.log('触发了事件')
      if (!immediate) {
        timer = setTimeout(() => {
          const result = fn.apply(this, args)
          callBack(result)
          timer = null
        }, delay)
      }

      if (immediate && immediateFlag) {
        const result = fn.apply(this, args)
        callBack(result)
        immediateFlag = false
      }
      if (immediate) {
        timer = setTimeout(() => {
          console.log('时间到了------------')
          timer = null
          immediateFlag = true
        }, delay)
      }
    }
    //给防抖函数绑定一个取消函数
    _debounce.cancel = function () {
      if (timer) clearTimeout(timer)
      timer = null
    }
    return _debounce
  }

  const input = document.querySelector('input')
  const btnEl = document.querySelector('button')

  const debounce = ykDebounce(function () {
    console.log(`发送了${index++}次网络请求`, this.value)
    return this.value
  }, 600, function(res){
    console.log(`拿到返回值:`,res)
  },false)

  input.oninput = debounce
  btnEl.onclick = debounce.cancel
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# 2.7.优化四:优化返回值(Promise方案)

<label for="shop">
  商品输入:<input type="text" id="shop">
    </label>
<button>取消</button>
<script>
  let index = 1
  function ykDebounce(fn, delay, immediate = false) {
    let timer = null
    let immediateFlag = true
    const _debounce = function (...args) {
      return new Promise((resolve, reject) => {
        try {
          if (timer) clearTimeout(timer)
          console.log('触发了事件')
          if (!immediate) {
            timer = setTimeout(() => {
              const result = fn.apply(this, args)
              resolve(result)
              timer = null
            }, delay)
          }
          //立即执行效果
          if (immediate && immediateFlag) {
            const result = fn.apply(this, args)
            resolve(result)
            immediateFlag = false
          }
          if (immediate) {
            timer = setTimeout(() => {
              console.log('时间到了------------')
              timer = null
              immediateFlag = true
            }, delay)
          }
        } catch (error) {
          reject(error)
        }
      })
    }
    //给防抖函数绑定一个取消函数
    _debounce.cancel = function () {
      if (timer) clearTimeout(timer)
      timer = null
      console.log('取消操作')
    }
    return _debounce
  }

  const input = document.querySelector('input')
  const btnEl = document.querySelector('button')

  const debounce = ykDebounce(function () {
    console.log(`发送了${index++}次网络请求`, this.value)
    return '我是返回值'
  }, 600, false)
  input.oninput = debounce

  const debounce1 = ykDebounce(function (name, age) {
    return { name: name, age: age }
  }, 1000)
  debounce1('kobe', 39).then(res => {
    console.log(res)
  })

  btnEl.onclick = debounce1.cancel

</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

# 3、节流throttle函数的深入学习

# 3.1.认识节流throttle函数

  • 我们用一副图来理解一下节流的过程

    • 当事件触发时,会执行这个事件的响应函数;

    • 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数;

    • 不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的;

vGgpCD.png

  • 节流的应用场景:
    • ➢ 监听页面的滚动事件;
    • ➢ 鼠标移动事件;
    • ➢ 用户频繁点击按钮操作;
    • ➢ 游戏中的一些设计;

# 3.2.节流函数的应用场景

  • 很多人都玩过类似于飞机大战的游戏
  • 在飞机大战的游戏中,我们按下空格会发射一个子弹:
    • 很多飞机大战的游戏中会有这样的设定,即使按下的频率非常快,子弹也会保持一定的频率来发射;
    • 比如1秒钟只能发射一次,即使用户在这1秒钟按下了10次,子弹会保持发射一颗的频率来发射;
    • 但是事件是触发了10次的,响应的函数只触发了一次;

vGcRun.png

# 3.3.生活中的例子:节流

  • 比如说有一天我上完课,我说大家有什么问题来问我,但是在一个5分钟之内,不管有多少同学来问问题,我只会解答一个问题;
  • 如果在解答完一个问题后,5分钟之后还没有同学问问题,那么就下课

# 3.4.Underscore库使用节流功能

<label for="shop">
  商品输入:<input type="text" id="shop">
    </label>
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.4/underscore-umd-min.js"></script>
<script>
  let index = 1
  const input = document.querySelector('input')

  // _.throttle(function, wait, [options])
  //options参数:如果您想禁用前沿调用,请传递{leading: false},如果您想禁用后沿执行,请传递{trailing: false}。默认:它两值都为true
  input.oninput = _.throttle(function () {
    console.log(`发送了${index++}次网络请求`, this.value)
  }, 1000,{leading:true,trailing:false})

  const throttle = _.throttle(function () {
    return 'lyk'
  }, 3000)
  console.log(throttle())//lyk
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 下面我们试着手写一下节流功能函数吧

# 4、自定义实现节流函数

# 4.1.我们按照如下思路来实现:

# 4.2.节流函数的基本实现:

  • 可以实现节流效果(并优化参数和this指向 )

  • (实现到这里你就知道节流的核心代码了)

商品输入:<input type="text">
<script>
  const inputEl = document.querySelector('input')
  let index = 1
  function ykThrottle(fn, interval) {
    let startTime = 0
    return function _throttle(...args) {
      // 1.获取当前时间
      const nowTime = new Date().getTime()
      // 2.计算需要等待的时间执行函数
      const waitTime = interval - (nowTime - startTime)
      if (waitTime <= 0) {
        fn.apply(this,args)
        startTime = nowTime
      }
    }
  }

  const throttle = ykThrottle(function () {
    console.log('发送网络请求', index++)
  },1000)
  inputEl.oninput = throttle
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 4.3.优化一:节流的立即执行效果(第一次立即执行)控制

商品输入:<input type="text">
<script>
  const inputEl = document.querySelector('input')
  let index = 1
  function ykThrottle(fn, interval, { leading = true } = {}) {
    let startTime = 0
    return function _throttle(...args) {
      // 1.获取当前时间
      const nowTime = new Date().getTime()
      // 对立即执行进行控制leading
      if (!leading && startTime===0) {
        startTime = nowTime
      }
      // 2.计算需要等待的时间执行函数
      const waitTime = interval - (nowTime - startTime)
      if (waitTime <= 0) {
        fn.apply(this, args)
        startTime = nowTime
      }
    }
  }

  const throttle = ykThrottle(function () {
    console.log('发送网络请求', index++)
  }, 1000, { leading: false })
  inputEl.oninput = throttle
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 4.4.优化二:节流最后一次也可以执行(尾部控制)

商品输入:<input type="text">
  <script>
  const inputEl = document.querySelector('input')
  let index = 1
  function ykThrottle(fn, interval, { leading = true, trailing = true } = {}) {
    let startTime = 0
    let timer = null
    return function _throttle(...args) {
      if (timer) clearTimeout(timer)
      // 1.获取当前时间
      const nowTime = new Date().getTime()
      // 对立即执行进行控制leading
      if (!leading && startTime === 0) {
        startTime = nowTime
      }
      // 2.计算需要等待的时间执行函数
      const waitTime = interval - (nowTime - startTime)
      if (waitTime <= 0) {
        console.log('正常调用')
        fn.apply(this, args)
        startTime = nowTime
        // return 
      }
      //3.对最后一次执行进行控制(尾部控制)trailing
      if (waitTime > 0 && trailing) {
        timer = setTimeout(() => {
          console.log('timer')
          fn.apply(this, args)
          startTime = nowTime
          timer = null
        }, waitTime)
      }

    }
  }
  const throttle = ykThrottle(function () {
    console.log('发送网络请求', index++)
  }, 1000, { leading: false, trailing: true })


  inputEl.oninput = throttle
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 4.5.优化三:优化添加取消功能cancel()

商品输入:<input type="text">
  <button>取消节流阀</button>
<script>
  const inputEl = document.querySelector('input')
  const btnEl = document.querySelector('button')
  let index = 1
  function ykThrottle(fn, interval, { leading = true, trailing = true } = {}) {
    let startTime = 0
    let timer = null
    function _throttle(...args) {
      if (timer) clearTimeout(timer)
      // 1.获取当前时间
      const nowTime = new Date().getTime()
      // 对立即执行进行控制leading
      if (!leading && startTime === 0) {
        startTime = nowTime
      }
      // 2.计算需要等待的时间执行函数
      const waitTime = interval - (nowTime - startTime)
      if (waitTime <= 0) {
        console.log('正常调用')
        fn.apply(this, args)
        startTime = nowTime
        return
      }
      //3.对最后一次执行进行控制(尾部控制)trailing
      if (trailing) {
        timer = setTimeout(() => {
          console.log('timer')
          fn.apply(this, args)
          startTime = Date.now()
          timer = null
        }, waitTime)
      }

    }
    
    //4.添加取消功能cancel()
    _throttle.cancel = function() {
      startTime = 0
      if(timer) clearTimeout(timer)
      timer = null
    }
    return _throttle
  }

  const throttle = ykThrottle(function () {
    console.log('发送网络请求', index++)
  }, 3000, { leading: true, trailing: true })


  inputEl.oninput = throttle

  btnEl.addEventListener('click',function() {
    console.log('取消了节流阀')
    throttle.cancel()
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# 4.6.优化四:优化返回值问题(Promise)

商品输入:<input type="text">
<button>取消节流阀</button>
<script>
  const inputEl = document.querySelector('input')
  const btnEl = document.querySelector('button')
  let index = 1
  function ykThrottle(fn, interval, { leading = true, trailing = true } = {}) {
    let startTime = 0
    let timer = null
    function _throttle(...args) {
      return new Promise((resolve, reject) => {
        try {
          if (timer) clearTimeout(timer)
          // 1.获取当前时间
          const nowTime = new Date().getTime()
          // 对立即执行进行控制leading
              if (!leading && startTime === 0) {
                startTime = nowTime
              }

              // 2.计算需要等待的时间执行函数
              const waitTime = interval - (nowTime - startTime)
              if (waitTime <= 0) {
                console.log('正常调用')
                resolve(fn.apply(this, args))
                startTime = nowTime
                return
              }

              //3.对最后一次执行进行控制(尾部控制)trailing
              if (trailing) {
                timer = setTimeout(() => {
                  console.log('timer')
                  resolve(fn.apply(this, args))
                  startTime = Date.now()
                  timer = null
                }, waitTime)
              }
            } catch (err) {
              reject(err)
            }
          })
        }
        //4.添加取消功能cancel()
        _throttle.cancel = function () {
          startTime = 0
          if (timer) clearTimeout(timer)
          timer = null
        }
        return _throttle
      }

  const throttle = ykThrottle(function (name, age) {
    console.log('发送网络请求', index++)
    return { name, age }
  }, 3000, { leading: true, trailing: true })

  inputEl.oninput = throttle

  btnEl.addEventListener('click', function () {
    console.log('取消了节流阀')
    throttle.cancel()
  })

  throttle('kobe', 39).then(res => {
    console.log('拿到返回值:', res)
  }).catch(error => {
    console.log(error)
  })
  throttle('kobe', 39).then(res => {
    console.log('拿到返回值:', res)
  }).catch(error => {
    console.log(error)
  })
  throttle('kobe', 39).then(res => {
    console.log('拿到返回值:', res)
  }).catch(error => {
    console.log(error)
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

# 5、自定义深拷贝函数

  • 前面我们已经学习了对象相互赋值的一些关系,分别包括:
    • 引入的赋值:指向同一个对象,相互之间会影响;
    • 对象的浅拷贝:只是浅层的拷贝,内部引入对象时,依然会相互影响;
    • 对象的深拷贝:两个对象不再有任何关系,不会相互影响;
  • 前面我们已经可以通过一种方法来实现深拷贝了:JSON.parse(JSON.stringify(value))
    • 这种深拷贝的方式其实对于函数、Symbol等是无法处理的
    • 并且如果存在对象的循环引用,也会报错的
  • 自定义深拷贝函数:
    • 自定义深拷贝的基本功能;
    • 对Symbol的key进行处理;
    • 其他数据类型的值进程处理:对象,数组、函数、Symbol、Set、Map;
    • 对循环引用的处理;
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <script>
    // 一:封装一个函数判断 是否是引用类型
    function isObject(value) {
      const type = typeof value
      return value !== null && (type === 'object' || type === 'function')
    }

    // 二:自定义深拷贝的全部功能;
    function deepCopy(originValue, map = new WeakMap) {
      // 0.如果值是Symbol的类型
      if (typeof originValue === "symbol") {
        return Symbol(originValue.description)
      }
      
      //1.如果是原始数据类型 或者是函数function类型, 不需要进行深拷贝, 直接返回
      if (!isObject(originValue) || typeof originValue === 'function') {
        return originValue
      }
      
      //3.如果是set类型 则对Set类型进行处理
      if (originValue instanceof Set) {
        let newSet = new Set()
        for (const setItem of originValue) {
          newSet.add(deepCopy(setItem))
        }
        return newSet
      }
      
      //5.循环引用解决方案:WeakMap
      if (map.has(originValue)) {
        return map.get(originValue)
      }

      //2.如果是对象类型,才需要创建新的对象
      const newObj = Array.isArray(originValue) ? [] : {}
      map.set(originValue, newObj)
      console.log(map)
      
      for (const key in originValue) {
        newObj[key] = deepCopy(originValue[key], map)
      }
      
      //4. 单独遍历Symbol,以为属性为Symbol类型,通过枚举是枚举不出来的;
      //只能通过Object.getOwnPropertySymbols(obj)方法来获取,对象属性为Symbol类型 的值
      //下面对属性为Symbol类型的特殊处理
      const symbolKeys = Object.getOwnPropertySymbols(originValue)
      for (const symbolKey of symbolKeys) {
        newObj[Symbol(symbolKey.description)] = deepCopy(originValue[symbolKey], map)
      }

      return newObj
    }


    //三:用来测试自定义深拷贝工具函数
    const friends = ['curry', 'james']//测试set类型中的深拷贝
    const s1 = Symbol('s1')
    const obj = {
      name: 'kobe',
      address: {
        city: '上海',
        detail: '浦东新区'
      },
      friends: ['curry', 'james'],
      running() {
        console.log(this.friends[0])
      },
      set: new Set(['nba', 'cba', friends]),
      [s1]: 'ncaaya'
    }
    obj.info = obj //循环引用

    const newObj = deepCopy(obj)
    newObj.address.city = '广州'
    newObj.set.delete('nba')
    newObj.friends[0] = 'lyk'
    friends[0] = 'rose'//测试set类型中的深拷贝
    console.log('obj:', obj)
    console.log('newObj:', newObj)
    obj.running()
    newObj.running()
    console.log(obj[s1] === newObj[s1])
  </script>
</body>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97

# 6、自定义事件总线

  • 自定义事件总线属于一种观察者模式,其中包括三个角色:
    • 发布者(Publisher):发出事件(Event);
    • 订阅者(Subscriber):订阅事件(Event),并且会进行响应(Handler);
    • 事件总线(EventBus):无论是发布者还是订阅者都是通过事件总线作为中台的;
  • 当然我们可以选择一些第三方的库:
    • Vue2默认是带有事件总线的功能;
    • Vue3中推荐一些第三方库,比如mitt;
  • 当然我们也可以实现自己的事件总线:
    • 事件的监听方法on;
    • 事件的发射方法emit;
    • 事件的取消监听off;

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>

  <button class="nav-btn">nav button</button>
  
  <script>

    // 类EventBus -> 事件总线对象
    class ykEventBus {
      constructor() {
        this.eventMap = {}// {navclick:[fn]}
      }
      on(eventName, eventFn) {//监听方法
        let eventFns = this.eventMap[eventName]
        if (!eventFns) {
          eventFns = []
          this.eventMap[eventName] = eventFns
        }
        eventFns.push(eventFn)
      }
      
      emit(eventName, ...args) {//发出事件
        let eventFns = this.eventMap[eventName]
        if (!eventFns) return
        eventFns.forEach(fn => {
          fn(...args)
        })
      }
      
      off(eventName, eventFn) {
        let eventFns = this.eventMap[eventName]
        if (!eventFns) return
        for (let i = 0; i < eventFns.length; i++) {
          const fn = eventFns[i]
          if (fn === eventFn) {
            eventFns.splice(i, 1)
            break
          }
        }

        // 如果eventFns已经清空了
        if (eventFns.length === 0) {
          delete this.eventMap[eventName]
        }
      }


    }


    // 使用过程
    const eventBus = new ykEventBus()

    // aside.vue组件中监听事件
    const click1 = (name, age, height) => {
      console.log("navclick listener 01", name, age, height)
    }
    const click2 =  () => {
      console.log("navclick listener 02")
    }

    eventBus.on("navclick", click1)


    eventBus.on("navclick", click2)

    setTimeout(() => {
      eventBus.off("navclick", click2)
    }, 5000);


    eventBus.on("asideclick", () => {
      console.log("asideclick listener")
    })


    // nav.vue
    const navBtnEl = document.querySelector(".nav-btn")
    navBtnEl.onclick = function() {
      console.log("自己监听到")
      eventBus.emit("navclick", "kobe", 39, 1.98)
    }

  </script>

</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
最后更新时间: 2022/08/16, 15:42:33
彩虹
周杰伦