javascript高级程序设计(第13章)-- 事件

第十三章:事件

本章内容:

  • 理解事件流
  • 使用事件处理程序
  • 不同的事件类型

13.1 事件流

页面的那个部分拥有特定的事件?想想画在一张纸上的同心圆,你手指按住了圆心。那么你手指的不是一个圆。而是纸上的所有圆。换句话说,你单击了页面上的某个按钮,你也单击了按钮的容器元素,甚至单击了整个页面。

事件流表述了从页面接受事件的顺序。

13.1.1 事件冒泡

IE的事件流叫做事件冒泡(event bubbling),即事件开始时从具体的元素接受,然后向上传播到不具体的节点(文档)l

<html>
    <head>
        <title>Event Bubbling Example</title>  
    </head>
    <body>
        <div id='myDiv'>
            click me
        </div>
    </body>
</html>

当你点击div的元素执行顺序如下图:

mark

所有现在浏览器都支持事件冒泡

13.1.2 事件捕获

Netscape提出了另一种事件流叫做事件捕获(event capturing)。事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点放到最后.

mark

13.1.3 DOM事件流

“DOM2级事件”规定事件流包括三个阶段:事件捕获阶段,处于目标阶段和事件冒泡阶段。

mark

13.2 事件处理程序

事件就是用户或浏览器自身执行的某种动作,而对应的相应事件的函数就是事件处理程序(或叫事件监听器)

13.2.1 HTML事件处理程序

<input type='button' value='click me' onclick='alert("click")'>

13.2.2 DOM0级事件处理程序

var btn = document.getElementById('myBtn');
btn.onclick = function(){
    alert("click")
}

缺点: 每个元素只能绑定一个同名事件。

13.2.3 DOM2级事件处理程序

DOM2级别事件定义了两个方法,用于处理添加和删除事件的操作:addEventListiner()和removeEventListiner()。

所有DOM都包含这两个方法,并且接受三个参数

  1. 要处理的事件名
  2. 作为事件处理的函数
  3. 一个布尔值,如果是true,表示在捕获阶段调用处理函数,如果是false,表示在冒泡阶段调用处理函数。

而且不像DOM0中的onclick只能绑定一次,DOM2级的时间可以绑定多个

var btn = document.getElementById('myBtn');
btn.addEventListener('click',function(){
    alert('click');
}, false)
btn.addEventListener('click',function(){
    alert('hello');
}, false)

通过addEventListner()添加的事件只能通过removeEventListener来移除;移除时候传递的参数与添加处理程序的参数必须要参数相同。这也意味着匿名函数无法移除。

var btn = document.getElementById('myBtn');
btn.addEventListener('click',function(){
    alert('click');
}, false)
btn.removeEventLisner('click',function(){ // 没有用!!!
    alert('click');
},false)
var btn = document.getElementById('myBtn');
var handler = function(){
    alert(this);
}
btn.addEventListener('click',handler, false)
btn.removeEventLisner('click',handler,false); //有效

因为可以绑定多个事件,函数是引用类型,匿名函数会生成一个新的函数。也即匿名函数无法移除。

13.3 事件对象

在DOM上触发事件时,会产生一个事件对象event。

关于target、currentTarget、this

在事件处理的程序内部,this始终等于currentTarget,而target则包含事件的实际目标。

document.body.onclick = function(event){
    alert(evnet.currentTarget == document.body); //ture
    alert(this == document.body); //ture
    alert(evnet.target == document.getElementById('myBtn')); //ture
}

在点击这个例子的按钮时候,this和currentTarget都指向body。因为事件处理器注册到这个元素的。然而target元素却等于按钮元素,因为它是click事件真正的目标。

13.5 内存和性能

13.5.1 事件委托

对”事件处理程序过多“问题的解决方案就是事件委托。事件委托利用了事件的冒泡,只指定了一个事件处理程序,来管理某一类型的所有事件。

<ul id="myLinks">
    <li id='goSomewhere'>go somewhere</li>
    <li id='goSomething'>go something</li>
    <li id='sayHi'>say hi</li>
</ul>

<script>
    var list = document.getElementById('myLinks');
    list.addEventListiner('click', function(event){
        var target = event.target;
        switch(target){
            case 'goSomeWhere': 
                alert('gosomeWhere')
                break;
            case 'goSomething': 
                alert('goSomething')
                break;
            case 'sayHi': 
                alert('sayHi')
                break;
        }
    },false)
</script>

延伸阅读1: 深入核心,详解事件循环机制

在学习事件循环机制之前,我默认你已经懂得了如下概念,如果仍然有疑问,可以回过头去看看我以前的文章。

  • 执行上下文(Execution context)
  • 函数调用栈(call stack)
  • 队列数据结构(queue)
  • Promise(我会在下一篇文章专门总结Promise的详细使用)

因为chrome浏览器中新标准中的事件循环机制与nodejs类似,因此此处就整合nodejs一起来理解,其中会介绍到几个nodejs有,但是浏览器中没有的API,大家只需要了解就好,不一定非要知道她是如何使用。比如process.nextTick,setImmediate

  • 我们知道JavaScript的一大特点就是单线程,而这个线程中拥有唯一的一个事件循环。
  • JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。
mark
  • 一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。
  • 任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
  • macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
  • micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)
  • setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
// setTimeout中的回调函数才是进入任务队列的任务
setTimeout(function() {
    console.log('xxxx');
})
// 非常多的同学对于setTimeout的理解存在偏差。所以大概说一下误解:
// setTimeout作为一个任务分发器,这个函数会立即执行,而它所要分发的任务,也就是它的第一个参数,才是延迟执行
  • 来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。
  • 事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列并不是一个任务)执行完毕,然后再执行所有的micro-task,这样一直循环下去。
  • 其中每一个任务的执行,无论是macro-task还是micro-task,都是借助函数调用栈来完成。

纯文字表述确实有点干涩,因此,这里我们通过2个例子,来逐步理解事件循环的具体顺序。

例1
// demo01  出自于上面我引用文章的一个例子,我们来根据上面的结论,一步一步分析具体的执行过程。
// 为了方便理解,我以打印出来的字符作为当前的任务名称
setTimeout(function() {
    console.log('timeout1');
})

new Promise(function(resolve){
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
     console.log('promise2');
}).then(function() {
    console.log('then1');
})
console.log('global1');

首先,事件循环从宏任务队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务。每一个任务的执行顺序,都依靠函数调用栈来搞定,而当遇到任务源时,则会先分发任务到对应的队列中去,所以,上面例子的第一步执行如下图所示。

mark

第二步:script任务执行时首先遇到了setTimeout,setTimeout为一个宏任务源,那么他的作用就是将任务分发到它对应的队列中。

setTimeout(function() {
    console.log('timeout1');
})
mark

第三步:script执行时遇到Promise实例。Promise构造函数中的第一个参数,是在new的时候执行,因此不会进入任何其他的队列,而是直接在当前任务直接执行了,而后续的.then则会被分发到micro-task的Promise队列中去。

new Promise(function(resolve){
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
     console.log('promise2');
}).then(function() {
    console.log('then1');
})

因此,构造函数执行时,里面的参数进入函数调用栈执行。for循环不会进入任何队列,因此代码会依次执行,所以这里的promise1和promise2会依次输出。

mark
mark
mark

script任务继续往下执行,最后只有一句输出了globa1,然后,全局任务就执行完毕了。

第四步:第一个宏任务script执行完毕之后,就开始执行所有的可执行的微任务。这个时候,微任务中,只有Promise队列中的一个任务then1,因此直接执行就行了,执行结果输出then1,当然,他的执行,也是进入函数调用栈中执行的。

mark

第五步:当所有的micro-tast执行完毕之后,表示第一轮的循环就结束了。这个时候就得开始第二轮的循环。第二轮循环仍然从宏任务macro-task开始。

mark

这个时候,我们发现宏任务中,只有在setTimeout队列中还要一个timeout1的任务等待执行。因此就直接执行即可。

mark

这个时候宏任务队列与微任务队列中都没有任务了,所以代码就不会再输出其他东西了。

例1执行结果
setTimeout(function() {
    console.log('timeout1');
})

new Promise(function(resolve){
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
     console.log('promise2');
}).then(function() {
    console.log('then1');
})
console.log('global1');

/* 输出结果为:
promise1
promise2
global1
then1
timeout1
*/
例2:
// demo02
console.log('golb1');

setTimeout(function() {
    console.log('timeout1');
    process.nextTick(function() {
        console.log('timeout1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout1_promise');
        resolve();
    }).then(function() {
        console.log('timeout1_then')
    })
})

setImmediate(function() {
    console.log('immediate1');
    process.nextTick(function() {
        console.log('immediate1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate1_promise');
        resolve();
    }).then(function() {
        console.log('immediate1_then')
    })
})

process.nextTick(function() {
    console.log('glob1_nextTick');
})
new Promise(function(resolve) {
    console.log('glob1_promise');
    resolve();
}).then(function() {
    console.log('glob1_then')
})

setTimeout(function() {
    console.log('timeout2');
    process.nextTick(function() {
        console.log('timeout2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout2_promise');
        resolve();
    }).then(function() {
        console.log('timeout2_then')
    })
})

process.nextTick(function() {
    console.log('glob2_nextTick');
})
new Promise(function(resolve) {
    console.log('glob2_promise');
    resolve();
}).then(function() {
    console.log('glob2_then')
})

setImmediate(function() {
    console.log('immediate2');
    process.nextTick(function() {
        console.log('immediate2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate2_promise');
        resolve();
    }).then(function() {
        console.log('immediate2_then')
    })
})

这个例子看上去有点复杂,乱七八糟的代码一大堆,不过不用担心,我们一步一步来分析一下。

第一步:宏任务script首先执行。全局入栈。glob1输出。

mark

第二步,执行过程遇到setTimeout。setTimeout作为任务分发器,将任务分发到对应的宏任务队列中。

setTimeout(function() {
    console.log('timeout1');
    process.nextTick(function() {
        console.log('timeout1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout1_promise');
        resolve();
    }).then(function() {
        console.log('timeout1_then')
    })
})
mark

第三步:执行过程遇到setImmediate。setImmediate也是一个宏任务分发器,将任务分发到对应的任务队列中。setImmediate的任务队列会在setTimeout队列的后面执行。

setImmediate(function() {
    console.log('immediate1');
    process.nextTick(function() {
        console.log('immediate1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate1_promise');
        resolve();
    }).then(function() {
        console.log('immediate1_then')
    })
})
mark

第四步:执行遇到nextTick,process.nextTick是一个微任务分发器,它会将任务分发到对应的微任务队列中去。

process.nextTick(function() {
    console.log('glob1_nextTick');
})
mark

第五步:执行遇到Promise。Promise的then方法会将任务分发到对应的微任务队列中,但是它构造函数中的方法会直接执行。因此,glob1_promise会第二个输出。

new Promise(function(resolve) {
    console.log('glob1_promise');
    resolve();
}).then(function() {
    console.log('glob1_then')
})
mark
mark

第六步:执行遇到第二个setTimeout。

setTimeout(function() {
    console.log('timeout2');
    process.nextTick(function() {
        console.log('timeout2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout2_promise');
        resolve();
    }).then(function() {
        console.log('timeout2_then')
    })
})
mark

第七步:先后遇到nextTick与Promise

process.nextTick(function() {
    console.log('glob2_nextTick');
})
new Promise(function(resolve) {
    console.log('glob2_promise');
    resolve();
}).then(function() {
    console.log('glob2_then')
})
mark

第八步:再次遇到setImmediate。

setImmediate(function() {
    console.log('immediate2');
    process.nextTick(function() {
        console.log('immediate2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate2_promise');
        resolve();
    }).then(function() {
        console.log('immediate2_then')
    })
})
mark

这个时候,script中的代码就执行完毕了,执行过程中,遇到不同的任务分发器,就将任务分发到各自对应的队列中去。接下来,将会执行所有微任务队列中的任务。

其中,nextTick队列会比Promie先执行。nextTick中的可执行任务执行完毕之后,才会开始执行Promise队列中的任务。

当所有可执行的微任务执行完毕之后,这一轮循环就表示结束了。下一轮循环继续从宏任务队列开始执行。

这个时候,script已经执行完毕,所以就从setTimeout队列开始执行。

mark

setTimeout任务的执行,也依然是借助函数调用栈来完成,并且遇到任务分发器的时候也会将任务分发到对应的队列中去。

只有当setTimeout中所有的任务执行完毕之后,才会再次开始执行微任务队列。并且清空所有的可执行微任务。

setTiemout队列产生的微任务执行完毕之后,循环则回过头来开始执行setImmediate队列。仍然是先将setImmediate队列中的任务执行完毕,再执行所产生的微任务。

当setImmediate队列执行产生的微任务全部执行之后,第二轮循环也就结束了。

大家需要注意这里的循环结束的时间节点。

当我们在执行setTimeout任务中遇到setTimeout时,它仍然会将对应的任务分发到setTimeout队列中去,但是该任务就得等到下一轮事件循环执行了。例子中没有涉及到这么复杂的嵌套,大家可以动手添加或者修改他们的位置来感受一下循环的变化。

例2执行结果
golb1
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
timeout1
timeout1_promise
timeout2
timeout2_promise
timeout1_nextTick
timeout2_nextTick
timeout1_then
timeout2_then
immediate1
immediate1_promise
immediate2
immediate2_promise
immediate1_nextTick
immediate2_nextTick
immediate1_then
immediate2_then

小结:

事件是将Javascript与网页联系在一起的主要方式。“DOM3级事件”规范和HTML5定义了常见的大多数事件。

在使用事件时,需要考虑如下一些内存和性能方面的问题。

  • 有必要限制一个页面中事件处理程序的数量,数量太多会导致占用大量内存,而且也会让用户感觉反映不太灵敏;
  • 建立在事件冒泡机制上的事件委托技术,可以有效减少事件处理程序的数量;
  • 建议在浏览器卸载页面之前移除页面中的所有事件处理程序;

参考:

前端基础进阶(十二):深入核心,详解事件循环机制

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,693评论 0 5
  • 静下心学了一波事件循环机制,好开心,我学会了,首先还是得感谢作者写的笔记特别详细 链接: http://www.c...
    Dianaou阅读 519评论 0 0
  • 一、JavaScript单线程模型 JavaScript是单线程的,JavaScript只在一个线程上运行,但是浏...
    Brolly阅读 1,130评论 4 6
  • 今日塔二电厂联系水冷壁失效分析报告,经过尉总审核认为泄漏原因为焊接缺陷,主要为应力集中导致,国电检测公司重新做。李...
    hjppljun阅读 170评论 0 0
  • 早上听了一节英语课,然后处理国网学籍。中午去交通大队处理违章,下午去了安乐村小学,刘校长说是学区中华...
    方圆_22cf阅读 216评论 0 0