学而实习之 不亦乐乎

jQuery基础:事件传播及处理

2019-02-12 11:12:44

一、事件传播

DOM标准规定事件传播的两种策略:
首先,事件要从一般元素到具体元素逐层捕获;然后,事件再通过冒泡返回DOM树的顶层。而事件处理程序可以注册到这个过程中的任何一个阶段。
为了确保跨浏览器的一致性,而且也为了让人容易理解, jQuery始终会在模型的冒泡阶段注册事件处理程序。因此,我们总是可以假定最具体的元素会首先获得响应事件的机会。
事件冒泡可能会导致始料不及的行为,特别是在错误的元素响应mouseover或mouseout事件的情况下。

在处理程序中使用事件对象,需要为函数添加一个参数(event),这个参数名并不一定非要命名为event,这里只是为了容易理解。
事件处理程序中的变量event保存着事件对象,而event.target属性保存着发生事件的目标元素。

示例:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <style>
        .switcher {
            float: right;
            background-color: #ddd;
            border: 1px solid #000;
            margin: 10px;
            padding: 10px;
            font-size: .9em;
        }

            .switcher h3 {
                margin: .5em 0;
            }

        .hidden {
            display: none;
        }
    </style>
    <script src="jquery.js"></script>
    <script>
        $(document).ready(function () {
            $('#switcher').click(function (event) {
                if (event.target == this) {
                    $('#switcher button').toggleClass('hidden');
                }
            });
        });
    </script>

</head>
<body>
    <div id="switcher" class="switcher">
        <h3>Font Size</h3>
        <button id="switcher-normal">
            Normal
        </button>
        <button id="switcher-small">
            Small
        </button>
        <button id="switcher-large">
            Large
        </button>
    </div>
</body>
</html>

二、停止事件传播

事件对象还提供了一个.stopPropagation()方法,该方法可以完全阻止事件冒泡。
与.target类似,这个方法也是一种基本的DOM特性,但在IE8及更早版本中则无法安全地使用。不过,只要我们通过jQuery来注册所有的事件处理程序,就可以放心地使用这个方法。
在下面的示例代码中,如果注释掉event.stopPropagation();可以看到点击按钮后,不仅按钮事件执行,父容器(div)的点击事件也执行了。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <style>
        .switcher {
            float: right;
            background-color: #ddd;
            border: 1px solid #000;
            margin: 10px;
            padding: 10px;
            font-size: .9em;
        }

            .switcher h3 {
                margin: .5em 0;
            }

        .hidden {
            display: none;
        }

        .selected {
            font-weight: bold;
        }

        .small {
            font-size: .5em;
        }

        .normal {
            font-size: 1em;
        }

        .large {
            font-size: 1.5em;
        }
    </style>
    <script src="jquery.js"></script>
    <script>
        $(document).ready(function () {
            $('#switcher-normal').addClass('selected');
            $('#switcher').click(function (event) {
                $('#switcher button').toggleClass('hidden');
            });
            $('#switcher button').click(function (event) {
                var bodyClass = this.id.split('-')[1];
                $('body').removeClass().addClass(bodyClass);
                $('#switcher button').removeClass('selected');
                $(this).addClass('selected');
                //event.stopPropagation();
            });
        });
    </script>

</head>
<body>
    <div id="switcher" class="switcher">
        <h3>Font Size</h3>
        <button id="switcher-normal">
            Normal
        </button>
        <button id="switcher-small">
            Small
        </button>
        <button id="switcher-large">
            Large
        </button>
    </div>
</body>
</html>

三、阻止默认操作

点击 <a> 标签,默认会加载一个新页面。它是单击锚元素的默认操作。
即便在事件对象上调用.stopPropagation()方法也不能禁止这种默认操作,因为默认操作不是在正常的事件传播流中发生的。在这种情况下, .preventDefault()方法则可以在触发
默认操作之前终止事件。

四、事件委托

当我们需要为更多元素注册处理程序怎么办?这种情况很常见。例如,有一个显示信息的大型表格,每一行都有一项需要注册单击处理程序。虽然不难通过隐式迭代来指定所有单击处理程序,但性能可能会很成问题,因为循环是由jQuery在内部完成的,而且要维护所有处理程序也需要占用很多内存。
为解决这个问题,可以只在DOM中的一个祖先元素上指定一个单击处理程序。由于事件会冒泡,未遭拦截的单击事件最终会到达这个祖先元素,而我们可以在此时再作出相应处理。
示例:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <style>
        .switcher {
            float: right;
            background-color: #ddd;
            border: 1px solid #000;
            margin: 10px;
            padding: 10px;
            font-size: .9em;
        }

            .switcher h3 {
                margin: .5em 0;
            }

        .hidden {
            display: none;
        }

        .selected {
            font-weight: bold;
        }

        .small {
            font-size: .5em;
        }

        .normal {
            font-size: 1em;
        }

        .large {
            font-size: 1.5em;
        }
    </style>
    <script src="jquery.js"></script>
    <script>
        $(document).ready(function () {
            $('#switcher-normal').addClass('selected');
            $('#switcher').click(function (event) {
                if ($(event.target).is('button')) {
                    var bodyClass = event.target.id.split('-')[1];
                    $('body').removeClass().addClass(bodyClass);
                    $('#switcher button').removeClass('selected');
                    $(event.target).addClass('selected');
                    event.stopPropagation();
                } else {
                    $('#switcher button').toggleClass('hidden');
                }
            });
        });
    </script>

</head>
<body>
    <div id="switcher" class="switcher">
        <h3>Font Size</h3>
        <button id="switcher-normal">
            Normal
        </button>
        <button id="switcher-small">
            Small
        </button>
        <button id="switcher-large">
            Large
        </button>
    </div>
</body>
</html>

上面的代码可以使用jQuery内置的事件委托

$(document).ready(function () {
	$('#switcher-normal').addClass('selected');
	$('#switcher').click(function (event) {
		if (!$(event.target).is('button')) {
			$('#switcher button').toggleClass('hidden');
		}
	});
	$('#switcher').on('click', 'button', function () {
		var bodyClass = event.target.id.split('-')[1];
		$('body').removeClass().addClass(bodyClass);
		$('#switcher button').removeClass('selected');
		$(this).addClass('selected');
	});
});

五、移除事件处理程序

有时候,我们需要停用以前注册的事件处理程序。处理这种情形可以在事件处理程序中使用条件语句。但是,如果能够完全移除处理程序绑定显然更有效率。
为此,可以调用.off()方法移除事件处理程序。
需要注意的是,在调用off()方法时,有可能会移出相应元素的子元素的事件等,这并不是我们想要的结果,这时需要使用事件命名空间,这样就对.off()的调用更有针对性,以避免把不需要移除的事件处理程序移除。
示例:

$(document).ready(function () {
	$('#switcher').on('click.collapse', function (event) {
		if (!$(event.target).is('button')) {
			$('#switcher button').toggleClass('hidden');
		}
	});
	$('#switcher-small, #switcher-large').click(function () {
		$('#switcher').off('click.collapse');
	});
});

对于事件处理系统而言,后缀.collapse是不可见的。这里仍然会像编写.on('click')一样,让注册的函数响应单击事件。但是,通过附加的命名空间信息,则可以解除对这个特定处理程序的绑定,同时不影响为按钮注册的其他单击处理程序。

六、重新绑定事件

首先,应该为事件处理程序起个名字,以便多次使用。如:

$(document).ready(function () {
	var toggleSwitcher = function (event) {
		if (!$(event.target).is('button')) {
			$('#switcher button').toggleClass('hidden');
		}
	};
	$('#switcher').on('click.collapse', toggleSwitcher);
});

注意:这里的事件处理程序是将一个匿名函数表达式指定给了一个局部变量。和命名函数相比,它们的功能都是等价的。使用命名函数时,必须省略函数名称后面的圆括号。圆括号会导致函数被调用,而非被引用。
示例:

$(document).ready(function () {
	var toggleSwitcher = function (event) {
		if (!$(event.target).is('button')) {
			$('#switcher button').toggleClass('hidden');
		}
	};
	$('#switcher').on('click.collapse', toggleSwitcher);
	$('#switcher-small, #switcher-large').click(function () {
		$('#switcher').off('click.collapse');
	});
	$('#switcher-normal').click(function () {
		$('#switcher').on('click.collapse', toggleSwitcher);
	});
});

上面的例子,展示的是将事件处理程序重新绑定到 div 的情形。但有一个问题,当把处理程序绑定到事件时(点击switcher-normal,还没点击switcher-small, switcher-large),之前绑定的处理程序仍然有效,此时,处理程序会执行两次。只有点击switcher-small, switcher-large后才能清除。所以需要进行改进,如下:

$(document).ready(function () {
	var toggleSwitcher = function (event) {
		if (!$(event.target).is('button')) {
			$('#switcher button').toggleClass('hidden');
		}
	};
	$('#switcher').on('click.collapse', toggleSwitcher);
	$('#switcher button').click(function () {
		$('#switcher').off('click.collapse');
		if (this.id == 'switcher-normal') {
			$('#switcher').on('click.collapse', toggleSwitcher);

		}
	});
});

对于只需触发一次,随后要立即解除绑定的情况也有一种简写方法:.one(),这个简写方法的用法如下:
$('#switcher').one('click', toggleSwitcher);
这样会使切换操作只发生一次,之后就再也不会发生。