目標:1、了解event的實現(xiàn)原理狼渊。2箱熬、了解Dom事件和自定義事件的區(qū)別。
????????平時開發(fā)過程中,組件間通訊城须,原生交互都離不開事件蚤认,對于一個組件元素,我們可以綁定原生JS事件(@click)糕伐,也可以綁定自定義事件(@emit)砰琢,非常靈活和方便。我們接下來會從源碼角度看它實現(xiàn)原理赤炒。
首先是編譯
編譯-parser:
???????標簽屬性attrsList做處理莺褒,判斷如果是指令,先解析出修飾符雪情,判斷如果是事件的指令遵岩,執(zhí)行addHandler(el, name, value, modifiers, false, warn)。?
????????parser建立AST節(jié)點樹后巡通,在節(jié)點處理過程中會執(zhí)行processAttrs方法尘执,它會對節(jié)點上的標簽屬性attrsList做處理。processAttrs宴凉,遍歷節(jié)點上的attrsList誊锭,拿到name屬性,先判斷name是否匹配模版指令的正則表達式(比如v-,:)弥锄,如果匹配到丧靡,給節(jié)點的hasBindings屬性設為true(標志是動態(tài)節(jié)點)。
? ??????解析修飾符籽暇,接下來對name做parseModifiers(name)操作得到modifier温治。如果有modifer,把name中modifer相關字段去掉戒悠。解析事件指令熬荆,對name中'v-on'字段檢測,滿足的話把這個字段也去掉绸狐,name就變成click卤恳,調用addHandler。1寒矿、根據modifier修飾符對事件名作修改突琳。如果name為click且modifer中有 .right ,把name變成'contextmenu'劫窒;如果name為click且modifer中有 .middle 本今,把name變成'mouseup'。2、根據modifer.native判斷原生事件還是普通事件冠息。接下來構造event對象挪凑,如果有 .native ,構造nativeEvents對象逛艰,否則構造events對象躏碳。3、按照name對事件做歸類散怖,并把回調函數的字符串保留到對應的事件中菇绵。接著構造newHandler對象{value: 事件名,如果modifer存在镇眷,把其中鍵值對寫入}咬最。接下來,把events[name]賦值給handlers欠动,把newHandler賦值給events[name]永乌。最終生成的節(jié)點有event或者nativeEvent對象(以事件名為key,值可能為newHandler,或者多個newHandler構成的數組)具伍,
? ??????parseModifiers(src/compiler/helpers.js)翅雏,對屬性名上的修飾符做處理(如.natice,.prevent),得到modifier對象,包含了一個個 修飾符名:true 鍵值對人芽。
? ??????addHandler望几,給AST節(jié)點添加event屬性,并根據modifer解析出來的標記給name上打標記(比如.once存在萤厅,name='~'+name)橄抹。
上面這個階段,例子中我們得到的結果是:
編譯-codegen:
? ? ? ? genData函數中根據AST元素節(jié)點上的events和nativeEvents生成data數據祈坠,它的定義在src/compiler/codegen/index.js中:
????????對于這兩個屬性害碾,會調用?genHandlers?函數(src/compiler/codegen/events.js)。目標是生成和事件相關的代碼赦拘。
? ??????genHandlers?根據isNative判斷res的值為'nativeOn:{' 或者 'on:{'慌随。遍歷events對象,拼接genHandler會生成handler的代碼躺同,拼接出一個JSON代碼對象阁猜。,返回值用","分割蹋艺。最終返回形式是[xxx,xxx]剃袍,res拼接得到{name:[function() {...},function() {...}],name1:[function() {...},function() {...}]...}
????????genHandlers方法遍歷事件對象?events,遍歷events對每個事件調用?genHandler(name, events[name])?方法捎谨,拼接結果(res+=`${name} : ${genHandler(name, events[name])}`)
? ? ? ??genHandler方法民效,如果events[name]是數組憔维,map遍歷它遞歸調用genHandler方法。1畏邢、正則去匹配event[name].value业扒,判斷它是一個函數的調用路徑還是一個函數表達式。2舒萎、如果handler的modifers修飾符不存在程储,return `function($event) {${handler.value}}`;存在的話臂寝,遍歷modifers對象key章鲤,根據不同的key做不同的邏輯操作,對于命中的key通過modiferCode(modiferCode是對不同修飾符key生成不同的代碼)生成的代碼臨時儲存在genModifierCode中,一步步把代碼拼接起來咆贬。
這一階段在我們的例子中得到的結果是:
? ??????整個編譯過程實際上就是對整個模版做解析败徊,解析過程中生成的代碼,完整描述了事件的定義素征,為最終去運行做準備集嵌。
? ??????那么到這里,編譯部分完了御毅,接下來我們來看一下運行時部分是如何實現(xiàn)的。其實 Vue 的事件有 2 種怜珍,一種是原生 DOM 事件端蛆,一種是用戶自定義事件,我們分別來看酥泛。
運行部分如何實現(xiàn)今豆?
????????vue的事件有兩種,原生DOM事件和用戶自定義事件柔袁,分別來看呆躲。
DOM原生事件:
????????還記得我們之前在?patch?的時候執(zhí)行各種?module?的鉤子函數嗎,當時這部分是略過的捶索,我們之前只分析了 DOM 是如何渲染的插掂,而 DOM 元素相關的屬性、樣式腥例、事件等都是通過這些?module?的鉤子函數完成設置的辅甥。
? ??????所有和 web 相關的?module?都定義在?src/platforms/web/runtime/modules?目錄下,我們這次只關注目錄下的?events.js?即可燎竖。
? ??????creatPatchFunction方法璃弄,創(chuàng)建patch方法。它先拿到modules(各個某塊,由baseModules和platformModules合并而來)和nodeOps(和平臺相關操作方法)构回。接下來遍歷hooks(一個數組['creat','active','update'...])夏块,對于每個hook疏咐,hook做為key,[]空數組做為值儲存在cbs對象中脐供。接下來遍歷modules浑塞,查找modules中有沒有定義這個hook,有的話患民,往前面的空數組[]內push, module對應的hook方法缩举。(比如事件對應的cbs為{'creat': creatFun, 'update': updateFun })。什么時候執(zhí)行cbs中hooks方法呢匹颤?
? ?????在createElm方法和creatComponent方法和更新節(jié)點patchVnode時會調用invokeCreateHooks方法仅孩,它會去遍歷cbs中鉤子函數進行執(zhí)行。我們要看的是create鉤子函數其中event相關方法,即updateDOMListener(oldVnode,newVnode)方法印蓖。
? ??????updateDOMListener辽慕,先去判斷oldVnode.data和newVnode.data是否都有on屬性(on屬性其實就是我們前面編譯階段執(zhí)行genHandlers時得到的,其值為代碼JSON對象)赦肃。都沒有的話直接return溅蛉,有的話去取出存在on,oldOn兩個變量中,再去取target=vnode.elm真實dom節(jié)點(因為我們需要在dom上添加事件)他宛。接著執(zhí)行normalizeEvents(on)(和v-model相關,先不管)船侧。再執(zhí)行updateListeners(on, oldOn, add, remove, vnode.context)方法。
????????updateListeners厅各,它是被單獨拿出來的文件镜撩,因為原生事件和自定義事件創(chuàng)建都會用到它葛作。首次創(chuàng)建事件,之后更新事件淘这。遍歷on對象,拿到當前事件值cur和舊事件值old腾供,對事件名執(zhí)行normalizeEvent方法(前面對不同事件修飾符在name上做了標記,如‘~’,現(xiàn)在需要把它們作為Boolean返回并從name去掉)憔古。
? ??????如果old未定義遮怜,cur=on[name]=createFnInvoker(cur),即把on[name]指向createFnInvoker返回的值。接下來執(zhí)行add(event.name,cur,event.once,event.capture,event.passive,event.params)方法鸿市。它就是通過addEventListener在真實dom上綁定事件了锯梁。
? ??????????????createFnInvoker,最終會返回一個invoker函數(最終添加事件的函數)灸芳。invoker函數涝桅,先拿到傳進來的on[name]賦值給fns。如果它是一個數組烙样,遍歷它依次去執(zhí)行其內定義的函數冯遂,否則直接執(zhí)行fns,通過它創(chuàng)建了一個回調函數谒获。
? ??????如果old定義了且old和cur不相同蛤肌,直接把old.fns指向cur壁却,同時把on[name]指向old,我們只要把invoker.fns改變即可,不需要重新創(chuàng)建事件裸准。(事件創(chuàng)建不用再重復執(zhí)行)
????????了解了?updateListeners?的實現(xiàn)后展东,我們來看一下在原生 DOM 事件中真正添加回調和移除回調函數的實現(xiàn)( src/platforms/web/runtime/modules/event.js)
????????實際上就是調用原生?addEventListener?和?removeEventListener,并根據參數傳遞一些配置炒俱,注意這里的?hanlder?會用?withMacroTask(hanlder)?包裹一下(src/core/util/next-tick.js)
? ??????實際上就是強制在 DOM 事件的回調函數執(zhí)行期間如果修改了數據盐肃,那么這些數據更改推入的隊列會被當做?macroTask?在?nextTick?后執(zhí)行。
自定義事件:
? ??????除了原生 DOM 事件权悟,Vue 還支持了自定義事件砸王,并且自定義事件只能作用在組件上,如果在組件上使用原生事件峦阁,需要加?.native?修飾符谦铃,普通元素上使用?.native?修飾符無效,接下來我們就來分析它的實現(xiàn)榔昔。
????????自定義事件只能作用在組件中驹闰,我們回顧一下組件vnode創(chuàng)建過程(子組件在父組件中占位符vnode的創(chuàng)建過程)createComponent。在創(chuàng)建組件vnode之前撒会,會對事件做處理嘹朗,會把data.on(自定義事件)賦值給listeners,listeners在組件實例化成VNode時會作為參數傳入诵肛。
????????我們只關注事件相關的邏輯,可以看到曾掂,它把?data.on?賦值給了?listeners,把?data.nativeOn賦值給了?data.on壁顶,這樣所有的原生 DOM 事件處理跟我們剛才介紹的一樣珠洗,它是在當前組件環(huán)境中處理的(即子組件的原生dom事件在子組件占位符節(jié)點所在的父組件實例中處理)。而對于自定義事件若专,我們把?listeners?作為?vnode?的?componentOptions?傳入许蓖,它是在子組件初始化階段中處理的,所以它的處理環(huán)境是子組件(即子組件的自定義事件在子組件本身實例環(huán)境中處理)调衰。
????????然后在子組件的初始化合并options的時候膊爪,會執(zhí)行?initInternalComponent?方法(src/core/instance/init.js)
????????這里拿到了父組件傳入的?listeners,然后在執(zhí)行?initEvents?的過程中嚎莉,會處理這個?listeners(src/core/instance/events.js)
拿到?listeners?后米酬,執(zhí)行?updateComponentListeners(vm, listeners)?方法:
????????updateListeners?我們之前介紹過,所以對于自定義事件和原生 DOM 事件處理的差異就在事件添加和刪除的實現(xiàn)上趋箩,來看一下自定義事件?add?和?remove?的實現(xiàn):
實際上是利用 Vue 定義的事件中心赃额,簡單分析一下它的實現(xiàn):
????????非常經典的事件中心的實現(xiàn)加派,把所有的事件用?vm._events?存儲起來,當執(zhí)行?vm.$on(event,fn)?的時候跳芳,按事件的名稱?event?把回調函數?fn?存儲起來?vm._events[event].push(fn)芍锦。當執(zhí)行?vm.$emit(event)?的時候,根據事件名?event?找到所有的回調函數?let cbs = vm._events[event]飞盆,然后遍歷執(zhí)行所有的回調函數娄琉。當執(zhí)行?vm.$off(event,fn)?的時候會移除指定事件名?event?和指定的?fn 。當執(zhí)行?vm.$once(event,fn)?的時候吓歇,內部就是執(zhí)行?vm.$on孽水,并且當回調函數執(zhí)行一次后再通過?vm.$off?移除事件的回調,這樣就確保了回調函數只執(zhí)行一次照瘾。
????????所以對于用戶自定義的事件添加和刪除就是利用了這幾個事件中心的 API匈棘。需要注意的事一點,vm.$emit?是給當前的?vm?上派發(fā)的實例析命,之所以我們常用它做父子組件通訊主卫,是因為它的回調函數的定義是在父組件中,對于我們這個例子而言鹃愤,當子組件的?button?被點擊了簇搅,它通過?this.$emit('select')?派發(fā)事件,那么子組件的實例就監(jiān)聽到了這個?select?事件软吐,并執(zhí)行它的回調函數——定義在父組件中的?selectHandler?方法瘩将,這樣就相當于完成了一次父子組件的通訊。