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__的标记,即表示是响应式数据。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。