Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!
2013年7月28日,尤雨溪第一次在 GItHub 上为 Vue.js 提交代码;2015年10月26日,Vue.js 1.0.0版本发布;2016年10月1日,Vue.js 2.0发布。
最早的 Vue.js 只做视图层,没有路由, 没有状态管理,也没有官方的构建工具,只有一个库,放到网页里就可以直接用了。
后来,Vue.js 慢慢开始加入了一些官方的辅助工具,比如路由(Router)、状态管理方案(Vuex)和构建工具(Vue-cli)等。此时,Vue.js 的定位是:The Progressive Framework。翻译成中文,就是渐进式框架。
Vue.js2.0 引入了很多特性,比如虚拟 DOM,支持 JSX 和 TypeScript,支持流式服务端渲染,提供了跨平台的能力等。Vue.js 在国内的用户有阿里巴巴、百度、腾讯、新浪、网易、滴滴出行、360、美团等等。
Vue 已是一名前端工程师必备的技能,现在就让我们开始深入学习 Vue.js 内部的核心技术原理吧!
什么是实例方法
在 Vue.js 的内部有这样一段代码:
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
上方代码定义了一个类,名字叫做 Vue,然后在创建实例时,调用 _init 方法,执行一系列初始化操作。并且调用了 initMixin、stateMixin、eventMixin、lifecycleMixin 和 renderMixin 方法,向 Vue 构造函数的原型上挂载方法,也就是这里的实例方法。
initMixin:用来初始化操作的,比如整个生命周期的流程以及响应式系统流程的启动
stateMixin:向Vue原型上挂载数据相关的实例方法,vm.$watch、vm.$set、vm.$delete
eventsMixin:向Vue原型上挂载事件相关的实例方法,vm.$on、vm.$once、vm.$off 和 vm.$emit
lifecycleMixin:向Vue原型上挂载生命周期相关的实例方法,vm.$forceUpdate 和 vm.$destory
renderMixin:向Vue原型上挂载生命周期相关的实例方法,vm.$nextTick
本篇文章我们着重介绍事件相关的实例方法,其他实例方法将陆续推出。
什么是 vm
可能有人疑惑,什么是 vm?这里粘贴 Vue 部分源码来解释:
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$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
}
上方代码是 eventsMixin 函数中定义 vm.$on 的源码部分,从第二行我们可以看到,vm 其实就是 this 的别名,那么这里为什么要这么定义呢?是为了保证后续代码的 this 指向正确。
所以大家不必要纠结 vm 到底是什么,其实所有的实例方法都是通过Vue.prototype.$on = function(){} 的形式,向 Vue 的原型上挂载的,在实例执行$on方法时,会向原型链上寻找 $on。
$on 也是一个函数名字而已,其实也可以叫 helloword,像Vue.prototype.helloworld = function(){} 这样,就是向 Vue 的原型上挂载了helloworld方法。
vm.$on
vm.$on 用来监听当前实例上的自定义事件,也就是注册一个监听事件回调函数,每当监听到有人调用这个事件的时候,注册的回调就会被调用。调用事件就要用到 vm.$emit 函数。执行效果如下:
vm.$on('hfun',function(arg){
console.log(arg)
}) // 注册了'hfun'事件以及对应的回调函数
vm.$emit('hfun','hahaha')
// hahaha
上方代码的 hahaha 输出内容,是 vm.$on 监听的回调函数执行输出的内容。
事件实现逻辑是这样的,在注册事件时将事件回调函数收集起来,在触发事件时将收集起来的回调函数依次调用。当 event 参数为数组时,遍历数组,将其中的每一项递归调用 vm.$on,使回调可以被注册到数组中每项事件名所指向的事件列表中,也就是说,每个事件名,可能对应多个事件,每次触发当前事件,会导致多个回调函数同时触发。
如果不是数组,我们就将当前回调存储起来。vm._events 是一个对象,用来存储事件的,是我们在执行 new Vue() 时,执行 this._init 方法时进行的初始化操作,在 vue 的实例上创建的一个 _events 属性。
在代码中,我们使用事件名作为对象的属性,取出当前事件对应的事件列表,如果列表不存在,就使用空数组初始化,然后再将回调函数添加到事件列表中。
vm.$off
vm.$off 用来移除自定义事件监听器。
如果没有提供参数,则移除所有的事件监听器。
如果只提供了事件,则移除当前事件的所有监听器。
如果既提供了事件,又提供了回调函数,则只移除这个事件的这个监听器。
(1)第一种,没有提供参数情况:
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
}
}
上面是 vue 关于 vm.$off 没有参数的情况对应的源码部分,首先判断参数长度是否为零,如果没有参数,我们直接 vm._events 属性重置为空,就等同于将所有事件都清楚了。
(2)第二种,提供一个参数情况:
首先,当第一个参数是一个数组时,我们就遍历这个数组,并且对每一项都执行 vm.$off,这样就对数组内所有的事件都清除了。
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// array of events
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
}
其次,如果不是数组的话,我们就移除该事件所有的监听器,我们只需要将 vm._events[event] 重置为空即可。
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// specific event
const cbs = vm._events[event]
if (!cbs) { // 先判断这个事件有没有被监听,没有的话直接退出
return vm
}
if (!fn) { // 有被监听,直接将对应的事件列表清空即可
vm._events[event] = null
return vm
}
}
(3)第三种,既提供了事件,又提供了回调函数:
既提供了事件,又提供了回调函数,我们要只需要使用参数中提供的事件名,从 vm._events 中去除事件列表,然后遍历事件列表,找到和提供的回调函数一样的那个,将他从事件列表中移除即可。
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// 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
}
vm.$once
vm.$once 用来监听一个自定义事件,但是只触发一次,在第一次触发之后就移除监听器。我们可以结合 vm.$on 和 vm.$off 实现,当自定义事件触发后,将监听器从事件列表中移除。
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
}
vm.$emit
vm.$emit 用来触发事件,附加的参数会传递给事件监听器回调。我们只需要从 vm._events 中取出对应的事件列表,然后依次执行列表中的监听器回调函数即可(前面我们说过,一个事件可能对应不止一个回调函数)。
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
let cbs = vm._events[event]
if (cbs) {
const args = toArray(arguments, 1)
for (let i = 0, l = cbs.length; i < l; i++) {
cb[i].apply(vm,args)
}
}
return vm
}
Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!
叶阳辉
HFun 前端攻城狮
往期精彩: