這個系列講到這裏,Vue基本核心的東西已經分析完,可是Vue之因此強大,離不開它提供給用戶的一些實用功能,開發者能夠更偏向於業務邏輯而非基本功能的實現。例如,在平常開發中,咱們將
@click=***
用得飛起,可是咱們是否思考,Vue如何在後面爲咱們的模板作事件相關的處理,而且咱們常常利用組件的自定義事件去實現父子間的通訊,那這個事件和和原生dom事件又有不一樣的地方嗎,可以實現通訊的原理又是什麼,帶着疑惑,咱們深刻源碼展開分析。vue
Vue
在掛載實例前,有至關多的工做是進行模板的編譯,將template
模板進行編譯,解析成AST
樹,再轉換成render
函數,而有了render
函數後纔會進入實例掛載過程。對於事件而言,咱們常用v-on
或者@
在模板上綁定事件。所以對事件的第一步處理,就是在編譯階段對事件指令作收集處理。node
從一個簡單的用法分析編譯階段收集的信息:算法
<div id="app">
<div v-on:click.stop="doThis">點擊</div>
<span>{{count}}</span>
</div>
<script>
var vm = new Vue({
el: '#app',
data() {
return {
count: 1
}
},
methods: {
doThis() {
++this.count
}
}
})
</script>
複製代碼
咱們以前在將模板編譯的時候大體說過編譯的流程,模板編譯的入口是在var ast = parse(template.trim(), options);
中,parse
經過拆分模板字符串,將其解析爲一個AST
樹,其中對於屬性的處理,在processAttr
中,因爲分支較多,咱們只分析例子中的流程。api
var dirRE = /^v-|^@|^:/;
function processAttrs (el) {
var list = el.attrsList;
var i, l, name, rawName, value, modifiers, syncGen, isDynamic;
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name; // v-on:click
value = list[i].value; // doThis
if (dirRE.test(name)) { // 匹配v-或者@開頭的指令
el.hasBindings = true;
modifiers = parseModifiers(name.replace(dirRE, ''));// parseModifiers('on:click')
if (modifiers) {
name = name.replace(modifierRE, '');
}
if (bindRE.test(name)) { // v-bind分支
// ...留到v-bind指令時分析
} else if (onRE.test(name)) { // v-on分支
name = name.replace(onRE, ''); // 拿到真正的事件click
isDynamic = dynamicArgRE.test(name);// 動態事件綁定
if (isDynamic) {
name = name.slice(1, -1);
}
addHandler(el, name, value, modifiers, false, warn$2, list[i], isDynamic);
} else { // normal directives
// 其餘指令相關邏輯
} else {}
}
}
複製代碼
processAttrs
的邏輯雖然較多,可是理解起來較爲簡單,var dirRE = /^v-|^@|^:/;
是匹配事件相關的正則,命中匹配的記過會獲得事件指令相關內容,包括事件自己,事件回調以及事件修飾符。最終經過addHandler
方法,爲AST
樹添加事件相關的屬性。而addHandler
還有一個重要功能是對事件修飾符進行特殊處理。數組
// el是當前解析的AST樹
function addHandler (el,name,value,modifiers,important,warn,range,dynamic) {
modifiers = modifiers || emptyObject;
// passive 和 prevent不能同時使用,能夠參照官方文檔說明
if (
warn &&
modifiers.prevent && modifiers.passive
) {
warn(
'passive and prevent can\'t be used together. ' + 'Passive handler can\'t prevent default event.',
range
);
}
// 這部分的邏輯會對特殊的修飾符作字符串拼接的處理,以備後續的使用
if (modifiers.right) {
if (dynamic) {
name = "(" + name + ")==='click'?'contextmenu':(" + name + ")";
} else if (name === 'click') {
name = 'contextmenu';
delete modifiers.right;
}
} else if (modifiers.middle) {
if (dynamic) {
name = "(" + name + ")==='click'?'mouseup':(" + name + ")";
} else if (name === 'click') {
name = 'mouseup';
}
}
if (modifiers.capture) {
delete modifiers.capture;
name = prependModifierMarker('!', name, dynamic);
}
if (modifiers.once) {
delete modifiers.once;
name = prependModifierMarker('~', name, dynamic);
}
/* istanbul ignore if */
if (modifiers.passive) {
delete modifiers.passive;
name = prependModifierMarker('&', name, dynamic);
}
// events 用來記錄綁定的事件
var events;
if (modifiers.native) {
delete modifiers.native;
events = el.nativeEvents || (el.nativeEvents = {});
} else {
events = el.events || (el.events = {});
}
var newHandler = rangeSetItem({ value: value.trim(), dynamic: dynamic }, range);
if (modifiers !== emptyObject) {
newHandler.modifiers = modifiers;
}
var handlers = events[name];
/* istanbul ignore if */
// 綁定的事件能夠多個,回調也能夠多個,最終會合併到數組中
if (Array.isArray(handlers)) {
important ? handlers.unshift(newHandler) : handlers.push(newHandler);
} else if (handlers) {
events[name] = important ? [newHandler, handlers] : [handlers, newHandler];
} else {
events[name] = newHandler;
}
el.plain = false;
}
複製代碼
修飾符的處理會改變最終字符串的拼接結果,咱們看最終轉換的AST
樹:promise
模板編譯的最後一步是根據解析完的AST
樹生成對應平臺的渲染函數,也就是render
函數的生成過程, 對應var code = generate(ast, options);
。bash
function generate (ast,options) {
var state = new CodegenState(options);
var code = ast ? genElement(ast, state) : '_c("div")';
return {
render: ("with(this){return " + code + "}"), // with函數
staticRenderFns: state.staticRenderFns
}
}
複製代碼
其中核心處理在getElement
中,getElement
函數會根據不一樣指令類型處理不一樣的分支,對於普通模板的編譯會進入genData
函數中處理,一樣分析只針對事件相關的處理,從前面解析出的AST
樹明顯看出,AST
樹中多了events
的屬性,genHandlers
函數會爲event
屬性作邏輯處理。app
function genData (el, state) {
var data = '{';
// directives first.
// directives may mutate the el's other properties before they are generated. var dirs = genDirectives(el, state); if (dirs) { data += dirs + ','; } //其餘處理 ··· // event handlers if (el.events) { data += (genHandlers(el.events, false)) + ","; } ··· return data } 複製代碼
genHandlers
的邏輯,會遍歷解析好的AST
樹,拿到event
對象屬性,並根據屬性上的事件對象拼接成字符串。dom
function genHandlers (events,isNative) {
var prefix = isNative ? 'nativeOn:' : 'on:';
var staticHandlers = "";
var dynamicHandlers = "";
// 遍歷ast樹解析好的event對象
for (var name in events) {
//genHandler本質上是將事件對象轉換成可拼接的字符串
var handlerCode = genHandler(events[name]);
if (events[name] && events[name].dynamic) {
dynamicHandlers += name + "," + handlerCode + ",";
} else {
staticHandlers += "\"" + name + "\":" + handlerCode + ",";
}
}
staticHandlers = "{" + (staticHandlers.slice(0, -1)) + "}";
if (dynamicHandlers) {
return prefix + "_d(" + staticHandlers + ",[" + (dynamicHandlers.slice(0, -1)) + "])"
} else {
return prefix + staticHandlers
}
}
// 事件模板書寫匹配
var isMethodPath = simplePathRE.test(handler.value); // doThis
var isFunctionExpression = fnExpRE.test(handler.value); // () => {} or function() {}
var isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, '')); // doThis($event)
function genHandler (handler) {
if (!handler) {
return 'function(){}'
}
// 事件綁定能夠多個,多個在解析ast樹時會以數組的形式存在,若是有多個則會遞歸調用getHandler方法返回數組。
if (Array.isArray(handler)) {
return ("[" + (handler.map(function (handler) { return genHandler(handler); }).join(',')) + "]")
}
// value: doThis 能夠有三種方式
var isMethodPath = simplePathRE.test(handler.value); // doThis
var isFunctionExpression = fnExpRE.test(handler.value); // () => {} or function() {}
var isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, '')); // doThis($event)
// 沒有任何修飾符
if (!handler.modifiers) {
// 符合函數定義規範,則直接返回調用函數名 doThis
if (isMethodPath || isFunctionExpression) {
return handler.value
}
// 不符合則經過function函數封裝返回
return ("function($event){" + (isFunctionInvocation ? ("return " + (handler.value)) : handler.value) + "}") // inline statement
} else {
// 包含修飾符的場景
}
}
複製代碼
模板中事件的寫法有三種,分別對應上訴上個正則匹配的內容。異步
<div @click="doThis"></div>
<div @click="doThis($event)"></div>
<div @click="()=>{}"></div> <div @click="function(){}"></div>
上述對事件對象的轉換,若是事件不帶任何修飾符,而且知足正確的模板寫法,則直接返回調用事件名,若是不知足,則有多是<div @click="console.log(11)"></div>
的寫法,此時會封裝到function($event){}
中。
包含修飾符的場景較多,咱們單獨列出分析。以上文中的例子說明,modifiers: { stop: true }
會拿到stop
對應須要添加的邏輯腳本'$event.stopPropagation();'
,並將它添加到函數字符串中返回。
function genHandler() {
// ···
} else {
var code = '';
var genModifierCode = '';
var keys = [];
// 遍歷modifiers上記錄的修飾符
for (var key in handler.modifiers) {
if (modifierCode[key]) {
// 根據修飾符添加對應js的代碼
genModifierCode += modifierCode[key];
// left/right
if (keyCodes[key]) {
keys.push(key);
}
// 針對exact的處理
} else if (key === 'exact') {
var modifiers = (handler.modifiers);
genModifierCode += genGuard(
['ctrl', 'shift', 'alt', 'meta']
.filter(function (keyModifier) { return !modifiers[keyModifier]; })
.map(function (keyModifier) { return ("$event." + keyModifier + "Key"); })
.join('||')
);
} else {
keys.push(key);
}
}
if (keys.length) {
code += genKeyFilter(keys);
}
// Make sure modifiers like prevent and stop get executed after key filtering
if (genModifierCode) {
code += genModifierCode;
}
// 根據三種不一樣的書寫模板返回不一樣的字符串
var handlerCode = isMethodPath
? ("return " + (handler.value) + "($event)")
: isFunctionExpression
? ("return (" + (handler.value) + ")($event)")
: isFunctionInvocation
? ("return " + (handler.value))
: handler.value;
return ("function($event){" + code + handlerCode + "}")
}
}
var modifierCode = {
stop: '$event.stopPropagation();',
prevent: '$event.preventDefault();',
self: genGuard("$event.target !== $event.currentTarget"),
ctrl: genGuard("!$event.ctrlKey"),
shift: genGuard("!$event.shiftKey"),
alt: genGuard("!$event.altKey"),
meta: genGuard("!$event.metaKey"),
left: genGuard("'button' in $event && $event.button !== 0"),
middle: genGuard("'button' in $event && $event.button !== 1"),
right: genGuard("'button' in $event && $event.button !== 2")
};
複製代碼
通過這一轉換後,生成with
封裝的render
函數以下:
"_c('div',{attrs:{"id":"app"}},[_c('div',{on:{"click":function($event){$event.stopPropagation();return doThis($event)}}},[_v("點擊")]),_v(" "),_c('span',[_v(_s(count))])])"
複製代碼
前面花了大量的篇幅介紹了模板上的事件標記在構建AST
樹上是怎麼處理,而且如何根據構建的AST
樹返回正確的render
渲染函數,可是真正事件綁定仍是離不開綁定註冊事件。這一個階段就是發生在組件掛載的階段。 有了render
函數,天然能夠生成實例掛載須要的Vnode
樹,而且會進行patchVnode
的環節進行真實節點的構建,若是發現過程已經遺忘,能夠回顧以往章節。 Vnode
樹的構建過程和以前介紹的內容沒有明顯的區別,因此這個過程就不作贅述,最終生成的vnode
以下:
有了Vnode
,接下來會遍歷子節點遞歸調用createElm
爲每一個子節點建立真實的DOM
,因爲Vnode
中有data
屬性,在建立真實DOM
時會進行註冊相關鉤子的過程,其中一個就是註冊事件相關處理。
function createElm() {
···
// 針對指令的處理
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
cbs.create[i$1](emptyNode, vnode);
}
i = vnode.data.hook; // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) { i.create(emptyNode, vnode); }
if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
}
}
var events = {
create: updateDOMListeners,
update: updateDOMListeners
};
複製代碼
咱們常常會在template
模板中定義v-on
事件,v-bind
動態屬性,v-text
動態指令等,和v-on
事件指令同樣,他們都會在編譯階段和Vnode
生成階段建立data
屬性,所以invokeCreateHooks
就是一個模板指令處理的任務,他分別針對不一樣的指令爲真實階段建立不一樣的任務。針對事件,這裏會調用updateDOMListeners
對真實的DOM
節點註冊事件任務。
function updateDOMListeners (oldVnode, vnode) {
// on是事件指令的標誌
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
// 新舊節點不一樣的事件綁定解綁
var on = vnode.data.on || {};
var oldOn = oldVnode.data.on || {};
// 拿到須要添加事件的真實DOM節點
target$1 = vnode.elm;
// normalizeEvents是對事件兼容性的處理
normalizeEvents(on);
updateListeners(on, oldOn, add$1, remove$2, createOnceHandler$1, vnode.context);
target$1 = undefined;
}
複製代碼
其中normalizeEvents
是針對v-model
的處理,例如在IE下不支持change
事件,只能用input
事件代替。
updateListeners
的邏輯也很簡單,它會遍歷on
事件對新節點事件綁定註冊事件,對舊節點移除事件監聽,它即要處理原生DOM
事件的添加和移除,也要處理自定義事件的添加和移除,關於自定義事件,後續內容再分析。
function updateListeners (on,oldOn,add,remove$$1,createOnceHandler,vm) {
var name, def$$1, cur, old, event;
// 遍歷事件
for (name in on) {
def$$1 = cur = on[name];
old = oldOn[name];
event = normalizeEvent(name);
if (isUndef(cur)) {
// 事件名非法的報錯處理
warn(
"Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
vm
);
} else if (isUndef(old)) {
// 舊節點不存在
if (isUndef(cur.fns)) {
// createFunInvoker返回事件最終執行的回調函數
cur = on[name] = createFnInvoker(cur, vm);
}
// 只觸發一次的事件
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture);
}
// 執行真正註冊事件的執行函數
add(event.name, cur, event.capture, event.passive, event.params);
} else if (cur !== old) {
old.fns = cur;
on[name] = old;
}
}
// 舊節點存在,接觸舊節點上的綁定事件
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name);
remove$$1(event.name, oldOn[name], event.capture);
}
}
}
複製代碼
在初始構建實例時,舊節點是不存在的,此時會調用createFnInvoker
函數對事件回調函數作一層封裝,因爲單個事件的回調能夠有多個,所以createFnInvoker
的做用是對單個,多個回調事件統一封裝處理,返回一個當事件觸發時真正執行的匿名函數。
function createFnInvoker (fns, vm) {
// 當事件觸發時,執行invoker方法,方法執行fns
function invoker () {
var arguments$1 = arguments;
var fns = invoker.fns;
// fns是多個回調函數組成的數組
if (Array.isArray(fns)) {
var cloned = fns.slice();
for (var i = 0; i < cloned.length; i++) {
// 遍歷執行真正的回調函數
invokeWithErrorHandling(cloned[i], null, arguments$1, vm, "v-on handler");
}
} else {
// return handler return value for single handlers
return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler")
}
}
invoker.fns = fns;
// 返回最終事件執行的回調函數
return invoker
}
複製代碼
其中invokeWithErrorHandling
會執行定義好的回調函數,這裏作了同步異步回調的錯誤處理。try-catch
用於同步回調捕獲異常錯誤,Promise.catch
用於捕獲異步任務返回錯誤。
function invokeWithErrorHandling (handler,context,args,vm,info) {
var res;
try {
res = args ? handler.apply(context, args) : handler.call(context);
if (res && !res._isVue && isPromise(res)) {
// issue #9511
// reassign to res to avoid catch triggering multiple times when nested calls
// 當生命週期鉤子函數內部執行返回promise對象是,若是捕獲異常,則會對異常信息作一層包裝返回
res = res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
}
} catch (e) {
handleError(e, vm, info);
}
return res
}
複製代碼
若是事件只觸發一次(即便用了once
修飾符),則調用createOnceHandler
匿名,在執行完回調以後,移除事件綁定。
function createOnceHandler (event, handler, capture) {
var _target = target$1;
return function onceHandler () {
//調用事件回調
var res = handler.apply(null, arguments);
if (res !== null) {
// 移除事件綁定
remove$2(event, onceHandler, capture, _target);
}
}
}
複製代碼
add
和remove
是真正在DOM
上綁定事件和解綁事件的過程,它的實現也是利用了原生DOM
的addEventListener,removeEventListener api
。
function add (name,handler,capture,passive){
···
target$1.addEventListener(name,handler,
supportsPassive
? { capture: capture, passive: passive }
: capture);
}
function remove (name,handler,capture,_target) {
(_target || target$1).removeEventListener(
name,
handler._wrapper || handler,
capture
);
}
複製代碼
另外事件的解綁除了發生在只觸發一次的事件,也發生在組件更新patchVnode
過程,具體不展開分析,能夠參考以前介紹組件更新的內容研究updateListeners
的過程。
Vue
如何處理原生的Dom
事件基本流程已經講完,然而針對事件還有一個重要的概念不可忽略,那就是組件的自定義事件。咱們知道父子組件能夠利用事件進行通訊,子組件經過vm.$emit
向父組件分發事件,父組件經過v-on:(event)
接收信息並處理回調。所以針對自定義事件在源碼中天然有不一樣的處理邏輯。咱們先經過簡單的例子展開。
<script>
var child = {
template: `<div @click="emitToParent">點擊傳遞信息給父組件</div>`,
methods: {
emitToParent() {
this.$emit('myevent', 1)
}
}
}
new Vue({
el: '#app',
components: {
child
},
template: `<div id="app"><child @myevent="myevent" @click.native="nativeClick"></child></div>`,
methods: {
myevent(num) {
console.log(num)
},
nativeClick() {
console.log('nativeClick')
}
}
})
</script>
複製代碼
從例子中能夠看出,普通節點只能使用原生DOM
事件,而組件上卻可使用自定義的事件和原生的DOM
事件,而且經過native
修飾符區分,有了原生DOM
對於事件處理的基礎,接下來咱們看看自定義事件有什麼特別之處。
回過頭來看看事件的模板編譯,在生成AST
樹階段,以前分析說過addHandler
方法會對事件的修飾符作不一樣的處理,當遇到native
修飾符時,事件相關屬性方法會添加到nativeEvents
屬性中。 下圖是child
生成的AST
樹:
不論是組件仍是普通標籤,事件處理代碼都在genData
的過程當中,和以前分析原生事件一致,genHandlers
用來處理事件對象並拼接成字符串。
function genData() {
···
if (el.events) {
data += (genHandlers(el.events, false)) + ",";
}
if (el.nativeEvents) {
data += (genHandlers(el.nativeEvents, true)) + ",";
}
}
複製代碼
getHandlers
的邏輯前面已經講過,處理組件原生事件和自定義事件的區別在isNative
選項上,咱們看最終生成的代碼爲:
with(this){return _c('div',{attrs:{"id":"app"}},[_c('child',{on:{"myevent":myevent},nativeOn:{"click":function($event){return nativeClick($event)}}})],1)}
複製代碼
有了render
函數接下來會根據它建立Vnode
實例,其中遇到組件佔位符節點時會建立子組件Vnode
, 此時爲on,nativeOn
作了一層特殊的轉換,將nativeOn
賦值給on
,這樣後續的處理方式和普通節點一致。另外,將on
賦值給listeners
,在建立VNode
時以組件配置componentOptions
傳入。
// 建立子組件過程
function createComponent (){
···
var listeners = data.on;
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn;
···
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 },
asyncFactory
);
return vnode
}
複製代碼
接下來是經過Vnode
生成真實節點的過程,這個過程遇到子Vnode
會實例化子組件實例。實例化子類構造器的過程又回到以前文章分析的初始化選項配置的過程,在系列最開始的時候分析Vue.prototype.init
的過程,跳過了組件初始化的流程,其中針對自定義事件的處理的關鍵以下
Vue.prototype._init = function(options) {
···
// 針對子組件的事件處理邏輯
if (options && options._isComponent) {
// 初始化內部組件
initInternalComponent(vm, options);
} else {
// 選項合併,將合併後的選項賦值給實例的$options屬性
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
// 初始化事件處理
initEvents(vm);
}
function initInternalComponent (vm, options) {
var opts = vm.$options = Object.create(vm.constructor.options);
···
opts._parentListeners = vnodeComponentOptions.listeners;
···
}
複製代碼
此時,子組件拿到了父佔位符節點定義的@myevent="myevent"
事件。接下來進行子組件的初始化事件處理,此時vm.$options._parentListeners
會拿到父組件自定義的事件。而帶有自定義事件的組件會執行updateComponentListeners
函數。
function initEvents (vm) {
vm._events = Object.create(null);
vm._hasHookEvent = false;
// init parent attached events
var listeners = vm.$options._parentListeners;
if (listeners) {
// 帶有自定義事件屬性的實例
updateComponentListeners(vm, listeners);
}
}
複製代碼
以後又回到了以前分析的updateListeners
過程,和原生DOM
事件不一樣的是,自定義事件的添加移除的方法不一樣。
var target = vm;
function add (event, fn) {
target.$on(event, fn);
}
function remove$1 (event, fn) {
target.$off(event, fn);
}
function updateComponentListeners (vm,listeners,oldListeners) {
target = vm;
updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm);
target = undefined;
}
複製代碼
咱們回頭來看看Vue
在引入階段對事件的處理還作了哪些初始化操做。Vue
在實例上用一個_events
屬性存貯管理事件的派發和更新,暴露出$on, $once, $off, $emit
方法給外部管理事件和派發執行事件。
eventsMixin(Vue); // 定義事件相關函數
function eventsMixin (Vue) {
var hookRE = /^hook:/;
// $on方法用來監聽事件,執行回調
Vue.prototype.$on = function (event, fn) {
var vm = this;
// event支持數組形式。
if (Array.isArray(event)) {
for (var i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn);
}
} else {
// _events數組中記錄須要監聽的事件以及事件觸發的回調
(vm._events[event] || (vm._events[event] = [])).push(fn);
if (hookRE.test(event)) {
vm._hasHookEvent = true;
}
}
return vm
};
// $once方法用來監聽一次事件,執行回調
Vue.prototype.$once = function (event, fn) {
var vm = this;
// 對fn作一層包裝,先解除綁定再執行fn回調
function on () {
vm.$off(event, on);
fn.apply(vm, arguments);
}
on.fn = fn;
vm.$on(event, on);
return vm
};
// $off方法用來解除事件監聽
Vue.prototype.$off = function (event, fn) {
var vm = this;
// 若是$off方法沒有傳遞任何參數時,將_events屬性清空。
if (!arguments.length) {
vm._events = Object.create(null);
return vm
}
// 數組處理
if (Array.isArray(event)) {
for (var i$1 = 0, l = event.length; i$1 < l; i$1++) {
vm.$off(event[i$1], fn);
}
return vm
}
var cbs = vm._events[event];
if (!cbs) {
return vm
}
if (!fn) {
vm._events[event] = null;
return vm
}
// specific handler
var cb;
var i = cbs.length;
while (i--) {
cb = cbs[i];
if (cb === fn || cb.fn === fn) {
// 將監聽的事件回調移除
cbs.splice(i, 1);
break
}
}
return vm
};
// $emit方法用來觸發事件,執行回調
Vue.prototype.$emit = function (event) {
var vm = this;
{
var 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 + "\"."
);
}
}
var cbs = vm._events[event];
// 找到已經監聽事件的回調,執行
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs;
var args = toArray(arguments, 1);
var info = "event handler for \"" + event + "\"";
for (var i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info);
}
}
return vm
};
}
複製代碼
有了這些事件api,自定義事件的添加移除理解起來也簡單不少。組件經過this.$emit
在組件實例中派發了事件,而在這以前,組件已經將須要監聽的事件以及回調添加到實例的_events
屬性中,觸發事件時即可以直接執行監聽事件的回調。
最後,咱們換一個角度理解父子組件通訊,組件自定義事件的觸發和監聽本質上都是在當前的組件實例中進行,之因此能產生父子組件通訊的效果是由於事件監聽的回調函數寫在了父組件中。
事件是咱們平常開發中必不可少的功能點,Vue
在應用層暴露了@,v-on
的指令供開發者在模板中綁定事件。事件指令在模板編譯階段會以屬性的形式存在,而在真實節點渲染階段會根據事件屬性去綁定相關的事件。對於組件的事件而言,咱們能夠利用事件進行子父組件間的通訊,他本質上是在同個子組件內部維護了一個事件總線,從分析結果能夠看出,之因此有子父組件通訊的效果,緣由僅僅是由於回調函數寫在了父組件中。