Vue之event(事件)

目標:1、了解event的實現(xiàn)原理狼渊。2箱熬、了解Dom事件和自定義事件的區(qū)別

????????平時開發(fā)過程中,組件間通訊城须,原生交互都離不開事件蚤认,對于一個組件元素,我們可以綁定原生JS事件(@click)糕伐,也可以綁定自定義事件(@emit)砰琢,非常靈活和方便。我們接下來會從源碼角度看它實現(xiàn)原理赤炒。

例子為一對父子組件,child組件定義了原生事件@click和自定義事件@select氯析,child組件的button標簽定義了原生事件@click。

首先是編譯

編譯-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卤恳,調用addHandler1寒矿、根據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)橄抹。

上面這個階段,例子中我們得到的結果是:

child組件的click原生事件和select自定義事件解析出來的結果
子組件的?button?節(jié)點的原生click事件解析出來的結果

編譯-codegen

? ? ? ? genData函數中根據AST元素節(jié)點上的events和nativeEvents生成data數據祈坠,它的定義在src/compiler/codegen/index.js中:

genData

????????對于這兩個屬性害碾,會調用?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中,一步步把代碼拼接起來咆贬。

這一階段在我們的例子中得到的結果是:

child組件生成的data串
child組件中button標簽生成的data串

? ??????整個編譯過程實際上就是對整個模版做解析败徊,解析過程中生成的代碼,完整描述了事件的定義素征,為最終去運行做準備集嵌。

? ??????那么到這里,編譯部分完了御毅,接下來我們來看一下運行時部分是如何實現(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)

原生 DOM 事件添加回調和移除回調函數的實現(xiàn)

????????實際上就是調用原生?addEventListener?和?removeEventListener,并根據參數傳遞一些配置炒俱,注意這里的?hanlder?會用?withMacroTask(hanlder)?包裹一下(src/core/util/next-tick.js)

withMacroTask

? ??????實際上就是強制在 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時會作為參數傳入诵肛。

createComponent骡显,得到組件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)

initEvents

拿到?listeners?后米酬,執(zhí)行?updateComponentListeners(vm, listeners)?方法:

updateComponentListeners

????????updateListeners?我們之前介紹過,所以對于自定義事件和原生 DOM 事件處理的差異就在事件添加和刪除的實現(xiàn)上趋箩,來看一下自定義事件?add?和?remove?的實現(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?方法瘩将,這樣就相當于完成了一次父子組件的通訊

$on
$once
$off
$emit
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末凹耙,一起剝皮案震驚了整個濱河市姿现,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌肖抱,老刑警劉巖备典,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異意述,居然都是意外死亡提佣,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進店門荤崇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拌屏,“玉大人,你說我怎么就攤上這事术荤∫形梗” “怎么了?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵喜每,是天一觀的道長务唐。 經常有香客問我雳攘,道長,這世上最難降的妖魔是什么枫笛? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任吨灭,我火速辦了婚禮,結果婚禮上刑巧,老公的妹妹穿的比我還像新娘喧兄。我一直安慰自己,他們只是感情好啊楚,可當我...
    茶點故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布吠冤。 她就那樣靜靜地躺著,像睡著了一般恭理。 火紅的嫁衣襯著肌膚如雪拯辙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天颜价,我揣著相機與錄音涯保,去河邊找鬼。 笑死周伦,一個胖子當著我的面吹牛夕春,可吹牛的內容都是我干的。 我是一名探鬼主播专挪,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼及志,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了寨腔?” 一聲冷哼從身側響起速侈,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎迫卢,沒想到半個月后锌畸,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡靖避,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了比默。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片幻捏。...
    茶點故事閱讀 39,769評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖命咐,靈堂內的尸體忽然破棺而出篡九,到底是詐尸還是另有隱情,我是刑警寧澤醋奠,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布榛臼,位于F島的核電站伊佃,受9級特大地震影響,放射性物質發(fā)生泄漏沛善。R本人自食惡果不足惜航揉,卻給世界環(huán)境...
    茶點故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望金刁。 院中可真熱鬧帅涂,春花似錦、人聲如沸尤蛮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽产捞。三九已至醇锚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間坯临,已是汗流浹背焊唬。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留尿扯,地道東北人求晶。 一個月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像衷笋,于是被迫代替她去往敵國和親芳杏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,678評論 2 354

推薦閱讀更多精彩內容

  • 這篇筆記主要包含 Vue 2 不同于 Vue 1 或者特有的內容辟宗,還有我對于 Vue 1.0 印象不深的內容爵赵。關于...
    云之外閱讀 5,050評論 0 29
  • 1.安裝 可以簡單地在頁面引入Vue.js作為獨立版本,Vue即被注冊為全局變量泊脐,可以在頁面使用了空幻。 如果希望搭建...
    Awey閱讀 11,016評論 4 129
  • 觀光完橫濱中華街,順便去參觀了下中華街旁邊免費的山下公園容客,風景真的很贊呢秕铛! 放眼看過去藍色的天以及藍色的海,終于明...
    悠然小蝦閱讀 750評論 1 3
  • 剛剛忙完缩挑,好累但两!感恩幸苦的一天,這段時間的事情終于有了結果供置!感恩裝門的師傅谨湘,現(xiàn)在還在工作!感恩門口市政的帥哥,幫忙...
    今天的心情好閱讀 174評論 0 2
  • 中午從學校出來的時候,又看到一號門擅耽、四號門和校大門站著很多等待的家長活孩,突然很生感慨。 自己...
    稚與稚超閱讀 578評論 0 8