为DOM2级事件函数传参——bind的应用

在复杂的 web 开发中,我们应采用 DOM 2 级事件来绑定和移除函数。即使用 addEventListenerremoveEventListener 方法为 DOM 节点绑定和解绑函数。因为 DOM 1 级事件不支持多个函数的绑定。这意味着,当你使用 DOM 1 级为 DOM 绑定新函数时,旧的函数会被取代。这在很多情况下是不被希望看到的。

例如,我们经常会在 document 对象上绑定多个鼠标事件,使用 DOM 2 级事件既不会取消之前的函数,也不用担心对项目组其他成员编写的事件绑定造成影响。

一个问题

一个常见的 DOM 2 级事件绑定和解绑如下:

1
2
3
4
5
6
7
8
9
const fn = () => {
console.log('in function');
}

// 绑定
element.addEventListener('cilck', fn);

// 解绑
element.removeEventListener('click', fn);

如上,由于一个 DOM 2 级事件可以被绑定多个函数,所以当我们解绑函数时,需指明要解绑的函数名。这意味着,如果在 DOM 2 级事件上绑定了一个匿名函数,那么是难以解绑改函数的。所以应尽量避免绑定匿名函数。

但通常,绑定的函数并不是完全孤立的,我们需要传入一些参数。新手常会犯的一个错误就是企图通过以下语句传参:

1
element.addEventListener('click', fn(params));

然而,上面的写法会直接执行 fn 函数,而不是等待点击事件的触发时才执行。给 click 绑定的也是 fn(params) 的返回值,而非 fn 函数。

实例

下面是一个例子。该实例实现点击小球时可以拖拽小球,当松开鼠标时停止拖拽。考虑到要在 document 上绑定事件,我们使用 DOM 2 级事件。

通常,实现拖拽效果时,大体思路是:

  • 点击目标对象时,为 document 绑定 mousemove 事件,当鼠标移动时,根据鼠标位置设置目标对象的位置。

  • 当鼠标弹起时,解绑 documentmousemove 事件。

    这样需要监听目标对象的 mousedown 事件, documentmousemovemouseup 事件。

    当鼠标在目标对象上按下时,我们需要计算出此时目标对象的位置,然后将位置信息作为参数,传入 mousemove 事件函数中。这就涉及了 DOM 2 级事件的传参问题。

    该例中的实现代码如下:

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
// 主函数
const main = () => {
const ball = document.getElementById('ball');

let isDraging = false; // 记录是否在拖动中

let dragWithParams; // 绑定参数的拖动方法

const drag = (params, e) => {
let x = e.clientX - params.offsetLeft;
let y = e.clientY - params.offsetTop;

// 避免小球移出视口
if (x < 0) {
x = 0;
} else if (x > document.documentElement.clientWidth - ball.offsetWidth) {
x = document.documentElement.clientWidth - ball.offsetWidth
}
if (y < 0) {
y = 0;
} else if (y > document.documentElement.clientHeight - ball.offsetHeight) {
y = document.documentElement.clientHeight - ball.offsetHeight;
}

ball.style.left = x + 'px';
ball.style.top = y + 'px';
}

// 当鼠标弹起时判断是否移除 mousemove 事件
document.addEventListener('mouseup', () => {
if (isDraging) {
console.log('up');
document.removeEventListener('mousemove', dragWithParams);
isDraging = false;
}
})

// 点击球时为 document 绑定 mousemove 事件
ball.addEventListener('mousedown', e => {
const offsetLeft = e.clientX - ball.offsetLeft;
const offsetTop = e.clientY - ball.offsetTop;

isDraging = true;

// 绑定参数
dragWithParams = drag.bind(null, {
offsetLeft,
offsetTop,
})

document.addEventListener('mousemove', dragWithParams);
});

}

document.addEventListener('DOMContentLoaded', main);

代码分析

上例中利用了 bind 方法可以绑定参数的特点来为事件函数绑定参数。点击查看 bind 详情

同样需要注意的是,代码中为绑定参数后的 drag 函数赋予了新的引用 dragWithParams。这是因为 bind 方法返回的是原函数的拷贝而非原函数。所以,如果我们不赋予它引用名,该函数就变成了匿名函数,我们依然无法解绑。例如,下面的例子中,是无法解绑的。

1
2
3
4
5
6
7
8
9
10
11
12
const fn = (params) => {
console.log(params);
}

const params = 'some params';

// 此处相当于绑定了一个匿名函数
document.addEventListener('mousemove', fn.bind(null, params));

// 无法解绑事件,因为该事件绑定的不是 fn 函数,而是它的一个拷贝。
document.remvoeEventListener('mousemove', fn);

bind 方法第一个参数指定函数中 this 的指向,其余的参数会被作为绑定的参数传入新的函数,并且会出现在传入参数的前面位置。所以在示例代码 drag 函数中,将 event 对象参数放在了 params 后面接收。