一、Vue构造函数
import { initMinxin } from './init'
import { stateMinxin } from './state'
import { renderMinxin } from './render'
import { eventsMinxin } from './events'
import { lifecycleMinxin } 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)
}
initMinxin(Vue)
stateMinxin(Vue)
eventsMinxin(Vue)
lifecycleMinxin(Vue)
renderMinxin(Vue)
export default Vue
(1)其中定义了Vue构造函数,然后分别调用了initMinxin、stateMinxin、eventsMinxin、lifecycleMinxin、renderMinxin这5个函数,并将Vue构造函数当作参数传给了这5个函数。
(2)这5个函数的作用就是向Vue的原型中挂载方法。
export function initMinxin(Vue){
Vue.prototype._init = function(options){
//做些什么
}
}
1、当函数initMinxin被调用时,会向Vue构造函数的prototype属性添加_init方法。
2、执行new Vue()时,会调用 _init 方法,该方法实现了一系列初始化操作,包括整个生命周期的流程以及响应式系统流程的启动等。
3、其他4个函数也是如此,只是它们会在Vue构造函数的prototype属性上挂载不同的方法而已。
二、数据相关的实例方法
(1)与数据相关的实例方法有3个,分别是vm.$watch、vm.$set 和 vm.$delete,它们是在stateMinxin中挂载到Vue的原型上的。
import{ set,del} from '../observer.index'
export function stateMinxin(Vue){
Vue.prototype.$set = set;
Vue.prototype.$delete = del;
Vue.prototype.$watch = function(expOrFn,cb,options){}
}
1、当stateMinxin被调用时,会向Vue构造函数的prototype属性挂载上面说的3个与数据相关的实例方法。
三、事件相关的实例方法
(1)与事件相关的实例方法有4个,分别是:vm.$on、vm.$once、vm.$off 和 vm.$emit。这4个方法是在eventsMinxin中挂载到Vue构造函数的prototype属性中的。
export function eventsMinxin(Vue){
Vue.prototype.$on = function(event,fn){
//做点什么
}
Vue.prototype.$once = function(event,fn){
//做点什么
}
Vue.prototype.$off = function(event,fn){
//做点什么
}
Vue.prototype.$emit = function(event,fn){
//做点什么
}
}
1、当eventsMinxin被调用时,会向Vue构造函数的prototype属性添加4个实例方法。
四、vm.$on
vm.$on(event,callback)
(1)参数
- { string | Array<string> } event
- { Function } callback
(2)用法
监听当前实例上的自定义事件,事件可以由vm.$emit触发。回调函数会接收所有传入事件所触发的函数的额外参数。
(3)示例
vm.$on('test',function(msg){
console.log(msg);
})
vm.$emit('test','hi');
// => "hi"
(4)内部原理:
1、只需要在注册事件时将回调函数收集起来,在触发事件时将收集起来的回调函数依次调用即可。
Vue.prototype.$on = function(event,fn){
const vm = this;
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);
}
return vm;
}
2、当event参数为数组时,需要遍历数组,将其中的每一项递归调用vm.$on,使回调可以被注册到数组中每项事件名所指定的事件列表中。
3、当event参数不为数组时,就向事件列表中添加回调。通俗地讲,就是将回调注册到事件列表中。
4、vm._events是一个对象,用来存储事件。
5、使用事件名(event)从vm._events中取出事件列表,如果列表不存在,则使用空数组初始化,然后再将回调函数添加到事件列表中。
6、再执行 new Vue()时,Vue会执行 this._init方法进行一系列初始化操作,其中就会再Vue.js的实例上创建一个_events属性,用来存储事件。
vm._events = Object.create(null)
五、vm.$off
vm.$off([event,callback])
(1)参数
- { string | Array<string> } event
- { Function } callback
(2)用法:移除自定义事件监听器
1、如果没有提供参数,则移除所有的事件监听器。
2、如果只提供了事件,则移除该事件所有的监听器。
3、如果同时提供了事件与回调,则只移除这个回调的监听器。
(3)没有提供参数,则移除所有的事件监听器
Vue.prototype.$off = function(event,fn){
const vm = this;
<!-- 移除所有事件的监听器 -->
if(!arguments.length){
vm._events = Object.create(null);
return vm;
}
return vm;
}
(4)event参数为数组
只需要将数组遍历一遍,然后数组中每一项依次调用vm.$off即可
Vue.prototype.$off = function(event,fn){
const vm = this;
<!-- 移除所有事件的监听器 -->
if(!arguments.length){
vm._events = Object.create(null);
return vm;
}
<!-- event支持数组 -->
if(Array.isArray(event)){
for(let i = 0,l = event.length;i<l;i++){
this.$off(event[i],fn);
}
return vm;
}
return vm;
}
(5)如果只提供了事件,则移除该事件所有的监听器
Vue.prototype.$off = function(event,fn){
const vm = this;
<!-- 移除所有事件的监听器 -->
if(!arguments.length){
vm._events = Object.create(null);
return vm;
}
<!-- event支持数组 -->
if(Array.isArray(event)){
for(let i = 0,l = event.length;i<l;i++){
this.$off(event[i],fn);
}
return vm;
}
<!-- 安全监测 -->
const cbs = vm._events[event];
if(!cbs){
return vm;
}
<!-- 移除该事件的所有监听器 -->
if(arguments.length ===1 ){
vm._events[event] = null;
return vm;
}
return vm;
}
1、需要进行一个安全监测。如果这个事件没有被监听,也就是说vm._events中找不到任何监听器,那么什么都不需要做,直接退出程序即可。
2、然后判断是否只有一个参数,如果是,将事件名在vm._events中的所有事件都移除。
(6)如果同时提供了事件与回调,则只移除这个回调的监听器。
只需要使用参数中提供的事件名从vm._events上取出事件列表,然后从列表中找到与参数中提供的回调函数相同的那个函数,并将它从列表中移除即可。
Vue.prototype.$off = function(event,fn){
const vm = this;
<!-- 移除所有事件的监听器 -->
if(!arguments.length){
vm._events = Object.create(null);
return vm;
}
<!-- event支持数组 -->
if(Array.isArray(event)){
for(let i = 0,l = event.length;i<l;i++){
this.$off(event[i],fn);
}
return vm;
}
<!-- 安全监测 -->
const cbs = vm._events[event];
if(!cbs){
return vm;
}
<!-- 移除该事件的所有监听器 -->
if(arguments.length ===1 ){
vm._events[event] = null;
return vm;
}
if(fn){
const cbs = vm._events[event];
let cb;
let i = cbs.length;
while(i--){
cb = cbs[i];
if(cb === fn || cb.fn === fn ){
cbs.splice(i,1);
break;
}
}
}
return vm;
}
1、首先判断是否有fn函数,有则说明用户同时提供了event和fn这两个参数。
2、从vm._events中取出事件监听器列表并遍历它,如果列表中的某一项与fn相同,或者某一项的fn属性与fn相同,或者某一项的fn属性与fn相同,说明已经找到了需要删除的监听器(也就是回调函数),这时使用splice方法将它从列表中移除即可。
3、当循环结束后,列表中所有与用户在参数中提供的fn相同的监听器都会被移除。
4、注意:在代码中遍历列表是从后向前循环,这样在列表中移除当前位置的监听器时,不会影响列表中未遍历到的监听器的位置。如果是从前向后遍历,那么从列表中移除一个监听器时,后面的监听器会自动向前移动一个位置,这会导致下一轮循环时跳过一个元素。
六、vm.$once
vm.$once(event,callback)
(1)参数
- { string | Array<string> } event
- { Function } callback
(2)用法
监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器。
(3)区别
vm.$on和vm.$once的区别是vm.$once只能触发一次,所以实现这个功能的一个思路是:在vm.$once中调用vm.$on来实现监听自定义事件的功能,当自定义事件触发后会执行拦截器,将监听器从事件列表中移除。
Vue.prototype.$once = function(event,fn){
const vm = this;
function on (){
vm.$off(event,on);
fn.apply(vm.arguments);
}
on.fn = fn;
vm.$on(event,on);
return vm;
}
1、首先,将函数on注册到事件中。当自定义事件被触发后,会先执行函数on(在这个函数中个,会使用vm.$off(event,on)将自定义事件移除),然后手动执行函数fn,并将参数arguments传递给函数fn,这就可以实现vm.$once的功能。
(4)注意
1、注意 on.fn = fn;这行代码。在移除监听器时,需要将用户提供的监听器函数与列表中的监听器函数进行对比,相同部分会被移除,这导致当我们使用拦截器代替监听器注入到事件列表中时,拦截器和用户提供的函数是不相同的,此时用户使用vm.$off来移除事件监听器,移除操作会失败。
2、解决方案
将用户提供的原始监听器保存到拦截器的fn属性中,当vm.$off方法遍历事件监听器列表时,同时会检查监听器和监听器的fn属性是否与用户提供的监听器函数相同,只要有一个相同,就说明需要被移除的监听器被找到了,将被找到的拦截器从事件监听器列表中移除即可。
3、在vm.$off中,会检查监听器(cb)和监听器的fn属性是否与用户提供的fn相同
if(cb === fn || cb.fn === fn ){
//做些什么
}
七、vm.$emit
vm.$emit(event,[...args])
(1)参数
- { string } event
- [ args ]
(2)用法
触发当前实例上的事件。附加参数都会传给监听器回调。
(3)实现思路
使用事件名从vm._events中取出对应的事件监听器回调函数列表,然后依次执行列表中的监听器回调并将参数传递给监听器回调。
Vue.prototype.$emit = function(event,fn){
const vm = this;
let cbs = vm._events[event];
if(cbs){
const args = toArray(arguments,1);
for(let i =0,l = cbs.length; i<l i++){
try{
cbs[i].apply(vm,args);
}catch(e){
handleError(e,vm,'event handler for "${event}"')
}
}
}
return vm;
}
1、使用event从vm._events中取出事件监听器回调函数列表,并将其赋值给变量cbs。
2、如果cbs存在,则循环它,依次调用每一个监听器回调函数并将所有参数传给监听器回调函数。
3、toArray的作用是将类似于数组的数据转换成真正的数组,它的第二个参数是起始位置。即args是一个数组,里面包含除第一个参数之外的所有参数。
4、使用try。。catch语句来捕获事件监听器回调的错误。当监听器回调发生错误时,会触发handleError函数,在控制台打印出错误提示。同时如果在Vue.config.errorHandler配置了错误处理函数,它将被触发。