Vue源码学习(六)- 实例方法
创始人
2024-03-03 06:07:37
0

目标

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

  • 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 | 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]=valreturn val}const ob = (target:any).__ob__//不能向Vue实例或者$data动态添加响应式属性,vmCount的用处之一;//this.$data的ob.vmCount = 1,表示根组件,其它子组件的vm.vmCount都是0if(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] = valreturn 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 | 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方法中实例化的watcheroptions = optoins || {}options.user = true//创建watcherconst 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,fn: Function):Component{const vm: Component = thisif(Array.isArray(event)){//event是有多个事件名组成的数组,则遍历这些事件,一次递归调用$onfor(let i=0,l=event.length;ivm.$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 = thisif(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;iinvokeWithErrorHandling(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, 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.$offif(Array.isArray(event)){for(let i = 0,l=event.length;ivm.$off(event[i],fn)}return vm}// 除了vm.$off()之外,最终都会走到这里,移除指定事件const cbs = vm._events[event]if(!cbs){//表示没有注册过该事件return vm}if(!fn){//如果没有传递回调函数,则删除该事件名对应的所有回调函数,vm._event[event]=nullvm._events[event]=null;return vm;}//移除指定事件的指定回调函数,就是从事件的回调数组找到该回调函数,然后将其删除let cb;let i  = cbs.lengthwhile(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=fnvm.$on(event,on)return vm}

vm._update

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

vm.$ destroy

/src/core/instance/lifecycle.js

/*** 完全销毁一个实例,清除它与其它实例的连接,解绑它的全部指令及事件监听器
*/
Vue.prototype.$destory = function(){var vm = thisif(vm._isBeingDestroyed){ return; }//调用beforeDestroy钩子callHook$1(vm,'beforeDestroy')// 表示实例已经销毁vm._isBeingDestroyed = true//把自己从老爹($parent)的肚子里($children)移除const parent = vm.$parentif(parent && !parent._isBeingDestroyed && !vm.$options.abstract){remove(parent.$children,vm)}//移除依赖监听if(vm._watcher){vm._watcher.teardown()}let i = vm._watchers.lengthwhile(i--){vm.watchers[i].teardown()}//remove rederence from data ob//frozen object may not have observerif(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__ referenceif(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 = thisconst {render , _parentVnode} = vm.$optionsif(_parentVnode){vm.$scopedSlots = normalizeScopedSlots(_parentVnode..data.scopedSlots,vm.$slots,vm.$scopedSlots)}//设置父vnode,这使得渲染函数可以访问占位符节点上的数据vm.$vnode = _parentVnode//render selflet vnode try{currentRenderingInstance = vm///执行render函数,生成vnodevnode = 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 函数出错时,返回一个空的vnodeif(!(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 = _parentVnoderetur vnode
}

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 = markOncetarget._n = toNumbertarget._s = toStringtarget._l = renderListtarget._t = renderSlottarget._q = looseEqualtarget._i = looseIndexOftarget._m = renderStatictarget._f = resolveFiltertarfet._k = checkKeyCodestarget._b = bindObjetProptarget._v = createTextVNodetarget._e = createEmptyVNodetarget._u = resolveScopedSlotstarget._g = bindObjectListenerstarget._d = bindDynamicKeystarget._p = prependModifier}

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
群晖外网访问终极解决方法:IP... 写在前面的话 受够了群晖的quickconnet的小水管了,急需一个新的解决方法&#x...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
Azure构建流程(Power... 这可能是由于配置错误导致的问题。请检查构建流程任务中的“发布构建制品”步骤,确保正确配置了“Arti...