Vue源碼系列講解——實例方法篇【二】(事件相關方法)

目錄

0.前言

1. vm.$on

1.1 用法回顧

1.2 內部原理

2. vm.$emit

2.1 用法回顧

2.2 內部原理

3. vm.$off

3.1 用法回顧

3.2 內部原理

4. vm.$once

4.1 用法回顧

4.2 內部原理


0.前言

與事件相關的實例方法有4個,分別是vm.$onvm.$emitvm.$offvm.$once。它們是在eventsMixin函數中掛載到Vue原型上的,代碼如下:

export function eventsMixin (Vue) {Vue.prototype.$on = function (event, fn) {}Vue.prototype.$once = function (event, fn) {}Vue.prototype.$off = function (event, fn) {}Vue.prototype.$emit = function (event) {}
}

當執行eventsMixin函數后,會向Vue原型上掛載上述4個實例方法。

接下來,我們就來分析這4個與事件相關的實例方法其內部的原理都是怎樣的。

1. vm.$on

1.1 用法回顧

在介紹方法的內部原理之前,我們先根據官方文檔示例回顧一下它的用法。

vm.$on( event, callback )

  • 參數

    • {string | Array<string>} event?(數組只在 2.2.0+ 中支持)
    • {Function} callback
  • 作用

    監聽當前實例上的自定義事件。事件可以由vm.$emit觸發。回調函數會接收所有傳入事件觸發函數的額外參數。

  • 示例

    vm.$on('test', function (msg) {console.log(msg)
    })
    vm.$emit('test', 'hi')
    // => "hi"
    

1.2 內部原理

在介紹內部原理之前,我們先有一個這樣的概念:$on$emit這兩個方法的內部原理是設計模式中最典型的發布訂閱模式,首先定義一個事件中心,通過$on訂閱事件,將事件存儲在事件中心里面,然后通過$emit觸發事件中心里面存儲的訂閱事件。

OK,有了這個概念之后,接下來,我們就先來看看$on方法的內部原理。該方法的定義位于源碼的src/core/instance/event.js中,如下:

Vue.prototype.$on = function (event, fn) {const vm: Component = thisif (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {this.$on(event[i], fn)}} else {(vm._events[event] || (vm._events[event] = [])).push(fn)}return vm
}

$on方法接收兩個參數,第一個參數是訂閱的事件名,可以是數組,表示訂閱多個事件。第二個參數是回調函數,當觸發所訂閱的事件時會執行該回調函數。

首先,判斷傳入的事件名是否是一個數組,如果是數組,就表示需要一次性訂閱多個事件,就遍歷該數組,將數組中的每一個事件都遞歸調用$on方法將其作為單個事件訂閱。如下:

if (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {this.$on(event[i], fn)}
}

如果不是數組,那就當做單個事件名來處理,以該事件名作為key,先嘗試在當前實例的_events屬性中獲取其對應的事件列表,如果獲取不到就給其賦空數組為默認值,并將第二個參數回調函數添加進去。如下:

else {(vm._events[event] || (vm._events[event] = [])).push(fn)
}

那么問題來了,當前實例的_events屬性是干嘛的呢?

還記得我們在介紹生命周期初始化階段的初始化事件initEvents函數中,在該函數中,首先在當前實例上綁定了_events屬性并給其賦值為空對象,如下:

export function initEvents (vm: Component) {vm._events = Object.create(null)// ...}

這個_events屬性就是用來作為當前實例的事件中心,所有綁定在這個實例上的事件都會存儲在事件中心_events屬性中。

以上,就是$on方法的內部原理。

2. vm.$emit

2.1 用法回顧

在介紹方法的內部原理之前,我們先根據官方文檔示例回顧一下它的用法。

vm.$emit( eventName, […args] )

  • 參數
    • {string} eventName
    • [...args]
  • 作用: 觸發當前實例上的事件。附加參數都會傳給監聽器回調。

2.2 內部原理

該方法接收的第一個參數是要觸發的事件名,之后的附加參數都會傳給被觸發事件的回調函數。該方法的定義位于源碼的src/core/instance/event.js中,如下:

Vue.prototype.$emit = function (event: string): Component {const vm: Component = thislet cbs = vm._events[event]if (cbs) {cbs = cbs.length > 1 ? toArray(cbs) : cbsconst args = toArray(arguments, 1)for (let i = 0, l = cbs.length; i < l; i++) {try {cbs[i].apply(vm, args)} catch (e) {handleError(e, vm, `event handler for "${event}"`)}}}return vm}
}

該方法的邏輯很簡單,就是根據傳入的事件名從當前實例的_events屬性(即事件中心)中獲取到該事件名所對應的回調函數cbs,如下:

let cbs = vm._events[event]

然后再獲取傳入的附加參數args,如下:

const args = toArray(arguments, 1)

由于cbs是一個數組,所以遍歷該數組,拿到每一個回調函數,執行回調函數并將附加參數args傳給該回調。如下:

for (let i = 0, l = cbs.length; i < l; i++) {try {cbs[i].apply(vm, args)} catch (e) {handleError(e, vm, `event handler for "${event}"`)}
}

以上,就是$emit方法的內部原理。

3. vm.$off

3.1 用法回顧

在介紹方法的內部原理之前,我們先根據官方文檔示例回顧一下它的用法。

vm.$off( [event, callback] )

  • 參數

    • {string | Array<string>} event?(只在 2.2.2+ 支持數組)
    • {Function} [callback]
  • 作用

    移除自定義事件監聽器。

    • 如果沒有提供參數,則移除所有的事件監聽器;
    • 如果只提供了事件,則移除該事件所有的監聽器;
    • 如果同時提供了事件與回調,則只移除這個回調的監聽器。

3.2 內部原理

通過用法回顧我們知道,該方法用來移除事件中心里面某個事件的回調函數,根據所傳入參數的不同,作出不同的處理。該方法的定義位于源碼的src/core/instance/event.js中,如下:

Vue.prototype.$off = function (event, fn) {const vm: Component = this// allif (!arguments.length) {vm._events = Object.create(null)return vm}// array of eventsif (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {this.$off(event[i], fn)}return vm}// specific eventconst cbs = vm._events[event]if (!cbs) {return vm}if (!fn) {vm._events[event] = nullreturn vm}if (fn) {// specific handlerlet cblet i = cbs.lengthwhile (i--) {cb = cbs[i]if (cb === fn || cb.fn === fn) {cbs.splice(i, 1)break}}}return vm
}

可以看到,在該方法內部就是通過不斷判斷所傳參數的情況進而進行不同的邏輯處理,接下來我們逐行分析。

首先,判斷如果沒有傳入任何參數(即arguments.length為0),這就是第一種情況:如果沒有提供參數,則移除所有的事件監聽器。我們知道,當前實例上的所有事件都存儲在事件中心_events屬性中,要想移除所有的事件,那么只需把_events屬性重新置為空對象即可。如下:

if (!arguments.length) {vm._events = Object.create(null)return vm
}

接著,判斷如果傳入的需要移除的事件名是一個數組,就表示需要一次性移除多個事件,那么我們只需同訂閱多個事件一樣,遍歷該數組,然后將數組中的每一個事件都遞歸調用$off方法進行移除即可。如下:

if (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {this.$off(event[i], fn)}return vm
}

接著,獲取到需要移除的事件名在事件中心中對應的回調函數cbs。如下:

const cbs = vm._events[event]

接著,判斷如果cbs不存在,那表明在事件中心從來沒有訂閱過該事件,那就談不上移除該事件,直接返回,退出程序即可。如下:

if (!cbs) {return vm
}

接著,如果cbs存在,但是沒有傳入回調函數fn,這就是第二種情況:如果只提供了事件,則移除該事件所有的監聽器。這個也不難,我們知道,在事件中心里面,一個事件名對應的回調函數是一個數組,要想移除所有的回調函數我們只需把它對應的數組設置為null即可。如下:

if (!fn) {vm._events[event] = nullreturn vm
}

接著,如果既傳入了事件名,又傳入了回調函數,cbs也存在,那這就是第三種情況:如果同時提供了事件與回調,則只移除這個回調的監聽器。那么我們只需遍歷所有回調函數數組cbs,如果cbs中某一項與fn相同,或者某一項的fn屬性與fn相同,那么就將其從數組中刪除即可。如下:

if (fn) {// specific handlerlet cblet i = cbs.lengthwhile (i--) {cb = cbs[i]if (cb === fn || cb.fn === fn) {cbs.splice(i, 1)break}}
}

以上,就是$off方法的內部原理。

4. vm.$once

4.1 用法回顧

在介紹方法的內部原理之前,我們先根據官方文檔示例回顧一下它的用法。

vm.$once( event, callback )

  • 參數

    • {string} event
    • {Function} callback
  • 作用

    監聽一個自定義事件,但是只觸發一次。一旦觸發之后,監聽器就會被移除。

4.2 內部原理

該方法的作用是先訂閱事件,但是該事件只能觸發一次,也就是說當該事件被觸發后會立即移除。要實現這個功能也不難,我們可以定義一個子函數,用這個子函數來替換原本訂閱事件所對應的回調,也就是說當觸發訂閱事件時,其實執行的是這個子函數,然后再子函數內部先把該訂閱移除,再執行原本的回調,以此來達到只觸發一次的目的。

下面我們就來看下源碼的實現。該方法的定義位于源碼的src/core/instance/event.js中,如下:

Vue.prototype.$once = function (event, fn) {const vm: Component = thisfunction on () {vm.$off(event, on)fn.apply(vm, arguments)}on.fn = fnvm.$on(event, on)return vm
}

可以看到,在上述代碼中,被監聽的事件是event,其原本對應的回調是fn,然后定義了一個子函數on

在該函數內部,先通過$on方法訂閱事件,同時所使用的回調函數并不是原本的fn而是子函數on,如下:

vm.$on(event, on)

也就是說,當事件event被觸發時,會執行子函數on

然后在子函數內部先通過$off方法移除訂閱的事件,這樣確保該事件不會被再次觸發,接著執行原本的回調fn,如下:

function on () {vm.$off(event, on)fn.apply(vm, arguments)
}

另外,還有一行代碼on.fn = fn是干什么的呢?

上文我們說了,我們用子函數on替換了原本的訂閱事件所對應的回調fn,那么在事件中心_events屬性中存儲的該事件名就會變成如下這個樣子:

vm._events = {'xxx':[on]
}

但是用戶自己卻不知道傳入的fn被替換了,當用戶在觸發該事件之前想調用$off方法移除該事件時:

vm.$off('xxx',fn)

此時就會出現問題,因為在_events屬性中的事件名xxx對應的回調函數列表中沒有fn,那么就會移除失敗。這就讓用戶費解了,用戶明明給xxx事件傳入的回調函數是fn,現在反而找不到fn導致事件移除不了了。

所以,為了解決這一問題,我們需要給on上綁定一個fn屬性,屬性值為用戶傳入的回調fn,這樣在使用$off移除事件的時候,$off內部會判斷如果回調函數列表中某一項的fn屬性與fn相同時,就可以成功移除事件了。

以上,就是$once方法的內部原理。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/715327.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/715327.shtml
英文地址,請注明出處:http://en.pswp.cn/news/715327.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

前端面試知識點合集

原型和原型鏈 任何函數都可以作為構造函數。當該函數通過 new 關鍵字調用的時候&#xff0c;就稱之為構造函數。 var Parent function(){}//定義一個函數&#xff0c;那它只是一個普通的函數&#xff0c;不能稱它為構造函數var instance new Parent(); //這時這個Parent就不…

C#理論 —— WPF 應用程序Console 控制臺應用

文章目錄 1. WPF 應用程序1.1 工程創建1.2 控件1.2.1 控件的公共屬性1.2.1 TextBox 文本框1.2.1 Button 按鈕 *. Console 控制臺應用1.1 工程創建 1. WPF 應用程序 1.1 工程創建 Visual Studio 中新建項目 - 選擇WPF 應用程序&#xff1b; 1.2 控件 1.2.1 控件的公共屬性 …

如何備份和恢復MySQL數據庫?有哪些常見的備份工具和策略?

如何備份和恢復MySQL數據庫&#xff1f;有哪些常見的備份工具和策略&#xff1f; 在數據庫管理中&#xff0c;備份和恢復是非常重要的環節&#xff0c;它們保障了數據的安全性和可恢復性。對于MySQL這樣的關系型數據庫管理系統&#xff0c;了解并實施有效的備份策略至關重要。…

Linux網絡編程——網絡基礎

Linux網絡編程——網絡基礎 1. 網絡結構模式1.1 C/S 結構1.2 B/S 結構 2. MAC 地址3. IP地址3.1 簡介3.2 IP 地址編址方式 4. 端口4.1 簡介4.2 端口類型 5. 網絡模型5.1 OSI 七層參考模型5.2 TCP/IP 四層模型 6. 協議6.1 簡介6.2 常見協議6.3 UDP 協議6.4 TCP 協議6.5 IP 協議6…

【兔子機器人】根據自身機器人參數修改simulink模型

關節電機 機體初始高度 &#xff01;&#xff01;&#xff01;接下來嘗試修改各腿的坐標朝向

LeetCode54題:螺旋矩陣(python3)

路徑的長度即為矩陣中的元素數量&#xff0c;當路徑的長度達到矩陣中的元素數量時即為完整路徑&#xff0c;將該路徑返回。 循環打印&#xff1a; “從左向右、從上向下、從右向左、從下向上” 四個方向循環打印。 class Solution:def spiralOrder(self, matrix: List[List[i…

怎么對App進行功能測試

測試人員常被看作是bug的尋找者&#xff0c;但你曾想過他們實際是如何開展測試的嗎&#xff1f;你是否好奇他們究竟都做些什么&#xff0c;以及他們如何在一個典型的技術項目中體現價值&#xff1f;本文將帶你經歷測試人員的思維過程&#xff0c;探討他們測試app時的各種考慮. …

Android和Linux的嵌入式開發差異

最近開始投入Android的懷抱。說來慚愧&#xff0c;08年就聽說這東西&#xff0c;當時也有同事投入去看&#xff0c;因為惡心Java&#xff0c;始終對這玩意無感&#xff0c;沒想到現在不會這個嵌入式都快要沒法搞了。為了不中年失業&#xff0c;所以只能回過頭又來學。 首先還是…

虛擬內存與mmap,brk

虛擬內存與mmap,brk 基本概念及相關術語 1.1 基本概念 虛擬內存使得應用程序認為它擁有連續的可用的內存&#xff08;一個連續完整的地址空間&#xff09;&#xff0c;而實際上&#xff0c;它通常是被分隔成多個物理內存碎片&#xff0c;還有部分暫時存儲在外部磁盤存儲器上&…

【C語言】linux內核generic_xdp_tx

一、中文注釋 /* 在執行通用XDP時&#xff0c;我們必須繞過qdisc層和網絡挖掘點&#xff0c;* 以匹配驅動內XDP的行為。*/ void generic_xdp_tx(struct sk_buff *skb, struct bpf_prog *xdp_prog) {struct net_device *dev skb->dev; // 獲取skb對應的網絡設備struct netd…

面試高頻率問答題目

索引&#xff1a; 主鍵索引&#xff1a;表的id &#xff08;唯一 且 不能為空&#xff09; 唯一索引&#xff1a;表User 假設有account 字段 &#xff0c;用戶名不重復 &#xff08;唯一 可以為空&#xff09; 復合索引&#xff1a;where() 的條件 用戶名&#xff0c;密碼 …

MySQL:函數

提醒&#xff1a; 設定下面的語句是在數據庫名為 db_book里執行的。 創建user_info表 注意&#xff1a;pwd為密碼字段&#xff0c;這里使用了VARCHAR(128)類型&#xff0c;為了后面方便對比&#xff0c;開發項目里一般使用char(32)&#xff0c;SQL語句里使用MD5加密函數 USE db…

【博圖TIA-Api】通過Excel自動新建文件夾和導入FB塊

【博圖TIA-Api】通過Excel自動新建文件夾和導入FB塊 說明思路準備獲取Excel表格內文件名和FB塊名等信息新建文件夾部分篩分獲取的文件夾數據&#xff0c;去掉重復內容創建文件夾 導入FB塊導出FB塊的xml文件查找需要放置的文件夾導入塊 說明 續上一篇文章&#xff0c;這次是根據…

多線程 --- [ 線程池、線程安全、其他常見的鎖 ]

目錄 1. 線程池 模塊一&#xff1a;線程的封裝 模塊二&#xff1a;線程池的封裝 模塊三&#xff1a;互斥量的封裝 (RAII風格) 模塊四&#xff1a;任務的封裝 模塊五&#xff1a;日志的封裝 模塊六&#xff1a;時間的封裝 模塊六&#xff1a;主函數 模塊七&#xff1a…

備戰藍橋杯---狀態壓縮DP進階題1

我們來看一看一道比較難的問題&#xff08;十分十分的巧妙&#xff09;&#xff1a; 顯然我們應該一行一行放&#xff0c;又豎的會對下一行產生影響&#xff0c;我們令橫著放為0&#xff0c;豎著放的上方為1. 對于下一行&#xff0c;前一行放1的下面為0&#xff0c;但是會出現…

【Redis | 第九篇】一篇文章看懂Redis持久化機制

文章目錄 9.一篇文章看懂Redis持久化機制9.1Redis的兩種持久化機制9.1.1為什么有持久化&#xff1f; 9.2RDB機制9.2.1介紹9.2.2觸發機制&#xff08;1&#xff09;save命令觸發&#xff08;2&#xff09;bgsave命令觸發&#xff08;3&#xff09;自動觸發 9.2.3執行流程9.2.4優…

C++知識點總結(22):模擬算法真題 ★★★★☆《卡牌游戲》《移動距離》

一、卡牌游戲 1. 審題 題目描述 A , B , C A,B,C A,B,C 三人在玩一個卡牌游戲&#xff0c;規則如下&#xff1a; 游戲開始時&#xff0c; 3 3 3 人分別會得到若干張手牌, 每張牌上寫著 a&#xff0c;b&#xff0c;c 中某一個字母。手牌的順序嚴格按照輸入順序排列&#xff0c…

前端【技術類】資源學習網站整理(那些年的小網站)

學習網站整理 值得分享的視頻博主&#xff1a;學習網站鏈接 百度首頁的資源收藏里的截圖&#xff08;排列順序沒有任何意義&#xff0c;隨性而已~&#xff09;&#xff0c;可根據我標注的關鍵詞百度搜索到這些網站呀&#xff0c;本篇末尾會一一列出來&#xff0c;供大家學習呀 …

徹底搞懂回溯算法(例題詳解)

目錄 什么是回溯算法&#xff1a; 子集問題&#xff1a; 子集問題II(元素可重復但不可復選): 組合問題&#xff1a; 組合問題II(元素可重復但不可復選): 排列問題&#xff1a; 排列問題II(元素可重復但不可復選): 什么是回溯算法&#xff1a; 「回溯是遞歸的副產品&…

最小生成樹---Kruskal算法

最小生成樹定義&#xff1a; 給定一張邊帶權的無向圖 G(V,E)&#xff0c;其中 V 表示圖中點的集合&#xff0c;E 表示圖中邊的集合。 由 V 中的全部 n 個頂點和 E 中 n?1 條邊構成的無向連通子圖被稱為 G 的一棵生成樹&#xff0c;其中邊的權值之和最小的生成樹被稱為無向圖 G…