vue源碼解析之事件機(jī)制原理
上一章沒什么經(jīng)驗(yàn)。直接寫了組件機(jī)制。感覺涉及到的東西非常的多,不是很方便講。今天看了下vue的關(guān)于事件的機(jī)制。有一些些體會(huì)。寫出來。大家一起糾正,分享。源碼都是基于最新的Vue.js v2.3.0。下面我們來看看vue中的事件機(jī)制:
老樣子還是先上一段貫穿全局的代碼,常見的事件機(jī)制demo都會(huì)包含在這段代碼中:
<div id="app">
<div id="test1" @click="click1">click1</div>
<div id="test2" @click.stop="click2">click2</div>
<my-component v-on:click.native="nativeclick" v-on:componenton="parentOn">
</my-component>
</div>
</body>
<script src="vue.js"></script>
<script type="text/javascript">
var Child = {
template: '<div>A custom component!</div>'
}
Vue.component('my-component', {
name: 'my-component',
template: '<div>A custom component!<div @click.stop="toParent">test click</div></div>',
components: {
Child:Child
},
created(){
console.log(this);
},
methods: {
toParent(){
this.$emit('componenton','toParent')
}
},
mounted(){
console.log(this);
}
})
new Vue({
el: '#app',
data: function () {
return {
heihei:{name:3333},
a:1
}
},
components: {
Child:Child
},
methods: {
click1(){
alert('click1')
},
click2(){
alert('click2')
},
nativeclick(){
alert('nativeclick')
},
parentOn(value){
alert(value)
}
}
})
</script>
上面的demo中一共有四個(gè)事件?;竞w了vue中最經(jīng)典的事件的四種情況
普通html元素上的事件
好吧。想想我們還是一個(gè)個(gè)來看。如果懂vue組件相關(guān)的機(jī)制會(huì)更容易懂。那么首先我們看看最簡(jiǎn)單的第一、二個(gè)(兩個(gè)事件只差了個(gè)修飾符):
<div id="test1" @click="click1">click1</div>
這是簡(jiǎn)單到不能在簡(jiǎn)單的一個(gè)點(diǎn)擊事件。
我們來看看建立這么一個(gè)簡(jiǎn)單的點(diǎn)擊事件,vue中發(fā)生了什么。
1:new Vue()中調(diào)用了initState(vue):看代碼
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) { initProps(vm, opts.props); }
if (opts.methods) { initMethods(vm, opts.methods); }//初始化事件
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
if (opts.computed) { initComputed(vm, opts.computed); }
if (opts.watch) { initWatch(vm, opts.watch); }
}
//接著看看initMethods
function initMethods (vm, methods) {
var props = vm.$options.props;
for (var key in methods) {
vm[key] = methods[key] == null ? noop : bind(methods[key], vm);//調(diào)用了bind方法,我們?cè)倏纯碽ind
{
if (methods[key] == null) {
warn(
"method \"" + key + "\" has an undefined value in the component definition. " +
"Did you reference the function correctly?",
vm
);
}
if (props && hasOwn(props, key)) {
warn(
("method \"" + key + "\" has already been defined as a prop."),
vm
);
}
}
}
}
//我們接著看看bind
function bind (fn, ctx) {
function boundFn (a) {
var l = arguments.length;
return l
? l > 1
? fn.apply(ctx, arguments)//通過返回函數(shù)修飾了事件的回調(diào)函數(shù)。綁定了事件回調(diào)函數(shù)的this。并且讓參數(shù)自定義。更加的靈活
: fn.call(ctx, a)
: fn.call(ctx)
}
// record original fn length
boundFn._length = fn.length;
return boundFn
}
總的來說。vue初始化的時(shí)候,將method中的方法代理到vue[key]的同時(shí)修飾了事件的回調(diào)函數(shù)。綁定了作用域。
2:vue進(jìn)入compile環(huán)節(jié)需要將該div變成ast(抽象語(yǔ)法樹)。當(dāng)編譯到該div時(shí)經(jīng)過核心函數(shù)genHandler:
function genHandler (
name,
handler
) {
if (!handler) {
return 'function(){}'
}
if (Array.isArray(handler)) {
return ("[" + (handler.map(function (handler) { return genHandler(name, handler); }).join(',')) + "]")
}
var isMethodPath = simplePathRE.test(handler.value);
var isFunctionExpression = fnExpRE.test(handler.value);
if (!handler.modifiers) {
return isMethodPath || isFunctionExpression//假如沒有修飾符。直接返回回調(diào)函數(shù)
? handler.value
: ("function($event){" + (handler.value) + "}") // inline statement
} else {
var code = '';
var genModifierCode = '';
var keys = [];
for (var key in handler.modifiers) {
if (modifierCode[key]) {
genModifierCode += modifierCode[key];//處理修飾符數(shù)組,例如.stop就在回調(diào)函數(shù)里加入event.stopPropagation()再返回。實(shí)現(xiàn)修飾的目的
// left/right
if (keyCodes[key]) {
keys.push(key);
}
} 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
? handler.value + '($event)'
: isFunctionExpression
? ("(" + (handler.value) + ")($event)")
: handler.value;
return ("function($event){" + code + handlerCode + "}")
}
}
genHandler函數(shù)簡(jiǎn)單明了,如果事件函數(shù)有修飾符。就處理完修飾符,添加修飾符對(duì)應(yīng)的函數(shù)語(yǔ)句。再返回。這個(gè)過程還會(huì)單獨(dú)對(duì)native修飾符做特殊處理。這個(gè)等會(huì)說。compile完后自然就render。我們看看render函數(shù)中這塊區(qū)域長(zhǎng)什么樣子:
_c('div',{attrs:{"id":"test1"},on:{"click":click1}},[_v("click1")]),_v(" "),_c('div',{attrs:{"id":"test2"},on:{"click":function($event){$event.stopPropagation();click2($event)}}}
一目了然。最后在虛擬dom-》真實(shí)dom的時(shí)候。會(huì)調(diào)用核心函數(shù):
function add$1 (
event,
handler,
once$$1,
capture,
passive
) {
if (once$$1) {
var oldHandler = handler;
var _target = target$1; // save current target element in closure
handler = function (ev) {
var res = arguments.length === 1
? oldHandler(ev)
: oldHandler.apply(null, arguments);
if (res !== null) {
remove$2(event, handler, capture, _target);
}
};
}
target$1.addEventListener(
event,
handler,
supportsPassive
? { capture: capture, passive: passive }//此處綁定點(diǎn)擊事件
: capture
);
}
組件上的事件
好了下面就是接下來的組件上的點(diǎn)擊事件了??梢灶A(yù)感到他走的和普通的html元素應(yīng)該是不同的道路。事實(shí)也是如此:
<my-component v-on:click.native="nativeclick" v-on:componenton="parentOn"> </my-component>
最簡(jiǎn)單的一個(gè)例子。兩個(gè)事件的區(qū)別就是一個(gè)有.native的修飾符。我們來看看官方.native的作用:在原生dom上綁定事件。好吧。很簡(jiǎn)單。我們跟隨源碼看看有何不同。這里可以往回看看我少的可憐的上一章組件機(jī)制。vue中的組件都是擴(kuò)展的vue的一個(gè)新實(shí)例。在compile結(jié)束的時(shí)候你還是可以發(fā)現(xiàn)他也是類似的一個(gè)樣子。如下圖:
可以看到加了.native修飾符的會(huì)被放入nativeOn的數(shù)組中。等待后續(xù)特殊處理。等不及了。我們直接來看看特殊處理。render函數(shù)在執(zhí)行時(shí)。如果遇到組件。看過上一章的可以知道。會(huì)執(zhí)行
function createComponent (
Ctor,
data,
context,
children,
tag
) {
if (isUndef(Ctor)) {
return
}
var baseCtor = context.$options._base;
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
{
warn(("Invalid Component definition: " + (String(Ctor))), context);
}
return
}
// async component
if (isUndef(Ctor.cid)) {
Ctor = resolveAsyncComponent(Ctor, baseCtor, context);
if (Ctor === undefined) {
// return nothing if this is indeed an async component
// wait for the callback to trigger parent update.
return
}
}
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor);
data = data || {};
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data);
}
// extract props
var propsData = extractPropsFromVNodeData(data, Ctor, tag);
// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
var listeners = data.on;//listeners緩存data.on的函數(shù)。這里就是componenton事件
// replace with listeners with .native modifier
data.on = data.nativeOn;//正常的data.on會(huì)被native修飾符的事件所替換
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners
data = {};
}
// merge component management hooks onto the placeholder node
mergeHooks(data);
// return a placeholder vnode
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 }
);
return vnode
}
整段代碼關(guān)于事件核心操作:
var listeners = data.on;//listeners緩存data.on的函數(shù)。這里就是componenton事件 // replace with listeners with .native modifier data.on = data.nativeOn;//正常的data.on會(huì)被native修飾符的事件所替換
經(jīng)過這兩句話。.native修飾符的事件會(huì)被放在data.on上面。接下來data.on上的事件(這里就是nativeclick)會(huì)按普通的html事件往下走。最后執(zhí)行target.add('',''')掛上原生的事件。而先前的data.on上的被緩存在listeneners的事件就沒著么愉快了。接下來他會(huì)在組件init的時(shí)候。它會(huì)進(jìn)入一下分支:
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);
}
}
function updateComponentListeners (
vm,
listeners,
oldListeners
) {
target = vm;
updateListeners(listeners, oldListeners || {}, add, remove$1, vm);
}
function add (event, fn, once$$1) {
if (once$$1) {
target.$once(event, fn);
} else {
target.$on(event, fn);
}
}
發(fā)現(xiàn)組件上的沒有.native的修飾符調(diào)用的是$on方法。這個(gè)好熟悉。進(jìn)入到$on,$emit大致想到是一個(gè)典型的觀察者模式的事件。看看相關(guān)$on,$emit代碼。我加點(diǎn)注解:
Vue.prototype.$on = function (event, fn) {
var this$1 = this;
var vm = this;
if (Array.isArray(event)) {
for (var i = 0, l = event.length; i < l; i++) {
this$1.$on(event[i], fn);
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn);//存入事件
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true;
}
}
return vm
};
Vue.prototype.$emit = function (event) {
var vm = this;
console.log(vm);
{
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];
console.log(cbs);
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[i].apply(vm, args);//當(dāng)emit的時(shí)候調(diào)用該事件。注意上面說的vue在初始化的守候。用bind修飾了事件函數(shù)。所以組件上掛載的事件都是在父作用域中的
}
}
return vm
};
看了上面的on,emit用法下面這個(gè)demo也就瞬間秒解了(一個(gè)經(jīng)常用的非父子組件通信):
var bus = new Vue()
// 觸發(fā)組件 A 中的事件
bus.$emit('id-selected', 1)
// 在組件 B 創(chuàng)建的鉤子中監(jiān)聽事件
bus.$on('id-selected', function (id) {
// ...
})
是不是豁然開朗。
又到了愉快的總結(jié)時(shí)間了。segementfault的編輯器真難用。內(nèi)容多就卡。哎。煩。卡的時(shí)間夠看好多肥皂劇了。
總的來說。vue對(duì)于事件有兩個(gè)底層的處理邏輯。
1:普通html元素和在組件上掛了.native修飾符的事件。最終EventTarget.addEventListener() 掛載事件
2:組件上的,vue實(shí)例上的事件會(huì)調(diào)用原型上的$on,$emit(包括一些其他api $off,$once等等)
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
vue3 template轉(zhuǎn)為render函數(shù)過程詳解
在 Vue 中,template 模板是我們編寫組件的主要方式之一,而 Vue 內(nèi)部會(huì)將這些模板轉(zhuǎn)換為 render 函數(shù),render 函數(shù)是用于創(chuàng)建虛擬 DOM 的函數(shù),通過它,Vue 能夠高效地追蹤 DOM 的變化并進(jìn)行更新,下面我會(huì)通俗易懂地詳細(xì)解釋 Vue 如何將 template 轉(zhuǎn)換為 render 函數(shù)2024-10-10
Pinia 的 Setup Stores 語(yǔ)法使用實(shí)例詳解
這篇文章主要為大家介紹了Pinia 的 Setup Stores 語(yǔ)法使用實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
vue-cli 使用axios的操作方法及整合axios的多種方法
這篇文章主要介紹了vue-cli 使用axios的操作方法及整合axios的多種方法,vue-cli整合axios的多種方法,小編一一給大家列出來了,大家根據(jù)自身需要選擇,需要的朋友可以參考下2018-09-09
快速解決vue在ios端下點(diǎn)擊響應(yīng)延時(shí)的問題
今天小編就為大家分享一篇快速解決vue在ios端下點(diǎn)擊響應(yīng)延時(shí)的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-08-08
vue和H5 draggable實(shí)現(xiàn)拖拽并替換效果
這篇文章主要為大家詳細(xì)介紹了vue和H5 draggable實(shí)現(xiàn)拖拽并替換效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-07-07

