咱們在開發組件時有時須要和父組件溝通,此時能夠用自定義事件來實現html
組件的事件分爲自定義事件和原生事件,前者用於子組件給父組件發送消息的,後者用於在組件的根元素上直接監聽一個原生事件,區別就是綁定原生事件須要加一個.native修飾符。vue
子組件裏經過過this.$emit()將自定義事件以及須要發出的數據經過如下代碼發送出去,第一個參數是自定義事件的名稱,後面的參數是依次想要發送出去的數據,例如:node
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script> <title>Document</title> </head> <body> <div id="d"><com @myclick="MyClick" @mouseenter.native="Enter"></com></div> <script>
Vue.config.productionTip=false; Vue.config.devtools=false; Vue.component('com',{ template:'<button @click="childclick">Click</button>', methods:{ childclick:function(){this.$emit('myclick','gege','123')} //子組件的事件,經過this.$emit觸發父組件的myclick事件 } }) debugger var app = new Vue({ el:'#d', methods:{ MyClick:function(){console.log('parent MyClick method:',arguments)}, //響應子組件的事件函數 Enter:function(){console.log("MouseEnter")} //子組件的原生DOM事件 } }) </script> </body> </html>
子組件就是一個按鈕,渲染以下:git
咱們給整個組件綁定了兩個事件,一個DOM原生的mouseenter事件和自定義的MyClick組件事件,當鼠標移動到按鈕上時,打印出:MouseEnter,以下:github
當點擊按鈕時輸出子組件傳遞過來的信息,以下:npm
自定義事件實際上是存儲在組件實例的_events屬性上的,咱們在控制檯輸入console.log(app.$children[0]["_events"])就能夠打印出來,以下:數組
myclick就是咱們自定義的事件對象weex
源碼分析app
父組件在解析模板時會執行processAttrs()函數,會在AST對象上增長一個events和nativeevents屬性,以下async
function processAttrs (el) { //第9526行 對屬性進行解析 var list = el.attrsList; var i, l, name, rawName, value, modifiers, isProp; for (i = 0, l = list.length; i < l; i++) { //遍歷每一個屬性名 name = rawName = list[i].name; value = list[i].value; if (dirRE.test(name)) { // mark element as dynamic el.hasBindings = true; // modifiers modifiers = parseModifiers(name); if (modifiers) { name = name.replace(modifierRE, ''); } if (bindRE.test(name)) { // v-bind /*略*/ } else if (onRE.test(name)) { // v-on //若是name以@或v-on:開頭,表示綁定了事件 name = name.replace(onRE, ''); addHandler(el, name, value, modifiers, false, warn$2); 調用addHandler()函數將事件相關信息保存到el.events或nativeEvents裏面 } else { // normal directives /*略*/ } } else { /*略*/ } } } function addHandler ( //第6573行 給el這個AST對象增長event或nativeEvents el, name, value, modifiers, important, warn ) { modifiers = modifiers || emptyObject; /*略*/ var events; if (modifiers.native) { //若是存在native修飾符,則保存到el.nativeEvents裏面 delete modifiers.native; events = el.nativeEvents || (el.nativeEvents = {}); } else { //不然保存到el.events裏面 events = el.events || (el.events = {}); } /*略*/ var handlers = events[name]; //嘗試獲取已經存在的該事件對象 /* istanbul ignore if */ if (Array.isArray(handlers)) { //若是是數組,表示已經插入了兩次了,則再把newHandler添加進去 important ? handlers.unshift(newHandler) : handlers.push(newHandler); } else if (handlers) { //若是handlers存在且不是數組,則表示只插入過一次,則把events[name]變爲數組 events[name] = important ? [newHandler, handlers] : [handlers, newHandler]; } else { //不然表示是第一次新增該事件,則值爲對應的newHandler events[name] = newHandler; } el.plain = false; }
例子裏執行完後AST對象裏對應的信息以下:(AST能夠這樣認爲:Vue把模板經過正則解析後以對象的形式表現出來)
接下來在generate生成rendre函數的時候會調用genHandlers函數根據不一樣修飾符等生成對應的屬性(做爲_c函數的第二個data參數一部分),以下:
function genData$2 (el, state) { //第10274行 拼湊data值 var data = '{'; /*略*/ // event handlers if (el.events) { //若是el有綁定事件(沒有native修飾符時) data += (genHandlers(el.events, false, state.warn)) + ","; } if (el.nativeEvents) { //若是el有綁定事件(native修飾符時) data += (genHandlers(el.nativeEvents, true, state.warn)) + ","; } /*略*/ return data }
genHandlers會根據參數2的值將事件存儲在nativeOn或on屬性裏,以下:
function genHandlers ( //第9992行 拼湊事件的data函數 events, isNative, warn ) { var res = isNative ? 'nativeOn:{' : 'on:{'; //若是參數isNative爲true則設置res爲:nativeOn:{,不然爲:on:{ for (var name in events) { res += "\"" + name + "\":" + (genHandler(name, events[name])) + ","; } return res.slice(0, -1) + '}' }
例子裏執行到這裏時等於:
_render將rendre函數轉換爲VNode時候會調用createComponent()函數建立組件佔位符VNode,此時會有
function createComponent ( //第4182行 Ctor, data, context, children, tag ) { /*略*/ var listeners = data.on; //對自定義事件(沒有native修飾符)的處理,則保存到listeners裏面,一下子存到佔位符VNode的配置信息裏 // replace with listeners with .native modifier // so it gets processed during parent component patch. data.on = data.nativeOn; //對原生DOM事件,則保存到data.on裏面,這樣等該DOM渲染成功後會執行event模塊的初始化,就會綁定對應的函數了 /*略*/
var name = Ctor.options.name || tag;
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }, //自定義事件做爲listeners屬性存儲在組件Vnode的配置參數裏了
asyncFactory
);
// Weex specific: invoke recycle-list optimized @render function for
// extracting cell-slot template.
// https://github.com/Hanks10100/weex-native-directive/tree/master/component
/* istanbul ignore if */
return vnode
}
原生事件存儲在on屬性上,後面介紹v-on指令時再詳細介紹,對於自定義事件存儲在組件Vnode配置參數的listeners屬性裏了。
當組件實例化的時候執行_init()時首先執行initInternalComponent()函數,該函數會獲取listeners屬性,以下:
function initInternalComponent (vm, options) { //第4632行 初始化子組件 var opts = vm.$options = Object.create(vm.constructor.options); // doing this because it's faster than dynamic enumeration. var parentVnode = options._parentVnode; //該組件的佔位符VNode opts.parent = options.parent; opts._parentVnode = parentVnode; opts._parentElm = options._parentElm; opts._refElm = options._refElm; var vnodeComponentOptions = parentVnode.componentOptions; //佔位符VNode初始化傳入的配置信息 opts.propsData = vnodeComponentOptions.propsData; opts._parentListeners = vnodeComponentOptions.listeners; //將組件的自定義事件保存到_parentListeners屬性裏面 opts._renderChildren = vnodeComponentOptions.children; opts._componentTag = vnodeComponentOptions.tag; if (options.render) { opts.render = options.render; opts.staticRenderFns = options.staticRenderFns; } }
回到_init函數,接着執行initEvents()函數,該函數會初始化組件的自定義事件,以下:
function initEvents (vm) { //第2412行 初始化自定義事件 vm._events = Object.create(null); vm._hasHookEvent = false; // init parent attached events var listeners = vm.$options._parentListeners; //獲取佔位符VNode上的自定義事件 if (listeners) { updateComponentListeners(vm, listeners); //執行updateComponentListeners()新增事件 } }
updateComponentListeners函數用於新增/更新組件的事件,以下:
function add (event, fn, once) { //第2424行 if (once) { target.$once(event, fn); //自定義事件最終調用$once綁定事件的 } else { target.$on(event, fn); } } function remove$1 (event, fn) { target.$off(event, fn); } function updateComponentListeners ( //第2436行 vm, listeners, oldListeners ) { target = vm; updateListeners(listeners, oldListeners || {}, add, remove$1, vm); //調用updateListeners()更新DOM事件,傳入add函數 target = undefined; }
updateListeners內部會調用add()函數,這裏用了一個優化措施,實際上咱們綁定的是Vue內部的createFnInvoker函數,該函數會遍歷傳給updateListeners的函數,依次執行。
add()最終執行的是$on()函數,該函數定義以下:
Vue.prototype.$on = function (event, fn) { //第2448行 自定義事件的新增 event:函數名 fn:對應的函數 var this$1 = this; var vm = this; if (Array.isArray(event)) { //若是event是一個數組 for (var i = 0, l = event.length; i < l; i++) { //則遍歷該數組 this$1.$on(event[i], fn); //依次調用this$1.$on } } else { //若是不是數組 (vm._events[event] || (vm._events[event] = [])).push(fn); //則將事件保存到ev._event上 // optimize hook:event cost by using a boolean flag marked at registration // instead of a hash lookup if (hookRE.test(event)) { //若是事件名以hook:開頭 vm._hasHookEvent = true; //則設置vm._hasHookEvent爲true,這樣生命週期函數執行時也會執行這些函數 } } return vm };
從這裏能夠看到自定義事件實際上是保存到組件實例的_events屬性上的
當子組件經過$emit觸發當前實例上的事件時,會從_events上拿到對應的自定義事件並執行,以下:
Vue.prototype.$emit = function (event) { //第2518行 子組件內部經過$emit()函數執行到這裏 var vm = this; { var lowerCaseEvent = event.toLowerCase(); //先將事件名轉換爲小寫 if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) { //若是lowerCaseEvent不等於event則報錯(即事件名只能是小寫) 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 + "\"." ); } } var cbs = vm._events[event]; //從_events屬性裏獲取對應的函數數組 if (cbs) { cbs = cbs.length > 1 ? toArray(cbs) : cbs; //獲取全部函數 var args = toArray(arguments, 1); //去掉第一個參數,後面的都做爲事件的參數 for (var i = 0, l = cbs.length; i < l; i++) { //遍歷cbs try { cbs[i].apply(vm, args); //依次執行每一個函數,值爲子組件的vm實例 } catch (e) { handleError(e, vm, ("event handler for \"" + event + "\"")); } } } return vm };
大體流程跑完了,有點繁瑣,多調試一下就行了。