// 例子1: // parent 接收 this.$on('test', function (msg) { console.log(msg) // hi }) // children 传出 this.$emit('test', 'hi') // 例子2: // 父组件监听子组件传出事件 <my-component v-hook:created="dosomething"></my-component> // 例子3: // 一次监听回调后销毁监听 <my-component v-on:click.once="dosomething"></my-component>
上面的代码是vue的标准事件监听然后接收使用,一般用于父子组件通讯。
而在vue源码里面,是怎么实现这个功能呢?
在vue初始化过程中,事件的初始化在初始化生命周期(initLifecycle)后。
所有关于组件事件的都会在此被在此被收集记录
注: 本章对于dom事件的绑定不作解析,源码对于dom事件在render流程中,故后面render中作讲解
首先来看初始化方法
/*初始化事件*/ export function initEvents(vm: Component) { /*在vm上创建一个_events对象,用来存放事件。*/ vm._events = Object.create(null); /*这个bool标志位来表明是否存在钩子,而不需要通过哈希表的方法来查找是否有钩子,这样做可以减少不必要的开销,优化性能。*/ // 比如: v-hook:created="dosomething" vm._hasHookEvent = false; // init parent attached events /*初始化父组件attach的事件*/ const listeners = vm.$options._parentListeners; if (listeners) { updateComponentListeners(vm, listeners); } }
Object.create(null): 传建一个原型指向null的对象,它和 new object 还有 {} 对比的好处是,创建出来的对象原型没有Object附带的各种属性方法,可以减少副作用,MDN Object.create()
parentListeners: 此变量在父组件初始化子组件前,会在options加入此变量,代表父组件的监听事件
在events.js里面,定义了add、remove两个函数用于添加监听、取消监听,而这两个函数也只是很简单的调用用原型方法的$on、$off、$once来达到监听的效果
let target: Component; /*有once的时候注册一个只会触发一次的方法,没有once的时候注册一个事件方法*/ function add(event, fn, once) { if (once) { target.$once(event, fn); } else { target.$on(event, fn); } } /*销毁一个事件方法*/ function remove(event, fn) { target.$off(event, fn); } /*更新组件的监听事件*/ export function updateComponentListeners(vm: Component, listeners: Object, oldListeners: ?Object) { target = vm; // listeners 父on 事件 // oldListeners 旧的On事件,初始化时为空 updateListeners(listeners, oldListeners || {}, add, remove, vm); }
在events.js文件中,在vue的原型中挂载了 $on、$off、$emit、$once这四个方法,上面的添加监听以及取消监听就是运行了这四个函数的三个进行添加监听以及取消监听。
下面方法的代码都比较简单,里面都有注释,就不单独提出来讲。
方法 | 在vue中作用 |
---|---|
$on | 监听子组件的事件 |
$once | 监听子组件的事件,触发一次并取消监听 |
$off | 取消监听事件 |
$emit | 向父级通知监听事件 |
/*为Vue原型加入操作事件的方法*/ export function eventsMixin(Vue: Class<Component>) { // 如果父组件这样监听子组件的生命周期事件: v-hook:created const hookRE = /^hook:/; /*在vm实例上绑定事件方法*/ Vue.prototype.$on = function (event: string | Array<string>, 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 { // 将监听回调加入对应的event数组 (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其实就是对\$on方法的一个封装,生成一个on方法执行一次后取消监听自身。 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; }; /*注销一个事件,如果不传参则注销所有事件,如果只传event名则注销该event下的所有方法*/ Vue.prototype.$off = function (event?: string | Array<string>, 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 this.$off(event[i], fn); } return vm; } // specific event const cbs = vm._events[event]; /*本身不存在该事件则直接返回*/ 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; }; /*触发一个事件方法*/ 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}".`); // } // } // _events储存着父组件的监听 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; }; }
在updateComponentListeners函数中调用updateListeners,并传入add和remove方法
总得来说,此函数的作用是对比新旧监听,添加新监听事件,移除旧监听,如果在初始化中,旧值(oldOn)是没有的。
oldOn在生命周期的初始化和render时会传入值更新对比,这里就不展开,只讨论初始化的过程。
这里有一个注意点,on[name] 中的值其实是一个函数,createFnInvoker方法中返回的invoker函数,而我们绑定的事件都保存在invoker.fns中。
isUndef: 判断值是否为undefined或Null
/*返回一个函数,该函数的作用是将生成时的fns执行,如果fns是数组,则便利执行它的每一项*/ export function createFnInvoker(fns: Function | Array<Function>): Function { function invoker() { const fns = invoker.fns; if (Array.isArray(fns)) { for (let i = 0; i < fns.length; i++) { fns[i].apply(null, arguments); } } else { // return handler return value for single handlers return fns.apply(null, arguments); } } invoker.fns = fns; return invoker; } /*更新监听事件*/ export function updateListeners(on: Object, oldOn: Object, add: Function, remove: Function, vm: Component) { let name, cur, old, event; /*遍历新事件的所有方法*/ for (name in on) { cur = on[name]; old = oldOn[name]; /*取得并去除事件的~、!、&等前缀*/ event = normalizeEvent(name); /*isUndef用于判断传入对象不等于undefined或者null*/ if (isUndef(cur)) { // 开发警告代码,可忽略 // process.env.NODE_ENV !== 'production' && warn( // `Invalid handler for event "${event.name}": got ` + String(cur), // vm // ) } else if (isUndef(old)) { // 初始化由于oldOn什么都没有, 走这里 if (isUndef(cur.fns)) { /*createFnInvoker返回一个函数,该函数的作用是将生成时的fns执行,如果fns是数组,则便利执行它的每一项*/ cur = on[name] = createFnInvoker(cur); } add(event.name, cur, event.once, event.capture, event.passive); } else if (cur !== old) { old.fns = cur; on[name] = old; } } /*移除所有旧的事件*/ for (name in oldOn) { if (isUndef(on[name])) { event = normalizeEvent(name); remove(event.name, oldOn[name], event.capture); } } }
在updateListeners方法中,调用了normalizeEvent去除事件的~、!、&等前缀,并且返回name,once,capture,passive四个属性对象。
这里我们看一下辅助函数文件的一段代码
在源码的事件处理,其实是采用了~、!、&这三个代表一些含义
符号 | 含义 |
---|---|
~ | once,运行一次取消监听 |
& | passive模式,不阻止默认事件 |
! | capture模式,冒泡优先执行 |
所以normalizeEvent 的作用就是返回真正的时间名,并且返回这些事件的标志。
而在normalizeEvent 中先调用cached方法,而cached函数其实是一个高阶函数的应用,它返回一个函数,并且在函数第一次调用时保存传入的参数,并保存结果,在第二次以相同参数调用时,就可以直接返回结果,而不用运行函数,可节省性能,在细微处也能有这样的处理,尤大牛啤!
/** * Create a cached version of a pure function. */ /*根据str得到fn(str)的结果,但是这个结果会被闭包中的cache缓存起来,下一次如果是同样的str则不需要经过fn(str)重新计算,而是直接得到结果*/ export function cached<F: Function> (fn: F): F { const cache = Object.create(null) return (function cachedFn (str: string) { const hit = cache[str] return hit || (cache[str] = fn(str)) }: any) } const normalizeEvent = cached( ( name: string ): { name: string, once: boolean, capture: boolean, passive: boolean, } => { const passive = name.charAt(0) === "&"; name = passive ? name.slice(1) : name; const once = name.charAt(0) === "~"; // Prefixed last, checked first name = once ? name.slice(1) : name; const capture = name.charAt(0) === "!"; name = capture ? name.slice(1) : name; return { name, once, capture, passive, }; } );
以上就是initEvent相关流程代码了,已经将我看源码时的疑问写在上面,如有其他疑问不明白,可留言或私信,我将补充到博客中。