28 KiB
十五、处理事件
You have power over your mind—not outside events. Realize this, and you will find strength.
Marcus Aurelius, Meditations
有些程序需要处理用户的直接输入,比如鼠标和键盘动作。这种输入方式不是组织整齐的数据结构 - 它是一次一个地,实时地出现的,并且期望程序在发生时作出响应。
14.1 事件处理器
想象一下,有一个接口,若想知道键盘上是否有一个键是否被按下,唯一的方法是读取那个按键的当前状态。为了能够响应按键动作,你需要不断读取键盘状态,以在按键被释放之前捕捉到按下状态。这种方法在执行时间密集计算时非常危险,因为你可能错过按键事件。
一些原始机器可以像那样处理输入。有一种更进一步的方法,硬件或操作系统发现按键时间并将其放入队列中。程序可以周期性地检查队列,等待新事件并在发现事件时进行响应。
当然,程序必须记得监视队列,并经常做这种事,因为任何时候,按键被按下和程序发现事件之间都会使得软件反应迟钝。该方法被称为轮询。大多数程序员更希望避免这种方法。
一个更好的机制是,系统在发生事件时主动通知我们的代码。浏览器实现了这种特性,支持我们将函数注册为特定事件的处理器。
<p>Click this document to activate the handler.</p>
<script>
window.addEventListener("click", () => {
console.log("You knocked?");
});
</script>
window
绑定指向浏览器提供的内置对象。 它代表包含文档的浏览器窗口。 调用它的addEventListener
方法注册第二个参数,以便在第一个参数描述的事件发生时调用它。
14.2 事件与DOM节点
每个浏览器事件处理器被注册在上下文中。在为整个窗口注册处理器之前,我们在window
对象上调用了addEventListener
。 这种方法也可以在 DOM 元素和一些其他类型的对象上找到。 仅当事件发生在其注册对象的上下文中时,才调用事件监听器。
<button>Click me</button>
<p>No handler here.</p>
<script>
let button = document.querySelector("button");
button.addEventListener("click", () => {
console.log("Button clicked.");
});
</script>
示例代码中将处理器附加到按钮节点上。因此,点击按钮时会触发并执行处理器,而点击文档的其他部分则没有反应。
向节点提供onclick属性也有类似效果。这适用于大多数类型的事件 - 您可以为属性附加处理器,属性名称为前面带有on
的事件名称。
但是一个节点只能有一个onclick
属性,所以你只能用这种方式为每个节点注册一个处理器。 addEventListener
方法允许您添加任意数量的处理器,因此即使元素上已经存在另一个处理器,添加处理器也是安全的。
removeEventListener
方法将删除一个处理器,使用类似于addEventListener
的参数调用。
<button>Act-once button</button>
<script>
let button = document.querySelector("button");
function once() {
console.log("Done.");
button.removeEventListener("click", once);
}
button.addEventListener("click", once);
</script>
赋予removeEventListener
的函数必须是赋予addEventListener
的完全相同的函数值。 因此,要注销一个处理其,您需要为该函数提供一个名称(在本例中为once
),以便能够将相同的函数值传递给这两个方法。
14.3 事件对象
虽然目前为止我们忽略了它,事件处理器函数作为对象传递:事件(Event)对象。这个对象持有事件的额外信息。例如,如果我们想知道哪个鼠标按键被按下,我们可以查看事件对象的which属性。
<button>Click me any way you want</button>
<script>
let button = document.querySelector("button");
button.addEventListener("mousedown", event => {
if (event.button == 0) {
console.log("Left button");
} else if (event.button == 1) {
console.log("Middle button");
} else if (event.button == 2) {
console.log("Right button");
}
});
</script>
存储在各种类型事件对象中的信息是有差别的。随后本章将会讨论许多类型的事件。对象的type属性一般持有一个字符串,表示事件(例如“click”和“mousedown”)。
14.4 传播
对于大多数事件类型,在具有子节点的节点上注册的处理器,也将接收发生在子节点中的事件。若点击一个段落中的按钮,段落的事件处理器也会收到点击事件。
但若段落和按钮都有事件处理器,则先执行最特殊的事件处理器(按钮的事件处理器)。也就是说事件向外传播,从触发事件的节点到其父节点,最后直到文档根节点。最后,当某个特定节点上注册的所有事件处理器按其顺序全部执行完毕后,窗口对象的事件处理器才有机会响应事件。
事件处理器任何时候都可以调用事件对象的stopPropagation方法,阻止事件进一步传播。该方法有时很实用,例如,你将一个按钮放在另一个可点击元素中,但你不希望点击该按钮会激活外部元素的点击行为。
下面的示例代码将mousedown处理器注册到按钮和其外部的段落节点上。在按钮上点击鼠标右键,按钮的处理器会调用stopPropagation,组织段落上的事件处理器执行。当点击鼠标其他键时,两个处理器都会执行。
<p>A paragraph with a <button>button</button>.</p>
<script>
let para = document.querySelector("p");
let button = document.querySelector("button");
para.addEventListener("mousedown", () => {
console.log("Handler for paragraph.");
});
button.addEventListener("mousedown", event => {
console.log("Handler for button.");
if (event.button == 2) event.stopPropagation();
});
</script>
大多数事件对象都有target属性,指的是事件来源节点。你可以根据该属性防止无意中处理了传播自其他节点的事件。
我们也可以使用target属性来创建出特定类型事件的处理网络。例如,如果一个节点中包含了很长的按钮列表,比较方便的处理方式是在外部节点上注册一个点击事件处理器,并根据事件的target属性来区分用户按下了哪个按钮,而不是为每个按钮都注册独立的事件处理器。
<button>A</button>
<button>B</button>
<button>C</button>
<script>
document.body.addEventListener("click", event => {
if (event.target.nodeName == "BUTTON") {
console.log("Clicked", event.target.textContent);
}
});
</script>
14.5 默认动作
大多数事件都有与其关联的默认动作。若点击链接,就会跳转到链接目标。若点击向下的箭头,浏览器会向下翻页。若右击鼠标,可以得到一个上下文菜单等。
对于大多数类型的事件,JavaScript事件处理器会在默认行为发生之前调用。若事件处理器不希望执行默认行为(通常是因为已经处理了该事件),会调用preventDefault事件对象的方法。
你可以实现你自己的键盘快捷键或交互式菜单。你也可以干扰用户期望的行为。例如,这里实现一个无法跳转的链接。
<a href="https://developer.mozilla.org/">MDN</a>
<script>
let link = document.querySelector("a");
link.addEventListener("click", event => {
console.log("Nope.");
event.preventDefault();
});
</script>
除非你有非常充足的理由,否则不要这样做。当预期的行为被打破时,使用你的页面的人会感到不快。
在有些浏览器中,你完全无法拦截某些事件。比如在Chrome中,关闭键盘快捷键(CTRL-W或COMMAND-W)无法由JavaScript处理。
14.6 按键事件
当按下键盘上的按键时,浏览器会触发“keydown”事件。当松开按键时,会触发“keyup”事件。
<p>This page turns violet when you hold the V key.</p>
<script>
window.addEventListener("keydown", event => {
if (event.key == "v") {
document.body.style.background = "violet";
}
});
window.addEventListener("keyup", event => {
if (event.key == "v") {
document.body.style.background = "";
}
});
</script>
尽管从keydown这个事件名上看应该是物理按键按下时触发,但当持续按下某个按键时,会循环触发该事件。有时,你想谨慎对待它。例如,如果您在按下某个按键时向 DOM 添加按钮,并且在释放按键时再次将其删除,则可能会在按住某个按键的时间过长时,意外添加数百个按钮。
该示例查看了事件对象的key
属性,来查看事件关于哪个键。 该属性包含一个字符串,对于大多数键,它对应于按下该键时将键入的内容。 对于像Enter
这样的特殊键,它包含一个用于命名键的字符串(在本例中为"Enter"
)。 如果你按住一个键的同时按住Shift
键,这也可能影响键的名称 - "v"
变为"V"
,"1"
可能变成"!"
,这是按下Shift-1
键 在键盘上产生的东西。
诸如shift、ctrl、alt和meta(Mac上的command)之类的修饰按键会像普通按键一样产生事件。但在查找组合键时,你也可以查看键盘和鼠标事件的shiftKey、ctrlKey、altKey和metaKey属性来判断这些键是否被按下。
<p>Press Ctrl-Space to continue.</p>
<script>
window.addEventListener("keydown", event => {
if (event.key == " " && event.ctrlKey) {
console.log("Continuing!");
}
});
</script>
按键事件发生的 DOM 节点取决于按下按键时具有焦点的元素。 大多数节点不能拥有焦点,除非你给他们一个tabindex
属性,但像链接,按钮和表单字段可以。 我们将在第 18 章中回顾表单字段。 当没有特别的焦点时,document.body
充当按键事件的目标节点。
当用户键入文本时,使用按键事件来确定正在键入的内容是有问题的。 某些平台,尤其是 Android 手机上的虚拟键盘,不会触发按键事件。 但即使你有一个老式键盘,某些类型的文本输入也不能直接匹配按键,例如其脚本不适合键盘的人所使用的 IME(“输入法编辑器”)软件 ,其中组合多个热键来创建字符。
要注意什么时候输入了内容,每当用户更改其内容时,可以键入的元素(例如<input>
和<textarea>
标签)触发"input"
事件。为了获得输入的实际内容,最好直接从焦点字段中读取它。 第 18 章将展示如何实现。
指针事件
目前有两种广泛使用的方式,用于指向屏幕上的东西:鼠标(包括类似鼠标的设备,如触摸板和轨迹球)和触摸屏。 它们产生不同类型的事件。
鼠标点击
点击鼠标按钮会触发一系列事件。“mousedown”事件和“mouseup”事件类似于“keydown”和“keyup”事件,当鼠标按钮按下或释放时触发。当事件发生时,由鼠标指针下方的DOM节点触发事件。
在mouseup事件后,包含鼠标按下与释放的特定节点会触发“click”事件。例如,如果我在一个段落上按下鼠标,移动到另一个段落上释放鼠标,“click”事件会发生在包含这两个段落的元素上。
若两次点击事件触发时机接近,则在第二次点击事件之后,也会触发“dbclick”(双击,double-click)事件。
为了获得鼠标事件触发的精确信息,你可以查看事件中的clientX
和clientY
属性,包含了事件相对于窗口左上角的坐标(以像素为单位)。或pageX
和pageY
,它们相对于整个文档的左上角(当窗口被滚动时可能不同)。
下面的代码实现了简单的绘图程序。每次点击文档时,会在鼠标指针下添加一个点。还有一个稍微优化的绘图程序,请参见第 19 章。
<style>
body {
height: 200px;
background: beige;
}
.dot {
height: 8px; width: 8px;
border-radius: 4px; /* rounds corners */
background: blue;
position: absolute;
}
</style>
<script>
window.addEventListener("click", event => {
let dot = document.createElement("div");
dot.className = "dot";
dot.style.left = (event.pageX - 4) + "px";
dot.style.top = (event.pageY - 4) + "px";
document.body.appendChild(dot);
});
</script>
鼠标移动
每次鼠标移动时都会触发“mousemove”事件。该事件可用于跟踪鼠标位置。当实现某些形式的鼠标拖拽功能时,该事件非常有用。
举一个例子,下面的程序展示一条栏,并设置一个事件处理器,当向左拖动这个栏时,会使其变窄,若向右拖动则变宽。
<p>Drag the bar to change its width:</p>
<div style="background: orange; width: 60px; height: 20px">
</div>
<script>
let lastX; // Tracks the last observed mouse X position
let bar = document.querySelector("div");
bar.addEventListener("mousedown", event => {
if (event.button == 0) {
lastX = event.clientX;
window.addEventListener("mousemove", moved);
event.preventDefault(); // Prevent selection
}
});
function moved(event) {
if (event.buttons == 0) {
window.removeEventListener("mousemove", moved);
} else {
let dist = event.clientX - lastX;
let newWidth = Math.max(10, bar.offsetWidth + dist);
bar.style.width = newWidth + "px";
lastX = event.clientX;
}
}
</script>
请注意,mousemove处理器注册在窗口对象上。即使鼠标在改变窗口尺寸时在栏外侧移动,只要按住按钮,我们仍然想要更新其大小。
释放鼠标按键时,我们必须停止调整栏的大小。 为此,我们可以使用buttons
属性(注意复数形式),它告诉我们当前按下的按键。 当它为零时,没有按下按键。 当按键被按住时,其值是这些按键的代码总和 - 左键代码为 1,右键为 2,中键为 4。 这样,您可以通过获取buttons
的剩余值及其代码,来检查是否按下了给定按键。
请注意,这些代码的顺序与button
使用的顺序不同,中键位于右键之前。 如前所述,一致性并不是浏览器编程接口的强项。
触摸事件
我们使用的图形浏览器的风格,是考虑到鼠标界面的情况下而设计的,那个时候触摸屏非常罕见。 为了使网络在早期的触摸屏手机上“工作”,在某种程度上,这些设备的浏览器假装触摸事件是鼠标事件。 如果你点击你的屏幕,你会得到'mousedown'
,'mouseup'
和'click'
事件。
函数isInside会沿着指定节点父节点链接寻找节点,直到到达文档顶部(当node为null时),或找到了目标父节点为止。
我们可以使用CSS的伪选择子(pseudoselector):hover来更轻松地实现同样的效果。但当浮动效果更加复杂(不只是改变目标节点效果)时,你必须使用mouseover和mouseout事件这种技巧。
<style>
p:hover { color: red }
</style>
<p>Hover over this <strong>paragraph</strong>.</p>
14.9 滚动事件
每当元素滚动时,会触发scroll事件。该事件用处极多,比如知道用户当前查看的元素(禁用用户视线以外的动画,或向邪恶的指挥部发送监视报告),或展示一些滚动的迹象(通过高亮表格的部分内容,或显示页码)。
下面的示例在文档右下角绘制进度条,向下滚动时更新进度条:
<style>
.progress {
border: 1px solid blue;
width: 100px;
position: fixed;
top: 10px; right: 10px;
}
.progress > div {
height: 12px;
background: blue;
width: 0%;
}
body {
height: 2000px;
}
</style>
<div class="progress"><div></div></div>
<p>Scroll me...</p>
<script>
var bar = document.querySelector(".progress div");
addEventListener("scroll", function() {
var max = document.body.scrollHeight - innerHeight;
var percent = (pageYOffset / max) * 100;
bar.style.width = percent + "%";
});
</script>
将元素的position属性指定为fixed时,其行为和absolute很像,但可以防止在文档滚动时期跟着文档一起滚动。其效果是使及进度条一直待在角落。进度条内还有其他元素,会随着当前进度改变尺寸。我们使用%,而非px作为宽度单位,这使得元素尺寸是相对于整个进度条来计算的。
innerHeight全局变量是窗口高度,我们必须要减去滚动条的高度。你点击文档底部的时候是无法继续滚动的(和innerHeight一样,还有innerWidth变量)。使用pageYOffset(当前滚动位置)除以最大滚动位置,并乘以100,就可以得到进度条长度。
调用滚动事件的preventDefault无法阻止滚动。实际上,事件处理器是在进行滚动之后才触发的。
14.10 焦点事件
当元素获得焦点时,浏览器会触发其上的focus事件。当失去焦点时触发blur事件。
与前文讨论的事件不同,这两个事件不会传播。子元素获得或失去焦点时,不会激活父元素的处理器。
下面的示例中,文本域在拥有焦点时会显示帮助文本。
<p>Name: <input type="text" data-help="Your full name"></p>
<p>Age: <input type="text" data-help="Age in years"></p>
<p id="help"></p>
<script>
var help = document.querySelector("#help");
var fields = document.querySelectorAll("input");
for (var i = 0; i < fields.length; i++) {
fields[i].addEventListener("focus", function(event) {
var text = event.target.getAttribute("data-help");
help.textContent = text;
});
fields[i].addEventListener("blur", function(event) {
help.textContent = "";
});
}
</script>
当用户从浏览器标签或窗口移开时,窗口对象会收到focus事件,当移动到标签或窗口上时,则收到blur事件。
14.11 加载事件
当界面结束装载时,会触发窗口对象和文档body对象的“load”事件。该事件通常用于在当整个文档构建完成时,进行初始化。请记住<script>标签的内容是一遇到就执行的。很多时候这样执行得太早,比如有时脚本需要处理在<script>标签后出现的内容。
诸如image或script这类会装载外部文件的标签都有load事件,指示其引用文件装载完毕。类似于焦点事件,装载事件是不会传播的。
当页面关闭或跳转(比如跳转到一个链接)时,会触发beforeunload事件。该事件用于防止用户突然关闭文档而丢失工作结果。你无法使用preventDefault方法阻止页面卸载。该处理器通过返回字符串来完成该功能。该字符串会用在询问用户是否关闭页面的对话框中。该机制确保用户可以离开页面,即使是那些想要保持页面,让用户看广告的恶意脚本也无能为力。
14.12 脚本执行时间线
有许多事件会触发脚本开始执行,读取<script>标签就是其中一个,事件触发是另一种。第13章讨论了requestAnimationFrame函数,该函数用于在下一次页面重绘之前调用某个函数。这是启动脚本运行的另一方法。
需要重点理解的是即使任何时候都可以触发事件,但同一文档中无法同时执行两个脚本。若一个脚本已经在运行,事件处理器和使用其他方法调度的代码会使该脚本等待执行。这就是长时间执行脚本时文档无法响应的原因。此时浏览器无法回应点击或其他文档内事件,因为其无法在当前脚本完成运行之前去执行时间处理器。
许多程序设计环境都支持在同一时间执行多线程。同时执行许多任务可以使得程序运行更快。但当多个线程同一时间访问系统中同一部分时,构思一个程序就比原来难很多了。
JavaScript程序同时只能做一件事情让事情变得更简单。若读者想将耗时操作放到后台进行,同时防止页面无响应,可以使用浏览器提供的Web Worker。这个工作单元是独立的JavaScript环境,可以独立于文档主程序运行,而且只能和其发送接收消息。
假设我们有以下代码,名为code/squareworker.js。
addEventListener("message", function(event) {
postMessage(event.data * event.data);
});
想象一下,计算数字平方的任务是艰巨耗时的计算任务,我们想要将其放在后台线程执行。该代码会产生一个工作单元,相继发送一些消息,并输出响应。
var squareWorker = new Worker("code/squareworker.js");
squareWorker.addEventListener("message", function(event) {
console.log("The worker responded:", event.data);
});
squareWorker.postMessage(10);
squareWorker.postMessage(24);
函数postMessage会发送一条消息,触发接收方的message事件。创建工作单元的脚本通过Worker对象收发消息,而worker则直接向其全局作用域(新的全局作用域,与初始脚本不同)发送消息,或监听其消息。
14.13 设置定时器
函数setTimeout类似于requestAnimationFrame。该函数随后会调用另一个函数。但不是下次重绘时,而是在等待指定毫秒数之后调用函数。该页面会在两秒之后从蓝色变为黄色。
<script>
document.body.style.background = "blue";
setTimeout(function() {
document.body.style.background = "yellow";
}, 2000);
</script>
有时读者需要取消调度的函数。可以存储setTimeout的返回值,并将作为参数调用clearTimeout。
var bombTimer = setTimeout(function() {
console.log("BOOM!");
}, 500);
if (Math.random() < 0.5) { // 50% chance
console.log("Defused.");
clearTimeout(bombTimer);
}
函数cancelAnimationFrame作用与clearTimeout相同,使用requestAnimationFrame的返回值调用该函数,可以取消框架(假定函数还没有被调用)。
还有setInterval和clearInterval这种相似的函数,用于设置计时器,每隔一定毫秒数重复执行一次。
var ticks = 0;
var clock = setInterval(function() {
console.log("tick", ticks++);
if (ticks == 10) {
clearInterval(clock);
console.log("stop.");
}
}, 200);
14.14 降频
某些类型的事件可能会连续、迅速触发多次(例如mousemove和scroll事件)。处理这类事件时,你必须小心谨慎,防止处理任务耗时过长,否则处理器会占据过多事件,导致用户与文档交互变得非常慢且卡顿。
若你需要在这类处理器中编写一些重要任务,可以使用setTimeout来确保不会频繁进行这些任务。我们通常称之为“事件降频(Debounce)”。有许多方法可以完成该任务。
在第一个示例中,当用户输入某些字符时,我们想要完成某些任务,但我们不想在每个按键事件中立即处理该任务。当用户输入过快时,我们希望暂停一下然后进行处理。我们不是立即在事件处理器中执行动作,而是设置一个定时器。我们也会清除上一次的定时器(如果有),因此当两个事件触发间隔过短(比定时器延时短),就会取消上一次事件设置的定时器。
<textarea>Type something here...</textarea>
<script>
var textarea = document.querySelector("textarea");
var timeout;
textarea.addEventListener("keydown", function() {
clearTimeout(timeout);
timeout = setTimeout(function() {
console.log("You stopped typing.");
}, 500);
});
</script>
将undefined传递给clearTimeout或在一个已结束的定时器上调用clearTimeout是没有效果的。因此,我们不需要关心何时调用该方法,只需要每个事件中都这样做即可。
如果我们想要保证每次响应之间至少间隔一段时间,但不希望每次事件发生时都重置定时器,而是在一连串事件连续发生时能够定时触发响应,那么我们可以使用一个略有区别的方法来解决问题。例如,我们想要响应“mousemove”事件来显示当前鼠标坐标,但频率只有250ms。
<script>
function displayCoords(event) {
document.body.textContent =
"Mouse at " + event.pageX + ", " + event.pageY;
}
var scheduled = false, lastEvent;
addEventListener("mousemove", function(event) {
lastEvent = event;
if (!scheduled) {
scheduled = true;
setTimeout(function() {
scheduled = false;
displayCoords(lastEvent);
}, 250);
}
});
</script>
14.15 本章小结
事件处理器可以检测并响应事件,而不需要直接控制事件。addEventListener方法用于注册处理器。
每个事件都有标识事件的类型(keydown、focus等)。大多数方法都会在特定DOM元素上调用,接着向其父代节点传播,允许每个父代元素的处理器都能处理这些事件。
JavaScript调用事件处理器时,会传递一个包含事件额外信息的事件对象。该对象也有方法支持停止进一步传播(stopPropagation),也支持阻止浏览器执行事件的默认处理器(preventDefault)。
按下键盘按键时会触发keydown、keypress和keyup事件。按下鼠标按钮时,会触发mousedown、mouseup和click事件。移动鼠标会触发mousemove事件,也可能触发mouseenter和mouseout事件。
我们可以通过scroll事件监测滚动行为,可以通过focus和blur事件监控焦点改变。当文档完成加载后,会触发窗口的load事件。
同时只能执行一段JavaScript程序。因此,事件处理器和其他调度脚本需要等待脚本结束,才能得到机会执行。
14.16 习题
14.16.1 少了几个按键的键盘
在1928年和1933年之间,土耳其法律禁止在官方文档中使用Q、W和X。这是用于扼杀库尔德的文化大量措施中的一部分。这些字母在库尔德人中使用,但伊斯坦布尔的土耳其人不使用。
作为一个练习,通过技术来实现这种阉割键盘按键的功能,编写一个文本域(使用<input type="text"标签),使得用户无法输入这些字母。
(不用处理复制粘贴,以及其他可能的漏洞。)
<input type="text">
<script>
var field = document.querySelector("input");
// Your code here.
</script>
14.16.2 鼠标轨迹
在JavaScript早期,有许多主页都会在页面上使用大量的动画,人们想出了许多该语言的创造性用法。
其中一种用途是“鼠标踪迹”,也就是沿着鼠标指针在页面上滑动的轨迹画出一连串图像。
在本习题中实现鼠标轨迹的功能。使用绝对定位、固定尺寸的<div>元素,背景为黑色(请参考鼠标点击一节中的示例)。创建一系列此类元素,当鼠标移动时,伴随鼠标指针显示它们。
有许多方案可以实现我们所需的功能。你可以根据你的需要实现简单的或复杂的方法。简单的解决方案是保存固定鼠标的轨迹元素并循环使用它们,每次mousemove事件触发时将下一个元素移动到鼠标当前位置。
<style>
.trail { /* className for the trail elements */
position: absolute;
height: 6px; width: 6px;
border-radius: 3px;
background: teal;
}
body {
height: 300px;
}
</style>
<script>
// Your code here.
</script>
14.16.3 选项卡界面
选项卡界面是常见的设计模型。选项卡支持用户通过选择一系列元素上突出的标签来选择一个面板。
本习题要求实现一个简单的选项卡界面。编写asTabs函数,接受一个DOM节点并创建选项卡界面来展现该节点的子元素。该函数应该在顶层节点中插入大量<button>元素,与每个孩子元素一一对应,按钮文本从孩子的data-tabname中获取。除了显示一个初始孩子节点,其他孩子节点都应该隐藏(将display样式设置成none),并通过点击按钮来选择当前显示的节点。
当该函数工作时,扩展该函数,支持在当前激活按钮上使用不同的样式。
<div id="wrapper">
<div data-tabname="one">Tab one</div>
<div data-tabname="two">Tab two</div>
<div data-tabname="three">Tab three</div>
</div>
<script>
function asTabs(node) {
// Your code here.
}
asTabs(document.querySelector("#wrapper"));
</script>