• 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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    vm.$ set

    /src/core/instance/state.js

    Vue.prototype.$set = set
    
    • 1

    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
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    vm.$ delete

    /src/core/instance/state.js

    Vue.prototype.$delete = del
    
    • 1

    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()
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    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()
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    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
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    vm._update

    /src/core/instance/lifecycle.js
    
    • 1
    /**
      * 负责更新页面,页面首次渲染和后续更新的入口位置,也是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
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    vm.$ forceUpdate

    /src/core/instance/lifecycle.js

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

    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
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    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(
    			_parentVnode..data.scopedSlots,
    			vm.$slots,
    			vm.$scopedSlots
    		)
    	}
    	//设置父vnode,这使得渲染函数可以访问占位符节点上的数据
    	vm.$vnode = _parentVnode
    	//render self
    	let vnode 
    	try{
    		currentRenderingInstance = vm
    		///执行render函数,生成vnode
    		vnode = render.call(vm._renderProxy,vm.$createElement)
    	}catch(e){
    		handlerError(e,vm,'render')
    		//到这儿,说明执行render函数时出错了
    		//开发环境渲染错误信息,生产环境返回之前的vnode,以防止渲染错误导致组件空白
    		if(process.env.NODE_ENV !== 'production' && vm.$options.renderError){
    			try{
    				vnode = vm.$options.renderError.call(vm._renderProxy,vm.$createElement,e)
    			}catch(e){
    				handleError(e,vm,'renderError')
    				vnode = vm._vnode
    			}
    		
    		}else{
    			vnode = vm._vnode
    		}
    	}finally{
    		currentRenderingInstance = null
    	}
    	//如果返回的vnode是数组,并且只包含了一个元素,则直接打平
    	if(Array.isArray(vnode) && vnode.length===1){
    		vnode = vnode[0]
    	}
    	// render 函数出错时,返回一个空的vnode
    	if(!(vnode instanceof Vnode)){
    		if(process.env.NODE_ENV !== 'production' && Array.isArray(vnode){
    			warn(
    			'Multiple root nodes returned from render function. Render function ' + 'should return a single root node.', vm)
    		}
    	}
    	vnode = createEmptyVnode()
    	//set parent 
    	vnode.parent = _parentVnode
    	retur vnode
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    installRenderHelpers

    /src/core/instance/render-helpers/index.js

    该方法负责在实例上安装大量和渲染相关的简写的工具函数,这些工具函数用在编译器生成的渲染函数中,比如v-for编译后的vm._l,还有大家最熟悉的h函数(vm._c),不过它没在这里声明,而是在initRender函数中声明的。
    installRenderHelpers方法是在renderMixin中被调用的。

    /**
      * 在实例上挂载简写的渲染工具函数
      * @param{*} target Vue实例
    */
    export function installRenderHelpers(target:any){
    	target._o = markOnce
    	target._n = toNumber
    	target._s = toString
    	target._l = renderList
    	target._t = renderSlot
    	target._q = looseEqual
    	target._i = looseIndexOf
    	target._m = renderStatic
    	target._f = resolveFilter
    	tarfet._k = checkKeyCodes
    	target._b = bindObjetProp
    	target._v = createTextVNode
    	target._e = createEmptyVNode
    	target._u = resolveScopedSlots
    	target._g = bindObjectListeners
    	target._d = bindDynamicKeys
    	target._p = prependModifier
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
  • 相关阅读:
    html-docx-js网页转为word格式框架
    一篇五分生信临床模型预测文章代码复现——文章介绍
    pytorch 初始化
    使用springboot 配置一个websocket客户端
    Python 爬虫实战 —— 爬取小说
    解决git diff时的^M问题
    产品经理或项目经理考PMP,薪资会不会提高?
    利用Linked SQL Server提权
    springboot医疗管理系统毕业设计源码015221
    Eclipse导入springboot项目遇到的一些坑
  • 原文地址:https://blog.csdn.net/weixin_44374938/article/details/127928964