3.1 如何追踪变化
Object通过触发getter/setter来实现变化侦测,在Array中,使用push等方法来改变数据,并没有触发getter/setter,所以Object的侦测方式不适用于Array。
为了达到追踪变化的目的,vue使用了自定义的方法覆盖原生的原型的方法。具体的说,是用一个拦截器覆盖Array.prototype。每次使用Array原型上的方法操作数组时,其实执行的都是拦截器中提供的方法,比如push方法,然后在拦截器中使用原生Array的原型方法来操作数组。通过这个拦截器,我们追踪到了Array的方法。
3.2 拦截器
拦截器是在Array.prototype的基础上添加自定义方法的一个Object。
Array中原型方法有7个:push、pop、shift、unshift、splice、sort和reverse。
const arrayProto=Array.prototype
export const arrayMethods=Object.create(arrayProto)
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(function(method){
const original=arrayProto[method]
Object.defineProperty(arrayMethods,method,{
value:function mutator(...args){
return original.apply(this,args)
},
enumerable:false,
writable:true,
configurable:true
})
})
代码解析:
- arrayMethods继承自Array.prototype,具备其所有功能,我们用arrayMethods来覆盖Array.prototype。
- 在arrayMethods上使用Object.defineProperty方法封装数组的那七种原型方法。
- 使用Array原型方法时,实际调用的是mutator方法。
- mutator方法执行原型方法来完成工作。
比如,要使用push方法,实际调用的是arrayMethods.push,而arrayMethods.push是函数mutator,在mutator中调用原生的Array.prototype上的push方法来完成工作。这样,为了实现array的追踪变化,我们在mutator上编写“发送变化通知”的功能就好了。
3.3 使用拦截器覆盖Array原型的具体操作
使用拦截器直接覆盖Array.prototype会污染全局的Array,这不是我们想要的。
我们的目的是侦测到Array中变化了的数据,因此,希望拦截器只覆盖那些响应式数组的原型就好了。
第二章介绍过,在Observer中的数据是响应式的,因此,我们只需要在Observer中使用拦截器覆盖即将被转换成响应式Array类型数据的原型就好了:
export class Observer{
constructor(value){
this.value=value
if(Array.isArray(value)){
value._proto_=arrayMethods //新增
}else{
this.walk(value)
}
}
}
代码解析:
- 新增的代码将拦截器赋值给value._proto_,通过_proto_巧妙的实现覆盖value原型的功能。
补充说明:_proto_其实是Object.getPrototypeOf和Object.setPrototypeOf的早期实现,使用ES6中的Object.setPrototypeOf来代替_proto_可以实现同样的效果,但是,目前ES6在浏览器中的支持度还不够理想。
3.4 将拦截器挂载到数组的属性上
大部分浏览器都支持3.3的方法,但是只是大部分哦,不是100%哦,所以,还需要处理不能使用_proto_的情况。
不支持_proto_方法时,vue是怎么做的呢?
vue简单粗暴的将arrayMethods身上的这些方法设置到被侦测的数组上:
image.png
import { arrayMethods } from './array'
// _proto_是否可用
const hasProto='_proto_' in {}
const arrayKeys=Object.getOwnPropertyNames(arrayMethods)
export class Observer{
constructor (value){
this.value=value
if(Array.isArray(value)){
//修改
const augment=hasProto?protoAugment:copyAugment
augment(value,arrayMethods,arrayKeys)
}else{
this.walk(value)
}
}
.....
}
function protoAugment(target,src,keys){
target._proto_=src
}
function copyAugment(target,src,keys){
for(let i=0,l=keys.length;i<l;i++){
const key=keys[i]
def(target,key,src[key])
}
}
代码解析:
- 使用hasProto变量来判断当前浏览器是否支持_proto_。如果支持,使用protoAugment方法覆盖原型;如果不支持,调用copyAugment方法将拦截器中的方法挂载到value上。
- 使用copyAugment方法用于将已经加工了拦截操作的原型方法直接添加到value的属性中。
3.5 如何收集依赖
使用拦截器实现了发送变化通知的能力,但是通知给谁呢?
在Object中,变化的通知发送给了依赖(Watcher),在getter中使用Dep收集依赖,每个key都有一个对应的Dep列表来存储依赖。
在Array中,同样是在getter中收集依赖,但是是在拦截器中触发依赖。为了保证依赖在getter和拦截器中都可以访问到,我们将依赖保存在Observer的实例上,因为无论在getter还是拦截器,都可以访问到Observer实例。
function defineReactive(data,key,val){
let childOb=observe(val) //修改
let dep=new Dep()
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get:function(){
dep.depend()
//新增
if(childOb){
childOb.dep.depend()
}
return val
},
set:function(newVal){
if(val===newVal){
return
}
val=newVal
dep.notify()
}
})
}
export function observe(value,asRootData){
if(!isObject(vlaue)){
return
}
let ob
if(hasOwn(value,'_ob_')&&value._ob_ instanceof Observer){
ob=value._ob_
}else{
ob=new Observer(value)
}
return ob
}
代码解析:
- 尝试为value创建一个Observer实例,如果创建成功,直接返回该实例;如果value已经存在一个Observer实例,则直接返回它。这样可以避免重复侦测value变化的问题。
- 在defineReactive函数中调用了observe,它把val当做参数传进去并且拿到一个返回值,那就是Observer实例。
- 通过observe我们得到数组的Observer的实例(childOb),最后通过childOb的dep执行depend方法📱依赖。
3.6 在拦截器中获取Observer实例
Array拦截器是对原型的一种封装,所以可以在拦截器中访问到this(当前被操作的数组)。而dep保存在Observer中,所以需要再this上读到Observer的实例。
function dep(obj,key,val,enumerable){
Object.defineProperty(obj,key,{
value:val,
enumerable:!!enumerable,
writable:true,
configurable:true
})
}
export class Observer{
constructor(value){
this.value=value
this.dep=new Dep()
def(value,'_ob_',this) //新增
if(Array.isArray(value)){
const augment=hasProto?protoAugment:copyaugment
augment(value,arrayMethods,arrayKeys)
}else{
this.walk(value)
}
}
}
代码解析:
- 在value上新增一个不可枚举的属性ob,这个属性的值就是当前Observer的实例。
- ob可以在拦截器中访问Observer实例(value.ob),还可以标记当前value是否已经被Observer转换成响应式数据。
3.7 向数组的依赖发送通知
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(function(method){
// 缓存原始方法
const original=arrayProto[method]
def(arrayMethods,method,function mutator(...args){
const result=original.apply(this, args)
const ob=this._ob_
ob.dep.notify() // 向依赖发送消息
return result
})
})
ob.dep.notify()通知依赖(Watcher)数据发生了变化
3.8 侦测数组中元素的变化
除了要判断数组自身发生的变化(比如增减元素),还要侦测数组中元素的变化(比如数组中object身上某一属性的值发生了变化)
export class Observer{
constructor(value){
this.value=value
def(value,'_ob_',this)
//新增
if(Array.isArray(value)){
this.observeArray(value)
}else{
this.walk(value)
}
}
// 侦测数组中的每一个元素
observeArray(items){
for(let i=0;i<items.length;i++){
observe(items[i])
}
}
......
}
代码解析:
- 新增的observeArray函数循环Array中的每一项,执行observe函数来侦测变化。observe函数是将数组中的每个元素执行一遍new Observer。so这里是递归。
3.9 侦测新增元素的变化
数组的push、unshift和splice三种方法可以新增数组,因此,特殊处理这三种原型方法即可。
Observer会将自身的实例附加到value的_ob_属性上,所有被侦测了变化。
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(function(method){
// 缓存原始方法
const original=arrayProto[method]
def(arrayMethods,method,function mutator(...args){
const result=original.apply(this, args)
const ob=this._ob_
let inserted
switch(method){
case 'push'
case 'shift'
inserted =args
break
case 'splice'
inserted=args.slice(2)
break
}
if(inserted) ob.observeArray(inserted) //新增
ob.dep.notify()
return result
})
})
代码解析:
- 从this._ob_上拿到Observer实例后,如果有新增元素,则使用ob.observeArray来侦测这些新增元素的变化。
3.10 关于Array的问题
关于Array的变化侦测是通过拦截原型的方式实现的,so有些数组的操作vue.js是拦截不到的,比如修改数组中某一个元素的值或者直接修改数组的长度。
网友评论