細讀 JS | 事件詳解

配圖源自 Freepik

本文將會介紹事件、事件流贞间、事件對象贿条、事件處理程序、事件委托增热、以及兼容 IE 瀏覽器等內(nèi)容整以。

一、概念

就本文一些“術(shù)語”作簡單介紹峻仇,后續(xù)章節(jié)再詳述公黑。

1. 事件

事件,可以理解為用戶與瀏覽器(網(wǎng)頁)交互產(chǎn)生的一個動作摄咆。比如點擊(click)凡蚜、雙擊(dblclick)、聚焦(focus)吭从、鼠標懸停(mouseover)番刊、松開鍵盤(keyup)等動作。瀏覽器內(nèi)部包含了非常復雜的事件系統(tǒng)影锈,事件種類非常多氧映,JavaScript 與 HTML 的交互就是通過事件實現(xiàn)的侥钳。前面列舉這些僅僅是冰山一角惊奇。

2. 事件對象

在產(chǎn)生事件之后销凑,瀏覽器會生成一個對象并存儲起來蓝晒,它包含了當前事件的所有信息镇饺,比如發(fā)生事件的類型说订、導致事件的元素以及其他與事件相關(guān)的數(shù)據(jù)灵临。例如鼠標操作桅狠,該對象還會記錄事件產(chǎn)生時鼠標的位置等信息讼载。待事件完成使命(即事件完結(jié))之后轿秧,它將會被銷毀。這個對象咨堤,被稱為“事件對象”菇篡。

3. 事件流

事件對象在 DOM 中的傳播過程,被稱為“事件流”一喘。

經(jīng)常能看到類似 IE 事件流驱还、標準事件流(DOM 事件流)的說法,主要原因是早期瀏覽器廠商各干各的凸克,沒有一個“中間人”去進行統(tǒng)一议蟆。隨著 Web 的飛速發(fā)展,相關(guān)標準就由特定機構(gòu)制定萎战,瀏覽器廠商負責按照標準去實現(xiàn)咐容。例如 W3C、WHATWG蚂维、ECMAScript 等戳粒。但由于歷史遺留原因,不得不寫出一大堆的兼容方法以適配所有的瀏覽器鸟雏。

說到事件流享郊,就不得不提“事件冒泡”和“事件捕獲”了。它們分別由微軟孝鹊、網(wǎng)景團隊提出炊琉,是兩種幾乎完全相反的事件流方案。前者被所有瀏覽器支持又活,后者則是被所有的現(xiàn)代瀏覽器所支持(包括 IE9 及以上)苔咪。

由于事件捕獲不被舊版本瀏覽器(IE8 及以下)支持,因此實際中通常在冒泡階段觸發(fā)事件處理程序柳骄。

綜上团赏,我們可以簡單地,將 IE8 及以下瀏覽器的事件流處理方案稱為“IE 事件流”耐薯,其余的稱為標準事件流(或 DOM 事件流)舔清。

4. 事件處理程序

為響應(yīng)事件而調(diào)用的函數(shù)被稱為“事件處理程序”(也可稱為事件處理函數(shù)、事件監(jiān)聽器曲初、監(jiān)聽器)体谒。通常我們會在發(fā)生事件之前,需提前為某個 DOM 元素編寫事件處理監(jiān)聽器并進行綁定臼婆。待用戶與瀏覽器產(chǎn)生交互后抒痒,事件對象會通過事件流機制在 DOM 中進行傳播,一旦命中目標颁褂,事件監(jiān)聽器就被調(diào)用故响,并接收事件對象作為其唯一的參數(shù)傀广。(注意,IE8 需要通過 window.event 全局對象來獲取事件對象)

5. 事件委托

事件委托彩届,它是利用事件流機制來提高頁面性能的一種解決方案伪冰,也被稱為事件代理。

6. 其他

文中還會提到事件目標一詞惨缆,是指觸發(fā)事件的 DOM 元素糜值,但不一定是事件處理程序所在的 DOM 元素。當生成事件對象之后坯墨,事件目標(在 JavaScript 就是一個對象)將會被存儲在事件對象的 target 屬性(只讀寂汇,IE8 則是 srcElement 屬性)。

還有捣染,本文將大量使用到“現(xiàn)代瀏覽器”骄瓣、“主流瀏覽器”、“標準瀏覽器”耍攘、“IE 瀏覽器”等詞語榕栏。若無特殊說明,前三個指的是包括 IE9 ~ IE11 及其他常見的瀏覽器蕾各。而“IE 瀏覽器”通常指 IE8 及更低版本的瀏覽器扒磁。

二、事件流

前面提到式曲,事件流就是事件對象在 DOM 中的傳播過程妨托。標準事件流過程,如下:

捕獲階段 -> 目標階段 -> 冒泡階段

Captruing Phase -> Target Phase -> Bubbling Phase

其中 IE8 及以下瀏覽器吝羞,不支持事件捕獲兰伤。即 IE8 事件流則不含捕獲階段。

舉個例子:

<div id="div">
  Division
  <p id="p">Paragraph</p>
</div>

結(jié)合事件流钧排,當我們點擊 <p> 元素敦腔,產(chǎn)生一個點擊事件。事件對象的捕獲階段的過程恨溜,如下:

window -> document -> html -> body -> div -> p

注:到達 p 之前是捕獲階段的所有過程符衔,到達 p 處于目標階段。

目標階段過后糟袁,是冒泡階段的過程柏腻,如下:

p -> div -> body -> html -> document -> window

因此,事件流過程可以簡單繪成如下表格:

1. 事件捕獲系吭、事件冒泡

上面提到的捕獲階段和冒泡階段,所對應(yīng)的就是事件捕獲颗品、事件冒泡肯尺。

事件冒泡和事件捕獲沃缘,分別由微軟和網(wǎng)景團隊提出,這是幾乎完全相反的兩個概念则吟,是為了解決頁面中事件流而提出的槐臀。

  • 事件冒泡(Event Bubbling)

想象一下:氣泡從水中冒出水面的過程,它是從里(底)到外的氓仲。事件冒泡跟這個過程很相似水慨,事件對象會從最內(nèi)層的元素開始,一直向上傳播敬扛,直到 window 對象晰洒。因此冒泡過程如下:

p -> div -> body -> html -> document -> window

注意,并非所有事件都支持冒泡行為啥箭,比如 onblur谍珊、onfocus 等事件。

  • 事件捕獲(Event Capture)

既然事件捕獲與事件冒泡是相反的急侥,捕獲過程如下:

window -> document -> html -> body -> div -> p

總的來說砌滞,可以簡單概括為:冒泡過程是由里到外,而捕獲過程則是由外到里坏怪。兩者剛好相反贝润。

注意,現(xiàn)代瀏覽器都是從 window 對象開始捕獲事件的铝宵,冒泡最后一站也是 window 對象打掘。而 IE8 及以下瀏覽器,只會冒泡到 document 對象捉超。

2. 示例

我們分別給 divp 元素添加了兩個事件處理程序胧卤,如下:

const elem1 = document.getElementById('div')
const elem2 = document.getElementById('p')

// 捕獲階段觸發(fā)事件處理程序
elem1.addEventListener('click', () => console.log('div capturing'), true)
elem2.addEventListener('click', () => console.log('p capturing'), true)

// 冒泡階段觸發(fā)事件處理程序
elem1.addEventListener('click', () => console.log('div bubbling'), false)
elem2.addEventListener('click', () => console.log('p bubbling'), false)

// 注意,本示例在現(xiàn)代瀏覽器中可正常執(zhí)行拼岳。
1. 點擊 div 元素區(qū)域(不包含 p 元素區(qū)域)枝誊,先后打印:

div capturing
div bubbling

2. 點擊 p 元素區(qū)域惜纸,先后打右度觥:

div capturing
p capturing
p bubbling
div bubbling
3. 注意點

通過 DOM2 Event 提供的 addEventListener() 方法,可以在捕獲階段觸發(fā)事件監(jiān)聽器耐版。在事件監(jiān)聽器中祠够,除了可以寫業(yè)務(wù)邏輯外,還經(jīng)常做阻止事件冒泡粪牲、取消元素默認行為等處理古瓤。

如果不支持某個階段,或事件對象已停止傳播,則將跳過該階段落君。例如穿香,將 addEventListener() 方法的第三個參數(shù) useCapture 設(shè)為 true,則將跳過冒泡階段(但注意它是會到達目標階段的)绎速。如果事件監(jiān)聽器中調(diào)用了 stopPropagation()皮获,則將跳過后續(xù)的所有階段。

還有纹冤,我們給某個 DOM 元素注冊一個點擊事件監(jiān)聽器洒宝,假設(shè)其后代元素未阻止冒泡行為,只要點擊該元素本身或其后代任意子元素萌京,最后都會觸發(fā)該事件監(jiān)聽器雁歌。因此,我們可以得出一個結(jié)論:事件監(jiān)聽函數(shù)的作用范圍枫夺,包含元素本身所占空間及其后代元素所占空間将宪。不論后代元素是否溢出當前元素范圍(長寬),或者是否脫離文檔流(指絕對布局等)橡庞。

四较坛、事件對象

前面提到,當用戶與瀏覽器發(fā)生交互會產(chǎn)生一個事件扒最,接著會創(chuàng)建生成一個事件對象丑勤。

1. 獲取事件對象

在標準瀏覽器中,無論是以哪種方式(DOM0 或 DOM2)注冊事件處理程序吧趣,事件對象都是傳給事件處理程序的唯一參數(shù)法竞。但是在 IE8 及以下瀏覽器下事件對象只能通過 window.event 獲取。

target.onclick = function (e) {
  // 以下方式可兼容所有瀏覽器
  var ev = e || window.event
}

// ?? 以下這塊內(nèi)容强挫,純屬滿足個人強迫癥岔霸,建議跳過!8┎场呆细!
//
// 關(guān)于 e 和 window.event 的異同(親測):
//
// 1. 在標準瀏覽器, 可能是為了兼容處理,同樣支持 window.event 對象八匠,實際中我們從事件監(jiān)聽函數(shù)參數(shù) e 取值即可絮爷。
//    事件發(fā)生過程中 e === window.event。當事件結(jié)束后梨树,window.event 的值變?yōu)?undefined坑夯。
//    這點 IE11 與標準瀏覽器表現(xiàn)是一致的。
//
// 2. 在 IE9抡四、IE10 中柜蜈,e 可能是 MouseEvent 等實例對象仗谆,
//    而 window.event 是 MSEventObj 實例對象,
//    因此跨释,e !== window.event胸私,但問題不大。
//
// 3. 在 IE8 及以下鳖谈,DOM0 事件處理程序中將不會接收到事件對象,即 e 為 undefined阔涉。
//    此時要獲取事件對象缆娃,只能通過全局 window.event 獲取。
//    而且 window.event 是 Object 的實例瑰排,IE8 下并沒有 MSEventObj 對象贯要,與 IE9 ~ 10 有細微差別。
//    同樣地椭住,window.event 只在事件發(fā)生過程有效崇渗,其他時候取到的值為 null。
//    還有偶然發(fā)現(xiàn)京郑,在 IE8 下竟然 Object.prototype.toString.call(null) === '[object Object]'宅广,無語!
//
// 4. 綜上些举,想要在事件監(jiān)聽函數(shù)中取到事件對象跟狱,只需通過:
//    var ev = e || window.event 
//    以上這條語句,可以兼容所有瀏覽器户魏,并正確獲取到事件對象驶臊。
//    至于 IE9 ~ 10 中 e 與 window.event 的細微差異,實際中無需關(guān)心叼丑,沒有影響关翎。
//
// 5. That's all.

需要注意的是,事件對象僅在事件發(fā)生過程有效鸠信,一旦事件完畢纵寝,就會被銷毀。這一點所有瀏覽器表現(xiàn)一致症副。

在標準瀏覽器里店雅,可以通過 window.event 或者事件對象的 eventPhase 屬性來驗證,如下:

target.onclick = function (e) {
  console.log(e === window.event) // true

  setTimeout(() => {
    console.log(e.eventPhase) // 0
    console.log(window.event) // undefined
  })
}

// ?? 解釋:
// 1. 在標準瀏覽器贞铣,事件發(fā)生過程中監(jiān)聽器參數(shù) e 與全局對象 window.event 是一致的
// 2. 事件對象 eventPhase 屬性為 0 表示目前沒有事件正在執(zhí)行闹啦。
2. 內(nèi)置事件類型

在現(xiàn)代瀏覽器中,內(nèi)置了很多事件類型:

以上這些都是基于 Event 接口的派生類窍奋,而事件對象一般是派生類的實例化對象。比如:

target.onclick = function (e) {
  var ev = e || window.event
  console.log(ev instanceof Event) // true
  console.log(ev instanceof MouseEvent) // true
}

// ?? 
// 注意 IE8 不含 MouseEvent 等派生類,只有 Event 基類琳袄,
// 因此 IE8 中第 4 行會拋出錯誤江场。

再啰嗦一句,window.event 最初是由 IE 引入的全局屬性窖逗,且只有事件發(fā)生過程有效≈贩瘢現(xiàn)代瀏覽器為了兼容,也實現(xiàn)了這個全局屬性碎紊。

3. Event 對象

前面提到佑附,所有事件對象都源自 Event 基類,意味著 Event 基類(對象)本身包含適用于所有事件類型實例的屬性和方法仗考。主要是為了兼容性音同。

  • 標準瀏覽器中 Event 對象常用屬性:
Event = {
  bubbles        // (只讀,布爾值)表示當前事件是否會冒泡秃嗜。

  eventPhase     // (只讀权均,數(shù)值范圍 0 ~ 3)表示事件流正被處理到了哪個階段。
                 // 0 表示當前沒有事件正在被處理锅锨,
                 // 1 ~ 3 分別表示事件對象處于捕獲叽赊、目標、冒泡階段橡类。

  cancelable     // (只讀蛇尚,布爾值)表示事件是否可以取消元素的默認行為。
                 // 若為 true 可以使用 preventDefault() 來取消元素的默認行為顾画。
                 // 若為 false 調(diào)用 preventDefault() 沒有任何效果取劫。

  cancelBubble   // (布爾值)如果設(shè)為 true,相當于執(zhí)行 stopPropagation()研侣,事件對象將會停止傳播谱邪。
                 // 標準瀏覽器中,請使用 stopPropagation()庶诡;
                 // IE8 瀏覽器中惦银,只能使用 cancelBubble = true 來阻止傳播;

  currentTarget   // (只讀)返回當前事件處理程序所綁定的節(jié)點(不一定是事件觸發(fā)節(jié)點)末誓。
                  // 標準瀏覽器扯俱,this === currentTarget 總為 true。
                  // IE8 及以下瀏覽器不支持該屬性喇澡。

  target          // (只讀)返回事件觸發(fā)節(jié)點(即事件目標)迅栅。
                  // 當事件觸發(fā)節(jié)點與事件處理程序所綁定節(jié)點相同時,target === currentTarget 結(jié)果為 true晴玖。
                  // IE8 及以下瀏覽器中不支持該屬性读存,請使用 srcElement 屬性(相當于 target 屬性)为流。

  type            // (只讀,字符串)表示事件類型

  isTrusted       // (只讀让簿,布爾值)true 表示事件是由瀏覽器生成的敬察。
                  // false 表示由 JavaScript 創(chuàng)建,一般指自定義事件尔当。

  detail          // 數(shù)值莲祸。該屬性只有瀏覽器的 UI 事件才具有,表示事件的某種信息椭迎。
                  // 例如虫给,單擊為 1,雙擊為 2侠碧,三擊為 3。

  composed        // (只讀缠黍,布爾值)表示事件是否可以穿過 Shadow DOM 和常規(guī) DOM 之間的隔閡進行冒泡弄兜。
                  // 替代已廢棄的是 scoped 屬性。
}
  • 標準瀏覽器中 Event 對象常用方法:
Event = {
  preventDefault()    // 取消元素默認行為
                      // 僅當 cancelable 為 true 時瓷式,調(diào)用 preventDefault() 才有效替饿。
                      // IE8 及以下瀏覽器,請設(shè)置 returnValue = false 來取消元素默認行為

  stopPropagation()   // 停止事件對象的傳播贸典,后續(xù)事件流其他階段將會被取消视卢。
                      // IE8 及以下瀏覽器,請設(shè)置 cancelBubble = true廊驼,來阻止冒泡行為
                      // 注意 IE8 及以下瀏覽器“有且僅有”事件冒泡過程据过。

  stopImmediatePropagation() // 用來阻止同一時間的其他監(jiān)聽函數(shù)被調(diào)用。
                             // 當同一節(jié)點同一事件指定多個監(jiān)聽函數(shù)時妒挎,這些函數(shù)
                             // 會根據(jù)添加次序依次調(diào)用绳锅。但只要其中一個監(jiān)聽函數(shù)調(diào)用了
                             // stopImmediatePropagation() 方法,其他監(jiān)聽函數(shù)就不會執(zhí)行了酝掩。
                             // DOM3 Event 新增方法
}
  • IE8 及以下瀏覽器中 Event 對象常用屬性:
Event = {
  cancelBubble   // (可讀寫鳞芙,布爾值)設(shè)置為 true 可以阻止冒泡行為。
                 // 作用同標準瀏覽器中的 stopPropagation() 方法期虾。

  returnValue    // (可讀寫原朝,布爾值)默認為 true。
                 // 設(shè)置為 false 可以取消元素的默認行為
                 // 作用同標準瀏覽器中的 preventDefault() 方法镶苞。

  srcElement     // (只讀)返回事件觸發(fā)節(jié)點(即事件目標)喳坠。
                 // 作用同標準瀏覽器中的 target 屬性。

  type           // (只讀宾尚,字符串)表示事件類型(同標準瀏覽器的 type 屬性)丙笋。
}

以上這些方法(包括標準瀏覽器谢澈、IE 瀏覽器)列舉出來是為了方便下文封裝方法時,注意兼容處理御板。

五锥忿、事件冒泡與默認行為

前面提到,并非所有事件都支持冒泡行為怠肋。因此敬鬓,若給不支持冒泡行為的事件去 stopPropagation() 是多次一舉,且沒有意義笙各。取消元素默認行為同理钉答。

阻止事件冒泡和元素默認行為經(jīng)常放在一起討論。上一章節(jié)已經(jīng)清楚介紹了標準瀏覽器與 IE 瀏覽器的兼容性杈抢。如下:

// 阻止冒泡
ev.stopPropagation() // 標準瀏覽器
ev.cancelBubble = true // IE8 及以下瀏覽器

// 取消默認行為
ev.preventDefault() // 標準瀏覽器
ev.returnValue = false // IE8 及以下瀏覽器

// ?? ev 表示事件發(fā)生過程中的事件對象

還有数尿,在事件處理程序中慎用 return false 語句,不同環(huán)境下會發(fā)生非預期行為惶楼。例如:

// 原生 JS右蹦,只會取消元素默認行為,不會阻止事件冒泡行為
target.onclick = function (e) {
  return false
}

// JQuery歼捐,既會取消元素默認行為何陆,也會阻止事件冒泡行為
$(target).on('click', function (e) {
  return false
})

因此,無論使用 JQuery 庫或其他庫豹储,還是原生 JS 去編寫事件處理程序贷盲,都盡量避免使用 return 語句。

其實 JQuery 已經(jīng)給我們封裝了 stopPropagation()preventDefault() 方法剥扣,它是兼容所有瀏覽器的巩剖,因此按照標準瀏覽器的方式來去阻止冒泡或取消默認行為即可‰Γ可參考文章球及。

六、事件處理程序

事件處理程序呻疹,也常稱為事件監(jiān)聽器或監(jiān)聽器吃引。可通過以下幾種方式給 DOM 元素注冊事件處理程序:

  • HTML 事件處理程序(不推薦)
  • DOM0 事件處理程序(也不太推薦)
  • DOM2 事件處理程序
  • IE 事件處理程序

前兩種方式不推薦刽锤,后兩種就能覆蓋 99.9% 的瀏覽器了镊尺。如果不用兼容 IE,那么 DOM2 就更香了并思,至少可以減少 70% 的代碼量...

1. HTML 事件處理(不推薦使用)

這是最早的事件處理方式庐氮,說實話在項目中沒見過這種寫法。簡單了解下即可宋彼,不推薦使用弄砍。

<div>
  Division
  <p onclick="inner()">Paragraph</p>
  <p onclick="inner(event)">Paragraph</p>
  <p onclick="console.log('inner')">Paragraph</p>
</div>

<script>
  function inner() {
    // 事件處理程序中 this 指向 window 對象
    console.log('inner')
  }
</script>

<!--
  ?? 注意點:
  1. 內(nèi)聯(lián)式事件處理程序仙畦,this 執(zhí)行 window 對象;
  2. 不要使用全局內(nèi)置的方法音婶,例如 onclick="open()"慨畸,它會觸發(fā) window.open() 而不是自定義的 open() 方法;
  3. 瀏覽器不會給你傳入事件對象衣式,只能手動傳入:可以是 window.event 或 event(推薦前者寸士,后者也可,因為處于 window 環(huán)境)
  4. HTML 事件處理程序會被 DOM0 事件處理程序覆蓋碴卧。
  5. 以上種種原因弱卡,不推薦使用。事實上也沒見過項目這么用了住册。
-->
2. DOM0 事件處理程序

使用方法簡單婶博,也很常見。就是將一個函數(shù)賦值給一個 DOM 元素的事件屬性荧飞。其中元素的事件屬性通常是 on + type(事件類型)凡蜻,比如 onclickondblclick垢箕、onfocusonload 等兑巾。

舉個例子:

// 注冊事件處理程序
target.onclick = function (e) {
  // this 將會指向事件處理程序所綁定的元素
  // do something...
}

// 移除事件處理程序
target.onclick = null

這種方式只能夠注冊一個事件處理程序条获。若多次綁定,后者會覆蓋前者蒋歌。

3. DOM2 事件處理程序

在 DOM2 Event 標準中帅掘,新增了 addEventListener()removeEventListener() 方法來注冊或移除事件處理程序。它的優(yōu)勢有兩點:

  • 可以為同一元素同一事件注冊多個事件處理程序堂油。
  • 事件處理程序可以在“捕獲階段”被觸發(fā)修档。這也是目前唯一可以在捕獲階段命中事件處理程序的方法。

該特性僅在現(xiàn)代瀏覽器中被支持府框,IE8 及以下不支持吱窝。

語法:

target.addEventListener(type, listener, useCapture)
target.removeEventListener(type, listener, useCapture)
  • type:表示事件類型(字符串)。比如 click迫靖。

  • listener:通常是一個函數(shù)院峡,即事件處理程序。被觸發(fā)時將會接收到一個事件對象作為參數(shù)系宜。

  • useCapture:布爾值照激,默認為 false。該參數(shù)決定了 listener 是否在“捕獲階段”被觸發(fā)盹牧。

舉個例子:

function handler(e) {
  // this 將會指向事件處理程序所綁定的元素
  // do something...
}
// 注冊事件處理程序
target.addEventListener('click', handler, false)
// 移除事件處理程序
target.removeEventListener('click', handler, false)


// ?? 注意點:
// 1. 保留事件處理程序的引用俩垃,是移除監(jiān)聽事件的唯一方法励幼。
// 2. 換句話說,若使用匿名函數(shù)作為事件處理程序口柳,將無法移除監(jiān)聽事件苹粟。
// 3. 若多次注冊事件處理程序,對應(yīng)地需要多次移除事件啄清。
// 4. addEventListener() 是目前唯一一個可在“捕獲階段”觸發(fā)事件監(jiān)聽的方法碳竟。
// 5. 在注冊事件處理程序時,即使 listener 引用相同搬俊,若 useCapture 參數(shù)不同倦春,
//    也會被注冊多個。
// 6. 多次重復(指三個參數(shù)都完全相等時)注冊事件處理程序荣茫,僅第一次有效想帅。
//    這點與 DOM0 事件處理程序的方式是不同的。
// 7. 除了 DOM 元素啡莉,其他對象也有這個接口港准,比如 window、XMLHttpRequest 等咧欣。
// 8. IE8 及以下瀏覽器不支持浅缸,對應(yīng)的解決方法是 attachEvent() 和 detachEvent() 方法,
//    這是 IE 瀏覽器特有的魄咕。
4. IE 事件處理程序

盡管 IE8 及以下瀏覽器不支持 addEventListener()removeEventListener() 方法衩椒,但它有兩個類似的方法來注冊或移除事件處理程序,那就是 attachEvent()detachEvent()哮兰。區(qū)別是不支持在事件捕獲階段觸發(fā)毛萌,因為 IE8 事件流只含事件冒泡。

語法如下:

// 只接受兩個參數(shù)
target.attachEvent(ontype, listener)
target.detachEvent(ontype, listener)
  • ontype:表示事件屬性(字符串)喝滞,即 on + type阁将,比如 onclick。這點與 addEventListener() 中的 type 參數(shù)是不同的右遭。

  • listener:同樣接受一個函數(shù)作為事件處理程序做盅。請注意 listener 函數(shù)內(nèi)部 this 將指向 window 對象。

舉個例子:

function handler(e) {
  // this 將會指向 window 對象
  // 要處理 this 指向問題很簡單窘哈,例如 Function.prototype.bind() 等
  // do something...
}
// 注冊事件處理程序
target.attachEvent('onclick', handler)
// 移除事件處理程序
target.detachEvent('onclick', handler)


// ?? 注意點:
// 1. 注意第一個參數(shù)是 on + type 的形式言蛇,這點與 DOM2 是不同的。
// 2. 若無特殊處理宵距,事件處理程序內(nèi) this 指向 window 對象腊尚,這點與 DOM0、DOM2 是不同的满哪。
// 3. 若 Function.prototype.bind() 去處理 this 問題婿斥,注意保持事件處理程序引用問題劝篷。
// 4. attachEvent() 不支持在捕獲階段觸發(fā)事件處理程序。
5. 跨瀏覽器事件處理程序

跨瀏覽器民宿,就是說要同時兼容 IE 瀏覽器和現(xiàn)代瀏覽器娇妓。其實上面已經(jīng)將所有方式都介紹了一遍,寫起來就很簡單了活鹰。

// 注冊
function addHandler(el, type, fn) {
  if (el.addEventListener) {
    el.addEventListener(type, fn, false)
  } else if (el.attachEvent) {
    el.attachEvent('on' + type, fn)
  } else {
    el['on' + type] = listener
  }
}

// 移除
function removeHandler(el, type, fn) {
  if (el.removeEventListener) {
    el.removeEventListener(type, fn, false)
  } else if (el.detachEvent) {
    el.detachEvent('on' + type, fn)
  } else {
    el['on' + type] = null
  }
}
6. DOM3 自定義事件(擴展內(nèi)容)

前面介紹的哈恰,全都是瀏覽器內(nèi)置事件,當用戶與瀏覽器發(fā)生交互時志群,事件(對象)就誕生了着绷,它接著會在 DOM 中進行傳播,命中目標后會觸發(fā)相應(yīng)的時間處理函數(shù)锌云。

在 DOM3 Event 標準上荠医,除了在 DOM2 Event 的基礎(chǔ)上,新增了很多事件類型桑涎,而且它還允許自定義事件彬向。但是自定義事件,需要“手動”觸發(fā)攻冷,即主動調(diào)用 dispatchEvent() 方法才可以娃胆。

至于用處嘛,假設(shè)動態(tài)加載腳本等曼,需在加載完成后才能做一些事情缕棵,例如配置什么的。那么我們需要監(jiān)聽腳本什么時候加載完涉兽,這時候自定義事件就能發(fā)揮其作用了。舉個例子:

// 創(chuàng)建自定義事件
const customEvent = new CustomEvent('ready')

// 創(chuàng)建元素
const el = document.createElement('script')
el.src = 'https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js'
el.onload = () => el.dispatchEvent(customEvent) // 派發(fā)事件
el.onerror = err => { /* 腳本加載失敗... */ }

// 注冊事件處理程序
el.addEventListener('ready', e => {
  // 腳本加載完成后篙程,可以做些配置什么的...
})

// 插入 document
const s = document.getElementsByTagName('script')[0]
if (s && s.parentNode) s.parentNode.insertBefore(el, s)

關(guān)于 CustomEvent 的一些語法注意點:

// 1. 為 watch 事件添加監(jiān)聽事件函數(shù)
target.addEventListener('watch', function () { /* ... */ }, false)

// 2. 創(chuàng)建 watch 事件枷畏,若無需傳入指定數(shù)據(jù),可直接使用 new Event('watch')
var watchEvent = new CustomEvent('watch', 
    detail: { /* ... */ }, // 可以是任意值虱饿,通常是與本事件相關(guān)的一些信息
    bubbles: false, // 是否冒泡
    cancelable: false // 是否取消默認行為
)

// 3. 手動觸發(fā)事件
target.dispatchEvent(watchEvent)


// ?? 注意點:
// 1. 前面 1 和 2 的順序可以調(diào)換過來拥诡,沒關(guān)系的。
// 2. 上述我們給事件目標 target(DOM 元素)注冊了 watch 事件監(jiān)聽函數(shù)氮发,
//    是無法通過鼠標點擊或觸控等形式去觸發(fā)事件監(jiān)聽函數(shù)的渴肉,
//    需要主動觸發(fā),即調(diào)用 dispatchEvent() 方法爽冕。
// 3. 還有 CustomEvent 是有兼容性的仇祭,IE 是不支持的,
// 4. 至于兼容性如何處理颈畸,暫時不展開講述乌奇。后面有空再另起一文吧没讲。
7. 小結(jié)
  • 可能有人會好奇為什么不介紹 DOM1,原因是 DOM1 沒有關(guān)于事件的新增或改動礁苗,因而沒提及爬凑。

  • 若同時存在 HTML 事件處理程序和 DOM0 處理函數(shù),后者會覆蓋掉前者试伙。
    DOM2 不會覆蓋 HTML 事件處理程序或 DOM0 事件處理程序嘁信。

  • 為同一事件目標注冊多個事件處理程序時,執(zhí)行次序為:HTML 事件處理程序或 DOM0 > DOM2 或 IE 事件處理程序疏叨。跟事件注冊先后順序無關(guān)潘靖。

若有興趣想了解 DOM0 ~ DOM3 新增了哪些內(nèi)容,可看文章考廉。另外附上 WHATWG 的 DOM 標準:DOM Living Standard秘豹。

七、總結(jié)(上半部分)

就前面關(guān)于事件處理程序所有兼容性問題昌粤,我們來進一步封裝下既绕。

由于要兼容 IE8 的原因,這里并沒有使用 class 類的寫法涮坐。因為經(jīng)過 Babel 處理凄贩,類似 Object.assgin()Object.defindProperty() 等 ES5 方法在 IE8 及更低版本瀏覽器壓根不支持袱讹。

先寫個構(gòu)造函數(shù)吧:

function CreateMyEvent({ type, el, fn }) {
  this.el = el
  this.type = type
  this.fn = fn
  this.listener = function (e) {
    // IE8 DOM0 需要從 window.event 獲取事件對象
    const ev = e || window.event

    // 為 IE8 的事件對象添加以下屬性和方法:
    // target疲扎、currentTarget、stopPropagation()捷雕、preventDefault()
    if (!ev.stopPropagation) {
      ev.target = ev.srcElement
      ev.currentTarget = el
      ev.stopPropagation = () => {
        ev.cancelBubble = true
      }
      ev.preventDefault = () => {
        ev.returnValue = false
      }
    }

    // 統(tǒng)一 this 指向
    fn.call(el, ev)
  }
}

CreateMyEvent.prototype.addEventListener = function () {
  const { type, el, listener } = this
  if (el.addEventListener) {
    // 為了兼容所有瀏覽器椒丧,這里將 useCapture 設(shè)為 false
    el.addEventListener(type, listener, false)
  } else if (el.attachEvent) {
    el.attachEvent('on' + type, listener)
  } else {
    el['on' + type] = listener
  }
}

CreateMyEvent.prototype.removeEventListener = function () {
  const { type, el, listener } = this
  if (el.removeEventListener) {
    el.removeEventListener(type, listener, false)
  } else if (el.detachEvent) {
    el.detachEvent('on' + type, listener)
  } else {
    el['on' + type] = null
  }
}

一些注意點在注釋有標注體現(xiàn),然后進行實例化并調(diào)用救巷,如下:

// 創(chuàng)建實例對象
const myEvent = new CreateMyEvent({
  el: document.getElementById('directory'),
  type: 'click',
  fn: e => {
    // 解決以下痛點:
    // 1. this 總指向監(jiān)聽事件函數(shù)所綁定節(jié)點
    // 2. e 總會得到事件對象
    // 3. IE8 也可以輕松使用 target壶熏、currentTarget、stopPropagation()浦译、preventDefault()
    // do something...
  }
})

// 注冊/移除監(jiān)聽器
myEvent.addEventListener()
setTimeout(() => {
  myEvent.removeEventListener()
}, 5000)

以上示例部分使用了 ES6 的語法棒假,都 2021 年了,如需兼容 ES5 請放心交給 Babel 吧精盅。(親測 IE8 下可正常運行)

八帽哑、事件委托

除了面試中常被問及,實際應(yīng)用場景里也是優(yōu)化性能的一種手段叹俏。

前面提到妻枕,事件流的傳播路徑:捕獲階段 -> 目標階段 -> 冒泡階段。我們都知道,所有瀏覽器都支持事件冒泡佳头,但事件捕獲并不是都支持的鹰贵,例如 IE8。

需要注意的是康嘉,多數(shù)情況下利用事件冒泡行為實現(xiàn)“事件委托”(或稱為事件代理)碉输。但其實,捕獲階段也是可以實現(xiàn)事件委托的亭珍。可能為了兼容敷钾,選擇前者居多。

為什么要事件委托肄梨?

想象一個生活場景:一本書的目錄包含大章節(jié)阻荒、小章節(jié),每個小章節(jié)都會對應(yīng)一個頁碼众羡,然后根據(jù)頁碼就可以翻到對應(yīng)的內(nèi)容侨赡。

如果用程序?qū)崿F(xiàn)的話,“最笨”的做法是:給每個小章節(jié)注冊一個點擊監(jiān)聽器并實現(xiàn)跳轉(zhuǎn)粱侣。似乎也沒太大問題羊壹,是嗎?如果這本書有 1000 個小章節(jié)齐婴,意味著要注冊 1000 個事件監(jiān)聽函數(shù)油猫。先不說性能問題,寫代碼的是不是得瘋掉柠偶。假設(shè)還沒瘋情妖,哪天產(chǎn)品經(jīng)理又新增 500 章節(jié),是不是又得改诱担,總有一天會逼瘋你的毡证!

如果用“事件委托”怎么做呢?我把監(jiān)聽器注冊到目錄上蔫仙。當點擊某章節(jié)時料睛,利用點擊事件的冒泡行為,事件會被傳遞到目錄并命中來觸發(fā)監(jiān)聽器匀哄。而且,這是一勞永逸的事情雏蛮,無論產(chǎn)品經(jīng)理如何增刪章節(jié)涎嚼,都無需再改動了。這不就有時間摸魚了對吧挑秉。

在 JavaScript 中法梯,頁面中事件處理程序的數(shù)量與頁面整體性能直接相關(guān)。原因有很多。首先每個函數(shù)都是對象立哑,都占用內(nèi)存空間夜惭,對象越多,性能越差铛绰。其次诈茧,為指定事件處理程序所需訪問 DOM 的次數(shù)會先期造成整個頁面交互的延遲。只要在事件處理程序時多注意一些方法捂掰,就可以改善頁面性能敢会。(這段話摘自《JavaScript 高級程序設(shè)計》)

舉個例子:

<!-- 通常將與渲染無關(guān)的信息放到 dataset 里面,即 data-* 的形式  -->
<div id="directory">
  Directory
  <ol>
    <div class="chapter">Chapter1</div>
    <li data-page="10">section1</li>
    <li data-page="20">section2</li>
  </ol>
  <ol>
    <div class="chapter">Chapter2</div>
    <li data-page="30">section1</li>
    <li data-page="40">section2</li>
  </ol>
</div>

以上示例中这嚣,有多組 <ol>鸥昏、<li> 的目錄,想要點擊 <li>(即 section 部分)的時候姐帚,打開書本對應(yīng)頁面吏垮。

根據(jù)上述給出的 HTML 示例,事件委托最粗糙罐旗、最不靈活膳汪、最簡單的實現(xiàn)如下:

function delegate() {
  const el = document.getElementById('directory')
  el.addEventListener('click', e => {
    const { page } = e.target.dataset
    if (page === undefined) return
    // do something...
    // 處理業(yè)務(wù)邏輯,比如:
    console.log(`Please turn to page ${page} of the book.`)
  })
}

上述示例尤莺,只要點擊 <div id="directory"> 及其后代元素都會命中事件處理程序旅敷,按需求是點擊 <li> 才要執(zhí)行業(yè)務(wù)邏輯。像點擊 <div class="chapter"> 其實是沒有實際意義的颤霎。而且明顯上面的方法并不靈活媳谁。

我們再改一下:

/**
 * 事件委托
 *
 * @param {Element} el 事件委托的目標元素
 * @param {string} type 事件類型
 * @param {Function} fn 監(jiān)聽器
 * @param {string} selectors CSS 選擇器
 */
function delegate({ el, type, fn, selectors }) {
  el.addEventListener(type, e => {
    if (!selectors) {
      fn.call(el, e)
      return // 請注意不要 return false 避免取消默認行為
    }

    // myClosest() 作用:向上查找最近的 selectors 元素。
    // 1. 內(nèi)置 closest() 方法兼容性較差友酱,不支持 IE 瀏覽器晴音;
    // 2. 內(nèi)置 closest() 方法內(nèi)部從 Document 開始檢索的,若結(jié)合事件委托場景缔杉,
    //    如果從事件綁定元素(含)開始檢索锤躁,效果更優(yōu)。
    // 3. 因而或详,基于 Element.closest() 稍作修改系羞,并添加到 Element 原型上。
    if (!Element.prototype.myClosest) {
      ;(() => {
        Element.prototype.myClosest = function (s, root) {
          let el = this
          let i
          // 其實改成 root.querySelectorAll(s) 也行霸琴,
          // 目前這種寫法是為了讓 root === el 也正常匹配到椒振,
          // 但是回頭想一下,這種情況還有必要事件委托嗎梧乘,對吧澎迎!自個看著辦吧庐杨,問題也不大
          const matches = root.parentElement.querySelectorAll(s)

          do {
            i = matches.length
            // eslint-disable-next-line no-empty
            while (--i >= 0 && matches.item(i) !== el) {}
          } while (i < 0 && (el = el.parentElement))

          return el
        }
      })()
    }

    // 若匹配不到 selectors 則返回 null
    const matchEl = e.target.myClosest(selectors, el)

    matchEl && fn.call(matchEl, e)
    // 如果像這樣綁定 this,請注意:
    // 1. fn 的 this 指向 selectors 對應(yīng)的元素
    // 2. e.target 仍指向事件目標
    // 3. e.currentTarget 仍指向監(jiān)聽器綁定元素夹供。
    // 4. 由于事件對象的 target灵份、currentTarget 屬性只讀,唯有改變 this 來指向引用 selectors 元素
  })
}

按上述方式封裝的好處是方便靈活哮洽,一些實現(xiàn)思路或注意事項填渠,在相應(yīng)位置的注釋已標注。

delegate({
  el: document.getElementById('directory'),
  type: 'click',
  selectors: 'li',
  fn: e => {
    const { page } = e.target.dataset
    console.log(`Please turn to page ${page} of the book.`)
    // other statements...
  }
})

思路就這樣袁铐,其實很簡單揭蜒,麻煩在于兼容各瀏覽器而已。若不需要兼容 IE 瀏覽器剔桨,那簡直不能太爽了屉更,后面會給出一個簡化版。

當然上面還不支持 IE8 瀏覽器洒缀。因為 e.target瑰谜、el.addEventListener 還沒兼容處理。請稍等树绩,下一章節(jié)結(jié)合前面的內(nèi)容再整合一下萨脑。

九、最終總結(jié)

這里會將本文所有的內(nèi)容都封裝在一起饺饭,包括 addEventListener()渤早、attachEvent()、事件委托以及兼容問題等等瘫俊。

1. 兼容所有瀏覽器的版本(包括 IE8)

先給一個可兼容 IE8 的版本鹊杖。可以輕松地按現(xiàn)代瀏覽器的方式去注冊扛芽、移除事件監(jiān)聽器骂蓖,以及方便處理冒泡、默認行為等川尖。

但前提是登下,使用 Babel 轉(zhuǎn)換一下以兼容 ES5。

function CreateMyEvent({ el, type, fn }) {
  this.el = el
  this.type = type
  this.fn = fn
  this.listener = function (e) {
    // 使得 IE8 中叮喳,正常用上 target被芳、stopPropagation() 等屬性或方法
    const ev = CreateMyEvent.eventPolyfill(e, el)
    // 統(tǒng)一 this 指向
    fn.call(el, ev)
  }
  this.added = false // 是否已添加監(jiān)聽器
}

CreateMyEvent.eventPolyfill = function (e, currentTarget) {
  // 處理 IE8 兼容性問題
  const ev = e || window.event
  if (!ev.stopPropagation) {
    ev.target = ev.srcElement
    ev.currentTarget = currentTarget
    ev.stopPropagation = () => {
      ev.cancelBubble = true
    }
    ev.preventDefault = () => {
      ev.returnValue = false
    }
  }
  return ev
}

CreateMyEvent.closestPolyfill = function () {
  Element.prototype.myClosest = function (s, root) {
    let el = this
    let i
    const matches = root.parentElement.querySelectorAll(s)

    do {
      i = matches.length
      // eslint-disable-next-line no-empty
      while (--i >= 0 && matches.item(i) !== el) {}
    } while (i < 0 && (el = el.parentElement))

    return el
  }
}

CreateMyEvent.prototype.addEventListener = function () {
  if (this.added) {
    console.warn('Please note that you have added event handler before and will not be added again.')
    return
  }

  const { type, el, listener } = this
  if (el.addEventListener) {
    // 為了兼容所有瀏覽器,這里將 useCapture 設(shè)為 false
    el.addEventListener(type, listener, false)
  } else if (el.attachEvent) {
    el.attachEvent('on' + type, listener)
  } else {
    el['on' + type] = listener
  }

  this.added = true
}

CreateMyEvent.prototype.removeEventListener = function () {
  const { type, el, listener } = this
  if (el.removeEventListener) {
    el.removeEventListener(type, listener, false)
  } else if (el.detachEvent) {
    el.detachEvent('on' + type, listener)
  } else {
    el['on' + type] = null
  }

  this.added = false
}

CreateMyEvent.prototype.delegate = function (selectors) {
  const { el, fn } = this

  if (!selectors) {
    this.addEventListener()
    return
  }

  // 在重寫 listener 監(jiān)聽器之前馍悟,確保移除此前的監(jiān)聽器
  if (this.added) console.warn('Please note that the previously registered event handler will be deleted and a new event handler will be added.')
  this.removeEventListener()

  // 重寫監(jiān)聽器
  this.listener = function (e) {
    const ev = CreateMyEvent.eventPolyfill(e)
    // 在 Element 原型上添加 myClosest 方法
    if (!Element.prototype.myClosest) CreateMyEvent.closestPolyfill()
    const matchEl = ev.target.myClosest(selectors, el)
    matchEl && fn.call(matchEl, ev)
  }

  // 重新注冊監(jiān)聽器
  this.addEventListener()
}

使用方式如下:

// 創(chuàng)建實例對象
const myEvent = new CreateMyEvent({
  type: 'click',
  el: document.getElementById('directory'),
  fn: e => {
    // 1. e 總是能獲取到事件對象畔濒。
    // 2. 阻止冒泡:e.stopPropagation()
    // 3. 取消默認行為:e.preventDefault()
    // 4. 唯一要注意的是:
    //    當使用事件委托時,this 指向 selectors 對應(yīng)元素赋朦;
    //    其他的均指向事件監(jiān)聽器所綁定的元素篓冲,即 e.currentTarget。
  }
})

// 注冊事件監(jiān)聽器
myEvent.addEventListener() // 有效

// 重復注冊事件監(jiān)聽器宠哄,會被阻止(實際上注冊同一個也是無效的)壹将。
myEvent.addEventListener() // 無效

// 移除事件監(jiān)聽器
myEvent.removeEventListener() // 有效

// 移除后,重新注冊事件處理程序
myEvent.addEventListener() // 有效

// 事件委托毛嫉,可傳入 selectors 作為參數(shù)(實質(zhì)上也就是注冊事件處理程序)
// 不傳入 selectors 參數(shù)時诽俯,相當于 myEvent.addEventListener() 所以無效
myEvent.delegate() // 無效

// 事件委托:內(nèi)部會先移除上一個事件監(jiān)聽器,在重新注冊
myEvent.delegate('li') // 有效

// 事件委托承粤,這將會注冊全新的一個事件監(jiān)聽器(同樣的上一個會被移除)
myEvent.delegate('li') // 有效

哎暴区,丑陋的代碼......由于 IE8 不支持類似 Object.assgin()Object.defindProperty() 等 ES5 方法辛臊,上面只能使用最原始的構(gòu)造函數(shù)去寫了仙粱。

?? 暫不支持添加多個事件監(jiān)聽器,實際中我暫時想不到需要添加多個事件監(jiān)聽器的場景彻舰。實現(xiàn)倒是不難伐割,處理起來也很簡單,但我感覺沒必要刃唤。

另外隔心,前面示例是將頁面數(shù)據(jù)定義在 data-* 上的,但由于 IE10 及更低版本瀏覽器并不支持 Element.prototype.dataset 屬性尚胞,因此也要處理一下硬霍。例如:

// 也要用 Babel 轉(zhuǎn)換一下
function getDataset(el) {
  if (el.dataset) return el.dataset

  const attrs = el.attributes
  const dataset = {}

  for (let i = 0, re1 = /^data-(.+)/, re2 = /-([a-z\d])/gi, len = attrs.length; i < len; i++) {
    // data-camel-case to camel-case
    const matchName = re1.exec(attrs[i].name)
    if (!matchName) continue
    // camel-case to camelCase
    const name = matchName[1].replace(re2, (...args) => {
      return args[1].toUpperCase()
    })
    // add to dataset
    dataset[name] = attrs[i].value
  }

  return dataset
}

不需要兼容 IE8 的版本如下,將會使用 class 寫法笼裳,會簡潔很多唯卖。辣雞 IE...

2. 兼容現(xiàn)代瀏覽器版本(包括 IE9 ~ IE11)
class CreateMyEvent {
  constructor({ el, type, fn }) {
    this.el = el
    this.type = type
    this.fn = fn
    this.listener = fn.bind(el)
    this.added = false // 是否已添加監(jiān)聽器
  }

  static closestPolyfill() {
    Element.prototype.myClosest = function (s, root) {
      let el = this
      let i
      const matches = root.parentElement.querySelectorAll(s)

      do {
        i = matches.length
        // eslint-disable-next-line no-empty
        while (--i >= 0 && matches.item(i) !== el) {}
      } while (i < 0 && (el = el.parentElement))

      return el
    }
  }

  addEventListener() {
    if (this.added) {
      console.warn('Please note that you have added event handler before and will not be added again.')
      return
    }
    const { type, el, listener } = this
    el.addEventListener(type, listener, false)
    this.added = true
  }

  removeEventListener() {
    const { type, el, listener } = this
    el.removeEventListener(type, listener, false)
    this.added = false
  }

  delegate(selectors) {
    const { el, fn } = this

    if (!selectors) {
      this.addEventListener()
      return
    }

    // 在重寫 listener 監(jiān)聽器之前,確保移除此前的監(jiān)聽器
    if (this.added) console.warn('Please note that the previously registered event handler will be deleted and a new event handler will be added.')
    this.removeEventListener()

    // 重寫監(jiān)聽器
    this.listener = function (e) {
      // 在 Element 原型上添加 myClosest 方法
      if (!Element.prototype.myClosest) CreateMyEvent.closestPolyfill()
      const matchEl = e.target.myClosest(selectors, el)
      matchEl && fn.call(matchEl, e)
    }

    // 重新注冊監(jiān)聽器
    this.addEventListener()
  }
}

到這里侍咱,好像就完了耐床,改了好幾版,想吐血...

The end.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末楔脯,一起剝皮案震驚了整個濱河市撩轰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌昧廷,老刑警劉巖堪嫂,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異木柬,居然都是意外死亡皆串,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門眉枕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恶复,“玉大人怜森,你說我怎么就攤上這事“担” “怎么了副硅?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長翅萤。 經(jīng)常有香客問我恐疲,道長,這世上最難降的妖魔是什么套么? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任培己,我火速辦了婚禮,結(jié)果婚禮上胚泌,老公的妹妹穿的比我還像新娘省咨。我一直安慰自己,他們只是感情好玷室,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布茸炒。 她就那樣靜靜地躺著,像睡著了一般阵苇。 火紅的嫁衣襯著肌膚如雪壁公。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天绅项,我揣著相機與錄音紊册,去河邊找鬼。 笑死快耿,一個胖子當著我的面吹牛囊陡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播掀亥,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼撞反,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了搪花?” 一聲冷哼從身側(cè)響起遏片,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎撮竿,沒想到半個月后吮便,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡幢踏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年髓需,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片房蝉。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡僚匆,死狀恐怖微渠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情咧擂,我是刑警寧澤敛助,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站屋确,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏续扔。R本人自食惡果不足惜攻臀,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望纱昧。 院中可真熱鬧刨啸,春花似錦、人聲如沸识脆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽灼捂。三九已至离例,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間悉稠,已是汗流浹背宫蛆。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留的猛,地道東北人耀盗。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像卦尊,于是被迫代替她去往敵國和親叛拷。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容