目前社区有很多 Vue.js 的源码解读文章,但是质量层次不齐,不够系统和全面,最近我将从各个细枝末节解读 Vue.js 的实现原理,针对 vue3 版本之前,让同学们可以彻底掌握 Vue.js。 友情提示:阅读本文大概需要** 30****分钟**
前言
我们平时开发工作中,处理组件间的通讯,原生的交互,都离不开事件。通过官网源码的解读得知,Vue 支持 2 种事件类型,原生 DOM 事件和自定义事件,它们主要的区别在于添加和删除事件的方式不一样,并且自定义事件的派发是往当前实例上派发,但是可以利用在父组件环境定义回调函数来实现父子组件的通讯。对于一个组件元素,我们不仅仅可以绑定原生的 DOM 事件,还可以绑定自定义事件,非常灵活和方便。那么接下来将具体分析一下 Vue 的事件监听机制。
Vue事件机制
Vue定义了四种添加事件监听的方法,EventsMixin:
Vue事件API
Vue 事件修饰符及其他在官网已有详细说明,这里只讨论这四种事件监听。在Vue3之前,Vue提供了四个事件API,分别是
$on
,
$once
,
$off
,
$emit
。
!-- 阻止单击事件继续传播 --
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
初始化事件
初始化事件在vm上创建一个_events对象,用来存放事件。_events的内容如下:
{
eventName: [function1, function2, function3]
}
存放事件名以及对应执行方法。
/*初始化事件*/
export function initEvents (vm: Component) {
/*在vm上创建一个_events对象,用来存放事件。*/
vm._events = Object.create(null)
/*这个bool标志位来表明是否存在钩子,而不需要通过哈希表的方法来查找是否有钩子,这样做可以减少不必要的开销,优化性能。*/
vm._hasHookEvent = false
// init parent attached events
/*初始化父组件attach的事件*/
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
$on
$on
方法用来在 vm 实例上监听一个自定义事件,该事件可用emit 触发。
Vue.prototype.$on = function (event: string | Arraystring, fn: Function): Component {
const vm: Component = this
/*如果是数组的时候,则递归$on,为每一个成员都绑定上方法*/
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i l; i++) {
this.$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
/*这里在注册事件的时候标记bool值也就是个标志位来表明存在钩子,而不需要通过哈希表的方法来查找是否有钩子,这样做可以减少不必要的开销,优化性能。*/
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
$once
$once</code>监听一个只能触发一次的事件,在触发以后会自动移除该事件。</p><pre style="box-sizing: border-box;font-size: 14px;color: rgb(62, 62, 62);line-height: inherit;text-align: start;background-color: rgb(255, 255, 255);"><code class="hljs cs" style="box-sizing: border-box;margin-right: 2px;margin-left: 2px;padding: 0.5em;color: rgb(220, 220, 220);line-height: 18px;border-radius: 0px;background: rgb(63, 63, 63);font-family: Consolas, Inconsolata, Courier, monospace;display: block;overflow-x: auto;letter-spacing: 0px;overflow-wrap: normal !important;word-break: normal !important;overflow-y: auto !important;"> Vue.prototype.$once = function (event: string, fn: Function): Component {
const vm: Component = this
function on () {
/*在第一次执行的时候将该事件销毁*/
vm.$off(event, on)
/*执行注册的方法*/
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}
$off
$off
用来移除自定义事件
Vue.prototype.$off = function (event?: string | Arraystring, fn?: Function): Component {
const vm: Component = this
// all
/*如果不传参数则注销所有事件*/
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events
/*如果event是数组则递归注销事件*/
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i l; i++) {
this.$off(event[i], fn)
}
return vm
}
// specific event
const cbs = vm._events[event]
/*Github:https://github.com/answershuto*/
/*本身不存在该事件则直接返回*/
if (!cbs) {
return vm
}
/*如果只传了event参数则注销该event方法下的所有方法*/
if (arguments.length === 1) {
vm._events[event] = null
return vm
}
// specific handler
/*遍历寻找对应方法并删除*/
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
return vm
}
$emit
$emit
用来触发指定的自定义事件。
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (process.env.NODE_ENV !== 'production') {
const 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}".`
)
}
}
let cbs = vm._events[event]
if (cbs) {
/*将类数组的对象转换成数组*/
cbs = cbs.length 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
/*遍历执行*/
for (let i = 0, l = cbs.length; i l; i++) {
cbs[i].apply(vm, args)
}
}
return vm
}
HTML元素上点击事件
用 v-on 指令监听 DOM 事件,并在触发时运行一些JS代码,也可以通过@简写的方式,原因:Vue定义了这样的正则表达式 var onRE = /^@|^v-on:/;,在 processAttrs() 解析属性时通过onRE.test(name) 判断来添加属性。
// Html 代码
body
div id="app"
button @click="test"测试/button
/div
script
new Vue({
'el':'#app',
methods:{
test:function(){
alert('测试按钮被点击了');
}
}
});
/script
/body
Vue 初始化时,调用 initEvents(vm) 对事件进行初始化:
function initEvents (vm) {
vm._events = Object.create(null);
//_hasHookEvent标志位表明是否存在钩子,而不需要通过哈希表来查找是否有钩子,这样做可以减少不必要的开销,优化性能。
vm._hasHookEvent = false;
//初始化父组件attach的事件
var listeners = vm.$options._parentListeners;
if (listeners) {
updateComponentListeners(vm, listeners);
}
}
initEvents方法在vm上创建一个_events对象,用来存放事件。接下来就调用initState()方法初始化事件,相关代码段:
if (opts.methods) { initMethods(vm, opts.methods); }
这个例子 methods 内有方法 test,因此会执行initMethods。
vm[key] = methods[key] == null ? noop : bind(methods[key], vm);
initMethods 遍历定义的 methods,通过调用 bind 改变函数的 this 指向,修饰了事件的回调函数,组件上挂载的事件都是在父作用域中的。
function nativeBind (fn, ctx) {
//fn的this指向ctx
return fn.bind(ctx)
}
var bind = Function.prototype.bind
? nativeBind
: polyfillBind;
接下来 Vue 将 html 解析成 AST,接下来调用
add$1
通过addEventListener 将事件绑定到 target上。
//
function add$1 (event,handler,once$$1,apture,passive) {
handler = withMacroTask(handler);
if (once$$1) { handler = createOnceHandler(handler, event, capture); }
//绑定点击事件
target$1.addEventListener(
event,
handler,
supportsPassive
? { capture: capture, passive: passive }
: capture
);
}
component上点击事件
body
div id="app"
child-test :test-message="mess" v-on:click.native="test01" v-on:componenton="test02"/child-test
/div
template id="tplt01"
button{{testMessage}}/button
/template
script
new Vue({
'el':'#app',
data:{mess:'组件点击001'},
methods:{
test01:function(){
alert('01号测试按钮被点击');
},
test02:function(){
alert('01号测试按钮被点击!');
}
},
components:{
'childTest':{
template:"#tplt01",
props:['testMessage'],
methods:{
test02:function(){
alert('001号测试按钮被点击');
}
},
}
}
});
/script
/body
function genHandlers (
events,
isNative,
warn
) {
var res = isNative ? 'nativeOn:{' : 'on:{';
for (var name in events) {
res += """ + name + "":" + (genHandler(name, events[name])) + ",";
}
return res.slice(0, -1) + '}'
}
html解析成vnode之后会调用createComponent进行处理。
function createComponent (
Ctor,
data,
context,
children,
tag
) {
if (isUndef(Ctor)) {
return
}
var baseCtor = context.$options._base;
/*其他代码省略*/
//缓存data.on的函数,这些需要作为子组件监听器而不是DOM监听器来处理。就是componenton事件
var listeners = data.on;
//data.on被native修饰符的事件所替换
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
}
createComponent将.native修饰符的事件放在data.on上面。接下来data.on上的事件(本文中的alert(‘001号测试按钮被点击’);) 会按普通的html事件往下走。
function updateDOMListeners (oldVnode, vnode) {
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
var on = vnode.data.on || {};
var oldOn = oldVnode.data.on || {};
target$1 = vnode.elm;
normalizeEvents(on);
updateListeners(on, oldOn, add$1, remove$2, vnode.context);
target$1 = undefined;
}
最终通过
target$1.addEventListener
添加事件监听。而标签内没有.native的修饰符调用的是
$on
方法。
function updateComponentListeners (
vm,
listeners,
oldListeners
) {
target = vm;
updateListeners(listeners, oldListeners || {}, add, remove$1, vm);
target = undefined;
}
updateListeners又调用了add方法
function add (event, fn, once) {
if (once) {
target.$once(event, fn);
} else {
target.$on(event, fn);
}
}
也就是说对于普通html元素和在组件标签内添加了.native修饰符的事件,都通过
target$1.addEventListener()
来挂载事件。而定义在组件上的事件会调用原型上的
$on
等方法。
总结
上面从源码里扯了那么多代码,可能还是云里雾里的,既然是4种事件监听,这里用class模拟一下Vue的事件机制,算是对文章的总结吧。
class Event{
constructor() {
this._events = Object.create(null);
}
$on(event,fn) {
if(Array.isArray(event)) { //如果传的是数组就递归为每个成员绑定方法
for(let i = 0; i event.length; i++) {
this.$on(event[i],fn);
}
}else {
(this._events[event] || (this._events[event] = [])).push(fn);
}
}
$off(event,fn) {
if (!arguments.length) { //没传参数则全部销毁、
this._events = Object.create(null);
return
}
if (Array.isArray(event)) { //数组则递归销毁
for(let i = 0; i event.length; i++) {
this.$off(event[i],fn);
}
return
}
// 特殊处理event
const cbs = this._events[event];
if (!cbs) { //如果不存在就返回
return
}
if(arguments.length === 1) { //如果只传了event参数,则销毁event下的所有方法
this._events[event] = null;
return
}
// 特殊处理fn
// 遍历所有方法找到对应方法删除
let cb;
let i = cbs.length - 1;
while(i = 0) {
cb = cbs[i];
if (cb === fn || cb.fn === fn) {
cbs.splice(i,1)
break;
}
i --
}
}
$once(event,fn) {
function on () {
//每次执行事件先销毁
this.$off(event,on);
//执行handler
fn.apply(this,arguments)
}
this.$on(event,on);
}
$emit(event,arg) {
let cbs = this._events[event];
let args = Array.from(Array.prototype.splice.call(arguments,1,arguments.length-1));
if (cbs) {
cbs = cbs.length 1 ? Array.from(cbs) : cbs;
for (let i = 0; i cbs.length; i++) {
cbs[i].apply(this,args);
}
}
}
}
最后
今天的 Vue2总结(七)事件机制 就分享到这里,我的公众号没有留言功能哈,有问题大家心里默念,我能感受到,谢谢 ~
原文始发于微信公众号(程序员思语):