Vue源码探究-事件系统
Vue源碼探究-事件系統(tǒng)
本篇代碼位于vue/src/core/instance/events.js
緊跟著生命周期之后的就是繼續(xù)初始化事件相關(guān)的屬性和方法。整個事件系統(tǒng)的代碼相對其他模塊來說非常簡短,分幾個部分來詳細(xì)看看它的具體實(shí)現(xiàn)。
頭部引用
import {tip,toArray,hyphenate,handleError,formatComponentName } from '../util/index' import { updateListeners } from '../vdom/helpers/index'頭部先是引用了的一些工具方法,沒有什么難點(diǎn),具體可以查看相應(yīng)文件。唯一值得注意的是引用自虛擬節(jié)點(diǎn)模塊的一個叫 updateListeners 方法。顧名思義,是用來更新監(jiān)聽器的,至于為什么要有這樣的一個方法,主要是因?yàn)槿绻搶?shí)例的父組件已經(jīng)存在一些事件監(jiān)聽器,為了正確捕獲到事件并向上冒泡,父級事件是需要繼承下來的,這個原因在下面的初始化代碼中有佐證;另外,如果在實(shí)例初始化的時候綁定了同名的事件處理器,也需要為同名事件添加新的處理器,以實(shí)現(xiàn)同一事件的多個監(jiān)聽器的綁定。
事件初始化
// 定義并導(dǎo)出initEvents函數(shù),接受Component類型的vm參數(shù) export function initEvents (vm: Component) {// 創(chuàng)建例的_events屬性,初始化為空對象vm._events = Object.create(null)// 創(chuàng)建實(shí)例的_hasHookEvent屬性,初始化為falsevm._hasHookEvent = false// 初始化父級附屬事件// init parent attached eventsconst listeners = vm.$options._parentListeners// 如果父級事件存在,則更新實(shí)例事件監(jiān)聽器if (listeners) {updateComponentListeners(vm, listeners)} }// 設(shè)置target值,目標(biāo)是引用實(shí)例 let target: any// 添加事件函數(shù),接受事件名稱、事件處理器、是否一次性執(zhí)行三個參數(shù) function add (event, fn, once) {if (once) {target.$once(event, fn)} else {target.$on(event, fn)} }// 移除事件函數(shù),接受事件名稱和時間處理器兩個參數(shù) function remove (event, fn) {target.$off(event, fn) }// 定義并導(dǎo)出函數(shù)updateComponentListeners,接受實(shí)例對象,新舊監(jiān)聽器參數(shù) export function updateComponentListeners (vm: Component,listeners: Object,oldListeners: ?Object ) {// 設(shè)置target為vmtarget = vm// 執(zhí)行更新監(jiān)聽器函數(shù),傳入新舊事件監(jiān)聽對象、添加事件與移除事件函數(shù)、實(shí)例對象updateListeners(listeners, oldListeners || {}, add, remove, vm)// 置空引用target = undefined }如上述代碼所示,事件監(jiān)聽系統(tǒng)的初始化首先是創(chuàng)建了私有的事件對象和是否有事件鉤子的標(biāo)志兩個屬性,然后根據(jù)父級是否有事件處理器來決定是否更新當(dāng)前實(shí)例的事件監(jiān)聽器,具體如何實(shí)現(xiàn)監(jiān)聽器的更新,貼上這段位于虛擬節(jié)點(diǎn)模塊的輔助函數(shù)中的代碼片段來仔細(xì)看看。
更新事件監(jiān)聽器
// 定義并導(dǎo)出updateListeners哈數(shù) // 接受新舊事件監(jiān)聽器對象,事件添加和移除函數(shù)以及實(shí)例對象參數(shù)。 export function updateListeners (on: Object,oldOn: Object,add: Function,remove: Function,vm: Component ) {// 定義一些輔助變量let name, def, cur, old, event// 遍歷新的監(jiān)聽器對象for (name in on) {// 為def和cur賦值為新的事件對象def = cur = on[name]// 為old賦值為舊的事件對象old = oldOn[name]// 標(biāo)準(zhǔn)化事件對象并賦值給event。// normalizeEvent函數(shù)主要用于將傳入的帶有特殊前綴的事件修飾符分解為具有特定值的事件對象event = normalizeEvent(name)// 下面代碼是weex框架專用,處理cur變量和格式化好的事件對象的參數(shù)屬性/* istanbul ignore if */if (__WEEX__ && isPlainObject(def)) {cur = def.handlerevent.params = def.params}// 如果新事件不存在,在非生產(chǎn)環(huán)境中提供報(bào)錯信息,否則不執(zhí)行任何操作if (isUndef(cur)) {process.env.NODE_ENV !== 'production' && warn(`Invalid handler for event "${event.name}": got ` + String(cur),vm)// 當(dāng)舊事件不存在時} else if (isUndef(old)) {// 如果新事件對象cur的fns屬性不存在if (isUndef(cur.fns)) {// 創(chuàng)建函數(shù)調(diào)用器并重新復(fù)制給cur和on[name]cur = on[name] = createFnInvoker(cur)}// 添加新的事件處理器add(event.name, cur, event.once, event.capture, event.passive, event.params)// 如果新舊事件不完全相等} else if (cur !== old) {// 用新事件處理函數(shù)覆蓋舊事件對象的fns屬性old.fns = cur// 將事件對象重新復(fù)制給onon[name] = old}}// 遍歷舊事件監(jiān)聽器for (name in oldOn) {// 如果新事件對象不存在if (isUndef(on[name])) {// 標(biāo)準(zhǔn)化事件對象event = normalizeEvent(name)// 移除事件處理器remove(event.name, oldOn[name], event.capture)}} }這段代碼中用到了 normalizeEvent 和 createFnInvoker 兩個主要的函數(shù)來完成更新監(jiān)聽器的實(shí)現(xiàn),代碼與 updateListeners 函數(shù)位于同一文件中。
- normalizeEvent:主要是用于返回一個定制化的事件對象,這個函數(shù)接受4個必選參數(shù)和2兩個可選參數(shù),分別是事件名稱name屬性、是否一次性執(zhí)行的once屬性、是否捕獲事件的capture屬性、是否使用被動模式passive屬性、事件處理器handler方法、事件處理器參數(shù)params數(shù)組。屬性的含義都比較好理解,特別注意一下 once、capture、passive 屬性,這三個屬性是用來修飾事件的,分別對應(yīng)了 ~、!、& 修飾符,貼上一個官方文檔中的使用示例,引用自事件 & 按鍵修飾符。啟動被動模式的用途是使事件處理器無法阻止默認(rèn)事件,比如 <a> 標(biāo)簽自帶的鏈接跳轉(zhuǎn)事件,如果設(shè)置passive為true,則事件處理器即便是設(shè)置了阻止默認(rèn)事件也是沒辦法阻止跳轉(zhuǎn)的。
- createFnInvoker: 接受一個fns參數(shù),可以傳入一個事件處理器函數(shù),也可以傳入一個包含多個處理器的數(shù)組。在該函數(shù)內(nèi)部定義了一個 invoker 函數(shù)并且最終返回它,函數(shù)有一個fns屬性是用來存放所傳入的處理器的,調(diào)用這個函數(shù)后,會按fns的類型來分別執(zhí)行處理器數(shù)組的調(diào)用或單個處理器的調(diào)用。這個實(shí)現(xiàn)即是真正執(zhí)行事件處理器調(diào)用的過程。
事件相關(guān)的原型方法
在事件的初始化過程里有用到幾個以 & 開頭的類原型方法,它們是在mixin函數(shù)里掛載到核心類上的。初始化的時候定義的方法都是在這些方法的基礎(chǔ)上再進(jìn)行了一次封裝,其綁定事件、觸發(fā)事件和移除事件的具體實(shí)現(xiàn)都在這些方法中,當(dāng)然不會放過對這些細(xì)節(jié)的探索。
// 導(dǎo)出eventsMixin函數(shù),接收形參Vue, // 使用Flow進(jìn)行靜態(tài)類型檢查指定為Component類 export function eventsMixin (Vue: Class<Component>) {// 定義hook正則檢驗(yàn)const hookRE = /^hook:/// 給Vue原型對象掛載$on方法// 參數(shù)event可為字符串或數(shù)組類型,fn是事件監(jiān)聽函數(shù)// 方法返回實(shí)例對象本身Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {// 定義實(shí)例變量const vm: Component = this// 如果傳入的event參數(shù)是數(shù)組,遍歷event數(shù)組,為所有事件注冊fn監(jiān)聽函數(shù)if (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {this.$on(event[i], fn)}} else {// event參數(shù)為字符串時,檢查event事件監(jiān)聽函數(shù)數(shù)組是否存在// 已存在事件監(jiān)聽數(shù)組則直接添加新監(jiān)聽函數(shù)// 否則建立空的event事件監(jiān)聽函數(shù)數(shù)組,再添加新監(jiān)聽函數(shù)(vm._events[event] || (vm._events[event] = [])).push(fn)// 此處做了性能優(yōu)化,使用正則檢驗(yàn)hook:是否存在的布爾值// 而不是hash值查找設(shè)置實(shí)例對象的_hasHookEvent值// 此次優(yōu)化是很久之前版本的修改,暫時不太清楚以前hash值查找是什么邏輯,留待以后查證// optimize hook:event cost by using a boolean flag marked at registration// instead of a hash lookupif (hookRE.test(event)) {vm._hasHookEvent = true}}// 返回實(shí)例本身return vm}// 為Vue原型對象掛載$once方法// 參數(shù)event只接受字符串,fn是監(jiān)聽函數(shù)Vue.prototype.$once = function (event: string, fn: Function): Component {// 定義實(shí)例變量const vm: Component = this// 創(chuàng)建on函數(shù)function on () {// 函數(shù)執(zhí)行后先清除event事件綁定的on監(jiān)聽函數(shù),即函數(shù)本身// 這樣以后就不會再繼續(xù)監(jiān)聽event事件vm.$off(event, on)// 在實(shí)例上運(yùn)行fn監(jiān)聽函數(shù)fn.apply(vm, arguments)}// 為on函數(shù)設(shè)置fn屬性,保證在on函數(shù)內(nèi)能夠正確找到fn函數(shù)on.fn = fn// 為event事件注冊on函數(shù)vm.$on(event, on)// 返回實(shí)例本身return vm}// 為Vue原型對象掛載$off方法// event參數(shù)可為字符串或數(shù)組類型// fn是監(jiān)聽函數(shù),為可選參數(shù)Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {// 定義實(shí)例變量const vm: Component = this// 如果沒有傳入?yún)?shù),則清除實(shí)例對象的所有事件// 將實(shí)例對象的_events私有屬性設(shè)置為null,并返回實(shí)例// allif (!arguments.length) {vm._events = Object.create(null)return vm}// 如果event參數(shù)傳入數(shù)組,清除所有event事件的fn監(jiān)聽函數(shù)返回實(shí)例// 這里是$off方法遞歸執(zhí)行,最終會以單一事件為基礎(chǔ)來實(shí)現(xiàn)監(jiān)聽的清除// array of eventsif (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {this.$off(event[i], fn)}return vm}// 如果指定單一事件,將事件的監(jiān)聽函數(shù)數(shù)組賦值給cbs變量// specific eventconst cbs = vm._events[event]// 如果沒有注冊此事件監(jiān)聽則返回實(shí)例if (!cbs) {return vm}// 如果沒有指定監(jiān)聽函數(shù),則清除所有該事件的監(jiān)聽函數(shù),返回實(shí)例if (!fn) {vm._events[event] = nullreturn vm}// 如果指定監(jiān)聽函數(shù),則遍歷事件監(jiān)聽函數(shù)數(shù)組,移除指定監(jiān)聽函數(shù)返回實(shí)例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}// 為Vue原型對象掛載$emit方法,只接受單一eventVue.prototype.$emit = function (event: string): Component {// 定義實(shí)例變量const vm: Component = this// 在非生產(chǎn)環(huán)境下,傳入的事件字符串如果是駝峰值且有相應(yīng)的小寫監(jiān)聽事件// 則提示事件已注冊,且無法使用駝峰式注冊事件if (process.env.NODE_ENV !== 'production') {const lowerCaseEvent = event.toLowerCase()if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {tip(`Event "${lowerCaseEvent}" is emitted in component ` +`${formatComponentName(vm)} but the handler is registered for "${event}". ` +`Note that HTML attributes are case-insensitive and you cannot use ` +`v-on to listen to camelCase events when using in-DOM templates. ` +`You should probably use "${hyphenate(event)}" instead of "${event}".`)}}// 將事件監(jiān)聽函數(shù)數(shù)組賦值 給cbslet cbs = vm._events[event]// 如果監(jiān)聽函數(shù)數(shù)組存在if (cbs) {// 重置cbs變量,為何要使用toArray方法轉(zhuǎn)換一次數(shù)組不太明白?cbs = cbs.length > 1 ? toArray(cbs) : cbs// 將event之后傳入的所有參數(shù)定義為args數(shù)組const args = toArray(arguments, 1)// 遍歷所有監(jiān)聽函數(shù),為實(shí)例執(zhí)行每一個監(jiān)聽函數(shù),并傳入args參數(shù)數(shù)組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} }eventsMixin的內(nèi)容非常直觀,分別為實(shí)例原型對象掛載了$on、$once、$off、$emit四個方法。這是實(shí)例事件監(jiān)聽函數(shù)的注冊、一次性注冊、移除和觸發(fā)的內(nèi)部實(shí)現(xiàn)。在使用的過程中會對這些實(shí)現(xiàn)有一個更清晰的理解。
終于對Vue的事件系統(tǒng)的實(shí)現(xiàn)有了一個大致了解,沒有什么特別高深的處理,但完整的事件系統(tǒng)的實(shí)現(xiàn)有很多細(xì)致的功能這里其實(shí)并沒有特別詳細(xì)地探討,比如事件修飾符,可以參考官方文檔里的解說會有一個更清晰的了解。事件系統(tǒng)的重要作用首先是為實(shí)例制定了一套處理事件的方案和標(biāo)準(zhǔn),其次是在實(shí)例數(shù)據(jù)更新的過程中保持對事件監(jiān)聽器的更新,這兩個部分的處理是最需要細(xì)致去琢磨的。
原文地址:https://segmentfault.com/a/1190000016757343
轉(zhuǎn)載于:https://www.cnblogs.com/lalalagq/p/9901082.html
總結(jié)
以上是生活随笔為你收集整理的Vue源码探究-事件系统的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: cxf客户端访问方式
- 下一篇: 日常开发技巧:使用notify-send