深入浅出vue.js----实例方法与全局API的实现原理---数据、事件相关的实例方法

一、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配置了错误处理函数,它将被触发。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章