Vue源码学习之关于对Array的数据侦听实现
摘要
我们都知道Vue的响应式是通过Object.defineProperty来进行数据劫持。但是那是针对Object类型可以实现,如果是数组呢?通过set/get方式是不行的。
但是Vue作者使用了一个方式来实现Array类型的监测:拦截器。
核心思想
通过创建一个拦截器来覆盖数组本身的原型对象Array.prototype。
拦截器
通过查看Vue源码路径vue/src/core/observer/array.js。
/** *Vue对数组的变化侦测 *思想:通过一个拦截器来覆盖Array.prototype。 *拦截器其实就是一个Object,它的属性与Array.prototype一样。只是对数组的变异方法进行了处理。 */ functiondef(obj,key,val,enumerable){ Object.defineProperty(obj,key,{ value:val, enumerable:!!enumerable, writable:true, configurable:true }) } //数组原型对象 constarrayProto=Array.prototype //拦截器 constarrayMethods=Object.create(arrayProto) //变异数组方法:执行后会改变原始数组的方法 constmethodsToPatch=[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(function(method){ //缓存原始的数组原型上的方法 constoriginal=arrayProto[method] //对每个数组编译方法进行处理(拦截) def(arrayMethods,method,functionmutator(...args){ //返回的value还是通过数组原型方法本身执行的结果 constresult=original.apply(this,args) //每个value在被observer()时候都会打上一个__ob__属性 constob=this.__ob__ //存储调用执行变异数组方法导致数组本身值改变的数组,主要指的是原始数组增加的那部分(需要重新Observer) letinserted switch(method){ case'push': case'unshift': inserted=args break case'splice': inserted=args.slice(2) break } //重新Observe新增加的数组元素 if(inserted)ob.observeArray(inserted) //发送变化通知 ob.dep.notify() returnresult }) })
关于Vue什么时候对data属性进行Observer
如果熟悉Vue源码的童鞋应该很快能找到Vue的入口文件vue/src/core/instance/index.js。
functionVue(options){ if(process.env.NODE_ENV!=='production'&& !(thisinstanceofVue) ){ warn('Vueisaconstructorandshouldbecalledwiththe`new`keyword') } this._init(options) } initMixin(Vue) //给原型绑定代理属性$props,$data //给Vue原型绑定三个实例方法:vm.$watch,vm.$set,vm.$delete stateMixin(Vue) //给Vue原型绑定事件相关的实例方法:vm.$on,vm.$once,vm.$off,vm.$emit eventsMixin(Vue) //给Vue原型绑定生命周期相关的实例方法:vm.$forceUpdate,vm.destroy,以及私有方法_update lifecycleMixin(Vue) //给Vue原型绑定生命周期相关的实例方法:vm.$nextTick,以及私有方法_render,以及一堆工具方法 renderMixin(Vue) exportdefaultVue
this.init()
源码路径:vue/src/core/instance/init.js。
exportfunctioninitMixin(Vue:Class){ Vue.prototype._init=function(options?:Object){ //当前实例 constvm:Component=this //auid //实例唯一标识 vm._uid=uid++ letstartTag,endTag /*istanbulignoreif*/ //开发模式,开启Vue性能检测和支持performance.markAPI的浏览器上。 if(process.env.NODE_ENV!=='production'&&config.performance&&mark){ startTag=`vue-perf-start:${vm._uid}` endTag=`vue-perf-end:${vm._uid}` //处于组件初始化阶段开始打点 mark(startTag) } //aflagtoavoidthisbeingobserved //标识为一个Vue实例 vm._isVue=true //mergeoptions //把我们传入的optionsMerge到$options if(options&&options._isComponent){ //optimizeinternalcomponentinstantiation //sincedynamicoptionsmergingisprettyslow,andnoneofthe //internalcomponentoptionsneedsspecialtreatment. initInternalComponent(vm,options) }else{ vm.$options=mergeOptions( resolveConstructorOptions(vm.constructor), options||{}, vm ) } /*istanbulignoreelse*/ if(process.env.NODE_ENV!=='production'){ initProxy(vm) }else{ vm._renderProxy=vm } //exposerealself vm._self=vm //初始化生命周期 initLifecycle(vm) //初始化事件中心 initEvents(vm) initRender(vm) callHook(vm,'beforeCreate') initInjections(vm)//resolveinjectionsbeforedata/props //初始化State initState(vm) initProvide(vm)//resolveprovideafterdata/props callHook(vm,'created') /*istanbulignoreif*/ if(process.env.NODE_ENV!=='production'&&config.performance&&mark){ vm._name=formatComponentName(vm,false) mark(endTag) measure(`vue${vm._name}init`,startTag,endTag) } //挂载 if(vm.$options.el){ vm.$mount(vm.$options.el) } } }
initState()
源码路径:vue/src/core/instance/state.js。
exportfunctioninitState(vm:Component){ vm._watchers=[] constopts=vm.$options if(opts.props)initProps(vm,opts.props) if(opts.methods)initMethods(vm,opts.methods) if(opts.data){ initData(vm) }else{ observe(vm._data={},true/*asRootData*/) } if(opts.computed)initComputed(vm,opts.computed) if(opts.watch&&opts.watch!==nativeWatch){ initWatch(vm,opts.watch) } }
这个时候你会发现observe出现了。
observe
源码路径:vue/src/core/observer/index.js
exportfunctionobserve(value:any,asRootData:?boolean):Observer|void{ if(!isObject(value)||valueinstanceofVNode){ return } letob:Observer|void if(hasOwn(value,'__ob__')&&value.__ob__instanceofObserver){ //value已经是一个响应式数据就不再创建Observe实例,避免重复侦听 ob=value.__ob__ }elseif( shouldObserve&& !isServerRendering()&& (Array.isArray(value)||isPlainObject(value))&& Object.isExtensible(value)&& !value._isVue ){ //出现目标,创建一个Observer实例 ob=newObserver(value) } if(asRootData&&ob){ ob.vmCount++ } returnob }
使用拦截器的时机
Vue的响应式系统中有个Observe类。源码路径:vue/src/core/observer/index.js。
//canweuse__proto__? exportconsthasProto='__proto__'in{} constarrayKeys=Object.getOwnPropertyNames(arrayMethods) functionprotoAugment(target,src:Object){ /*eslint-disableno-proto*/ target.__proto__=src /*eslint-enableno-proto*/ } functioncopyAugment(target:Object,src:Object,keys:Array){ //target:需要被Observe的对象 //src:数组代理原型对象 //keys:constarrayKeys=Object.getOwnPropertyNames(arrayMethods) //keys:数组代理原型对象上的几个编译方法名 //constmethodsToPatch=[ //'push', //'pop', //'shift', //'unshift', //'splice', //'sort', //'reverse' //] for(leti=0,l=keys.length;i ){ for(leti=0,l=items.length;i 如何收集依赖
Vue里面真正做数据响应式处理的是defineReactive()。defineReactive方法就是把对象的数据属性转为访问器属性,即为数据属性设置get/set。
functiondependArray(value:Array){ for(lete,i=0,l=value.length;i 存储数组依赖的列表
我们为什么需要把依赖存在Observer实例上。即
exportclassObserver{ constructor(value:any){ ... this.dep=newDep() } }首先我们需要在getter里面访问到Observer实例
//即上述的 letchildOb=!shallow&&observe(val) ... if(childOb){ //调用Observer实例上dep的depend()方法收集依赖 childOb.dep.depend() if(Array.isArray(value)){ //调用dependArray函数逐个触发数组每个元素的依赖收集 dependArray(value) } }另外我们在前面提到的拦截器中要使用Observer实例。
methodsToPatch.forEach(function(method){ ... //this表示当前被操作的数据 //但是__ob__怎么来的? constob=this.__ob__ ... //重新Observe新增加的数组元素 if(inserted)ob.observeArray(inserted) //发送变化通知 ob.dep.notify() ... })思考上述的this.__ob__属性来自哪里?
exportclassObserver{ constructor(){ ... this.dep=newDep() //在vue上新增一个不可枚举的__ob__属性,这个属性的值就是Observer实例 //因此我们就可以通过数组数据__ob__获取Observer实例 //进而获取__ob__上的dep def(value,'__ob__',this) ... } }牢记所有的属性一旦被侦测了都会被打上一个__ob__的标记,即表示是响应式数据。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。