一:vue中如何绑定事件?
vue事件分为两类,一个是原生dom事件,一个是组件自定义事件,绑定方法类似:
#绑定原生dom事件
<div id="example-1">
<button v-on:click="handle">Add 1</button>
<p>The button above has been clicked {
{
counter }} times.</p>
</div>
<div id="example-3">
<button v-on:click="say('hi')">Say hi</button>
<button v-on:click="say('what')">Say what</button>
</div>
#绑定自定义事件,通过组件内部 $emit('myEvent')触发
<my-component v-on:myEvent="doSomething"></my-component>
#在自定义组件上绑定原生事件
<my-component v-on:click.native="doSomething"></my-component>
#绑定动态事件,eventName为实例中能够访问到的变量
<my-component v-on[eventName]="doSomething"></my-component>
<my-component @[eventName]="doSomething"></my-component>
二:vue中的事件修饰符
dom原生事件往往我们需要的不只是绑定,我们还需要处理冒泡,捕获,取消默认事件等特殊场景。
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>
<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>
<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即元素自身触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>
具体大家可以自己去一一尝试,其中有一些按键是有系统兼容问题,大家参考文档注意处理。
三:核心源码解读
1.v-on 指令或者@on 实现
// 正则匹配 html字符串里的 a="xx" @a="xx" @click='xxx' v-on:click="xx" 等属性定义字符串
var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// 正则匹配 动态的属性写法 @[x]="handle1" v-on[x]="" :[x]=""
var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
function parseStartTag () {
var start = html.match(startTagOpen);
if (start) {
var match = {
tagName: start[1],
attrs: [],
start: index
};
advance(start[0].length);
var end, attr;
// 开始匹配,匹配后的属性压入到attrs里
while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
attr.start = index;
advance(attr[0].length);
attr.end = index;
match.attrs.push(attr);
}
if (end) {
match.unarySlash = end[1];
advance(end[0].length);
match.end = index;
return match
}
}
}
// 从属性中匹配和事件相关的属性
var onRE = /^@|^v-on:/;
// 匹配事件修饰符
function parseModifiers (name) {
var match = name.match(modifierRE);
if (match) {
var ret = {
};
match.forEach(function (m) {
ret[m.slice(1)] = true; });
return ret
}
}
//省略N行代码
if (onRE.test(name)) {
// 匹配v-on或者@开头的属性
name = name.replace(onRE, '');
isDynamic = dynamicArgRE.test(name);
if (isDynamic) {
name = name.slice(1, -1);
}
addHandler(el, name, value, modifiers, false, warn$2, list[i], isDynamic);
}
// 生成渲染函数字符串
function addHandler (
el,
name,
value,
modifiers,
important,
warn,
range,
dynamic
) {
modifiers = modifiers || emptyObject;//事件修饰符
// warn prevent and passive modifier
/* istanbul ignore if */
if (
warn &&
modifiers.prevent && modifiers.passive // prevent 和passive 不能一起使用,两个是互斥的
) {
warn(
'passive and prevent can\'t be used together. ' +
'Passive handler can\'t prevent default event.',
range
);
}
// normalize click.right and click.middle since they don't actually fire
// this is technically browser-specific, but at least for now browsers are
// the only target envs that have right/middle clicks.
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';
}
}
// check capture modifier
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) {
// 处理passive
delete modifiers.passive;
name = prependModifierMarker('&', name, dynamic);
}
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;
}
最终结果是el的events里维护了事件和事件对应的内容方法以及修饰符,以及是否是动态事件名等信息。
function genData$2 (el, state) {
var data = '{';
// 省略N行代码
// event handlers
if (el.events) {
data += (genHandlers(el.events, false)) + ",";
}
if (el.nativeEvents) {
data += (genHandlers(el.nativeEvents, true)) + ",";
}
// v-on data wrap
if (el.wrapListeners) {
data = el.wrapListeners(data);
}
return data
}
function createCompileToFunctionFn (compile) {
var cache = Object.create(null);
return function compileToFunctions (
template,
options,
vm
) {
options = extend({
}, options);
// 省略N行
// check cache
var key = options.delimiters
? String(options.delimiters) + template
: template;
if (cache[key]) {
return cache[key]
}
// 省略N行
// turn code into functions
var res = {
};
var fnGenErrors = [];
res.render = createFunction(compiled.render, fnGenErrors);
res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
return createFunction(code, fnGenErrors)
});
// 省略N行代码
// 转化后的渲染函数会缓存起来避免重复生成,生成的函数模版内容见下一个截图
return (cache[key] = res)
}
}
// 将代码字符串转化为函数
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({
err: err, code: code });
return noop
}
}
这里可以看到通过compile生成的虚拟树和render函数字符串。这是vue的核心之一。
# 进入挂载方法
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && query(el);
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
warn(
"Do not mount Vue to or - mount to normal elements instead."
);
return this
}
var options = this.$options;
// resolve template/el and convert to render function
if (!options.render) {
var template = options.template;
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template);
/* istanbul ignore if */
if (!template) {
warn(
("Template element not found or is empty: " + (options.template)),
this
);
}
}
} else if (template.nodeType) {
template = template.innerHTML;
} else {
{
warn('invalid template option:' + template, this);
}
return this
}
} else if (el) {
template = getOuterHTML(el);
}
if (template) {
/* istanbul ignore if */
if (config.performance && mark) {
mark('compile');
}
var ref = compileToFunctions(template, {
outputSourceRange: "development" !== 'production',
shouldDecodeNewlines: shouldDecodeNewlines,
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this);
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
options.render = render;
options.staticRenderFns = staticRenderFns;
/* istanbul ignore if */
if (config.performance && mark) {
mark('compile end');
measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
}
}
}
return mount.call(this, el, hydrating)
};
// 实际挂载方法
function mountComponent (
vm,
el,
hydrating
) {
vm.$el = el;
// 省略N行
} else {
updateComponent = function () {
// 执行模版更新,调用_render方法创建vnode,调用_update更新dom
vm._update(vm._render(), hydrating);
};
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// 创建watcher 来执行updateComponent,会初次执行一次
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */); //
hydrating = false;
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm
}
// 事件绑定函数
function add$1 (
name,
handler,
capture,
passive
) {
// async edge case #6566: inner click event triggers patch, event handler
// attached to outer element during patch, and triggered again. This
// happens because browsers fire microtask ticks between event propagation.
// the solution is simple: we save the timestamp when a handler is attached,
// and the handler would only fire if the event passed to it was fired
// AFTER it was attached.
if (useMicrotaskFix) {
var attachedTimestamp = currentFlushTimestamp;
var original = handler;
handler = original._wrapper = function (e) {
if (
// no bubbling, should always fire.
// this is just a safety net in case event.timeStamp is unreliable in
// certain weird environments...
e.target === e.currentTarget ||
// event is fired after handler attachment
e.timeStamp >= attachedTimestamp ||
// bail for environments that have buggy event.timeStamp implementations
// #9462 iOS 9 bug: event.timeStamp is 0 after history.pushState
// #9681 QtWebEngine event.timeStamp is negative value
e.timeStamp <= 0 ||
// #9448 bail if event is fired in another document in a multi-page
// electron/nw.js app, since event.timeStamp will be using a different
// starting reference
e.target.ownerDocument !== document
) {
return original.apply(this, arguments)
}
};
}
target$1.addEventListener(// 最终在这里绑定
name,
handler,
supportsPassive
? {
capture: capture, passive: passive }
: capture
);
}
四:vue如何优化事件?
vue在处理大列表绑定事件的时候,是有一定的性能问题的,框架内部没有把事件提到父节点上来做事件委托,唯一优化的是列表之间绑定的事件指向的函数都是同一个引用,且在dom销毁的时候能主动销毁事件,所以能负载一定的数据量,如果业务里的确存在非常大量的数据,建议还是自己在父节点上进行事件绑定,或者改变交互,进行分页。