Vue源码学习(六)- 实例方法

Posted ~往无前

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue源码学习(六)- 实例方法相关的知识,希望对你有一定的参考价值。

目标

深入理解以下实例方法的实现原理

  • vm.$set
  • vm.$delete
  • vm.$watch
  • vm.$on
  • vm.$emit
  • vm.$off
  • vm.$once
  • vm._update
  • vm.$forceUpdate
  • vm.$destroy
  • vm.$nextTick
  • vm._render

入口

/src/core/instance/index.js
该文件是Vue实例的入口文件,包括Vue构造函数的定义,各个实例方法的初始化

// Vue的构造函数
function Vue(options)
	//调用Vue.prototype_init方法,该方法是在initMixin中定义的
	this._init(options)

//定义Vue.protorype_init方法,给Vue构造函数原型绑定_init方法,才能使得上面可以用this._init方法
initMixin(Vue)
/**
  * 定义
  * Vue.prototype.$data
  * Vue.prototype.$props
  * Vue.prototype.$set
  * Vue.prototype.$delete
  * Vue.prototype.$watch
*/
stateMixin(Vue)
/**
  * 定义事件相关的方法
  * Vue.prototype.$on
  * Vue.prototype.$once
  * Vue.prototype.$off
  * Vue.prototype.$emit
*/
eventsMixins(Vue)
/**
  * 定义:
  * Vue.prototype._update
  * Vue.prototype.$forceUpdate
  * Vue.prototype.$destroy
*/
lifecycleMixin(Vue)
/**
  * 执行 installRenderHelpers,在Vue.prototype 对象上安装运行时便利程序
  * 定义:
  * 	Vue.prototype.$nextTick
  * 	Vue.prototype._render
*/
renderMixin(Vue)

vm.$ data、vm.$props

src/core/instance/state.js

这是两个属性,不是实例方法
function stateMixin(Vue)
	var dataDef=
	dataDef.get = function()
		return this._data;
	
	var propsDef=
	propsDef.get = function()
		return this._props;
	;
	
		dataDef.set = function()
			warn$2('Avoid replacing instance root $data.' + 'Usr nested data properties instend.' ,this)
		;
		props.set = fucntion ()
			warn$("$props is readonly.",this);
		
	
	//将data属性和props属性挂载到Vue.prototype对象上
	//这样在程序中就可以通过this.$data和this.$props来访问data和props对象了
	Object.defineProperty(Vue.prototype,'$data',dataDef);
	Object.defineProperty(Vue.prototype,'$props',propsDef);

vm.$ set

/src/core/instance/state.js

Vue.prototype.$set = set

set

/src/core/observer/index.js

/**
  * 通过Vue.set 或者this.$set 方法给target的指定key设置val
  * 如果target是对象,并且key原本不存在,则为新key设置响应式,然后执行依赖通知;
*/
export function set(target: Array<any> | Object,key:any , val:any):any
	if(process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target))
		warn(`Cannot set reactive property on undefined,null,or primitive value: $(target:any)`)
	
	// 更新数组指定下标的元素,Vue.set(array,idx,val)通过splice方法实现响应式更新
	if(Array.isArray(target) && isValidArrayIndex(key))
		traget.length = Math.max(target.length,key)
		target.splice(key,1,val)
		return val
	
	// 更新对象已有属性,Vue.set(obj,key,val),执行更新即可
	if(key in target && !(key in Object.prototype))
		target[key]=val
		return val
	
	const ob = (target:any).__ob__
	//不能向Vue实例或者$data动态添加响应式属性,vmCount的用处之一;
	//this.$data的ob.vmCount = 1,表示根组件,其它子组件的vm.vmCount都是0
	if(target._isVue || (ob && ob.vmCount))
		process.env.NODE_ENV !== 'production' && warn(`
			Avoid adding reactive properties to a Vue instance or its or root $data at runtime - declare it upfront in the data options
		`)
		return val
	
	// target不是响应式对象,新属性会被设置,但是不会做响应式
	if(!ob)
		target[key] = val
		return val
	
	// 给对象定义新属性,通过defineReactive 方法设置响应式,并触发依赖更新
	defineReactive(ob.value,key,val)
	ob.dep.notify(). //?
	return val


vm.$ delete

/src/core/instance/state.js

Vue.prototype.$delete = del

del

/src/core/observer/index.js

/**
  * 通过Vue.$delete 或者vm.$delete删除target对象的指定key
  * 数组通过splice方法实现,对象则通过delete运算符删除织综key,并执行依赖通知
*/
export function del(target:Array<any> | Object,key:any)
	if(process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(taret))
		warn(`Cannot delete reactive property on undefined ,null, or primitive value: $(target :any)`)
	
	// target为数组,则通过splice方法删除指定下标元素
	if(Array.isArray(target) && isValidArrayIndex(key))
		target.splice(key,1)
		return
	
	const ob = (target:any).__ob__
	//避免删除Vue实例的属性或者$data的数据
	if(target._isVue || (ob && ob.vmCount))
		process.env.NODE_ENV !== 'production' && warn(`
		Avoid deleting properties on a Vue instance or its root $data just set it to null
		`)
		return
	
	//如果属性不存在直接结束
	if(!hasOwn(target,key))
		return
	
	//通过delete运算符删除对象的属性
	delete target[key]
	if(!ob)
		return
	
	//执行依赖通知
	ob.dep.notify()


vm.$watch

/src/core/instance/state.js

/**
  * 创建watcher,返回unwatch,共完成如下5件事:
  * 	1.兼容性处理,保证最后new Watcher 的cb为函数
  * 	2.标示用户watcher
  * 	3.创建watcher实例
  * 	4.如果设置了immediate,则立即执行一次cb
  * 	5.返回unwatch函数,用于取消watch监听
*/
Vue.prototype.$watch = function(
	expOrFn:string | Function,
	cb: any,
	options?:Object
):Function
	const vm:Component = this
	//兼容性处理,因为用户调用vm.$watch设置的cb可能是对象
	if(isPlainObject(cb))
		return createWatcher(vm,expOrFn,cb,options)
	
	//options.user 表示用户watcher,还有渲染watcher,即updateComponent方法中实例化的watcher
	options = optoins || 
	options.user = true
	//创建watcher
	const watcher = new Watcher(vm,expOrFn,cb,options)
	//如果用户设置了immediate为true,则立即执行一次回掉函数
	if(options.immediate)
		try
			cb.call(vm,watcher.value)
		catch(error)
			handleError(error,vm,`callback for immediate watcher "$watcher.expression"`)
		
	
	//返回一个unwatcher函数,用于解除监听
	return function unwatchFn()
		watcher.teardown()
	


vm.$ on

/src/core/instance/events.js

const hookRE = /^hook:/
/**
  * 监听实例上的自定义事件,vm_event = eventName:[fn1,...],....
  * @param * event 单个的事件名称或者有多个事件名组成的数组
  * @param * fn 当event 被触发时执行的回掉函数
  * @returns
*/
Vue.prototype.$on = function (event:string | Array<string>,fn: Function):Component
	const vm: Component = this
	if(Array.isArray(event))
		//event是有多个事件名组成的数组,则遍历这些事件,一次递归调用$on
		for(let i=0,l=event.length;i<l;i++)
			vm.$on(event[i],fn)
		
	else 
		//将注册事件和回掉以键值对的形式存储到vm._event 对象中vm._event = eventName:[fn1,...]
		(vm._events[event] || (vm._events[event])).push(fn)
		// hookEvent,提供从外部为组件实例注入声明周期方法的机会
		// 比如从组件外部为组件的mounted方法注入额外的逻辑
		// 该能力是结合callhook方法实现的
		if(hookRE.test(event))
			vm._hasHookEvent = true
		
	
	return vm

vm.$ emit

/src/core/instance/events.js

/**
  * 触发实例上的指定事件,vm._event[event] =>cbs => loop cbs => cb(args)
  * @param * event 事件名
  * @returns
*/
Vue.prototype.$emit = function (event: string):Component
	const vm: Component = this
	if(process.env.NODE_ENV !== 'production')
		// 将事件名转换为小写
		const lowerCaseEvent = event.toLowerCase()
		// 意思是说,html 属性不区分大小写,所以你不能使用v-on监听小驼峰子形式的事件名(eventName),而应该使用连字符形式的事;
		if(lowerCaseEvent !== event && vm._events[loweCaseEvent])
			tip(
				`Event "$lowerCaseEvent“ is emitted in component $formatComponentName(vm) but the handler is registered for "$event"`
			)
		
		//从 vm._event对象上拿到当前事件的回掉函数数组,并依次调用数组中的回调函数,并且传递提供的参数
		let cbs = vm._events[event]
		if(cbs)
			cbs = cbs.length>1 ? toArray(cbs) : cbs
		
		//将参数转为数组
		const args = toArray(arguments,1)
		const info = `event handler for "$event"`
		for(let i = 0,l=cbs.length;i<l;i++)
			invokeWithErrorHandling(cbs[i],vm,args,vm,info)
		
	
	return vm

vm.$ off

/src/core/instance/events.js

/**
  * 移除自定义事件监听器,即从vm._event对象中找到对应过的事件,移除所有事件 或者移除指定事件的回调函数
  * @param * event
  * @param * fn
  * @returns
*/
Vue.prototype.$off = function(event?: string | Array<string>, fn?: Function):Component
	const vm:Component = this
	//vm.$off()移除实例上的所有监听器=>vm._events = 
	if(!arguments.length)
		vm._events = Object.create(null)
		return vm
	
	// 移除一些事件event = [event1,...],遍历event数组,递归调用vm.$off
	if(Array.isArray(event))
		for(let i = 0,l=event.length;i<l;i++)
			vm.$off(event[i],fn)
		
		return vm
	
	// 除了vm.$off()之外,最终都会走到这里,移除指定事件
	const cbs = vm._events[event]
	if(!cbs)
		//表示没有注册过该事件
		return vm
	
	if(!fn)
		//如果没有传递回调函数,则删除该事件名对应的所有回调函数,vm._event[event]=null
		vm._events[event]=null;
		return vm;
	
	//移除指定事件的指定回调函数,就是从事件的回调数组找到该回调函数,然后将其删除
	let cb;
	let i  = cbs.length
	while(i--)
		cb=cbs[i]
		if(cb===fn || cb.fn===fn)
			cb.splice(i,1);
			break;
		
	
	return vm

vm.$ once

/src/core/instance/events.js

/**
  * 监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除
  * vm.$on + vm.$off
  * @param* event
  * @param* fn
  * @returns
*/
Vue.prototype.$once = function(event:string,fn:Function):Component
	const vm:Component = this
	//调用$on,只是$on的回调函数被特殊处理了,触发时,执行回调函数,先移除事件监听,然后执行设置的回调函数
	function on()
		vm.$off(event,on)
		fn.apply(vm,argument)
	
	on.fn=fn
	vm.$on(event,on)
	return vm


vm._update

/src/core/instance/lifecycle.js
/**
  * 负责更新页面,页面首次渲染和后续更新的入口位置,也是patch的入口位置
*/
Vue.prototype._update = function(vnode:VNode,hrdrating?: boolean)
	const vm: Component = this
	const preEl = vm.$el
	const preVnode = vm._vnode  //旧的vnode
	const restoreActiveInstance = setActiveInstace(vm)
	//更新后的虚拟节点
	vm._vnode = vnode
	//Vue.prototype.__patch__ is injected in entry points
	//based on the rendering backend used
	if(!preVnode)
		//首次渲染,即初始化页面时走这里
		vm.$el = vm.__patch__(vm.$el, vnode, hydrating,false)
	else
		//响应式数据更新时,即更新页面时走这里
		vm.$el = vm.__patch__(prevVnode, vnode)
	
	restoreActiveInstance()
	// update __vue__ reference
	if(preEl)
		preEl.__vue__=null
	
	if(vm.$el)
		vm.$el.__vue__=vm
	
	//如果父元素是一个高阶组件,也同样更新他的$el
	if(vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode)
		vm.$parent.$el = vm.$el
	

vm.$ forceUpdate

/src/core/instance/lifecycle.js

/**
  * 首次调用watcher.update方法,迫使组件重新渲染
  * 它仅仅影响到实例本身和插入插槽内容的子组件,而不是所有子组件
*/
Vue.prototype.$forceUpate = function()
	const vm: Component = this
	if(vm._watcher)
		vm._watcher.update()
	

vm.$ destroy

/src/core/instance/lifecycle.js

/**
  * 完全销毁一个实例,清除它与其它实例的连接,解绑它的全部指令及事件监听器
*/
Vue.prototype.$destory = function()
	var vm = this
	if(vm._isBeingDestroyed) return; 
	//调用beforeDestroy钩子
	callHook$1(vm,'beforeDestroy')
	// 表示实例已经销毁
	vm._isBeingDestroyed = true
	//把自己从老爹($parent)的肚子里($children)移除
	const parent = vm.$parent
	if(parent && !parent._isBeingDestroyed && !vm.$options.abstract)
		remove(parent.$children,vm)
	
	//移除依赖监听
	if(vm._watcher)
		vm._watcher.teardown()
	
	let i = vm._watchers.length
	while(i--)
		vm.watchers[i].teardown()
	
	//remove rederence from data ob
	//frozen object may not have observer
	if(vm._data.__ob__)
		vm.__data.__ob__.vmCount-- //?
	
	//call the last hook...
	vm._isDestroyed = true
	//调用__patch__销毁节点
	vm.__patch__(vm._vnode,null)
	//调用destroyed钩子
	callHook(vm,'destroyed')
	//关闭实例的所有事件监听
	vm.$off()
	// remove __vue__ reference
	if(vm.$el)
		vm.$el.__vue__ = null
	
	if(vm.$vnode)
		vm.$vnode.parent = null
	

vm.$nextTick 上一节写过

vm._render

/src/core/instance/render.js

/**
  * 通过执行render函数生成Vnode
  * 不过里面加入大量的异常处理代码
*/
Vue.prototype._render = function():Vnode
	const vm: Component = this
	const render , _parentVnode = vm.$options
	if(_parentVnode)
		vm.$scopedSlots = normalizeScopedSlots(
		

以上是关于Vue源码学习(六)- 实例方法的主要内容,如果未能解决你的问题,请参考以下文章

VUE学习笔记

Eureka源码分析(六) TimedSupervisorTask

Vue.js 源码学习笔记 -- 分析前准备 待续

Vue2.x源码学习笔记-Vue构造函数

vue.js学习笔记六

vue.js学习笔记六