JavaScript

3/5/2021 JavaScript

# 一、事件模型

DOM1级于1998年10月1日成为W3C推荐标准。DOM1级标准中并没有定义事件相关的内容,所以没有所谓的DOM1级事件模型。

# 1.1 DOM0级事件

<button onclick="doSomething()">点击</button>
<script>
  function doSomething() {}
</script>
1
2
3
4

或者

<button id="btn">点击</button>
<script>
  const btn = document.getElementById('btn');
  btn.onclick = doSomething;
  function doSomething() {}
</script>
1
2
3
4
5
6

# 1.2 DOM2级事件

W3C 将 DOM2 级事件模型的定义为三个阶段:捕获阶段-目标阶段-冒泡阶段
捕获阶段: 当用户对界面上的一个元素执行交互事件(比如点击),事件会从 document 对象开始向内传播,一层一层传递给目标元素,这个过程中,每层元素都能接受到这个事件。
目标阶段: 用户操作的目标元素
冒泡阶段: 当用户对界面上的一个元素执行交互事件(比如点击),事件会从目标元素对象开始向外传播,一层一层传递给 document 对象(部分浏览器会传递到 window 对象),这个过程中,每层元素都能接受到这个事件。

1.2.1 相关API

/*
 * eventType: 字符串,指定事件名
 * function: 指定要事件触发时执行的函数
 * useCapture: 布尔值,指定事件是否在捕获或冒泡阶段执行
 */

// 事件监听
element.addEventListener(eventType, function, useCapture);
// 移除事件监听
element.removeEventListener(eventType, function, useCapture);
1
2
3
4
5
6
7
8
9
10

1.2.2 事件对象

interface Event {
  type: string; // 事件类型
  target: Element; // 目标元素
  stopPropagation: function; // 阻止事件继续捕获或者冒泡
  preventDefault: function; // 阻止事件默认行为
  [propertyName]?: any; // 其他
}
1
2
3
4
5
6
7

1.2.3 案例

<button id="btn">点击</button>
<script>
  const btn = document.getElementById('btn');
  btn.addEventListener('click', function(event) {
    event.stopPropagation();
    // Do something ...
  }, false);
  btn.addRemoveListener('click', function(event) {}, false);
</script>
1
2
3
4
5
6
7
8
9
Internet Explorer 8 及更早IE版本只有 目标阶段-冒泡阶段 两个阶段

相关API:

// 事件监听
element.attatchEvent(eventType, function);
// 移除事件监听
element.detachEvent(eventType, function);
1
2
3
4

事件对象

interface Event {
  type: string; // 事件类型
  srcElement: Element; // 目标元素
  cancelBubble: boolean; // 阻止事件继续冒泡
  returnValue: any; // 阻止事件默认行为
  [propertyName]?: any; // 其他
}
1
2
3
4
5
6
7

# 1.3 事件委托/代理

优势

  1. 节省内存占用,减少事件注册
  2. 新增子对象时无需再次对其绑定事件,适合动态添加元素

点击 <li> 标签,console 出对应的 innerText

<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
</ul>
1
2
3
4
5
6
7

解法一(不实用事件代理)

const list = document.querySelectorAll('li');
for (let i = 0; i < list.length; i++) {
  const item = list[i];
  item.onclick = function () {
    console.log(item.innerText);
  }
}
1
2
3
4
5
6
7

解法二(事件代理到 ul 元素):

const list = document.querySelector('ul');
list.onclick = function (event) {
  const target = event.target;
  if (target && target.tagName === 'LI') {
    console.log(target.innerText);
  }
}
1
2
3
4
5
6
7

或者

const list = document.querySelector('ul');
list.addEventListener('click', function (event) {
  const target = event.target;
  if (target && target.tagName === 'LI') {
    console.log(target.innerText);
  }
})
1
2
3
4
5
6
7

# 1.4 不会冒泡的事件

scrollfocusblurmouseleavemouseenterpauseplay等 这些事件都不支持冒泡,若需要事件委托,需要在捕获阶段进行处理。

此外 mouseoutmouseover 会触发冒泡。

参考 JavaScript 中那些不会冒泡的事件 (opens new window)

# 二、防抖节流

# 2.1 防抖

在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

// 防抖函数
function debounce(fn, delay) {
  let timer = null;
  return function () {
    let _this = this;
    let args = arguments;
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(_this, args);
    }, delay);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 2.2 节流

每隔一段时间,只执行一次函数。

// 节流函数 throttle
function throttle(fn, interval) {
  let last = 0;
  return function () {
    let _this = this;
    let args = arguments;
    let now = +new Date;
    if (now - last >= interval) {
      last = now;
      fn.apply(_this, args);
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 三、深拷贝

# 3.1 JSON方法

function deepCopy(source) {
  if (source === null || typeof source !== 'object') {
    return source;
  }
  return JSON.parse(JSON.stringify(source));
}
1
2
3
4
5
6

那么问题来了,我们知道如果对象中包含了值为 undefinedfunctionsymbol 类型的属性,在 JSON.stringify() 中将会被忽略,所以这个方法简单粗暴,但是也不能满足所有应用场景。

# 3.2 递归

function deepCopy(source) {
  if (source == null || typeof source !== 'object') {
    return source;
  }

  const target = Array.isArray(source) ? [] : {};

  Object.keys(source).forEach(key => {
    target[key] = deepCopy(source[key]);
  })

  return target;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

这样子看起开就好多了,适应了属性包含各种类型的对象的深拷贝,但是也存在了隐藏风险,当属性中存在循环引用的时候,会导致死循环,如何解决?

function deepCopy(source, cache = []) {
  if (source == null || typeof source !== 'object') {
    return source;
  }

  const hit = cache.find(item => item.original === source);
  if (hit) {
    return hit.target;
  }

  const target = Array.isArray(source) ? [] : {};
  cache.push({
    original: source,
    target
  });

  Object.keys(source).forEach(key => {
    target[key] = deepCopy(source[key], cache);
  })

  return target;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

完美!

# 四、箭头函数

箭头函数是普通函数的简写,和普通函数相比有以下几点差异:

  1. 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象
  2. 不可以使用 arguments 对象,该对像在函数体内不存在
  3. 不能用作 generator 函数,所以不可使用 yield 命令
  4. 不可以使用 new 命令,因为
    1. 没有自己的 this,无法调用 callapply
    2. 没有 prototype 属性,而 new 命令在执行是需要将构造函数的 prototype 赋值给新的对象的 __proto__

# 五、bind、call、apply

  1. bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
  2. call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
  3. apply() 方法调用一个具有给定 this 值的函数,以及以一个数组(或类数组对象)的形式提供的参数。

# 5.1 差异

  1. call()bind()的剩余参数是一个或多个,apply()则是使用数组(或类数组对象)提供参数。
  2. bind() 只指定 this 和参数,不自动调用函数。

# 5.2 实现

Function.prototype.myCall = function call(context, ...args) {
  context.fn = this;
  const result = content.fn(...args);
  delete context.fn;
  return result;
}

Function.prototype.myApply = function apply(context, args) {
  context.fn = this;
  const result = context.fn(...args);
  delete context.fn;
  return result;
}

Function.prototype.myBind = function bind(context, ...args) {
  return function () {
    context.myCall(context, ...args);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 六、Promise

# 6.1 手写 promise

const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected';

class MyPromise {
  constructor(fn) {
    this.state = PENDING;
    this.resolvedHandlers = [];
    this.rejectedHandlers = [];
    fn(this.resolve.bind(this), this.reject.bind(this));
    return this;
  }

  resolve(props) {
    setTimeout(() => {
      this.state = RESOLVED;
      const resolveHandler = this.resolvedHandlers.shift();
      if (!resolveHandler) return;
      const result = resolveHandler(props);
      if (result && result instanceof MyPromise) {
        result.then(...this.resolvedHandlers);
      }
    });
  }

  reject(error) {
    setTimeout(() => {
      this.state = REJECTED;
      const rejectHandler = this.rejectedHandlers.shift();
      if (!rejectHandler) return;
      const result = rejectHandler(error);
      if (result && result instanceof MyPromise) {
        result.catch(...this.rejectedHandlers);
      }
    });
  }

  then(...handlers) {
    this.resolvedHandlers = [...this.resolvedHandlers, ...handlers];
    return this;
  }

  catch(...handlers) {
    this.rejectedHandlers = [...this.rejectedHandlers, ...handlers];
    return this;
  }

  static all(promises) {
    return new MyPromise((resolve, reject) => {
      const results = [];
      for (let i = 0; i < promises.length; i++) {
        const promise = promises[i];
        promise.then(res => {
          results.push(res);
          if (results.length === promises.length) {
            resolve(results);
          }
        }).catch(reject);
      }
    })
  }

  static race() {
    return new MyPromise((promises, reject) => {
      for (let i = 0; i < promises.length; i++) {
        const promise = promises[i];
        promise.then(resolve).catch(reject);
      }
    })
  }
}
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

# 执行上下文

# 事件循环