Vue的事件可以分为原生的DOM事件和自定义的事件
原生的DOM事件:其实就是通过addEventListener添加绑定事件,而我们通过v-on, @添加的事件都会先经过Vue的模板编译,生成抽象语法树(ast),再生成render函数,通过render函数生成我们的Vnode,render函数上的参数,就会告诉Vue我们最终生成真实的DOM有哪些属性,是否是有事件的
我自己写了一个例子,然后使用完整版的Vue(就是包含编译部分),看了一下生成render函数的过程,而使用只包含运行时的Vue,脏活累活vue-loader已经都帮我们做好了。这里我用完整版的
main.js
import Vue from 'vue'
Vue.config.productionTip = false
new Vue({
el:"#app",
template:`<div id="apps">
<input type="text" v-model="inputValue"/>
<div @click="clickFn">绑定</div>
<div @click="clickFn2">发送</div>
</div>`,
data:function(){
return {
inputValue:"我是你爸爸"
}
},
methods:{
clickFn:function(){
console.log("点击事件1")
})
},
clickFn2:function(){
console.log("点击事件2")
}
},
})
这是我的main.js,不需要配置render函数,我们通过debugger会依次执行 $mount->compileToFunctions->compile->
baseCompile
baseCompile源码
var createCompiler = createCompilerCreator(function baseCompile (
template,
options
) {
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
optimize(ast, options);
}
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
});
这里代码就把整个大致的流程体现出来了,通过parse函数将我们的模板,转化成ast抽象语法树,再通过generate将ast树转化为render函数字符串
这个就是我们最总生成的render函数字符串,最终Vue会通过new Function()转成函数,挂载到当前Vue实例的render属性上
"with(this){
return _c('div',{attrs:{"id":"apps"}},
[
_c('input',{
directives:[{name:"model",rawName:"v-model",value:(inputValue),expression:"inputValue"}],
attrs:{"type":"text"},domProps:{"value":(inputValue)},
on:{
"input":function($event){if($event.target.composing)return;inputValue=$event.target.value}
}
}),
_v(" "),
_c('div',{on:{"click":clickFn}},[_v("绑定")]),
_v(" "),
_c('div',{on:{"click":clickFn2}},[_v("发送")])
])
}"
我大致整理的了一下,我们可以看到我们的三个dom转成的render函数字符串
v-mode属性:我们的render函数的on对象中会添加一个input参数,以及还会有一个directives属性(用来做后续的处理,会绑定额外的事件)
而通过@click绑定的事件,我们的render函数的on对象中会添加一个click属性
之后我们就会进入Vnode的创建和真实dom的创建和插入
每一个DOM的创建都会经过updateListeners这个函数,看他是否有要绑定的事件,他会遍历on对象中的属性,这个on对象就是我们上面render函数生成的属性
function updateListeners (
on,
oldOn,
add,
remove$$1,
createOnceHandler,
vm
) {
var name, def$$1, cur, old, event;
//遍历on 中的每一个属性 遍历on 中的每一个属性 遍历on 中的每一个属性 遍历on 中的每一个属性
for (name in on) {
def$$1 = cur = on[name];
old = oldOn[name];
event = normalizeEvent(name);
if (isUndef(cur)) {
process.env.NODE_ENV !== 'production' && warn(
"Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
vm
);
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
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);
}
}
}
第一次执行的时候会进入add函数,之后的更新还会比较一下新旧的事件是不是一样的,并不是每次都会绑定事件
add(event.name, cur, event.capture, event.passive, event.params);
function add$1 (
name,
handler,
capture,
passive
) {
if (useMicrotaskFix) {
var attachedTimestamp = currentFlushTimestamp;
var original = handler;
handler = original._wrapper = function (e) {
if (
e.target === e.currentTarget ||
e.timeStamp >= attachedTimestamp ||
e.timeStamp <= 0 ||
e.target.ownerDocument !== document
) {
return original.apply(this, arguments)
}
};
}
target$1.addEventListener(
name,
handler,
supportsPassive
? { capture: capture, passive: passive }
: capture
);
}
最下面 target$1.addEventListener就会为这个DOM绑定上事件,target$1是我们当前的节点,这个Vue在执行之前就已经赋好值的。
如果是v-model在DOM创建好之后,如果有directives(这个也是render函数生成的属性),还有进入这个inserted函数
inserted: function inserted (el, binding, vnode, oldVnode) {
if (vnode.tag === 'select') {
if (oldVnode.elm && !oldVnode.elm._vOptions) {
mergeVNodeHook(vnode, 'postpatch', function () {
directive.componentUpdated(el, binding, vnode);
});
} else {
setSelected(el, binding, vnode.context);
}
el._vOptions = [].map.call(el.options, getValue);
} else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
el._vModifiers = binding.modifiers;
if (!binding.modifiers.lazy) {
el.addEventListener('compositionstart', onCompositionStart);
el.addEventListener('compositionend', onCompositionEnd);
el.addEventListener('change', onCompositionEnd);
if (isIE9) {
el.vmodel = true;
}
}
}
}
为我们的DOM添加compositionstart, compositionend,change三个事件
所以Vue原生DOM的绑定,就是分析我们的模板,生成ast树,上面有我们节点对应的属性方法以及初始值,通过ast生成render函数,通过render函数生成Vnode,最后生成真实的DOM,每个节点会遍历on对象上的事件属性添加对应的监听事件绑定到当前的DOM上。
我们的自定义事件主要就是这四个函数$on $emit $off $once
$on:添加自定义事件
$emit:触发自定义事件
$off:删除自定义事件
$once:添加一个只执行一次的自定义事件
我们来分别看一下源码:
Vue.prototype.$on = function (event, fn) {
var vm = this;
if (Array.isArray(event)) {
for (var i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn);
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn);
if (hookRE.test(event)) {
vm._hasHookEvent = true;
}
}
return vm
};
$on首先判断是不是数组,如果是就是循环数组,执行$on
其实就是往当前实例vm的_event属性中添加自定义事件名对应的回调函数数组( “自定义事件名”:[fn …])
Vue.prototype.$emit = function (event) {
var vm = this;
if (process.env.NODE_ENV !== 'production') {
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
};
$emit首先判断大小写以及回调函数是否存在,如果不满足那么就会报错
如果回调函数存在那么就循环执行
Vue.prototype.$off = function (event, fn) {
var vm = this;
// all
if (!arguments.length) {
vm._events = Object.create(null);
return vm
}
// array of events
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
}
// specific event
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
};
$off 如果你什么都不传就会把这个实例上的所有自定义事件都清空
如果你的第一个参数是数组,就会循环自定义数组执行$off
第二个参数没传,就会把这个自定义事件上的所有回调函数清空
如果这个自定义事件不存在,就直接返回当前实例
如果都传了,就会清除掉这个自定义事件对应的引用函数
Vue.prototype.$once = function (event, fn) {
var vm = this;
function on () {
vm.$off(event, on);
fn.apply(vm, arguments);
}
on.fn = fn;
vm.$on(event, on);
return vm
};
$once就是$off和$on的结合,生成一个新的函数,当执行之后就调用$off()将这个自定义事件去除,从而达到了执行一次的效果
以上就是自定义事件的所有源码,就比较清晰,多看几遍就差不多懂了
如果可以还是自己debugger一下,印象会更加深刻
这就是我们今天的分享!!